diff options
| author | Adam Malczewski <[email protected]> | 2026-05-23 05:06:12 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-23 05:06:12 +0900 |
| commit | 9287cccb29d135ea19f2612c26f3090c94820d8c (patch) | |
| tree | 2d68e8cacf6d71786f305d5f4a512a68f19137c5 /packages/frontend/src/lib/components | |
| parent | ef427d3eae77fca716c203dd8bd84939710c518a (diff) | |
| download | dispatch-9287cccb29d135ea19f2612c26f3090c94820d8c.tar.gz dispatch-9287cccb29d135ea19f2612c26f3090c94820d8c.zip | |
feat: add is_subagent flag to agents, fix all lint/type/test issues
- Add is_subagent checkbox to agent editor; subagents are hidden from Chat Settings
- Add is_subagent field to AgentDefinition type, TOML serialization, and API route
- Filter subagents from ModelSelector agent list
- Fix all biome lint/format errors across codebase (useLiteralKeys, noNonNullAssertion, noExplicitAny, formatting, import sorting)
- Fix svelte-check errors (type narrowing in SkillsBrowser, ToolPermissions, SidebarPanel)
- Fix a11y warnings in App.svelte (label-control associations)
- Fix test mocks missing BackgroundShellStore, BackgroundTranscriptStore, createWebSearchTool, createYoutubeTranscribeTool
- Update stale 409 test to match current message-queuing behavior
- Exclude packaging/ and release/ dirs from biome to avoid linting stale build artifacts
Diffstat (limited to 'packages/frontend/src/lib/components')
8 files changed, 63 insertions, 19 deletions
diff --git a/packages/frontend/src/lib/components/AgentBuilder.svelte b/packages/frontend/src/lib/components/AgentBuilder.svelte index 4a2d5d8..65fd764 100644 --- a/packages/frontend/src/lib/components/AgentBuilder.svelte +++ b/packages/frontend/src/lib/components/AgentBuilder.svelte @@ -24,6 +24,7 @@ const modelCache = new Map(); scope: string; slug: string; cwd?: string; + is_subagent?: boolean; } interface DirEntry { @@ -65,6 +66,7 @@ const modelCache = new Map(); let formSkills = $state<Set<string>>(new Set()); let formTools = $state<Set<string>>(new Set()); let formModels = $state<AgentModelEntry[]>([]); + let formIsSubagent = $state(false); // Model selection modal state let modelModalIndex = $state<number | null>(null); @@ -121,6 +123,7 @@ const modelCache = new Map(); formSkills = new Set(); formTools = new Set(); formModels = []; + formIsSubagent = false; editing = true; // Allow the effect to skip the initial population setTimeout(() => { formReady = true; }, 0); @@ -136,6 +139,7 @@ const modelCache = new Map(); formSkills = new Set(agent.skills); formTools = new Set(agent.tools); formModels = agent.models.map((m) => ({ ...m })); + formIsSubagent = agent.is_subagent ?? false; editing = true; // Allow the effect to skip the initial population setTimeout(() => { formReady = true; }, 0); @@ -260,6 +264,7 @@ const modelCache = new Map(); scope: formScope, slug: editingSlug ?? slugify(formName.trim()), ...(formCwd.trim() ? { cwd: formCwd.trim() } : {}), + ...(formIsSubagent ? { is_subagent: true } : {}), }; saving = true; @@ -317,7 +322,8 @@ const modelCache = new Map(); // Auto-save with debounce whenever form fields change $effect(() => { // Read all reactive form fields to subscribe - const _ = [formName, formDescription, formScope, formCwd, formSkills, formTools, formModels]; + // noinspection: intentionally unused — reading these values subscribes the effect to them + void [formName, formDescription, formScope, formCwd, formSkills, formTools, formModels, formIsSubagent]; if (!formReady || !editing) return; if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { @@ -420,6 +426,21 @@ const modelCache = new Map(); /> </div> + <!-- Is Subagent --> + <div class="form-control"> + <label class="label cursor-pointer justify-start gap-3 py-1"> + <input + type="checkbox" + class="checkbox checkbox-sm rounded-sm" + bind:checked={formIsSubagent} + /> + <div> + <span class="label-text font-semibold">Is Subagent</span> + <p class="text-xs text-base-content/50">Subagents are hidden from Chat Settings and can only be used by other agents.</p> + </div> + </label> + </div> + <!-- Scope --> <div class="form-control gap-1"> <label class="label py-0" for="agent-scope"> diff --git a/packages/frontend/src/lib/components/MarkdownRenderer.svelte b/packages/frontend/src/lib/components/MarkdownRenderer.svelte index 0fbf314..de202b6 100644 --- a/packages/frontend/src/lib/components/MarkdownRenderer.svelte +++ b/packages/frontend/src/lib/components/MarkdownRenderer.svelte @@ -88,7 +88,7 @@ const loadCache = new Map<string, Promise<boolean>>(); async function ensureLanguage(lang: string): Promise<boolean> { const name = normalizeLang(lang); if (hljs.getLanguage(name)) return true; - if (loadCache.has(name)) return loadCache.get(name)!; + if (loadCache.has(name)) return loadCache.get(name) ?? false; const promise = (async () => { try { diff --git a/packages/frontend/src/lib/components/ModelSelector.svelte b/packages/frontend/src/lib/components/ModelSelector.svelte index 5949e71..19ce818 100644 --- a/packages/frontend/src/lib/components/ModelSelector.svelte +++ b/packages/frontend/src/lib/components/ModelSelector.svelte @@ -16,6 +16,7 @@ const modelCache = new Map<string, string[]>(); tools: string[]; models: Array<{ key_id: string; model_id: string }>; cwd?: string; + is_subagent?: boolean; } // Moves an element to document.body so modals escape the sidebar's @@ -90,6 +91,7 @@ const modelCache = new Map<string, string[]>(); let modeOverride = $state<"manual" | "agent" | null>(null); let mode = $derived(modeOverride ?? (activeAgentSlug ? "agent" : "manual")); let agents = $state<AgentInfo[]>([]); + let visibleAgents = $derived(agents.filter((a) => !a.is_subagent)); let loadingAgents = $state(false); $effect(() => { @@ -206,8 +208,8 @@ const modelCache = new Map<string, string[]>(); modeOverride = "agent"; await fetchAgents(); // Re-apply the active agent's settings (including cwd) - const current = agents.find(a => a.slug === activeAgentSlug); - const agentToApply = current ?? agents[0] ?? null; + const current = visibleAgents.find(a => a.slug === activeAgentSlug); + const agentToApply = current ?? visibleAgents[0] ?? null; if (agentToApply) { onAgentChange(agentToApply); // Force-update the input since the prop may not change (already set) @@ -258,11 +260,11 @@ const modelCache = new Map<string, string[]>(); <span class="loading loading-spinner loading-xs"></span> Loading agents... </div> - {:else if agents.length === 0} + {:else if visibleAgents.length === 0} <p class="text-base-content/50 text-sm py-2">No agents configured.</p> {:else} <div class="flex flex-col gap-1.5"> - {#each agents as agent (agent.slug + ":" + agent.scope)} + {#each visibleAgents as agent (agent.slug + ":" + agent.scope)} <button class="w-full text-left rounded-lg px-3 py-2 transition-colors {activeAgentSlug === agent.slug ? 'bg-primary text-primary-content' : 'bg-base-300 hover:bg-base-200'}" onclick={() => { diff --git a/packages/frontend/src/lib/components/ModelStatus.svelte b/packages/frontend/src/lib/components/ModelStatus.svelte index d6ff0f5..57c5efd 100644 --- a/packages/frontend/src/lib/components/ModelStatus.svelte +++ b/packages/frontend/src/lib/components/ModelStatus.svelte @@ -48,8 +48,6 @@ let keyModalError = $state<string | null>(null); let keyModalSaving = $state(false); let removingKey = $state<string | null>(null); - - async function loadCredentialStatus(): Promise<void> { try { const res = await fetch(`${apiBase}/models/credentials-status`); @@ -173,7 +171,7 @@ function timeAgo(ts: number | null): string { function truncate(str: string | null, max: number): string { if (!str) return ""; - return str.length > max ? str.slice(0, max) + "..." : str; + return str.length > max ? `${str.slice(0, max)}...` : str; } </script> diff --git a/packages/frontend/src/lib/components/SettingsPanel.svelte b/packages/frontend/src/lib/components/SettingsPanel.svelte index c19fe45..eadfef8 100644 --- a/packages/frontend/src/lib/components/SettingsPanel.svelte +++ b/packages/frontend/src/lib/components/SettingsPanel.svelte @@ -25,14 +25,18 @@ function saveBackendUrl(): void { config.setApiBase(trimmed); backendUrl = trimmed; backendUrlSaved = true; - setTimeout(() => { backendUrlSaved = false; }, 2000); + setTimeout(() => { + backendUrlSaved = false; + }, 2000); } function resetBackendUrl(): void { config.setApiBase(config.defaultApiBase); backendUrl = config.defaultApiBase; backendUrlSaved = true; - setTimeout(() => { backendUrlSaved = false; }, 2000); + setTimeout(() => { + backendUrlSaved = false; + }, 2000); } async function loadSettings(): Promise<void> { diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte index e89e351..371561a 100644 --- a/packages/frontend/src/lib/components/SidebarPanel.svelte +++ b/packages/frontend/src/lib/components/SidebarPanel.svelte @@ -10,6 +10,15 @@ import SkillsBrowser from "./SkillsBrowser.svelte"; import TaskListPanel from "./TaskListPanel.svelte"; import ToolPermissions from "./ToolPermissions.svelte"; +interface AgentInfo { + slug: string; + scope: string; + skills: string[]; + tools: string[]; + models: Array<{ key_id: string; model_id: string }>; + cwd?: string; +} + const { keys = [], tasks = [], @@ -23,7 +32,7 @@ const { onKeyChange, onModelChange, onReasoningChange, - onAgentChange = (_agent: any) => {}, + onAgentChange = (_agent: AgentInfo | null) => {}, onWorkingDirectoryChange = (_dir: string | null) => {}, onAddKey = () => {}, }: { @@ -39,7 +48,7 @@ const { onKeyChange: (keyId: string) => void; onModelChange: (keyId: string, modelId: string) => void; onReasoningChange: (effort: string) => void; - onAgentChange?: (agent: any) => void; + onAgentChange?: (agent: AgentInfo | null) => void; onWorkingDirectoryChange?: (dir: string | null) => void; onAddKey?: () => void; } = $props(); @@ -72,7 +81,7 @@ function addPanel() { function panelClass(selected: string): string { const base = "bg-base-200 rounded-lg p-3 flex flex-col min-h-0"; const fill = selected === "Key Usage" || selected === "Claude Reset" || selected === "Tasks"; - return fill ? base + " flex-1" : base; + return fill ? `${base} flex-1` : base; } function contentClass(selected: string): string { diff --git a/packages/frontend/src/lib/components/SkillsBrowser.svelte b/packages/frontend/src/lib/components/SkillsBrowser.svelte index add43e4..e697732 100644 --- a/packages/frontend/src/lib/components/SkillsBrowser.svelte +++ b/packages/frontend/src/lib/components/SkillsBrowser.svelte @@ -77,7 +77,7 @@ function dirKey(group: DirGroup): string { function isChecked(skill: Skill): boolean { const key = skillKey(skill); if (externalMode) { - return checkedSkills!.has(key); + return checkedSkills?.has(key) ?? false; } return appSettings.skillChecks[key] === true; } @@ -90,7 +90,7 @@ function isInjected(skill: Skill): boolean { function toggleCheck(skill: Skill): void { const key = skillKey(skill); if (externalMode) { - onSkillToggle!(key, !checkedSkills!.has(key)); + onSkillToggle?.(key, !checkedSkills?.has(key)); return; } appSettings.skillChecks = { ...appSettings.skillChecks, [key]: !isChecked(skill) }; @@ -156,7 +156,7 @@ $effect(() => { const checkedCount = $derived( externalMode - ? checkedSkills!.size + ? (checkedSkills?.size ?? 0) : Object.values(appSettings.skillChecks).filter((v) => v).length, ); diff --git a/packages/frontend/src/lib/components/ToolPermissions.svelte b/packages/frontend/src/lib/components/ToolPermissions.svelte index 6533c7b..7fdaad3 100644 --- a/packages/frontend/src/lib/components/ToolPermissions.svelte +++ b/packages/frontend/src/lib/components/ToolPermissions.svelte @@ -22,6 +22,16 @@ const toolPermissions: ToolPermission[] = [ label: "Summon agents", description: "Allow the AI to spawn child agents to work on tasks", }, + { + id: "web_search", + label: "Web search", + description: "Allow the AI to search the web via Firecrawl", + }, + { + id: "youtube_transcribe", + label: "YouTube transcripts", + description: "Allow the AI to fetch YouTube video transcripts", + }, ]; const { @@ -42,7 +52,7 @@ const { const externalMode = $derived(checkedTools !== null && onToolToggle !== null); function isChecked(id: string): boolean { - if (externalMode) return checkedTools!.has(id); + if (externalMode) return checkedTools?.has(id) ?? false; return appSettings.toolPerms[id] === true; } @@ -67,7 +77,7 @@ async function loadPermissions(): Promise<void> { function togglePermission(id: string): void { if (externalMode) { - onToolToggle!(id, !checkedTools!.has(id)); + onToolToggle?.(id, !checkedTools?.has(id)); return; } appSettings.toolPerms = { ...appSettings.toolPerms, [id]: !appSettings.toolPerms[id] }; |
