<script lang="ts">
import { appStore } from '$lib/store.svelte.js';
import { cancelSwarmAgent, extendSwarmAgent } from '$lib/events.js';
let agent = $derived(
appStore.selectedAgentId ? appStore.hiveAgents.get(appStore.selectedAgentId) ?? null : null
);
let controlBusy = $state(false);
let controlMessage = $state<string | null>(null);
function formatTokens(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
return String(n);
}
function close() {
appStore.selectedAgentId = null;
controlMessage = null;
}
async function handleCancel() {
if (!agent || controlBusy) return;
controlBusy = true;
controlMessage = null;
try {
const res = await cancelSwarmAgent('', agent.id, appStore.authToken ?? undefined);
controlMessage = res.message;
} catch (e) {
controlMessage = 'Failed to cancel agent';
}
controlBusy = false;
}
async function handleExtend(extra: number) {
if (!agent || controlBusy) return;
controlBusy = true;
controlMessage = null;
try {
const res = await extendSwarmAgent('', agent.id, extra, appStore.authToken ?? undefined);
controlMessage = res.message;
} catch (e) {
controlMessage = 'Failed to extend agent';
}
controlBusy = false;
}
</script>
{#if agent}
<div class="detail-panel">
<div class="panel-header">
<div class="header-left">
<span class="agent-name">{agent.name || agent.id}</span>
{#if agent.done}
<span class="badge" class:success={agent.success} class:fail={!agent.success}>
{agent.success ? 'OK' : 'FAIL'}
</span>
{:else if agent.approaching}
<span class="badge warn">LIMIT</span>
{:else if agent.iteration > 0}
<span class="badge running">RUN</span>
{:else}
<span class="badge pending">WAIT</span>
{/if}
</div>
<button class="close-btn" onclick={close}>×</button>
</div>
<!-- Task description -->
<div class="section">
<div class="section-title">Task</div>
<div class="task-text">{agent.task}</div>
</div>
<!-- Metrics grid -->
<div class="metrics">
<div class="metric">
<span class="metric-label">Iteration</span>
<span class="metric-value">{agent.iteration}</span>
</div>
<div class="metric">
<span class="metric-label">Tool Calls</span>
<span class="metric-value">{agent.toolCalls}</span>
</div>
<div class="metric">
<span class="metric-label">In Tokens</span>
<span class="metric-value">{formatTokens(agent.inputTokens)}</span>
</div>
<div class="metric">
<span class="metric-label">Out Tokens</span>
<span class="metric-value">{formatTokens(agent.outputTokens)}</span>
</div>
</div>
<!-- Approaching limit warning -->
{#if agent.approaching && !agent.done}
<div class="warning-bar">
Approaching iteration limit — {agent.approachingRemaining} remaining
</div>
{/if}
<!-- Agent control actions -->
{#if !agent.done}
<div class="control-bar">
<button class="control-btn cancel" onclick={handleCancel} disabled={controlBusy}>
Cancel
</button>
<button class="control-btn extend" onclick={() => handleExtend(10)} disabled={controlBusy}>
+10 iter
</button>
<button class="control-btn extend" onclick={() => handleExtend(25)} disabled={controlBusy}>
+25 iter
</button>
</div>
{/if}
{#if controlMessage}
<div class="control-message">{controlMessage}</div>
{/if}
<!-- Modified files -->
{#if agent.modifiedFiles.length > 0}
<div class="section">
<div class="section-title">Modified Files ({agent.modifiedFiles.length})</div>
<div class="file-list">
{#each agent.modifiedFiles as file}
<div class="file-entry">{file}</div>
{/each}
</div>
</div>
{/if}
<!-- Dependencies -->
{#if agent.dependencies.length > 0}
<div class="section">
<div class="section-title">Dependencies</div>
<div class="dep-list">
{#each agent.dependencies as dep}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span class="dep-tag" onclick={() => (appStore.selectedAgentId = dep)}>{dep}</span>
{/each}
</div>
</div>
{/if}
<!-- Agent output (scrollable chat-like area) -->
{#if agent.response}
<div class="section output-section">
<div class="section-title">Output</div>
<div class="output-text">{agent.response}</div>
</div>
{/if}
</div>
{/if}
<style>
.detail-panel {
background: var(--bg-surface);
border: 1px solid var(--fg-dim, #333);
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
max-height: 50vh;
overflow-y: auto;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.agent-name {
font-weight: 700;
font-size: 14px;
color: var(--cyan);
}
.badge {
font-size: 9px;
font-weight: 700;
padding: 2px 6px;
border-radius: 3px;
text-transform: uppercase;
}
.badge.success { background: var(--green); color: var(--bg); }
.badge.fail { background: var(--red); color: var(--bg); }
.badge.warn { background: var(--yellow); color: var(--bg); }
.badge.running { background: var(--accent); color: var(--bg); }
.badge.pending { background: var(--fg-dim); color: var(--bg); }
.close-btn {
background: none;
border: none;
color: var(--fg-dim);
font-size: 18px;
cursor: pointer;
padding: 0 4px;
}
.close-btn:hover { color: var(--fg); }
.section { display: flex; flex-direction: column; gap: 4px; }
.section-title {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
color: var(--fg-dim);
letter-spacing: 0.5px;
}
.task-text {
font-size: 12px;
color: var(--fg);
line-height: 1.5;
word-break: break-word;
}
.metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.metric {
background: var(--bg-elevated);
border-radius: 4px;
padding: 6px 8px;
display: flex;
flex-direction: column;
}
.metric-label {
font-size: 9px;
color: var(--fg-dim);
text-transform: uppercase;
}
.metric-value {
font-size: 16px;
font-weight: 700;
color: var(--fg);
font-family: var(--font-mono);
}
.warning-bar {
background: color-mix(in srgb, var(--yellow) 15%, transparent);
border: 1px solid var(--yellow);
border-radius: 4px;
padding: 6px 10px;
font-size: 11px;
color: var(--yellow);
font-weight: 600;
}
.file-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.file-entry {
font-family: var(--font-mono);
font-size: 11px;
color: var(--fg);
padding: 2px 6px;
background: var(--bg-elevated);
border-radius: 3px;
word-break: break-all;
}
.dep-list {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.dep-tag {
font-size: 10px;
font-family: var(--font-mono);
padding: 2px 8px;
background: var(--bg-elevated);
border-radius: 3px;
color: var(--accent);
cursor: pointer;
}
.dep-tag:hover {
background: var(--accent);
color: var(--bg);
}
.output-section {
flex: 1;
min-height: 0;
}
.output-text {
font-family: var(--font-mono);
font-size: 11px;
color: var(--fg);
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
max-height: 200px;
overflow-y: auto;
padding: 8px;
background: var(--bg);
border-radius: 4px;
}
.control-bar {
display: flex;
gap: 6px;
}
.control-btn {
flex: 1;
padding: 5px 8px;
border: 1px solid var(--fg-dim);
border-radius: 4px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
background: var(--bg-elevated);
color: var(--fg);
transition: background 0.15s, border-color 0.15s;
}
.control-btn:hover:not(:disabled) {
background: var(--bg-surface);
}
.control-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.control-btn.cancel {
border-color: var(--red);
color: var(--red);
}
.control-btn.cancel:hover:not(:disabled) {
background: color-mix(in srgb, var(--red) 15%, transparent);
}
.control-btn.extend {
border-color: var(--green);
color: var(--green);
}
.control-btn.extend:hover:not(:disabled) {
background: color-mix(in srgb, var(--green) 15%, transparent);
}
.control-message {
font-size: 10px;
color: var(--fg-dim);
padding: 4px 6px;
background: var(--bg-elevated);
border-radius: 3px;
text-align: center;
}
</style>