summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components/ModelStatus.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-22 15:24:13 +0900
committerAdam Malczewski <[email protected]>2026-05-22 15:24:13 +0900
commit9ecaabd87c0e51b8a7408dabb0133a9344586859 (patch)
tree4b5809ab23948a5e3f558f3aa34c52d5038f4e19 /packages/frontend/src/lib/components/ModelStatus.svelte
parent8f1dd855f0c4c877bff8d3a4ba193432b268b1c2 (diff)
downloaddispatch-9ecaabd87c0e51b8a7408dabb0133a9344586859.tar.gz
dispatch-9ecaabd87c0e51b8a7408dabb0133a9344586859.zip
feat: agent builder, CWD support, auto-save, UI polish, unavailable tool handling
- Agent Builder: full CRUD with card grid, drag-and-drop model reorder, edit/delete - Auto-save on edit with 600ms debounce, AbortController for concurrency, fieldset disabled until name entered - Agent definitions stored as TOML with cwd field, loaded from global/project dirs - Working directory: per-tab CWD override in Chat Settings, agent default CWD, auto-create on first message - CWD validation: check-dir endpoint with ~ expansion, real-time validity indicator - Subagent CWD validated against parent's effective CWD using path.relative - Unavailable tool calls: caught gracefully, shown as tool call with error badge, model retries - UI: tab bar border radius, sidebar border removed, chat input ghost style, scroll-to-bottom rectangle - Skills dir collapse uses CSS rotation, Model Choice renamed to Chat Settings, System Prompt view removed - Reusable SkillsBrowser/ToolPermissions with external mode for Agent Builder - ModelSelector: Agent/Manual toggle, agent list, Agent Settings link - Page router, skills recursive scanning, bin/up gopass removed, docker volume mounts
Diffstat (limited to 'packages/frontend/src/lib/components/ModelStatus.svelte')
-rw-r--r--packages/frontend/src/lib/components/ModelStatus.svelte254
1 files changed, 130 insertions, 124 deletions
diff --git a/packages/frontend/src/lib/components/ModelStatus.svelte b/packages/frontend/src/lib/components/ModelStatus.svelte
index a627de8..1270fcc 100644
--- a/packages/frontend/src/lib/components/ModelStatus.svelte
+++ b/packages/frontend/src/lib/components/ModelStatus.svelte
@@ -1,148 +1,154 @@
<script lang="ts">
- interface KeyInfo {
- id: string;
- provider: string;
- status: "active" | "exhausted";
- lastError: string | null;
- exhaustedAt: number | null;
- }
+interface KeyInfo {
+ id: string;
+ provider: string;
+ status: "active" | "exhausted";
+ lastError: string | null;
+ exhaustedAt: number | null;
+}
- interface CredentialStatus {
- keyId: string;
- provider: string;
- subscriptionType: string | null;
- importedAt: number;
- updatedAt: number;
- expired: boolean;
- }
+interface CredentialStatus {
+ keyId: string;
+ provider: string;
+ subscriptionType: string | null;
+ importedAt: number;
+ updatedAt: number;
+ expired: boolean;
+}
- const {
- keys = [],
- currentModel,
- apiBase = "",
- }: {
- keys?: KeyInfo[];
- currentModel?: string;
- apiBase?: string;
- } = $props();
+const {
+ keys = [],
+ currentModel,
+ apiBase = "",
+}: {
+ keys?: KeyInfo[];
+ currentModel?: string;
+ apiBase?: string;
+} = $props();
- const activeKeys = $derived(keys.filter((k) => k.status === "active").length);
- const totalKeys = $derived(keys.length);
- const allActive = $derived(totalKeys > 0 && activeKeys === totalKeys);
- const allExhausted = $derived(totalKeys > 0 && activeKeys === 0);
- const someExhausted = $derived(totalKeys > 0 && activeKeys < totalKeys && activeKeys > 0);
+const activeKeys = $derived(keys.filter((k) => k.status === "active").length);
+const totalKeys = $derived(keys.length);
+const allActive = $derived(totalKeys > 0 && activeKeys === totalKeys);
+const allExhausted = $derived(totalKeys > 0 && activeKeys === 0);
+const someExhausted = $derived(totalKeys > 0 && activeKeys < totalKeys && activeKeys > 0);
- let credentialStatus = $state<Record<string, CredentialStatus>>({});
- let importingKey = $state<string | null>(null);
- let importError = $state<string | null>(null);
- let importSuccess = $state<string | null>(null);
+let credentialStatus = $state<Record<string, CredentialStatus>>({});
+let importingKey = $state<string | null>(null);
+let importError = $state<string | null>(null);
+let importSuccess = $state<string | null>(null);
- let apiKeyStatus = $state<Record<string, { keyId: string; provider: string; importedAt: number; updatedAt: number }>>({});
- let showKeyModal = $state<string | null>(null); // keyId or null
- let keyModalValue = $state("");
- let keyModalError = $state<string | null>(null);
- let keyModalSaving = $state(false);
+let apiKeyStatus = $state<
+ Record<string, { keyId: string; provider: string; importedAt: number; updatedAt: number }>
+>({});
+let showKeyModal = $state<string | null>(null); // keyId or null
+let keyModalValue = $state("");
+let keyModalError = $state<string | null>(null);
+let keyModalSaving = $state(false);
- async function loadCredentialStatus(): Promise<void> {
- try {
- const res = await fetch(`${apiBase}/models/credentials-status`);
- if (!res.ok) return;
- const data = await res.json() as { credentials: CredentialStatus[] };
- const map: Record<string, CredentialStatus> = {};
- for (const cred of data.credentials) {
- map[cred.keyId] = cred;
- }
- credentialStatus = map;
- } catch {
- // ignore
+async function loadCredentialStatus(): Promise<void> {
+ try {
+ const res = await fetch(`${apiBase}/models/credentials-status`);
+ if (!res.ok) return;
+ const data = (await res.json()) as { credentials: CredentialStatus[] };
+ const map: Record<string, CredentialStatus> = {};
+ for (const cred of data.credentials) {
+ map[cred.keyId] = cred;
}
+ credentialStatus = map;
+ } catch {
+ // ignore
}
+}
- async function importCredentials(keyId: string): Promise<void> {
- importingKey = keyId;
- importError = null;
- importSuccess = null;
- try {
- const res = await fetch(`${apiBase}/models/import-credentials`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ keyId }),
- });
- const data = await res.json() as { success?: boolean; error?: string };
- if (!res.ok || !data.success) {
- importError = data.error ?? "Import failed";
- } else {
- importSuccess = keyId;
- await loadCredentialStatus();
- setTimeout(() => { importSuccess = null; }, 3000);
- }
- } catch (e) {
- importError = e instanceof Error ? e.message : "Network error";
- } finally {
- importingKey = null;
+async function importCredentials(keyId: string): Promise<void> {
+ importingKey = keyId;
+ importError = null;
+ importSuccess = null;
+ try {
+ const res = await fetch(`${apiBase}/models/import-credentials`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ keyId }),
+ });
+ const data = (await res.json()) as { success?: boolean; error?: string };
+ if (!res.ok || !data.success) {
+ importError = data.error ?? "Import failed";
+ } else {
+ importSuccess = keyId;
+ await loadCredentialStatus();
+ setTimeout(() => {
+ importSuccess = null;
+ }, 3000);
}
+ } catch (e) {
+ importError = e instanceof Error ? e.message : "Network error";
+ } finally {
+ importingKey = null;
}
+}
- async function loadApiKeyStatus(): Promise<void> {
- try {
- const res = await fetch(`${apiBase}/models/api-keys-status`);
- if (!res.ok) return;
- const data = await res.json() as { keys: Array<{ keyId: string; provider: string; importedAt: number; updatedAt: number }> };
- const map: Record<string, typeof data.keys[0]> = {};
- for (const k of data.keys) {
- map[k.keyId] = k;
- }
- apiKeyStatus = map;
- } catch {
- // ignore
+async function loadApiKeyStatus(): Promise<void> {
+ try {
+ const res = await fetch(`${apiBase}/models/api-keys-status`);
+ if (!res.ok) return;
+ const data = (await res.json()) as {
+ keys: Array<{ keyId: string; provider: string; importedAt: number; updatedAt: number }>;
+ };
+ const map: Record<string, (typeof data.keys)[0]> = {};
+ for (const k of data.keys) {
+ map[k.keyId] = k;
}
+ apiKeyStatus = map;
+ } catch {
+ // ignore
}
+}
- async function saveApiKey(): Promise<void> {
- if (!showKeyModal || !keyModalValue.trim()) return;
- keyModalSaving = true;
- keyModalError = null;
- try {
- const res = await fetch(`${apiBase}/models/set-api-key`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ keyId: showKeyModal, apiKey: keyModalValue.trim() }),
- });
- const data = await res.json() as { success?: boolean; error?: string };
- if (!res.ok || !data.success) {
- keyModalError = data.error ?? "Failed to save";
- } else {
- showKeyModal = null;
- keyModalValue = "";
- await loadApiKeyStatus();
- }
- } catch (e) {
- keyModalError = e instanceof Error ? e.message : "Network error";
- } finally {
- keyModalSaving = false;
+async function saveApiKey(): Promise<void> {
+ if (!showKeyModal || !keyModalValue.trim()) return;
+ keyModalSaving = true;
+ keyModalError = null;
+ try {
+ const res = await fetch(`${apiBase}/models/set-api-key`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ keyId: showKeyModal, apiKey: keyModalValue.trim() }),
+ });
+ const data = (await res.json()) as { success?: boolean; error?: string };
+ if (!res.ok || !data.success) {
+ keyModalError = data.error ?? "Failed to save";
+ } else {
+ showKeyModal = null;
+ keyModalValue = "";
+ await loadApiKeyStatus();
}
+ } catch (e) {
+ keyModalError = e instanceof Error ? e.message : "Network error";
+ } finally {
+ keyModalSaving = false;
}
+}
- $effect(() => {
- void loadCredentialStatus();
- void loadApiKeyStatus();
- });
+$effect(() => {
+ void loadCredentialStatus();
+ void loadApiKeyStatus();
+});
- function timeAgo(ts: number | null): string {
- if (ts === null) return "";
- const diffMs = Date.now() - ts;
- const diffSec = Math.floor(diffMs / 1000);
- if (diffSec < 60) return `${diffSec}s ago`;
- const diffMin = Math.floor(diffSec / 60);
- if (diffMin < 60) return `${diffMin}m ago`;
- const diffHr = Math.floor(diffMin / 60);
- return `${diffHr}h ago`;
- }
+function timeAgo(ts: number | null): string {
+ if (ts === null) return "";
+ const diffMs = Date.now() - ts;
+ const diffSec = Math.floor(diffMs / 1000);
+ if (diffSec < 60) return `${diffSec}s ago`;
+ const diffMin = Math.floor(diffSec / 60);
+ if (diffMin < 60) return `${diffMin}m ago`;
+ const diffHr = Math.floor(diffMin / 60);
+ return `${diffHr}h ago`;
+}
- function truncate(str: string | null, max: number): string {
- if (!str) return "";
- return str.length > max ? str.slice(0, max) + "..." : str;
- }
+function truncate(str: string | null, max: number): string {
+ if (!str) return "";
+ return str.length > max ? str.slice(0, max) + "..." : str;
+}
</script>
<div class="flex flex-col gap-3">