diff options
| author | Adam Malczewski <[email protected]> | 2026-05-21 20:59:24 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-21 20:59:24 +0900 |
| commit | 1e13f79899622dd8a5c268b5b8e854b14f82d87f (patch) | |
| tree | b15c3002c26a74e34e909e8d00e1d47e532634e0 /packages/frontend/src/lib/components | |
| parent | 13b670e5e93dc0243f970832673385e9855a1df6 (diff) | |
| download | dispatch-1e13f79899622dd8a5c268b5b8e854b14f82d87f.tar.gz dispatch-1e13f79899622dd8a5c268b5b8e854b14f82d87f.zip | |
feat: system prompt editor, tool permissions save-on-send, responsive sidebar, UI polish
- System Prompt sidebar view: editable textarea, save-on-send, reset button
- Tool permissions: save-on-send pattern (not immediate), reset button
- Dynamic system prompt: buildSystemPrompt reads from DB, tool list auto-generated
- Responsive sidebar: overlay on small screens with backdrop
- Chat bubbles: user=fit-width, assistant=full-width
- Fix infinite loops (use onMount for data fetching)
- Fix sendMessage race condition (await settings saves before chat POST)
- Model selector: auto-open model modal after key selection
- Rename views: Permissions->Tools, Tab Settings->Model Choice
- Shared appSettings store for cross-component reactive state
- Delete old chat.svelte.ts
Diffstat (limited to 'packages/frontend/src/lib/components')
5 files changed, 103 insertions, 31 deletions
diff --git a/packages/frontend/src/lib/components/ChatMessage.svelte b/packages/frontend/src/lib/components/ChatMessage.svelte index 4db57c3..398bee8 100644 --- a/packages/frontend/src/lib/components/ChatMessage.svelte +++ b/packages/frontend/src/lib/components/ChatMessage.svelte @@ -21,8 +21,8 @@ const isSystem = $derived(message.role === "system"); </div> </div> {:else} -<div class="chat chat-start mb-2"> - <div class="chat-bubble max-w-[80%] break-words {isUser ? 'chat-bubble-primary' : 'bg-transparent'}"> +<div class="chat chat-start mb-2 [&>.chat-bubble]:max-w-full"> + <div class="chat-bubble break-words {isUser ? 'chat-bubble-primary w-fit' : 'bg-transparent w-full'}"> {#if message.thinking} <div class="collapse collapse-arrow mb-2 p-1"> <input type="checkbox" checked={appSettings.autoExpandThinking} /> diff --git a/packages/frontend/src/lib/components/ModelSelector.svelte b/packages/frontend/src/lib/components/ModelSelector.svelte index b88b2e3..0af5ae6 100644 --- a/packages/frontend/src/lib/components/ModelSelector.svelte +++ b/packages/frontend/src/lib/components/ModelSelector.svelte @@ -44,16 +44,19 @@ function selectKey(keyId: string) { showKeyModal = false; onKeyChange(keyId); + // Immediately open model selection for the new key + openModelModal(keyId); } - async function openModelModal() { - if (!activeKeyId) return; + async function openModelModal(keyIdOverride?: string) { + const keyId = keyIdOverride ?? activeKeyId; + if (!keyId) return; showModelModal = true; modelError = null; // Check session cache - if (modelCache.has(activeKeyId)) { - availableModels = modelCache.get(activeKeyId)!; + if (modelCache.has(keyId)) { + availableModels = modelCache.get(keyId)!; loadingModels = false; return; } @@ -63,7 +66,7 @@ try { const res = await fetch( - `${config.apiBase}/models/available?keyId=${encodeURIComponent(activeKeyId)}`, + `${config.apiBase}/models/available?keyId=${encodeURIComponent(keyId)}`, ); if (!res.ok) { const data = await res.json().catch(() => ({})); @@ -73,7 +76,7 @@ const data = await res.json(); availableModels = data.models ?? []; // Cache for session - modelCache.set(activeKeyId, availableModels); + modelCache.set(keyId, availableModels); } catch (err) { modelError = err instanceof Error ? err.message : "Failed to fetch models"; } finally { @@ -99,7 +102,7 @@ <div class="flex items-center justify-between mt-2"> <span class="text-sm font-medium">Model</span> - <button class="btn btn-sm btn-outline" onclick={openModelModal} disabled={!activeKeyId}> + <button class="btn btn-sm btn-outline" onclick={() => openModelModal()} disabled={!activeKeyId}> {activeModelId ?? "Select Model"} </button> </div> diff --git a/packages/frontend/src/lib/components/PermissionLog.svelte b/packages/frontend/src/lib/components/PermissionLog.svelte index 06607de..7733dcf 100644 --- a/packages/frontend/src/lib/components/PermissionLog.svelte +++ b/packages/frontend/src/lib/components/PermissionLog.svelte @@ -1,5 +1,7 @@ <script lang="ts"> +import { onMount } from "svelte"; import type { LogEntry } from "../types.js"; +import { appSettings } from "../settings.svelte.js"; const { entries, apiBase = "" }: { entries: LogEntry[]; apiBase?: string } = $props(); @@ -16,46 +18,41 @@ const toolPermissions: ToolPermission[] = [ { id: "external_directory", label: "External directories", description: "Allow access to files outside the workspace" }, ]; -let permStates = $state<Record<string, boolean>>({ - read: true, - edit: false, - bash: false, - external_directory: false, -}); - async function loadPermissions(): Promise<void> { + const loaded: Record<string, boolean> = { ...appSettings.toolPerms }; for (const perm of toolPermissions) { try { const res = await fetch(`${apiBase}/tabs/settings/perm_${perm.id}`); if (res.ok) { const data = await res.json() as { value: string | null }; if (data.value !== null) { - permStates[perm.id] = data.value === "allow"; + loaded[perm.id] = data.value === "allow"; } } } catch { // ignore } } + appSettings.toolPerms = { ...loaded }; + appSettings.savedToolPerms = { ...loaded }; } -async function togglePermission(id: string): Promise<void> { - permStates[id] = !permStates[id]; - const value = permStates[id] ? "allow" : "ask"; - fetch(`${apiBase}/tabs/settings/perm_${id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ value }), - }).catch(() => {}); +function togglePermission(id: string): void { + appSettings.toolPerms = { ...appSettings.toolPerms, [id]: !appSettings.toolPerms[id] }; } -$effect(() => { - void loadPermissions(); +function resetPermissions(): void { + appSettings.toolPerms = { ...appSettings.savedToolPerms }; +} + +onMount(() => { + loadPermissions(); }); </script> <div class="flex flex-col gap-3"> <div class="text-xs font-semibold text-base-content/50 uppercase tracking-wide">Tool Permissions</div> + <p class="text-xs text-base-content/40">Changes are applied when you send your next message.</p> <div class="flex flex-col gap-1.5"> {#each toolPermissions as perm (perm.id)} @@ -63,7 +60,7 @@ $effect(() => { <input type="checkbox" class="checkbox checkbox-sm rounded-sm mt-0.5" - checked={permStates[perm.id]} + checked={appSettings.toolPerms[perm.id]} onchange={() => togglePermission(perm.id)} /> <div class="flex flex-col"> @@ -74,6 +71,14 @@ $effect(() => { {/each} </div> + <button + class="btn btn-sm btn-ghost w-full" + disabled={!appSettings.toolPermsDirty} + onclick={resetPermissions} + > + Reset + </button> + <p class="text-xs text-base-content/40">Warning: changing tool access will reset the AI's prompt cache for active conversations, which may increase usage costs.</p> <!-- Permission Log --> diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte index 9aca408..93d528e 100644 --- a/packages/frontend/src/lib/components/SidebarPanel.svelte +++ b/packages/frontend/src/lib/components/SidebarPanel.svelte @@ -8,6 +8,7 @@ import KeyUsage from "./KeyUsage.svelte"; import ClaudeReset from "./ClaudeReset.svelte"; import SettingsPanel from "./SettingsPanel.svelte"; + import SystemPromptPanel from "./SystemPromptPanel.svelte"; import type { TaskItem, LogEntry, KeyInfo } from "../types.js"; const { @@ -42,7 +43,7 @@ let nextId = 0; let panels = $state<Panel[]>([{ id: nextId++, selected: "Model Choice" }]); - const viewOptions = ["Select a view", "Model Choice", "Key Usage", "Claude Reset", "Model Status", "Tasks", "Config", "Skills", "Permissions", "Settings"]; + const viewOptions = ["Select a view", "Model Choice", "Key Usage", "Claude Reset", "Model Status", "Tasks", "Config", "Skills", "Tools", "System Prompt", "Settings"]; function addPanel() { panels = [...panels, { id: nextId++, selected: "Select a view" }]; @@ -113,9 +114,11 @@ <ConfigPanel {apiBase} /> {:else if panel.selected === "Skills"} <SkillsBrowser {apiBase} /> - {:else if panel.selected === "Permissions"} + {:else if panel.selected === "Tools"} <PermissionLog entries={permissionLog} {apiBase} /> - {:else if panel.selected === "Settings"} + {:else if panel.selected === "System Prompt"} + <SystemPromptPanel {apiBase} /> + {:else if panel.selected === "Settings"} <SettingsPanel {keys} {apiBase} /> {/if} </div> diff --git a/packages/frontend/src/lib/components/SystemPromptPanel.svelte b/packages/frontend/src/lib/components/SystemPromptPanel.svelte new file mode 100644 index 0000000..6b91ebe --- /dev/null +++ b/packages/frontend/src/lib/components/SystemPromptPanel.svelte @@ -0,0 +1,61 @@ +<script lang="ts"> + import { onMount } from "svelte"; + import { appSettings } from "../settings.svelte.js"; + + const { + apiBase = "", + }: { + apiBase?: string; + } = $props(); + + const DEFAULT_PROMPT = "You are Dispatch, a helpful AI coding assistant. Be concise and helpful."; + + async function loadPrompt(): Promise<void> { + try { + const res = await fetch(`${apiBase}/tabs/settings/system_prompt`); + if (res.ok) { + const data = await res.json() as { value: string | null }; + const value = data.value ?? DEFAULT_PROMPT; + appSettings.systemPrompt = value; + appSettings.savedSystemPrompt = value; + } + } catch { + // ignore + } + } + + function resetPrompt(): void { + appSettings.systemPrompt = appSettings.savedSystemPrompt; + } + + const isDirty = $derived(appSettings.systemPrompt !== appSettings.savedSystemPrompt); + + onMount(() => { + if (!appSettings.systemPrompt) { + appSettings.systemPrompt = DEFAULT_PROMPT; + appSettings.savedSystemPrompt = DEFAULT_PROMPT; + } + loadPrompt(); + }); +</script> + +<div class="flex flex-col gap-3 flex-1 min-h-0"> + <div class="text-xs font-semibold text-base-content/50 uppercase tracking-wide">System Prompt</div> + <p class="text-xs text-base-content/40">The base instructions sent to the AI at the start of every conversation. Tool descriptions are appended automatically. Changes are applied when you send your next message.</p> + + <textarea + class="textarea textarea-bordered w-full flex-1 min-h-32 text-xs font-mono leading-relaxed" + bind:value={appSettings.systemPrompt} + placeholder="Enter system prompt..." + ></textarea> + + <button + class="btn btn-sm btn-ghost w-full" + disabled={!isDirty} + onclick={resetPrompt} + > + Reset + </button> + + <p class="text-xs text-base-content/40">Warning: changing the system prompt will reset the AI's prompt cache for active conversations, which may increase usage costs.</p> +</div> |
