summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-23 05:06:12 +0900
committerAdam Malczewski <[email protected]>2026-05-23 05:06:12 +0900
commit9287cccb29d135ea19f2612c26f3090c94820d8c (patch)
tree2d68e8cacf6d71786f305d5f4a512a68f19137c5 /packages/frontend/src/lib/components
parentef427d3eae77fca716c203dd8bd84939710c518a (diff)
downloaddispatch-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')
-rw-r--r--packages/frontend/src/lib/components/AgentBuilder.svelte23
-rw-r--r--packages/frontend/src/lib/components/MarkdownRenderer.svelte2
-rw-r--r--packages/frontend/src/lib/components/ModelSelector.svelte10
-rw-r--r--packages/frontend/src/lib/components/ModelStatus.svelte4
-rw-r--r--packages/frontend/src/lib/components/SettingsPanel.svelte8
-rw-r--r--packages/frontend/src/lib/components/SidebarPanel.svelte15
-rw-r--r--packages/frontend/src/lib/components/SkillsBrowser.svelte6
-rw-r--r--packages/frontend/src/lib/components/ToolPermissions.svelte14
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] };