feat(electrobun): agent execution layer — sidecar manager + message adapters + store
- SidecarManager: spawns claude/codex/ollama runners via Bun.spawn(), NDJSON stdio protocol, Claude CLI auto-detection, env stripping, AbortController stop, Deno/Node runtime detection - MessageAdapter: parses Claude stream-json, Codex ThreadEvent, Ollama chunks into common AgentMessage format - agent-store.svelte.ts: per-project reactive session state, RPC event listeners for agent.message/status/cost - AgentPane: wired to real sessions (start/stop/prompt), stop button, thinking/system message rendering - ProjectCard: status dot from real agent status, cost/tokens from store - 5 new RPC types (agent.start/stop/prompt/list + events)
This commit is contained in:
parent
95f1f8208f
commit
ef0183de7f
8 changed files with 1566 additions and 61 deletions
|
|
@ -3,26 +3,18 @@
|
|||
import TerminalTabs from './TerminalTabs.svelte';
|
||||
import FileBrowser from './FileBrowser.svelte';
|
||||
import MemoryTab from './MemoryTab.svelte';
|
||||
import {
|
||||
startAgent, stopAgent, sendPrompt, getSession, hasSession,
|
||||
type AgentStatus, type AgentMessage,
|
||||
} from './agent-store.svelte.ts';
|
||||
|
||||
type AgentStatus = 'running' | 'idle' | 'stalled';
|
||||
type MsgRole = 'user' | 'assistant' | 'tool-call' | 'tool-result';
|
||||
type ProjectTab = 'model' | 'docs' | 'context' | 'files' | 'ssh' | 'memory';
|
||||
|
||||
interface AgentMessage {
|
||||
id: number;
|
||||
role: MsgRole;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
name: string;
|
||||
cwd: string;
|
||||
accent: string;
|
||||
status: AgentStatus;
|
||||
costUsd: number;
|
||||
tokens: number;
|
||||
messages: AgentMessage[];
|
||||
provider?: string;
|
||||
profile?: string;
|
||||
model?: string;
|
||||
|
|
@ -44,10 +36,6 @@
|
|||
name,
|
||||
cwd,
|
||||
accent,
|
||||
status,
|
||||
costUsd,
|
||||
tokens,
|
||||
messages: initialMessages,
|
||||
provider = 'claude',
|
||||
profile,
|
||||
model = 'claude-opus-4-5',
|
||||
|
|
@ -60,6 +48,18 @@
|
|||
onClone,
|
||||
}: Props = $props();
|
||||
|
||||
// ── Agent session (reactive from store) ──────────────────────────
|
||||
let session = $derived(getSession(id));
|
||||
let agentStatus: AgentStatus = $derived(session?.status ?? 'idle');
|
||||
let agentMessages: AgentMessage[] = $derived(session?.messages ?? []);
|
||||
let agentCost = $derived(session?.costUsd ?? 0);
|
||||
let agentTokens = $derived((session?.inputTokens ?? 0) + (session?.outputTokens ?? 0));
|
||||
let agentModel = $derived(session?.model ?? model);
|
||||
|
||||
// ── Demo messages (fallback when no real session) ────────────────
|
||||
const demoMessages: AgentMessage[] = [];
|
||||
let displayMessages = $derived(agentMessages.length > 0 ? agentMessages : demoMessages);
|
||||
|
||||
// ── Clone dialog state ──────────────────────────────────────────
|
||||
let showCloneDialog = $state(false);
|
||||
let cloneBranchName = $state('');
|
||||
|
|
@ -83,9 +83,6 @@
|
|||
}
|
||||
|
||||
let activeTab = $state<ProjectTab>('model');
|
||||
// svelte-ignore state_referenced_locally
|
||||
const seedMessages = initialMessages.slice();
|
||||
let messages = $state(seedMessages);
|
||||
// Track which project tabs have been activated (PERSISTED-LAZY pattern)
|
||||
let activatedTabs = $state<Set<ProjectTab>>(new Set(['model']));
|
||||
|
||||
|
|
@ -97,15 +94,23 @@
|
|||
}
|
||||
|
||||
function handleSend(text: string) {
|
||||
const newMsg: AgentMessage = { id: messages.length + 1, role: 'user', content: text };
|
||||
messages = [...messages, newMsg];
|
||||
setTimeout(() => {
|
||||
messages = [...messages, {
|
||||
id: messages.length + 1,
|
||||
role: 'assistant',
|
||||
content: `(demo) Received: "${text}"`,
|
||||
}];
|
||||
}, 400);
|
||||
if (hasSession(id)) {
|
||||
// Session exists — send follow-up prompt
|
||||
sendPrompt(id, text).catch((err) => {
|
||||
console.error('[agent.prompt] error:', err);
|
||||
});
|
||||
} else {
|
||||
// No session — start a new agent
|
||||
startAgent(id, provider, text, { cwd, model }).catch((err) => {
|
||||
console.error('[agent.start] error:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleStop() {
|
||||
stopAgent(id).catch((err) => {
|
||||
console.error('[agent.stop] error:', err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -117,12 +122,12 @@
|
|||
>
|
||||
<!-- Header -->
|
||||
<header class="project-header">
|
||||
<div class="status-dot-wrap" aria-label="Status: {status}">
|
||||
<div class="status-dot-wrap" aria-label="Status: {agentStatus}">
|
||||
<div
|
||||
class="status-dot {status}"
|
||||
class:blink-off={status === 'running' && !blinkVisible}
|
||||
class="status-dot {agentStatus}"
|
||||
class:blink-off={agentStatus === 'running' && !blinkVisible}
|
||||
role="img"
|
||||
aria-label={status}
|
||||
aria-label={agentStatus}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
|
|
@ -230,16 +235,17 @@
|
|||
aria-label="Model"
|
||||
>
|
||||
<AgentPane
|
||||
{messages}
|
||||
{status}
|
||||
{costUsd}
|
||||
{tokens}
|
||||
{model}
|
||||
messages={displayMessages}
|
||||
status={agentStatus}
|
||||
costUsd={agentCost}
|
||||
tokens={agentTokens}
|
||||
model={agentModel}
|
||||
{provider}
|
||||
{profile}
|
||||
{contextPct}
|
||||
{burnRate}
|
||||
onSend={handleSend}
|
||||
onStop={handleStop}
|
||||
/>
|
||||
<TerminalTabs projectId={id} {accent} />
|
||||
</div>
|
||||
|
|
@ -270,7 +276,7 @@
|
|||
<div class="ctx-stats-row">
|
||||
<div class="ctx-stat">
|
||||
<span class="ctx-stat-label">Tokens used</span>
|
||||
<span class="ctx-stat-value">{tokens.toLocaleString()}</span>
|
||||
<span class="ctx-stat-value">{agentTokens.toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="ctx-stat">
|
||||
<span class="ctx-stat-label">Context %</span>
|
||||
|
|
@ -278,7 +284,7 @@
|
|||
</div>
|
||||
<div class="ctx-stat">
|
||||
<span class="ctx-stat-label">Model</span>
|
||||
<span class="ctx-stat-value">{model}</span>
|
||||
<span class="ctx-stat-value">{agentModel}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctx-meter-wrap" title="{contextPct}% context used">
|
||||
|
|
@ -289,7 +295,7 @@
|
|||
</div>
|
||||
<div class="ctx-turn-list">
|
||||
<div class="ctx-section-label">Turn breakdown</div>
|
||||
{#each messages.slice(0, 5) as msg}
|
||||
{#each displayMessages.slice(0, 5) 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>
|
||||
|
|
@ -403,6 +409,8 @@
|
|||
|
||||
.status-dot.running { background: var(--ctp-green); }
|
||||
.status-dot.idle { background: var(--ctp-overlay1); }
|
||||
.status-dot.done { background: var(--ctp-green); }
|
||||
.status-dot.error { background: var(--ctp-red); }
|
||||
.status-dot.stalled { background: var(--ctp-peach); }
|
||||
.status-dot.blink-off { opacity: 0.3; }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue