From adc8bd185b54935e7a31aae04da3175b7989927a Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Wed, 20 May 2026 15:04:26 +0900 Subject: feat: phase 3 — config, skills, model groups, task list, and sidebar UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Config system: TOML-based dispatch.toml with hot-reload via chokidar - Model/key resolution: tag-based model selection, key fallback chains - Skills system: directory loader with TOML frontmatter, agent mappings - Task list tool: add/update/list/get operations with WebSocket events - API routes: GET /config, /skills, /skills/:name, /models, /models/resolve - Frontend: sidebar with model status, task list, config viewer, skills browser, permission log - Sliding sidebar animation using CSS transitions (not Svelte transitions) --- .../frontend/src/lib/components/ConfigPanel.svelte | 244 +++++++++++++++++++++ packages/frontend/src/lib/components/Header.svelte | 10 + .../src/lib/components/HotReloadIndicator.svelte | 35 +++ .../frontend/src/lib/components/ModelStatus.svelte | 135 ++++++++++++ .../src/lib/components/SkillsBrowser.svelte | 234 ++++++++++++++++++++ .../src/lib/components/TaskListPanel.svelte | 72 ++++++ 6 files changed, 730 insertions(+) create mode 100644 packages/frontend/src/lib/components/ConfigPanel.svelte create mode 100644 packages/frontend/src/lib/components/HotReloadIndicator.svelte create mode 100644 packages/frontend/src/lib/components/ModelStatus.svelte create mode 100644 packages/frontend/src/lib/components/SkillsBrowser.svelte create mode 100644 packages/frontend/src/lib/components/TaskListPanel.svelte (limited to 'packages/frontend/src/lib/components') diff --git a/packages/frontend/src/lib/components/ConfigPanel.svelte b/packages/frontend/src/lib/components/ConfigPanel.svelte new file mode 100644 index 0000000..25cbe98 --- /dev/null +++ b/packages/frontend/src/lib/components/ConfigPanel.svelte @@ -0,0 +1,244 @@ + + +
+ + Configuration + {#if modelCount > 0 || keyCount > 0} + {modelCount} models + {keyCount} keys + {/if} + {#if loading} + + {/if} + + +
+ {#if error} +
+ Failed to load config: {error} +
+ {/if} + +
+ +
+ + + {#if configData?.agents && Object.keys(configData.agents).length > 0} +
+
Agent Templates
+ {#each Object.entries(configData.agents) as [name, template]} +
+
+ {name} + {#if template.model_tag} + {template.model_tag} + {/if} +
+ {#if template.description} +

{template.description}

+ {/if} + {#if template.tools && template.tools.length > 0} +
+ {#each template.tools as tool} + {tool} + {/each} +
+ {/if} +
+ {/each} +
+ {/if} + + + {#if modelsData?.models && modelsData.models.length > 0} +
+
Models
+ {#each modelsData.models as model} +
+ {model.id} + {#if model.provider} + {model.provider} + {/if} + {#if model.tags && model.tags.length > 0} + {#each model.tags as tag} + {tag} + {/each} + {/if} +
+ {/each} +
+ {/if} + + + {#if modelsData?.keys && modelsData.keys.length > 0} +
+
API Keys
+ {#each modelsData.keys as key} +
+
+ {key.id} + {#if key.provider} + {key.provider} + {/if} + {#if key.status === "exhausted"} + exhausted + {:else} + active + {/if} +
+ {#if key.status === "exhausted" && key.lastError} +

{key.lastError}

+ {/if} + {#if key.exhaustedAt} +

Since {formatDate(key.exhaustedAt)}

+ {/if} +
+ {/each} +
+ {/if} + + + {#if configData?.fallback && configData.fallback.length > 0} +
+
Fallback Order
+
    + {#each configData.fallback as keyId, i} +
  1. + {i + 1} + {keyId} +
  2. + {/each} +
+
+ {/if} + + + {#if configData?.permissions && Object.keys(configData.permissions).length > 0} +
+
Permissions
+ {#each permissionEntries(configData.permissions) as entry} +
+ {entry.name} + {#if isSimpleRule(entry.value)} + {entry.value.action} + {:else if isPatternRule(entry.value)} + {#each Object.entries(entry.value) as [pattern, rule]} +
+ {pattern} + {#if typeof rule === "object" && rule !== null && "action" in rule} + {(rule as { action: string }).action} + {/if} +
+ {/each} + {:else} + {JSON.stringify(entry.value)} + {/if} +
+ {/each} +
+ {/if} + + {#if !loading && !error && !configData && !modelsData} +

No configuration loaded.

+ {/if} +
+
diff --git a/packages/frontend/src/lib/components/Header.svelte b/packages/frontend/src/lib/components/Header.svelte index cf466fe..5d14593 100644 --- a/packages/frontend/src/lib/components/Header.svelte +++ b/packages/frontend/src/lib/components/Header.svelte @@ -2,6 +2,8 @@ import { chatStore } from "../chat.svelte.js"; import ThemeSwitcher from "./ThemeSwitcher.svelte"; +const { onToggleSidebar }: { onToggleSidebar: () => void } = $props(); + let showThemeSwitcher = $state(false); let copyLabel = $state("Copy"); @@ -46,6 +48,14 @@ async function handleCopy() { > Theme + diff --git a/packages/frontend/src/lib/components/HotReloadIndicator.svelte b/packages/frontend/src/lib/components/HotReloadIndicator.svelte new file mode 100644 index 0000000..3e34d3b --- /dev/null +++ b/packages/frontend/src/lib/components/HotReloadIndicator.svelte @@ -0,0 +1,35 @@ + + +{#if visible} +
+ + Config reloaded +
+{/if} diff --git a/packages/frontend/src/lib/components/ModelStatus.svelte b/packages/frontend/src/lib/components/ModelStatus.svelte new file mode 100644 index 0000000..34a0563 --- /dev/null +++ b/packages/frontend/src/lib/components/ModelStatus.svelte @@ -0,0 +1,135 @@ + + +
+ {#if models.length === 0 && keys.length === 0} +

+ No models configured. Using environment defaults. +

+ {:else} + + {#if allActive} +
+ + All keys available +
+ {:else if allExhausted} +
+ + All keys exhausted — waiting for refresh +
+ {:else if someExhausted} +
+ + + Fallback active ({activeKeys}/{totalKeys} keys available) + +
+ {/if} + + + {#if currentModel} +
+

Current Model

+

{currentModel}

+
+ {/if} + + + {#if uniqueTags.length > 0} +
+

Tags

+
+ {#each uniqueTags as tag (tag)} + {tag} + {/each} +
+
+ {/if} + + + {#if keys.length > 0} +
+

API Keys

+
    + {#each keys as key (key.id)} +
  • +
    + + {key.status} + + {key.id} + {key.provider} +
    + {#if key.status === "exhausted"} +
    + {#if key.lastError} +

    + {truncate(key.lastError, 80)} +

    + {/if} + {#if key.exhaustedAt !== null} +

    {timeAgo(key.exhaustedAt)}

    + {/if} +
    + {/if} +
  • + {/each} +
+
+ {/if} + {/if} +
diff --git a/packages/frontend/src/lib/components/SkillsBrowser.svelte b/packages/frontend/src/lib/components/SkillsBrowser.svelte new file mode 100644 index 0000000..be1ad29 --- /dev/null +++ b/packages/frontend/src/lib/components/SkillsBrowser.svelte @@ -0,0 +1,234 @@ + + +
+ + Skills + {#if !loading} + {skills.length} + {/if} + + +
+ {#if loading} +
+ + Loading skills... +
+ {:else if error} +
{error}
+ {:else if skills.length === 0} +

+ No skills found. Create a .skills/default/ directory to get started. +

+ {:else} + {#snippet skillItem(skill: Skill)} + {@const key = `${skill.scope}:${skill.name}`} + {@const isExpanded = key in expandedSkills} + {@const detail = expandedSkills[key]} + {@const isLoading = loadingSkill[key]} +
+
+ + {#if isLoading} + + {/if} + {#each skill.tags as tag} + {tag} + {/each} +
+ {#if skill.description} +

{skill.description}

+ {/if} + {#if isExpanded} +
+ {#if detail} +
{detail.content}
+ {:else} +

Failed to load skill content.

+ {/if} + +
+ {/if} +
+ {/snippet} + + {#snippet scopeSection(label: string, scopeSkills: Skill[], scope: string)} + {#if scopeSkills.length > 0} + {@const defaultSkills = skillsByDirectory(scopeSkills, "default")} + {@const agentSkills = skillsByDirectory(scopeSkills, "agents")} + {@const projectDirSkills = skillsByDirectory(scopeSkills, "project")} + {@const scopeMappings = getMappingsForScope(scope)} +
+
+ {label} + {scope} +
+ + {#if defaultSkills.length > 0} +
+
default/
+
+ {#each defaultSkills as skill} + {@render skillItem(skill)} + {/each} +
+
+ {/if} + + {#if agentSkills.length > 0 || scopeMappings.length > 0} +
+
agents/
+
+ {#if scopeMappings.length > 0} + {#each scopeMappings as mapping} +
+
+ {mapping.agentType} + {#if mapping.isOrchestrator} + (orchestrator) + {/if} + + {#each mapping.skills as skillName} + {@const mappedSkill = agentSkills.find((s) => s.name === skillName)} + {#if mappedSkill} + {@render skillItem(mappedSkill)} + {:else} + {skillName} + {/if} + {/each} +
+
+ {/each} + {:else} + {#each agentSkills as skill} + {@render skillItem(skill)} + {/each} + {/if} +
+
+ {/if} + + {#if projectDirSkills.length > 0} +
+
project/
+
+ {#each projectDirSkills as skill} + {@render skillItem(skill)} + {/each} +
+
+ {/if} +
+ {/if} + {/snippet} + + {@render scopeSection("Global", globalSkills, "global")} + {@render scopeSection("Project", projectSkills, "project")} + {/if} +
+
diff --git a/packages/frontend/src/lib/components/TaskListPanel.svelte b/packages/frontend/src/lib/components/TaskListPanel.svelte new file mode 100644 index 0000000..5f2ffe2 --- /dev/null +++ b/packages/frontend/src/lib/components/TaskListPanel.svelte @@ -0,0 +1,72 @@ + + +
+ {#if tasks.length === 0} +

No tasks yet.

+ {:else} +

+ {tasks.length} task{tasks.length !== 1 ? "s" : ""} + ({doneCount} done, {inProgressCount} in progress) +

+ + {/if} +
-- cgit v1.2.3