feat(electrobun): final 5% — full integration, real data, polish

1. Claude CLI: additionalDirectories + worktreeName passthrough
2. Agent-store: reads settings (default_cwd, provider model, permission)
3. Project hydration: SQLite replaces hardcoded PROJECTS, add/remove UI
4. Group hydration: SQLite groups, add/delete in sidebar
5. Terminal auto-spawn: reads default_cwd from settings
6. Context tab: real tokens from agent-store, file refs, turn count
7. Memory tab: Memora DB integration (read-only, graceful if missing)
8. Docs tab: markdown viewer (files.list + files.read + inline renderer)
9. SSH tab: CRUD connections, spawn PTY with ssh command
10. Error handling: global unhandledrejection → toast notifications
11. Notifications: agent done/error/stall → toasts, 15min stall timer
12. Command palette: all 18 commands (was 10)

+1,198 lines, 13 files. Electrobun now 100% feature-complete vs Tauri v3.
This commit is contained in:
Hibryda 2026-03-22 02:02:54 +01:00
parent 4826b9dffa
commit 8e756d3523
13 changed files with 1199 additions and 239 deletions

View file

@ -5,6 +5,8 @@
import MemoryTab from './MemoryTab.svelte';
import CommsTab from './CommsTab.svelte';
import TaskBoardTab from './TaskBoardTab.svelte';
import DocsTab from './DocsTab.svelte';
import SshTab from './SshTab.svelte';
import {
startAgent, stopAgent, sendPrompt, getSession, hasSession,
loadLastSession,
@ -61,6 +63,32 @@
let agentCost = $derived(session?.costUsd ?? 0);
let agentTokens = $derived((session?.inputTokens ?? 0) + (session?.outputTokens ?? 0));
let agentModel = $derived(session?.model ?? model);
let agentInputTokens = $derived(session?.inputTokens ?? 0);
let agentOutputTokens = $derived(session?.outputTokens ?? 0);
// Context limit per model (approximate)
const MODEL_LIMITS: Record<string, number> = {
'claude-opus-4-5': 200000,
'claude-sonnet-4-5': 200000,
'claude-haiku-4-5': 200000,
'gpt-5.4': 128000,
'qwen3:8b': 32000,
};
let contextLimit = $derived(MODEL_LIMITS[agentModel] ?? 200000);
let computedContextPct = $derived(
agentInputTokens > 0 ? Math.min(100, Math.round((agentInputTokens / contextLimit) * 100)) : (contextPct ?? 0)
);
// File references from tool_call messages
let fileRefs = $derived(
agentMessages
.filter((m) => m.role === 'tool-call' && m.toolPath)
.map((m) => m.toolPath!)
.filter((p, i, arr) => arr.indexOf(p) === i)
.slice(0, 20)
);
let turnCount = $derived(agentMessages.filter((m) => m.role === 'user' || m.role === 'assistant').length);
// ── Demo messages (fallback when no real session) ────────────────
const demoMessages: AgentMessage[] = [];
@ -258,7 +286,7 @@
onSend={handleSend}
onStop={handleStop}
/>
<TerminalTabs projectId={id} {accent} />
<TerminalTabs projectId={id} {accent} {cwd} />
</div>
<!-- Docs tab -->
@ -270,7 +298,7 @@
role="tabpanel"
aria-label="Docs"
>
<div class="placeholder-pane">No markdown files open</div>
<DocsTab {cwd} />
</div>
{/if}
@ -286,30 +314,48 @@
<div class="context-pane">
<div class="ctx-stats-row">
<div class="ctx-stat">
<span class="ctx-stat-label">Tokens used</span>
<span class="ctx-stat-value">{agentTokens.toLocaleString()}</span>
<span class="ctx-stat-label">Input tokens</span>
<span class="ctx-stat-value">{agentInputTokens.toLocaleString()}</span>
</div>
<div class="ctx-stat">
<span class="ctx-stat-label">Context %</span>
<span class="ctx-stat-value">{contextPct}%</span>
<span class="ctx-stat-label">Output tokens</span>
<span class="ctx-stat-value">{agentOutputTokens.toLocaleString()}</span>
</div>
<div class="ctx-stat">
<span class="ctx-stat-label">Context</span>
<span class="ctx-stat-value">{computedContextPct}%</span>
</div>
<div class="ctx-stat">
<span class="ctx-stat-label">Model</span>
<span class="ctx-stat-value">{agentModel}</span>
</div>
<div class="ctx-stat">
<span class="ctx-stat-label">Turns</span>
<span class="ctx-stat-value">{turnCount}</span>
</div>
</div>
<div class="ctx-meter-wrap" title="{contextPct}% context used">
<div class="ctx-meter-bar" style:width="{contextPct}%"
class:meter-warn={contextPct >= 75}
class:meter-danger={contextPct >= 90}
<div class="ctx-meter-wrap" title="{computedContextPct}% of {contextLimit.toLocaleString()} tokens">
<div class="ctx-meter-bar" style:width="{computedContextPct}%"
class:meter-warn={computedContextPct >= 75}
class:meter-danger={computedContextPct >= 90}
></div>
</div>
{#if fileRefs.length > 0}
<div class="ctx-turn-list">
<div class="ctx-section-label">Turn breakdown</div>
{#each displayMessages.slice(0, 5) as msg}
<div class="ctx-section-label">File references ({fileRefs.length})</div>
{#each fileRefs as ref}
<div class="ctx-turn-row">
<span class="ctx-turn-preview" title={ref}>{ref}</span>
</div>
{/each}
</div>
{/if}
<div class="ctx-turn-list">
<div class="ctx-section-label">Recent turns</div>
{#each displayMessages.slice(-10) as msg}
<div class="ctx-turn-row">
<span class="ctx-turn-role ctx-role-{msg.role}">{msg.role}</span>
<span class="ctx-turn-preview">{msg.content.slice(0, 60)}{msg.content.length > 60 ? '…' : ''}</span>
<span class="ctx-turn-preview">{msg.content.slice(0, 60)}{msg.content.length > 60 ? '...' : ''}</span>
</div>
{/each}
</div>
@ -339,7 +385,7 @@
role="tabpanel"
aria-label="SSH"
>
<div class="placeholder-pane">No SSH connections configured</div>
<SshTab projectId={id} />
</div>
{/if}