diff --git a/ui-electrobun/src/bun/handlers/git-handlers.ts b/ui-electrobun/src/bun/handlers/git-handlers.ts index 022fac3..3d6189a 100644 --- a/ui-electrobun/src/bun/handlers/git-handlers.ts +++ b/ui-electrobun/src/bun/handlers/git-handlers.ts @@ -8,6 +8,15 @@ import { execSync, spawn } from "child_process"; export function createGitHandlers() { return { + "ssh.checkSshfs": async () => { + try { + const sshfsPath = execSync("which sshfs", { encoding: "utf8", timeout: 3000 }).trim(); + return { installed: true, path: sshfsPath }; + } catch { + return { installed: false, path: null }; + } + }, + "git.branches": async ({ path: repoPath }: { path: string }) => { try { const resolved = path.resolve(repoPath.replace(/^~/, process.env.HOME ?? "")); diff --git a/ui-electrobun/src/mainview/ModelConfigPanel.svelte b/ui-electrobun/src/mainview/ModelConfigPanel.svelte index 462c4c6..16e867d 100644 --- a/ui-electrobun/src/mainview/ModelConfigPanel.svelte +++ b/ui-electrobun/src/mainview/ModelConfigPanel.svelte @@ -3,303 +3,89 @@ import CustomDropdown from './ui/CustomDropdown.svelte'; import CustomRadio from './ui/CustomRadio.svelte'; - interface Props { - provider: ProviderId; - model: string; - config: Record; - onChange: (config: Record) => void; - } - + interface Props { provider: ProviderId; model: string; config: Record; onChange: (config: Record) => void; } let { provider, model, config, onChange }: Props = $props(); - // ── Claude config ────────────────────────────────────── let claudeThinking = $state<'disabled' | 'enabled' | 'adaptive'>('disabled'); - let claudeEffort = $state('medium'); - let claudeTemp = $state(1.0); - let claudeMaxTokens = $state(8192); + let claudeEffort = $state('medium'); let claudeTemp = $state(1.0); let claudeMaxTokens = $state(8192); let claudeTempLocked = $derived(claudeThinking === 'enabled'); - // ── Codex config ─────────────────────────────────────── - let codexSandbox = $state('workspace-write'); - let codexApproval = $state('on-request'); - let codexReasoning = $state('medium'); + let codexSandbox = $state('workspace-write'); let codexApproval = $state('on-request'); let codexReasoning = $state('medium'); - // ── Ollama config ────────────────────────────────────── - let ollamaTemp = $state(0.8); - let ollamaCtx = $state(32768); - let ollamaPredict = $state(0); - let ollamaTopK = $state(40); - let ollamaTopP = $state(0.9); + let ollamaTemp = $state(0.8); let ollamaCtx = $state(32768); let ollamaPredict = $state(0); + let ollamaTopK = $state(40); let ollamaTopP = $state(0.9); let ollamaCtxWarn = $derived(ollamaCtx < 8192); - // ── Gemini config ────────────────────────────────────── - let geminiTemp = $state(1.0); - let geminiThinkingMode = $state<'level' | 'budget'>('level'); - let geminiThinkingLevel = $state('medium'); - let geminiThinkingBudget = $state(8192); - let geminiMaxOutput = $state(8192); + let geminiTemp = $state(1.0); let geminiThinkingMode = $state<'level' | 'budget'>('level'); + let geminiThinkingLevel = $state('medium'); let geminiThinkingBudget = $state(8192); let geminiMaxOutput = $state(8192); - // Sync local state from config prop (re-runs when config changes) $effect(() => { const c = config; - claudeThinking = (c.thinking as string as typeof claudeThinking) ?? 'disabled'; - claudeEffort = (c.effort as string) ?? 'medium'; - claudeTemp = (c.temperature as number) ?? 1.0; - claudeMaxTokens = (c.max_tokens as number) ?? 8192; - codexSandbox = (c.sandbox as string) ?? 'workspace-write'; - codexApproval = (c.approval as string) ?? 'on-request'; - codexReasoning = (c.reasoning as string) ?? 'medium'; - ollamaTemp = (c.temperature as number) ?? 0.8; - ollamaCtx = (c.num_ctx as number) ?? 32768; - ollamaPredict = (c.num_predict as number) ?? 0; - ollamaTopK = (c.top_k as number) ?? 40; - ollamaTopP = (c.top_p as number) ?? 0.9; - geminiTemp = (c.temperature as number) ?? 1.0; - geminiThinkingMode = (c.thinkingMode as string as 'level' | 'budget') ?? 'level'; - geminiThinkingLevel = (c.thinkingLevel as string) ?? 'medium'; - geminiThinkingBudget = (c.thinkingBudget as number) ?? 8192; - geminiMaxOutput = (c.maxOutputTokens as number) ?? 8192; + claudeThinking = (c.thinking as typeof claudeThinking) ?? 'disabled'; claudeEffort = (c.effort as string) ?? 'medium'; + claudeTemp = (c.temperature as number) ?? 1.0; claudeMaxTokens = (c.max_tokens as number) ?? 8192; + codexSandbox = (c.sandbox as string) ?? 'workspace-write'; codexApproval = (c.approval as string) ?? 'on-request'; codexReasoning = (c.reasoning as string) ?? 'medium'; + ollamaTemp = (c.temperature as number) ?? 0.8; ollamaCtx = (c.num_ctx as number) ?? 32768; ollamaPredict = (c.num_predict as number) ?? 0; ollamaTopK = (c.top_k as number) ?? 40; ollamaTopP = (c.top_p as number) ?? 0.9; + geminiTemp = (c.temperature as number) ?? 1.0; geminiThinkingMode = (c.thinkingMode as 'level'|'budget') ?? 'level'; geminiThinkingLevel = (c.thinkingLevel as string) ?? 'medium'; geminiThinkingBudget = (c.thinkingBudget as number) ?? 8192; geminiMaxOutput = (c.maxOutputTokens as number) ?? 8192; }); - // ── Emit changes ─────────────────────────────────────── - function emitClaude() { - onChange({ - thinking: claudeThinking, - effort: claudeThinking === 'adaptive' ? claudeEffort : undefined, - temperature: claudeTempLocked ? 1.0 : claudeTemp, - max_tokens: claudeMaxTokens, - }); - } + function emitClaude() { onChange({ thinking: claudeThinking, effort: claudeThinking === 'adaptive' ? claudeEffort : undefined, temperature: claudeTempLocked ? 1.0 : claudeTemp, max_tokens: claudeMaxTokens }); } + function emitCodex() { onChange({ sandbox: codexSandbox, approval: codexApproval, reasoning: codexReasoning }); } + function emitOllama() { onChange({ temperature: ollamaTemp, num_ctx: ollamaCtx, num_predict: ollamaPredict || undefined, top_k: ollamaTopK, top_p: ollamaTopP }); } + function emitGemini() { onChange({ temperature: geminiTemp, thinkingMode: geminiThinkingMode, thinkingLevel: geminiThinkingMode === 'level' ? geminiThinkingLevel : undefined, thinkingBudget: geminiThinkingMode === 'budget' ? geminiThinkingBudget : undefined, maxOutputTokens: geminiMaxOutput }); } - function emitCodex() { - onChange({ sandbox: codexSandbox, approval: codexApproval, reasoning: codexReasoning }); - } + function fmtT(n: number): string { return n >= 1000 ? `${(n / 1000).toFixed(n % 1000 === 0 ? 0 : 1)}K` : String(n); } + function pf(e: Event) { return parseFloat((e.target as HTMLInputElement).value); } + function pi(e: Event, d: number) { return parseInt((e.target as HTMLInputElement).value, 10) || d; } - function emitOllama() { - onChange({ - temperature: ollamaTemp, - num_ctx: ollamaCtx, - num_predict: ollamaPredict || undefined, - top_k: ollamaTopK, - top_p: ollamaTopP, - }); - } - - function emitGemini() { - onChange({ - temperature: geminiTemp, - thinkingMode: geminiThinkingMode, - thinkingLevel: geminiThinkingMode === 'level' ? geminiThinkingLevel : undefined, - thinkingBudget: geminiThinkingMode === 'budget' ? geminiThinkingBudget : undefined, - maxOutputTokens: geminiMaxOutput, - }); - } - - const THINKING_OPTIONS = [ - { value: 'disabled', label: 'Disabled' }, - { value: 'enabled', label: 'Enabled' }, - { value: 'adaptive', label: 'Adaptive' }, - ]; - const EFFORT_ITEMS = [ - { value: 'low', label: 'Low' }, - { value: 'medium', label: 'Medium' }, - { value: 'high', label: 'High' }, - { value: 'max', label: 'Max' }, - ]; + const THINKING_OPTIONS = [{ value: 'disabled', label: 'Disabled' }, { value: 'enabled', label: 'Enabled' }, { value: 'adaptive', label: 'Adaptive' }]; + const EFFORT_ITEMS = [{ value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' }, { value: 'max', label: 'Max' }]; const SANDBOX_ITEMS = ['read-only', 'workspace-write', 'danger-full-access']; const APPROVAL_ITEMS = ['untrusted', 'on-request', 'never']; const REASONING_ITEMS = ['minimal', 'low', 'medium', 'high', 'xhigh']; - const GEMINI_LEVEL_ITEMS = [ - { value: 'minimal', label: 'Minimal' }, - { value: 'low', label: 'Low' }, - { value: 'medium', label: 'Medium' }, - { value: 'high', label: 'High' }, - ]; + const GEMINI_LEVELS = [{ value: 'minimal', label: 'Minimal' }, { value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' }];
{#if provider === 'claude'} -
Thinking mode - { claudeThinking = v as typeof claudeThinking; emitClaude(); }} - /> + { claudeThinking = v as typeof claudeThinking; emitClaude(); }} /> + {claudeThinking === 'disabled' ? 'thinking.type = disabled' : claudeThinking === 'enabled' ? 'thinking.type = enabled (always)' : 'thinking.type = adaptive (model decides)'}
- -
- Effort level - { claudeEffort = v; emitClaude(); }} - /> + Effort level (output_config.effort) + { claudeEffort = v; emitClaude(); }} />
- -
- - Temperature - {#if claudeTempLocked} - (locked) - {/if} - + Temperature {#if claudeTempLocked}Locked at 1.0{/if}
- { claudeTemp = parseFloat((e.target as HTMLInputElement).value); emitClaude(); }} /> + { claudeTemp = pf(e); emitClaude(); }} /> {claudeTempLocked ? '1.0' : claudeTemp.toFixed(1)}
- -
- Max tokens - { claudeMaxTokens = parseInt((e.target as HTMLInputElement).value, 10) || 8192; emitClaude(); }} /> + Max tokens {fmtT(claudeMaxTokens)} +
+ { claudeMaxTokens = pi(e, 8192); emitClaude(); }} /> + {fmtT(claudeMaxTokens)} +
- {:else if provider === 'codex'} - -
- Sandbox mode -
- {#each SANDBOX_ITEMS as s} - - {/each} -
-
- - -
- Approval policy -
- {#each APPROVAL_ITEMS as a} - - {/each} -
-
- - -
- Reasoning effort -
- {#each REASONING_ITEMS as r} - - {/each} -
-
- +
Sandbox mode
{#each SANDBOX_ITEMS as s}{/each}
+
Approval policy
{#each APPROVAL_ITEMS as a}{/each}
+
Reasoning effort
{#each REASONING_ITEMS as r}{/each}
{:else if provider === 'ollama'} - -
- Temperature -
- { ollamaTemp = parseFloat((e.target as HTMLInputElement).value); emitOllama(); }} /> - {ollamaTemp.toFixed(1)} -
-
- - -
- - Context window (num_ctx) - {#if ollamaCtxWarn} - Low context may cause truncation - {/if} - - { ollamaCtx = parseInt((e.target as HTMLInputElement).value, 10) || 32768; emitOllama(); }} /> -
- - -
- Max predict (num_predict, 0 = unlimited) - { ollamaPredict = parseInt((e.target as HTMLInputElement).value, 10) || 0; emitOllama(); }} /> -
- - -
- Top-K - { ollamaTopK = parseInt((e.target as HTMLInputElement).value, 10) || 40; emitOllama(); }} /> -
- - -
- Top-P -
- { ollamaTopP = parseFloat((e.target as HTMLInputElement).value); emitOllama(); }} /> - {ollamaTopP.toFixed(2)} -
-
- +
Temperature
{ ollamaTemp = pf(e); emitOllama(); }} />{ollamaTemp.toFixed(1)}
+
Context window {#if ollamaCtxWarn}Low context{/if}
{ ollamaCtx = pi(e, 32768); emitOllama(); }} />{fmtT(ollamaCtx)}
+
Max predict (0=unlimited)
{ ollamaPredict = pi(e, 0); emitOllama(); }} />{ollamaPredict === 0 ? '\u221E' : fmtT(ollamaPredict)}
+
Top-K
{ ollamaTopK = pi(e, 40); emitOllama(); }} />{ollamaTopK}
+
Top-P
{ ollamaTopP = pf(e); emitOllama(); }} />{ollamaTopP.toFixed(2)}
{:else if provider === 'gemini'} - -
- Temperature -
- { geminiTemp = parseFloat((e.target as HTMLInputElement).value); emitGemini(); }} /> - {geminiTemp.toFixed(1)} -
-
- - -
- Thinking configuration -
- - -
-
- - -
- Thinking level -
- {#each GEMINI_LEVEL_ITEMS as lvl} - - {/each} -
-
- - -
- Thinking budget (tokens) - { geminiThinkingBudget = parseInt((e.target as HTMLInputElement).value, 10) || 8192; emitGemini(); }} /> -
- - -
- Max output tokens - { geminiMaxOutput = parseInt((e.target as HTMLInputElement).value, 10) || 8192; emitGemini(); }} /> -
+
Temperature
{ geminiTemp = pf(e); emitGemini(); }} />{geminiTemp.toFixed(1)}
+
Thinking
+
Thinking level
{#each GEMINI_LEVELS as l}{/each}
+
Budget (tokens)
{ geminiThinkingBudget = pi(e, 8192); emitGemini(); }} />{fmtT(geminiThinkingBudget)}
+
Max output tokens
{ geminiMaxOutput = pi(e, 8192); emitGemini(); }} />{fmtT(geminiMaxOutput)}
{/if}
@@ -309,16 +95,17 @@ .mcp-label { font-size: 0.75rem; font-weight: 500; color: var(--ctp-subtext0); display: flex; align-items: center; gap: 0.375rem; } .mcp-hint { font-size: 0.6875rem; color: var(--ctp-overlay0); font-weight: 400; font-style: italic; } .mcp-warn { font-size: 0.6875rem; color: var(--ctp-peach); font-weight: 400; } - .mcp-input { padding: 0.375rem 0.5rem; width: 100%; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8125rem; font-family: var(--ui-font-family); appearance: textfield; -moz-appearance: textfield; } - .mcp-input::-webkit-inner-spin-button, .mcp-input::-webkit-outer-spin-button { -webkit-appearance: none; appearance: none; } - .mcp-input:focus { outline: none; border-color: var(--ctp-blue); } + .mcp-explain { font-size: 0.625rem; color: var(--ctp-overlay0); font-style: italic; } .mcp-slider-row { display: flex; align-items: center; gap: 0.5rem; } .mcp-slider { flex: 1; accent-color: var(--ctp-blue); } .mcp-slider:disabled { opacity: 0.4; } + .mcp-slider:focus-visible { outline: 2px solid var(--ctp-blue); outline-offset: 2px; } .mcp-slider-val { font-size: 0.8125rem; color: var(--ctp-text); min-width: 2.5rem; text-align: right; } + .mcp-slider-val-inline { font-size: 0.6875rem; color: var(--ctp-text); font-weight: 400; } .mcp-seg { display: flex; border-radius: 0.25rem; overflow: hidden; border: 1px solid var(--ctp-surface1); } .mcp-seg-btn { flex: 1; padding: 0.25rem 0.375rem; background: var(--ctp-surface0); border: none; color: var(--ctp-overlay1); font-size: 0.6875rem; cursor: pointer; font-family: var(--ui-font-family); border-right: 1px solid var(--ctp-surface1); } .mcp-seg-btn:last-child { border-right: none; } .mcp-seg-btn:hover { background: var(--ctp-surface1); color: var(--ctp-subtext1); } + .mcp-seg-btn:focus-visible { outline: 2px solid var(--ctp-blue); outline-offset: -2px; } .mcp-seg-btn.active { background: color-mix(in srgb, var(--ctp-blue) 20%, var(--ctp-surface0)); color: var(--ctp-blue); font-weight: 600; } diff --git a/ui-electrobun/src/mainview/ProjectWizard.svelte b/ui-electrobun/src/mainview/ProjectWizard.svelte index 8693f3a..75fe2d9 100644 --- a/ui-electrobun/src/mainview/ProjectWizard.svelte +++ b/ui-electrobun/src/mainview/ProjectWizard.svelte @@ -1,4 +1,5 @@ -