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
|
|
@ -1,20 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
import ChatInput from './ChatInput.svelte';
|
||||
|
||||
type MsgRole = 'user' | 'assistant' | 'tool-call' | 'tool-result';
|
||||
|
||||
interface AgentMessage {
|
||||
id: number;
|
||||
role: MsgRole;
|
||||
content: string;
|
||||
toolName?: string;
|
||||
toolPath?: string;
|
||||
}
|
||||
import type { AgentMessage, AgentStatus } from './agent-store.svelte.ts';
|
||||
|
||||
interface Props {
|
||||
messages: AgentMessage[];
|
||||
status: 'running' | 'idle' | 'stalled';
|
||||
status: AgentStatus;
|
||||
costUsd: number;
|
||||
tokens: number;
|
||||
model?: string;
|
||||
|
|
@ -23,6 +14,7 @@
|
|||
contextPct?: number;
|
||||
burnRate?: number;
|
||||
onSend?: (text: string) => void;
|
||||
onStop?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -34,11 +26,12 @@
|
|||
provider = 'claude',
|
||||
contextPct = 0,
|
||||
onSend,
|
||||
onStop,
|
||||
}: Props = $props();
|
||||
|
||||
let scrollEl: HTMLDivElement;
|
||||
let promptText = $state('');
|
||||
let expandedTools = $state<Set<number>>(new Set());
|
||||
let expandedTools = $state<Set<string>>(new Set());
|
||||
|
||||
// Drag-resize state
|
||||
let agentPaneEl: HTMLDivElement;
|
||||
|
|
@ -58,7 +51,7 @@
|
|||
onSend?.(text);
|
||||
}
|
||||
|
||||
function toggleTool(id: number) {
|
||||
function toggleTool(id: string) {
|
||||
const next = new Set(expandedTools);
|
||||
next.has(id) ? next.delete(id) : next.add(id);
|
||||
expandedTools = next;
|
||||
|
|
@ -67,10 +60,18 @@
|
|||
function fmtTokens(n: number) { return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n); }
|
||||
function fmtCost(n: number) { return `$${n.toFixed(3)}`; }
|
||||
|
||||
function dotClass(s: string) {
|
||||
function dotClass(s: AgentStatus) {
|
||||
if (s === 'running') return 'dot-progress';
|
||||
if (s === 'stalled') return 'dot-error';
|
||||
return 'dot-success';
|
||||
if (s === 'error') return 'dot-error';
|
||||
if (s === 'done') return 'dot-success';
|
||||
return 'dot-idle';
|
||||
}
|
||||
|
||||
function statusLabel(s: AgentStatus) {
|
||||
if (s === 'running') return 'Running';
|
||||
if (s === 'error') return 'Error';
|
||||
if (s === 'done') return 'Done';
|
||||
return 'Idle';
|
||||
}
|
||||
|
||||
function onResizeMouseDown(e: MouseEvent) {
|
||||
|
|
@ -98,12 +99,19 @@
|
|||
<!-- Status strip (top) -->
|
||||
<div class="status-strip">
|
||||
<span class="strip-dot {dotClass(status)}"></span>
|
||||
<span class="strip-label">{status === 'running' ? 'Running' : status === 'stalled' ? 'Stalled' : 'Done'}</span>
|
||||
<span class="strip-label">{statusLabel(status)}</span>
|
||||
<span class="strip-model">{model}</span>
|
||||
<span class="strip-spacer"></span>
|
||||
<span class="strip-tokens">{fmtTokens(tokens)} tok</span>
|
||||
<span class="strip-sep" aria-hidden="true"></span>
|
||||
<span class="strip-cost">{fmtCost(costUsd)}</span>
|
||||
{#if status === 'running' && onStop}
|
||||
<button class="strip-stop-btn" onclick={onStop} title="Stop agent" aria-label="Stop agent">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
||||
<rect x="3" y="3" width="10" height="10" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Main pane with floating input -->
|
||||
|
|
@ -126,6 +134,22 @@
|
|||
<div class="timeline-content">{msg.content}</div>
|
||||
</div>
|
||||
|
||||
{:else if msg.role === 'thinking'}
|
||||
<div class="timeline-row">
|
||||
{#if !isFirst}<div class="timeline-line-up"></div>{/if}
|
||||
<div class="timeline-diamond dot-thinking"></div>
|
||||
{#if !isLast}<div class="timeline-line-down"></div>{/if}
|
||||
<div class="thinking-content">{msg.content}</div>
|
||||
</div>
|
||||
|
||||
{:else if msg.role === 'system'}
|
||||
<div class="timeline-row">
|
||||
{#if !isFirst}<div class="timeline-line-up"></div>{/if}
|
||||
<div class="timeline-diamond dot-system"></div>
|
||||
{#if !isLast}<div class="timeline-line-down"></div>{/if}
|
||||
<div class="system-content">{msg.content}</div>
|
||||
</div>
|
||||
|
||||
{:else if msg.role === 'tool-call'}
|
||||
<div class="timeline-row">
|
||||
{#if !isFirst}<div class="timeline-line-up"></div>{/if}
|
||||
|
|
@ -222,6 +246,9 @@
|
|||
.dot-success { background: var(--ctp-green); }
|
||||
.dot-progress { background: var(--ctp-peach); }
|
||||
.dot-error { background: var(--ctp-red); }
|
||||
.dot-idle { background: var(--ctp-overlay1); }
|
||||
.dot-thinking { background: var(--ctp-mauve); }
|
||||
.dot-system { background: var(--ctp-overlay0); }
|
||||
|
||||
.strip-label { color: var(--ctp-subtext1); font-weight: 500; }
|
||||
.strip-model { color: var(--ctp-overlay1); margin-left: 0.25rem; }
|
||||
|
|
@ -234,6 +261,32 @@
|
|||
}
|
||||
.strip-cost { color: var(--ctp-text); font-weight: 500; }
|
||||
|
||||
.strip-stop-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
padding: 0;
|
||||
margin-left: 0.25rem;
|
||||
background: transparent;
|
||||
border: 1px solid var(--ctp-red);
|
||||
border-radius: 0.2rem;
|
||||
color: var(--ctp-red);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.strip-stop-btn:hover {
|
||||
background: color-mix(in srgb, var(--ctp-red) 20%, transparent);
|
||||
}
|
||||
|
||||
.strip-stop-btn svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/* ── Main pane ────────────────────────────────────────────── */
|
||||
.agent-pane {
|
||||
display: flex;
|
||||
|
|
@ -424,6 +477,27 @@
|
|||
|
||||
.tool-result-content { color: var(--ctp-teal); }
|
||||
|
||||
/* ── Thinking content ──────────────────────────────────────── */
|
||||
.thinking-content {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
color: var(--ctp-overlay1);
|
||||
font-style: italic;
|
||||
padding: 0.5rem 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* ── System content ────────────────────────────────────────── */
|
||||
.system-content {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
color: var(--ctp-overlay0);
|
||||
padding: 0.25rem 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tool-fade-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue