summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-21 20:59:24 +0900
committerAdam Malczewski <[email protected]>2026-05-21 20:59:24 +0900
commit1e13f79899622dd8a5c268b5b8e854b14f82d87f (patch)
treeb15c3002c26a74e34e909e8d00e1d47e532634e0 /packages/frontend/src/lib/components
parent13b670e5e93dc0243f970832673385e9855a1df6 (diff)
downloaddispatch-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')
-rw-r--r--packages/frontend/src/lib/components/ChatMessage.svelte4
-rw-r--r--packages/frontend/src/lib/components/ModelSelector.svelte17
-rw-r--r--packages/frontend/src/lib/components/PermissionLog.svelte43
-rw-r--r--packages/frontend/src/lib/components/SidebarPanel.svelte9
-rw-r--r--packages/frontend/src/lib/components/SystemPromptPanel.svelte61
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>