getTotalCost() recursively aggregates costUsd, inputTokens, outputTokens across parent and all child sessions. AgentPane done-bar displays total cost in yellow when children are present and total exceeds parent cost.
820 lines
21 KiB
Svelte
820 lines
21 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { marked, Renderer } from 'marked';
|
|
import { queryAgent, stopAgent, isAgentReady, restartAgent } from '../../adapters/agent-bridge';
|
|
import {
|
|
getAgentSession,
|
|
createAgentSession,
|
|
removeAgentSession,
|
|
getChildSessions,
|
|
getTotalCost,
|
|
type AgentSession,
|
|
} from '../../stores/agents.svelte';
|
|
import { focusPane } from '../../stores/layout.svelte';
|
|
import { isSidecarAlive, setSidecarAlive } from '../../agent-dispatcher';
|
|
import AgentTree from './AgentTree.svelte';
|
|
import { getHighlighter, highlightCode, escapeHtml } from '../../utils/highlight';
|
|
import type {
|
|
AgentMessage,
|
|
TextContent,
|
|
ThinkingContent,
|
|
ToolCallContent,
|
|
ToolResultContent,
|
|
CostContent,
|
|
ErrorContent,
|
|
} from '../../adapters/sdk-messages';
|
|
|
|
interface Props {
|
|
sessionId: string;
|
|
prompt?: string;
|
|
cwd?: string;
|
|
onExit?: () => void;
|
|
}
|
|
|
|
let { sessionId, prompt: initialPrompt = '', cwd, onExit }: Props = $props();
|
|
|
|
let session = $derived(getAgentSession(sessionId));
|
|
let inputPrompt = $state(initialPrompt);
|
|
let scrollContainer: HTMLDivElement | undefined = $state();
|
|
let autoScroll = $state(true);
|
|
let restarting = $state(false);
|
|
let showTree = $state(false);
|
|
let hasToolCalls = $derived(session?.messages.some(m => m.type === 'tool_call') ?? false);
|
|
let parentSession = $derived(session?.parentSessionId ? getAgentSession(session.parentSessionId) : undefined);
|
|
let childSessions = $derived(session ? getChildSessions(session.id) : []);
|
|
let totalCost = $derived(session && childSessions.length > 0 ? getTotalCost(session.id) : null);
|
|
|
|
const mdRenderer = new Renderer();
|
|
mdRenderer.code = function({ text, lang }: { text: string; lang?: string }) {
|
|
if (lang) {
|
|
const highlighted = highlightCode(text, lang);
|
|
if (highlighted !== escapeHtml(text)) return highlighted;
|
|
}
|
|
return `<pre><code>${escapeHtml(text)}</code></pre>`;
|
|
};
|
|
|
|
function renderMarkdown(source: string): string {
|
|
try {
|
|
return marked.parse(source, { renderer: mdRenderer, async: false }) as string;
|
|
} catch {
|
|
return escapeHtml(source);
|
|
}
|
|
}
|
|
|
|
onMount(async () => {
|
|
await getHighlighter();
|
|
if (initialPrompt) {
|
|
await startQuery(initialPrompt);
|
|
}
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (session?.status === 'running' || session?.status === 'starting') {
|
|
stopAgent(sessionId).catch(() => {});
|
|
}
|
|
});
|
|
|
|
let followUpPrompt = $state('');
|
|
|
|
async function startQuery(text: string, resume = false) {
|
|
if (!text.trim()) return;
|
|
|
|
const ready = await isAgentReady();
|
|
if (!ready) {
|
|
if (!resume) createAgentSession(sessionId, text);
|
|
const { updateAgentStatus } = await import('../../stores/agents.svelte');
|
|
updateAgentStatus(sessionId, 'error', 'Sidecar not ready — agent features unavailable');
|
|
return;
|
|
}
|
|
|
|
const resumeId = resume ? session?.sdkSessionId : undefined;
|
|
|
|
if (!resume) {
|
|
createAgentSession(sessionId, text);
|
|
} else {
|
|
const { updateAgentStatus } = await import('../../stores/agents.svelte');
|
|
updateAgentStatus(sessionId, 'starting');
|
|
}
|
|
|
|
await queryAgent({
|
|
session_id: sessionId,
|
|
prompt: text,
|
|
cwd,
|
|
max_turns: 50,
|
|
resume_session_id: resumeId,
|
|
});
|
|
inputPrompt = '';
|
|
followUpPrompt = '';
|
|
}
|
|
|
|
function handleSubmit(e: Event) {
|
|
e.preventDefault();
|
|
startQuery(inputPrompt);
|
|
}
|
|
|
|
function handleStop() {
|
|
stopAgent(sessionId).catch(() => {});
|
|
}
|
|
|
|
async function handleRestart() {
|
|
restarting = true;
|
|
try {
|
|
await restartAgent();
|
|
setSidecarAlive(true);
|
|
} catch {
|
|
// Still dead
|
|
} finally {
|
|
restarting = false;
|
|
}
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
if (autoScroll && scrollContainer) {
|
|
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
}
|
|
}
|
|
|
|
function handleScroll() {
|
|
if (!scrollContainer) return;
|
|
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
|
// Lock auto-scroll if user scrolled up more than 50px from bottom
|
|
autoScroll = scrollHeight - scrollTop - clientHeight < 50;
|
|
}
|
|
|
|
function handleTreeNodeClick(nodeId: string) {
|
|
if (!scrollContainer || !session) return;
|
|
// Find the message whose tool_call has this toolUseId
|
|
const msg = session.messages.find(
|
|
m => m.type === 'tool_call' && (m.content as ToolCallContent).toolUseId === nodeId
|
|
);
|
|
if (!msg) return;
|
|
autoScroll = false;
|
|
scrollContainer.querySelector('#msg-' + CSS.escape(msg.id))?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
|
|
// Auto-scroll when new messages arrive
|
|
$effect(() => {
|
|
if (session?.messages.length) {
|
|
scrollToBottom();
|
|
}
|
|
});
|
|
|
|
function formatToolInput(input: unknown): string {
|
|
if (typeof input === 'string') return input;
|
|
try {
|
|
return JSON.stringify(input, null, 2);
|
|
} catch {
|
|
return String(input);
|
|
}
|
|
}
|
|
|
|
function truncate(text: string, maxLen: number): string {
|
|
if (text.length <= maxLen) return text;
|
|
return text.slice(0, maxLen) + '...';
|
|
}
|
|
</script>
|
|
|
|
<div class="agent-pane">
|
|
{#if !session || session.messages.length === 0}
|
|
<div class="prompt-area">
|
|
<form onsubmit={handleSubmit} class="prompt-form">
|
|
<textarea
|
|
bind:value={inputPrompt}
|
|
placeholder="Ask Claude something..."
|
|
class="prompt-input"
|
|
rows="3"
|
|
onkeydown={(e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
startQuery(inputPrompt);
|
|
}
|
|
}}
|
|
></textarea>
|
|
<button type="submit" class="send-btn" disabled={!inputPrompt.trim()}>Send</button>
|
|
</form>
|
|
</div>
|
|
{:else}
|
|
{#if parentSession}
|
|
<div class="parent-link">
|
|
<span class="parent-badge">SUB</span>
|
|
<button class="parent-btn" onclick={() => focusPane(parentSession!.id)}>
|
|
← {parentSession.prompt ? parentSession.prompt.slice(0, 40) : 'Parent agent'}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
{#if childSessions.length > 0}
|
|
<div class="children-bar">
|
|
<span class="children-label">{childSessions.length} subagent{childSessions.length > 1 ? 's' : ''}</span>
|
|
{#each childSessions as child (child.id)}
|
|
<button class="child-chip" class:running={child.status === 'running'} class:done={child.status === 'done'} class:error={child.status === 'error'} onclick={() => focusPane(child.id)}>
|
|
{child.prompt.slice(0, 20)}{child.prompt.length > 20 ? '...' : ''}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{#if hasToolCalls}
|
|
<div class="tree-toggle">
|
|
<button class="tree-btn" onclick={() => showTree = !showTree}>
|
|
{showTree ? '▼' : '▶'} Agent Tree
|
|
</button>
|
|
</div>
|
|
{#if showTree && session}
|
|
<AgentTree {session} onNodeClick={handleTreeNodeClick} />
|
|
{/if}
|
|
{/if}
|
|
<div class="messages" bind:this={scrollContainer} onscroll={handleScroll}>
|
|
{#each session.messages as msg (msg.id)}
|
|
<div class="message msg-{msg.type}" id="msg-{msg.id}">
|
|
{#if msg.type === 'init'}
|
|
<div class="msg-init">
|
|
<span class="label">Session started</span>
|
|
<span class="model">{(msg.content as import('../../adapters/sdk-messages').InitContent).model}</span>
|
|
</div>
|
|
{:else if msg.type === 'text'}
|
|
<div class="msg-text markdown-body">{@html renderMarkdown((msg.content as TextContent).text)}</div>
|
|
{:else if msg.type === 'thinking'}
|
|
<details class="msg-thinking">
|
|
<summary>Thinking...</summary>
|
|
<pre>{(msg.content as ThinkingContent).text}</pre>
|
|
</details>
|
|
{:else if msg.type === 'tool_call'}
|
|
{@const tc = msg.content as ToolCallContent}
|
|
<details class="msg-tool-call">
|
|
<summary>
|
|
<span class="tool-name">{tc.name}</span>
|
|
<span class="tool-id">{truncate(tc.toolUseId, 12)}</span>
|
|
</summary>
|
|
<pre class="tool-input">{formatToolInput(tc.input)}</pre>
|
|
</details>
|
|
{:else if msg.type === 'tool_result'}
|
|
{@const tr = msg.content as ToolResultContent}
|
|
<details class="msg-tool-result">
|
|
<summary>Tool result</summary>
|
|
<pre class="tool-output">{formatToolInput(tr.output)}</pre>
|
|
</details>
|
|
{:else if msg.type === 'cost'}
|
|
{@const cost = msg.content as CostContent}
|
|
<div class="msg-cost">
|
|
<span>${cost.totalCostUsd.toFixed(4)}</span>
|
|
<span>{cost.inputTokens + cost.outputTokens} tokens</span>
|
|
<span>{cost.numTurns} turns</span>
|
|
<span>{(cost.durationMs / 1000).toFixed(1)}s</span>
|
|
</div>
|
|
{:else if msg.type === 'error'}
|
|
<div class="msg-error">{(msg.content as ErrorContent).message}</div>
|
|
{:else if msg.type === 'status'}
|
|
<div class="msg-status">{JSON.stringify(msg.content)}</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
<div class="footer">
|
|
{#if session.status === 'running' || session.status === 'starting'}
|
|
<div class="running-indicator">
|
|
<span class="pulse"></span>
|
|
<span>Running...</span>
|
|
{#if !autoScroll}
|
|
<button class="scroll-btn" onclick={() => { autoScroll = true; scrollToBottom(); }}>Scroll to bottom</button>
|
|
{/if}
|
|
<button class="stop-btn" onclick={handleStop}>Stop</button>
|
|
</div>
|
|
{:else if session.status === 'done'}
|
|
<div class="done-bar">
|
|
<span class="cost">${session.costUsd.toFixed(4)}</span>
|
|
{#if totalCost && totalCost.costUsd > session.costUsd}
|
|
<span class="total-cost">(total: ${totalCost.costUsd.toFixed(4)})</span>
|
|
{/if}
|
|
<span class="tokens">{session.inputTokens + session.outputTokens} tokens</span>
|
|
<span class="duration">{(session.durationMs / 1000).toFixed(1)}s</span>
|
|
{#if !autoScroll}
|
|
<button class="scroll-btn" onclick={() => { autoScroll = true; scrollToBottom(); }}>Scroll to bottom</button>
|
|
{/if}
|
|
</div>
|
|
{#if session.sdkSessionId}
|
|
<div class="follow-up">
|
|
<input
|
|
type="text"
|
|
class="follow-up-input"
|
|
bind:value={followUpPrompt}
|
|
placeholder="Follow up..."
|
|
onkeydown={(e) => {
|
|
if (e.key === 'Enter' && followUpPrompt.trim()) {
|
|
startQuery(followUpPrompt, true);
|
|
}
|
|
}}
|
|
/>
|
|
<button class="follow-up-btn" onclick={() => startQuery(followUpPrompt, true)} disabled={!followUpPrompt.trim()}>Send</button>
|
|
</div>
|
|
{/if}
|
|
{:else if session.status === 'error'}
|
|
<div class="error-bar">
|
|
<span>Error: {session.error ?? 'Unknown'}</span>
|
|
{#if session.error?.includes('Sidecar') || session.error?.includes('crashed')}
|
|
<button class="restart-btn" onclick={handleRestart} disabled={restarting}>
|
|
{restarting ? 'Restarting...' : 'Restart Sidecar'}
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
{#if session.sdkSessionId}
|
|
<div class="follow-up">
|
|
<input
|
|
type="text"
|
|
class="follow-up-input"
|
|
bind:value={followUpPrompt}
|
|
placeholder="Retry or follow up..."
|
|
onkeydown={(e) => {
|
|
if (e.key === 'Enter' && followUpPrompt.trim()) {
|
|
startQuery(followUpPrompt, true);
|
|
}
|
|
}}
|
|
/>
|
|
<button class="follow-up-btn" onclick={() => startQuery(followUpPrompt, true)} disabled={!followUpPrompt.trim()}>Send</button>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.agent-pane {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.parent-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 4px 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.parent-badge {
|
|
background: var(--ctp-mauve);
|
|
color: var(--ctp-crust);
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.parent-btn {
|
|
background: none;
|
|
border: none;
|
|
color: var(--ctp-mauve);
|
|
cursor: pointer;
|
|
font-size: 11px;
|
|
padding: 0;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.parent-btn:hover { color: var(--text-primary); text-decoration: underline; }
|
|
|
|
.children-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 4px 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
flex-wrap: wrap;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.children-label {
|
|
color: var(--text-muted);
|
|
font-size: 10px;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.child-chip {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
color: var(--text-secondary);
|
|
padding: 1px 6px;
|
|
border-radius: 3px;
|
|
font-size: 10px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.child-chip:hover { color: var(--text-primary); border-color: var(--accent); }
|
|
.child-chip.running { border-color: var(--ctp-blue); color: var(--ctp-blue); }
|
|
.child-chip.done { border-color: var(--ctp-green); color: var(--ctp-green); }
|
|
.child-chip.error { border-color: var(--ctp-red); color: var(--ctp-red); }
|
|
|
|
.tree-toggle {
|
|
padding: 4px 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.tree-btn {
|
|
background: none;
|
|
border: none;
|
|
color: var(--ctp-mauve);
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
font-family: var(--font-mono);
|
|
padding: 2px 4px;
|
|
}
|
|
|
|
.tree-btn:hover { color: var(--text-primary); }
|
|
|
|
.prompt-area {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
padding: 24px;
|
|
}
|
|
|
|
.prompt-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
width: 100%;
|
|
max-width: 600px;
|
|
}
|
|
|
|
.prompt-input {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--border-radius);
|
|
color: var(--text-primary);
|
|
font-family: inherit;
|
|
font-size: 13px;
|
|
padding: 10px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.prompt-input:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.send-btn {
|
|
align-self: flex-end;
|
|
background: var(--accent);
|
|
color: var(--ctp-crust);
|
|
border: none;
|
|
border-radius: var(--border-radius);
|
|
padding: 6px 16px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.send-btn:hover { opacity: 0.9; }
|
|
.send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
.messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 8px 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.message { padding: 4px 0; }
|
|
|
|
.msg-init {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: var(--text-muted);
|
|
font-size: 11px;
|
|
}
|
|
|
|
.msg-init .model {
|
|
background: var(--bg-surface);
|
|
padding: 1px 6px;
|
|
border-radius: 3px;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.msg-text {
|
|
word-break: break-word;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.msg-text.markdown-body :global(h1) {
|
|
font-size: 1.4em;
|
|
font-weight: 700;
|
|
margin: 0.6em 0 0.3em;
|
|
color: var(--ctp-lavender);
|
|
}
|
|
|
|
.msg-text.markdown-body :global(h2) {
|
|
font-size: 1.2em;
|
|
font-weight: 600;
|
|
margin: 0.5em 0 0.3em;
|
|
color: var(--ctp-blue);
|
|
}
|
|
|
|
.msg-text.markdown-body :global(h3) {
|
|
font-size: 1.05em;
|
|
font-weight: 600;
|
|
margin: 0.4em 0 0.2em;
|
|
color: var(--ctp-sapphire);
|
|
}
|
|
|
|
.msg-text.markdown-body :global(p) {
|
|
margin: 0.4em 0;
|
|
}
|
|
|
|
.msg-text.markdown-body :global(code) {
|
|
background: var(--bg-surface);
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.9em;
|
|
color: var(--ctp-green);
|
|
}
|
|
|
|
.msg-text.markdown-body :global(pre) {
|
|
background: var(--bg-surface);
|
|
padding: 10px 12px;
|
|
border-radius: var(--border-radius);
|
|
overflow-x: auto;
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
margin: 0.5em 0;
|
|
}
|
|
|
|
.msg-text.markdown-body :global(pre code) {
|
|
background: none;
|
|
padding: 0;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.msg-text.markdown-body :global(.shiki) {
|
|
background: var(--bg-surface) !important;
|
|
padding: 10px 12px;
|
|
border-radius: var(--border-radius);
|
|
overflow-x: auto;
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
margin: 0.5em 0;
|
|
}
|
|
|
|
.msg-text.markdown-body :global(.shiki code) {
|
|
background: none !important;
|
|
padding: 0;
|
|
}
|
|
|
|
.msg-text.markdown-body :global(blockquote) {
|
|
border-left: 3px solid var(--ctp-mauve);
|
|
margin: 0.4em 0;
|
|
padding: 2px 10px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.msg-text.markdown-body :global(ul), .msg-text.markdown-body :global(ol) {
|
|
padding-left: 20px;
|
|
margin: 0.3em 0;
|
|
}
|
|
|
|
.msg-text.markdown-body :global(li) {
|
|
margin: 0.15em 0;
|
|
}
|
|
|
|
.msg-text.markdown-body :global(a) {
|
|
color: var(--ctp-blue);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.msg-text.markdown-body :global(a:hover) {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.msg-text.markdown-body :global(table) {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
margin: 0.4em 0;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.msg-text.markdown-body :global(th), .msg-text.markdown-body :global(td) {
|
|
border: 1px solid var(--border);
|
|
padding: 4px 8px;
|
|
text-align: left;
|
|
}
|
|
|
|
.msg-text.markdown-body :global(th) {
|
|
background: var(--bg-surface);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.msg-thinking {
|
|
color: var(--ctp-overlay1);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.msg-thinking summary {
|
|
cursor: pointer;
|
|
color: var(--ctp-mauve);
|
|
}
|
|
|
|
.msg-thinking pre {
|
|
margin: 4px 0 0 12px;
|
|
white-space: pre-wrap;
|
|
font-size: 11px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.msg-tool-call, .msg-tool-result {
|
|
border-left: 2px solid var(--ctp-blue);
|
|
padding-left: 8px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.msg-tool-call summary, .msg-tool-result summary {
|
|
cursor: pointer;
|
|
color: var(--ctp-blue);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.tool-name {
|
|
font-weight: 600;
|
|
color: var(--ctp-green);
|
|
}
|
|
|
|
.tool-id {
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.tool-input, .tool-output {
|
|
margin: 4px 0 0 0;
|
|
white-space: pre-wrap;
|
|
font-size: 11px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
background: var(--bg-surface);
|
|
padding: 6px 8px;
|
|
border-radius: 3px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.msg-tool-result {
|
|
border-left-color: var(--ctp-teal);
|
|
}
|
|
|
|
.msg-tool-result summary {
|
|
color: var(--ctp-teal);
|
|
}
|
|
|
|
.msg-cost {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding: 4px 8px;
|
|
background: var(--bg-surface);
|
|
border-radius: 3px;
|
|
font-size: 11px;
|
|
color: var(--ctp-yellow);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.msg-error {
|
|
color: var(--ctp-red);
|
|
background: color-mix(in srgb, var(--ctp-red) 10%, transparent);
|
|
padding: 6px 8px;
|
|
border-radius: 3px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.msg-status {
|
|
color: var(--text-muted);
|
|
font-size: 11px;
|
|
font-style: italic;
|
|
}
|
|
|
|
.footer {
|
|
border-top: 1px solid var(--border);
|
|
padding: 6px 12px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.running-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 12px;
|
|
color: var(--ctp-blue);
|
|
}
|
|
|
|
.pulse {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--ctp-blue);
|
|
animation: pulse 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.3; }
|
|
}
|
|
|
|
.stop-btn {
|
|
margin-left: auto;
|
|
background: var(--ctp-red);
|
|
color: var(--ctp-crust);
|
|
border: none;
|
|
border-radius: 3px;
|
|
padding: 2px 10px;
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.stop-btn:hover { opacity: 0.9; }
|
|
|
|
.scroll-btn {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
color: var(--text-secondary);
|
|
border-radius: 3px;
|
|
padding: 2px 8px;
|
|
font-size: 10px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.scroll-btn:hover { color: var(--text-primary); }
|
|
|
|
.restart-btn {
|
|
margin-left: auto;
|
|
background: var(--ctp-peach);
|
|
color: var(--ctp-crust);
|
|
border: none;
|
|
border-radius: 3px;
|
|
padding: 2px 10px;
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.restart-btn:hover { opacity: 0.9; }
|
|
.restart-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
.done-bar, .error-bar {
|
|
display: flex;
|
|
gap: 12px;
|
|
font-size: 11px;
|
|
font-family: var(--font-mono);
|
|
align-items: center;
|
|
}
|
|
|
|
.done-bar { color: var(--ctp-green); }
|
|
.total-cost { color: var(--ctp-yellow); font-size: 10px; }
|
|
.error-bar { color: var(--ctp-red); }
|
|
|
|
.follow-up {
|
|
display: flex;
|
|
gap: 6px;
|
|
padding: 4px 0 0;
|
|
}
|
|
|
|
.follow-up-input {
|
|
flex: 1;
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 3px;
|
|
color: var(--text-primary);
|
|
font-size: 12px;
|
|
padding: 4px 8px;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.follow-up-input:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.follow-up-btn {
|
|
background: var(--accent);
|
|
color: var(--ctp-crust);
|
|
border: none;
|
|
border-radius: 3px;
|
|
padding: 4px 12px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.follow-up-btn:hover { opacity: 0.9; }
|
|
.follow-up-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
</style>
|