<script lang="ts">
import { agentGraphStore, type AgentLogEntry } from '$lib/stores/agent-graph.svelte.js';
let selectedAgent = $derived(
agentGraphStore.selectedAgentId
? agentGraphStore.agents.get(agentGraphStore.selectedAgentId)
: null
);
let logEl: HTMLDivElement | undefined = $state();
// Auto-scroll log on new entries
$effect(() => {
if (logEl && selectedAgent) {
logEl.scrollTop = logEl.scrollHeight;
}
});
function formatTimestamp(ts: number): string {
const d = new Date(ts);
return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
function getLogIcon(entry: AgentLogEntry): string {
switch (entry.kind) {
case 'tool_call': return 'π§';
case 'tool_result': return entry.success ? 'β
' : 'β';
case 'token': return 'π¬';
case 'response': return 'π';
case 'status': return 'π';
}
}
function getStatusColor(status: string): string {
switch (status) {
case 'running': return 'var(--accent)';
case 'done': return 'var(--green)';
case 'error': return 'var(--red)';
case 'paused': return 'var(--yellow)';
default: return 'var(--fg-dim)';
}
}
function getStatusIcon(status: string, success: boolean): string {
if (status === 'done') return success ? 'β' : 'β';
if (status === 'running') return 'β³';
return 'β';
}
</script>
{#if agentGraphStore.popupOpen && selectedAgent}
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="popup-overlay" onclick={() => agentGraphStore.closePopup()} onkeydown={() => {}}>
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="popup" onclick={(e) => e.stopPropagation()} onkeydown={() => {}}>
<!-- Header -->
<div class="popup-header">
<div class="header-left">
<span class="status-dot" style="background: {getStatusColor(selectedAgent.status)}"></span>
<span class="agent-name">{selectedAgent.name}</span>
<span class="status-badge" style="color: {getStatusColor(selectedAgent.status)}">
{getStatusIcon(selectedAgent.status, selectedAgent.success)} {selectedAgent.status}
</span>
</div>
<div class="header-right">
<span class="stat">π§ {selectedAgent.toolCalls}</span>
<span class="stat">β{selectedAgent.inputTokens}</span>
<span class="stat">β{selectedAgent.outputTokens}</span>
<button class="close-btn" onclick={() => agentGraphStore.closePopup()} aria-label="Close">β</button>
</div>
</div>
<!-- Task description -->
<div class="task-bar">
{selectedAgent.task}
</div>
<!-- Log area (chat-style) -->
<div class="log-area" bind:this={logEl} role="log" aria-label="Agent activity log">
{#each selectedAgent.log as entry}
<div class="log-entry entry-{entry.kind}">
<span class="log-icon">{getLogIcon(entry)}</span>
<div class="log-content">
<span class="log-time">{formatTimestamp(entry.timestamp)}</span>
{#if entry.kind === 'tool_call'}
<div class="tool-name">{entry.name}</div>
<pre class="tool-args">{entry.args ?? ''}</pre>
{:else if entry.kind === 'tool_result'}
<div class="tool-name">{entry.name}</div>
<pre class="tool-result-text" class:success={entry.success} class:failed={!entry.success}>
{entry.result ?? ''}
</pre>
{:else if entry.kind === 'token'}
<div class="token-text">{entry.text ?? ''}</div>
{:else if entry.kind === 'response'}
<div class="response-text">{entry.text ?? ''}</div>
{:else if entry.kind === 'status'}
<div class="status-text">{entry.message ?? ''}</div>
{/if}
</div>
</div>
{/each}
{#if selectedAgent.status === 'running'}
<div class="active-indicator">
<span class="pulse-dot"></span>
<span class="active-text">Agent is workingβ¦</span>
</div>
{/if}
</div>
<!-- Modified files -->
{#if selectedAgent.modifiedFiles.length > 0}
<div class="files-bar">
<span class="files-label">Modified:</span>
{#each selectedAgent.modifiedFiles as file}
<span class="file-tag">{file}</span>
{/each}
</div>
{/if}
<!-- Footer -->
<div class="popup-footer">
<span class="footer-stats">
Iteration #{selectedAgent.iteration} Β· {selectedAgent.toolCalls} tool calls
</span>
</div>
</div>
</div>
{/if}
<style>
.popup-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
}
.popup {
width: min(640px, 90vw);
max-height: 80vh;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius, 8px);
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
background: var(--bg-elevated);
border-radius: var(--radius, 8px) var(--radius, 8px) 0 0;
}
.header-left {
display: flex;
align-items: center;
gap: 0.5rem;
}
.header-right {
display: flex;
align-items: center;
gap: 0.75rem;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.agent-name {
font-weight: 700;
color: var(--fg);
font-size: 0.95rem;
}
.status-badge {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
}
.stat {
font-size: 0.7rem;
color: var(--fg-dim);
font-family: var(--font-mono);
}
.close-btn {
all: unset;
cursor: pointer;
color: var(--fg-dim);
padding: 0.125rem 0.375rem;
border-radius: 4px;
font-size: 1rem;
}
.close-btn:hover {
color: var(--fg);
background: var(--bg);
}
.task-bar {
padding: 0.5rem 1rem;
font-size: 0.8rem;
color: var(--fg-dim);
border-bottom: 1px solid var(--border);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.log-area {
flex: 1;
overflow-y: auto;
padding: 0.5rem 0;
min-height: 200px;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.log-entry {
display: flex;
gap: 0.5rem;
padding: 0.375rem 1rem;
font-size: 0.8rem;
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
.log-entry:hover {
background: color-mix(in srgb, var(--bg-elevated) 50%, transparent);
}
.log-icon {
flex-shrink: 0;
width: 1.25rem;
text-align: center;
font-size: 0.75rem;
}
.log-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.log-time {
font-size: 0.6rem;
color: var(--fg-dim);
opacity: 0.6;
}
.tool-name {
color: var(--cyan);
font-weight: 600;
font-size: 0.75rem;
}
.tool-args {
color: var(--fg-dim);
font-size: 0.7rem;
margin: 0;
padding: 0.25rem 0.375rem;
background: var(--bg);
border-radius: 3px;
max-height: 100px;
overflow-y: auto;
font-family: var(--font-mono);
white-space: pre-wrap;
word-break: break-all;
}
.tool-result-text {
color: var(--fg-dim);
font-size: 0.7rem;
margin: 0;
padding: 0.25rem 0.375rem;
background: var(--bg);
border-radius: 3px;
max-height: 150px;
overflow-y: auto;
font-family: var(--font-mono);
white-space: pre-wrap;
word-break: break-all;
}
.tool-result-text.success {
border-left: 2px solid var(--green);
}
.tool-result-text.failed {
border-left: 2px solid var(--red);
}
.token-text {
color: var(--fg);
font-size: 0.8rem;
white-space: pre-wrap;
word-break: break-word;
}
.response-text {
color: var(--fg);
font-size: 0.8rem;
white-space: pre-wrap;
word-break: break-word;
}
.status-text {
color: var(--fg-dim);
font-size: 0.7rem;
font-style: italic;
}
.active-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
color: var(--fg-dim);
font-size: 0.75rem;
}
.pulse-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.2); }
}
.active-text {
font-style: italic;
}
.files-bar {
padding: 0.375rem 1rem;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
font-size: 0.7rem;
}
.files-label {
color: var(--fg-dim);
flex-shrink: 0;
}
.file-tag {
background: var(--bg-elevated);
color: var(--cyan);
padding: 0.1rem 0.375rem;
border-radius: 3px;
border: 1px solid var(--border);
font-size: 0.65rem;
}
.popup-footer {
padding: 0.5rem 1rem;
border-top: 1px solid var(--border);
background: var(--bg-elevated);
border-radius: 0 0 var(--radius, 8px) var(--radius, 8px);
}
.footer-stats {
font-size: 0.7rem;
color: var(--fg-dim);
}
</style>