|
| 1 | +<script lang="ts"> |
| 2 | + import { formatCost, formatDuration } from "$lib/utils/format"; |
| 3 | + import { sessionKey, type SessionMeta } from "$lib/types/run"; |
| 4 | +
|
| 5 | + let { data } = $props(); |
| 6 | + let meta = $derived(data.meta); |
| 7 | +
|
| 8 | + let hasForks = $derived(meta.sessions.some((s: any) => s.fork_from)); |
| 9 | +
|
| 10 | + /** Group sessions: base sessions with their replicates nested underneath. */ |
| 11 | + interface SessionGroup { |
| 12 | + base: SessionMeta; |
| 13 | + replicates: SessionMeta[]; |
| 14 | + } |
| 15 | +
|
| 16 | + let sessionGroups = $derived.by(() => { |
| 17 | + const groups: SessionGroup[] = []; |
| 18 | + const byIndex = new Map<number, { base: SessionMeta | null; reps: SessionMeta[] }>(); |
| 19 | +
|
| 20 | + for (const s of meta.sessions) { |
| 21 | + const entry = byIndex.get(s.session_index) ?? { base: null, reps: [] }; |
| 22 | + if (s.replicate != null) { |
| 23 | + entry.reps.push(s); |
| 24 | + } else { |
| 25 | + entry.base = s; |
| 26 | + } |
| 27 | + byIndex.set(s.session_index, entry); |
| 28 | + } |
| 29 | +
|
| 30 | + // Sort by session_index |
| 31 | + const indices = [...byIndex.keys()].sort((a, b) => a - b); |
| 32 | + for (const idx of indices) { |
| 33 | + const entry = byIndex.get(idx)!; |
| 34 | + const reps = entry.reps.sort((a, b) => (a.replicate ?? 0) - (b.replicate ?? 0)); |
| 35 | + if (entry.base) { |
| 36 | + groups.push({ base: entry.base, replicates: reps }); |
| 37 | + } else if (reps.length > 0) { |
| 38 | + // No plain session, first replicate is the "base" |
| 39 | + groups.push({ base: reps[0], replicates: reps.slice(1) }); |
| 40 | + } |
| 41 | + } |
| 42 | + return groups; |
| 43 | + }); |
| 44 | +
|
| 45 | + let metrics = $derived([ |
| 46 | + { label: "sessions", value: meta.session_count }, |
| 47 | + { label: "steps", value: meta.total_steps }, |
| 48 | + { label: "tool_calls", value: meta.total_tool_calls }, |
| 49 | + { label: "file_writes", value: meta.total_file_writes }, |
| 50 | + { label: "compactions", value: meta.total_compaction_events }, |
| 51 | + { label: "subagents", value: meta.total_subagent_invocations }, |
| 52 | + ]); |
| 53 | +</script> |
| 54 | + |
| 55 | +{#snippet sessionRow(session: SessionMeta, isReplicate: boolean, showBorder: boolean)} |
| 56 | + {@const key = sessionKey(session)} |
| 57 | + {@const indent = isReplicate || (hasForks && session.fork_from)} |
| 58 | + <a |
| 59 | + href="/runs/{meta.run_name}/sessions/{key}" |
| 60 | + style="display: flex; align-items: center; justify-content: space-between; padding: 0.625rem 0.875rem; font-size: 12px; {showBorder ? 'border-top: 1px solid var(--border);' : ''} {indent ? 'padding-left: 2rem;' : ''}" |
| 61 | + class="session-row" |
| 62 | + > |
| 63 | + <div style="display: flex; align-items: center; gap: 0.75rem;"> |
| 64 | + {#if isReplicate} |
| 65 | + <span style="color: var(--muted-foreground); font-size: 11px;">↳</span> |
| 66 | + {:else if session.fork_from} |
| 67 | + <span style="color: var(--muted-foreground);">↳</span> |
| 68 | + {/if} |
| 69 | + <span style="color: {isReplicate ? 'var(--muted-foreground)' : 'var(--foreground)'};"> |
| 70 | + {#if isReplicate} |
| 71 | + r{String(session.replicate).padStart(2, "0")} |
| 72 | + {:else} |
| 73 | + session_{session.session_index}{#if session.replicate != null}<span style="color: var(--muted-foreground);"> r{String(session.replicate).padStart(2, "0")}</span>{/if} |
| 74 | + {/if} |
| 75 | + </span> |
| 76 | + {#if !isReplicate && session.fork_from} |
| 77 | + <span style="color: var(--muted-foreground); font-size: 11px;">fork↑{session.fork_from}</span> |
| 78 | + {/if} |
| 79 | + {#if session.error} |
| 80 | + <span style="color: var(--term-red); font-size: 11px;">[error]</span> |
| 81 | + {/if} |
| 82 | + {#if session.compaction_count > 0} |
| 83 | + <span style="color: var(--term-dim); font-size: 11px;">{session.compaction_count} compact</span> |
| 84 | + {/if} |
| 85 | + {#if session.subagent_count > 0} |
| 86 | + <span style="color: var(--term-dim); font-size: 11px;">{session.subagent_count} subagent{session.subagent_count > 1 ? "s" : ""}</span> |
| 87 | + {/if} |
| 88 | + </div> |
| 89 | + <div style="display: flex; align-items: center; gap: 1.5rem; color: var(--muted-foreground);"> |
| 90 | + <span>{session.step_count} steps</span> |
| 91 | + <span>{session.tool_call_count} tools</span> |
| 92 | + <span>{session.num_turns} turns</span> |
| 93 | + <span style="color: var(--term-amber);">{formatCost(session.total_cost_usd)}</span> |
| 94 | + <span>{formatDuration(session.started_at, session.finished_at)}</span> |
| 95 | + <span class="arrow" style="opacity: 0.3;">→</span> |
| 96 | + </div> |
| 97 | + </a> |
| 98 | +{/snippet} |
| 99 | + |
| 100 | +<!-- Metrics --> |
| 101 | +<div style="font-size: 12px; color: var(--muted-foreground); margin-bottom: 1.5rem; display: flex; flex-wrap: wrap; gap: 1.5rem;"> |
| 102 | + {#each metrics as m} |
| 103 | + <span> |
| 104 | + <span style="color: var(--muted-foreground);">{m.label}=</span><span style="color: var(--foreground);">{m.value}</span> |
| 105 | + </span> |
| 106 | + {/each} |
| 107 | +</div> |
| 108 | + |
| 109 | +<!-- Sessions list --> |
| 110 | +<div style="font-size: 11px; color: var(--muted-foreground); margin-bottom: 0.5rem; letter-spacing: 0.08em;">SESSIONS</div> |
| 111 | +<div style="display: flex; flex-direction: column; border: 1px solid var(--border);"> |
| 112 | + {#each sessionGroups as group, gi} |
| 113 | + {@render sessionRow(group.base, false, gi > 0)} |
| 114 | + {#each group.replicates as rep} |
| 115 | + {@render sessionRow(rep, true, true)} |
| 116 | + {/each} |
| 117 | + {/each} |
| 118 | +</div> |
| 119 | + |
| 120 | +<style> |
| 121 | + .session-row:hover { |
| 122 | + background: var(--card); |
| 123 | + } |
| 124 | + .session-row:hover .arrow { |
| 125 | + opacity: 1; |
| 126 | + color: var(--term-green); |
| 127 | + } |
| 128 | +</style> |
0 commit comments