From 13b670e5e93dc0243f970832673385e9855a1df6 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Thu, 21 May 2026 20:26:07 +0900 Subject: feat: tool permission toggles, settings improvements, UI polish - Tool permissions (read, edit, bash) stored in DB and control Agent tool registration - Agent invalidated when permissions change; cache warning in Permissions panel - Default: read=allow, edit=ask, bash=ask (matches UI checkboxes) - Settings: auto-save title model on selection (removed save button) - Settings: auto-expand thinking checkbox with shared reactive store - Permissions panel: renamed from Permission Log, shows tool toggles + collapsible log - Sidebar: renamed Current Model to Model Choice - Chat input: allow typing while agent runs (just disable send) - Chat cursor: fix doubled rectangle (removed unicode char) - Generic GET/PUT /tabs/settings/:key endpoints for key-value settings --- packages/api/src/agent-manager.ts | 21 ++-- packages/api/src/routes/tabs.ts | 31 +++++- .../frontend/src/lib/components/ChatInput.svelte | 3 +- .../frontend/src/lib/components/ChatMessage.svelte | 5 +- .../src/lib/components/PermissionLog.svelte | 116 +++++++++++++++++---- .../src/lib/components/SettingsPanel.svelte | 90 +++++++++------- .../src/lib/components/SidebarPanel.svelte | 10 +- packages/frontend/src/lib/settings.svelte.ts | 8 ++ 8 files changed, 205 insertions(+), 79 deletions(-) create mode 100644 packages/frontend/src/lib/settings.svelte.ts (limited to 'packages') diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts index 07af249..9d222fa 100644 --- a/packages/api/src/agent-manager.ts +++ b/packages/api/src/agent-manager.ts @@ -24,6 +24,7 @@ import { refreshAccountCredentials, refreshAccountCredentialsAsync, resolveApiKey, + getSetting, } from "@dispatch/core"; import type { PermissionManager } from "./permission-manager.js"; import { setConfigGetter } from "./routes/config.js"; @@ -47,6 +48,7 @@ interface TabAgent { keyId: string | null; modelId: string | null; taskList: TaskList; + _lastPermKey?: string; } export class AgentManager { @@ -193,10 +195,16 @@ export class AgentManager { const effectiveKeyId = keyId ?? tabAgent.keyId; const effectiveModelId = modelId ?? tabAgent.modelId; - // If the override differs from what the current agent was built with, invalidate the cache + // Read tool permission settings from DB (default: read=allow, edit=ask, bash=ask) + const permRead = getSetting("perm_read") !== "ask"; + const permEdit = getSetting("perm_edit") === "allow"; + const permBash = getSetting("perm_bash") === "allow"; + const permKey = `${permRead}:${permEdit}:${permBash}`; + + // If the override differs or permissions changed, invalidate the cached agent if ( tabAgent.agent && - (effectiveKeyId !== tabAgent.keyId || effectiveModelId !== tabAgent.modelId) + (effectiveKeyId !== tabAgent.keyId || effectiveModelId !== tabAgent.modelId || permKey !== tabAgent._lastPermKey) ) { tabAgent.agent = null; } @@ -204,13 +212,14 @@ export class AgentManager { if (!tabAgent.agent) { const workingDirectory = process.env.DISPATCH_WORKING_DIR ?? process.cwd(); + // Build tools list based on permission settings const tools = [ - createReadFileTool(workingDirectory), - createWriteFileTool(workingDirectory), - createListFilesTool(workingDirectory), - createRunShellTool(workingDirectory), + ...(permRead ? [createReadFileTool(workingDirectory), createListFilesTool(workingDirectory)] : []), + ...(permEdit ? [createWriteFileTool(workingDirectory)] : []), + ...(permBash ? [createRunShellTool(workingDirectory)] : []), createTaskListTool(tabAgent.taskList), ]; + tabAgent._lastPermKey = permKey; const ruleset = configToRuleset(this.config); diff --git a/packages/api/src/routes/tabs.ts b/packages/api/src/routes/tabs.ts index 7fe05d9..288cd51 100644 --- a/packages/api/src/routes/tabs.ts +++ b/packages/api/src/routes/tabs.ts @@ -10,6 +10,7 @@ import { getMessagesForTab, getSetting, setSetting, + deleteSetting, } from "@dispatch/core"; export const tabsRoutes = new Hono(); @@ -41,9 +42,15 @@ tabsRoutes.get("/settings/title-model", (c) => { }); tabsRoutes.put("/settings/title-model", async (c) => { - const body = await c.req.json<{ keyId?: string; modelId?: string }>(); - if (body.keyId) setSetting("title_model_key_id", body.keyId); - if (body.modelId) setSetting("title_model_id", body.modelId); + const body = await c.req.json<{ keyId?: string | null; modelId?: string | null }>(); + if (body.keyId !== undefined) { + if (body.keyId) setSetting("title_model_key_id", body.keyId); + else deleteSetting("title_model_key_id"); + } + if (body.modelId !== undefined) { + if (body.modelId) setSetting("title_model_id", body.modelId); + else deleteSetting("title_model_id"); + } return c.json({ success: true }); }); @@ -72,6 +79,24 @@ tabsRoutes.patch("/:id", async (c) => { return c.json(tab); }); +// ─── Settings ───────────────────────────────────────────────── + +tabsRoutes.get("/settings/:key", (c) => { + const key = c.req.param("key"); + const value = getSetting(key); + return c.json({ value }); +}); + +tabsRoutes.put("/settings/:key", async (c) => { + const key = c.req.param("key"); + const body = await c.req.json<{ value?: string }>(); + if (typeof body.value !== "string") { + return c.json({ error: "value is required" }, 400); + } + setSetting(key, body.value); + return c.json({ success: true }); +}); + tabsRoutes.delete("/:id", (c) => { const id = c.req.param("id"); const mgr = getAgentManager(); diff --git a/packages/frontend/src/lib/components/ChatInput.svelte b/packages/frontend/src/lib/components/ChatInput.svelte index 9563a9f..8910bb4 100644 --- a/packages/frontend/src/lib/components/ChatInput.svelte +++ b/packages/frontend/src/lib/components/ChatInput.svelte @@ -29,9 +29,8 @@ function submit() { bind:this={inputEl} bind:value={inputValue} type="text" - placeholder={isDisabled ? "Agent is running..." : "Type a message..."} + placeholder="Type a message..." class="input flex-1" - disabled={isDisabled} onkeydown={handleKeydown} />