@phnx-labs/agents-cli 1.22.45 → 1.22.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +303 -0
- package/LICENSE +102 -182
- package/README.md +54 -17
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.js +56 -12
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +229 -0
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +266 -15
- package/dist/commands/config.js +26 -4
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +55 -38
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/lease.js +3 -3
- package/dist/commands/routines.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +194 -16
- package/dist/commands/teams.js +34 -2
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +19 -32
- package/dist/commands/watchdog.js +9 -14
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +92 -6
- package/dist/lib/accounting/usage.js +329 -75
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +20 -7
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +33 -7
- package/dist/lib/browser/profiles.d.ts +106 -1
- package/dist/lib/browser/profiles.js +300 -17
- package/dist/lib/browser/remote-control.d.ts +26 -2
- package/dist/lib/browser/remote-control.js +33 -3
- package/dist/lib/browser/runtime-state.d.ts +9 -0
- package/dist/lib/browser/runtime-state.js +24 -8
- package/dist/lib/browser/service.d.ts +37 -0
- package/dist/lib/browser/service.js +78 -20
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/cli-resources.js +17 -15
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +17 -7
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon-ticks.js +2 -2
- package/dist/lib/device-config.d.ts +25 -8
- package/dist/lib/device-config.js +90 -17
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/harness-inventory.js +20 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +6 -0
- package/dist/lib/devices/stats-cache.js +10 -2
- package/dist/lib/exec.d.ts +25 -3
- package/dist/lib/exec.js +75 -22
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- package/dist/lib/hooks/cache.js +8 -0
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/passthrough.js +16 -0
- package/dist/lib/hosts/registry.js +7 -0
- package/dist/lib/identity/client.d.ts +53 -0
- package/dist/lib/identity/client.js +106 -0
- package/dist/lib/identity/index.d.ts +115 -0
- package/dist/lib/identity/index.js +82 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +13 -0
- package/dist/lib/menubar/install-menubar.js +26 -2
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/probe.d.ts +8 -0
- package/dist/lib/probe.js +105 -0
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +69 -1
- package/dist/lib/session/db.js +4 -2
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +12 -6
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +6 -1
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.js +5 -1
- package/dist/lib/session/trajectory-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +15 -0
- package/dist/lib/session/trajectory-html.js +750 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +277 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +335 -0
- package/dist/lib/startup/command-registry.d.ts +5 -2
- package/dist/lib/startup/command-registry.js +8 -5
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +4 -2
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +173 -27
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +17 -3
- package/dist/lib/usage-refresh.js +57 -11
- package/dist/lib/view-types.d.ts +2 -2
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/package.json +2 -2
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a {@link SessionTrajectory} as ONE self-contained HTML page for
|
|
3
|
+
* `agents sessions trace` (single-session layout).
|
|
4
|
+
*
|
|
5
|
+
* Self-contained on purpose, exactly like `share-html.ts`: an inline `<style>`,
|
|
6
|
+
* an inline SVG waterfall, no external asset, no CDN, no web font, no
|
|
7
|
+
* `artifacts-cli` dependency. The page is safe to open on any box or hand to a
|
|
8
|
+
* person. All transcript-derived text is escaped with `escapeHtml`, and the
|
|
9
|
+
* labels are already secret-redacted upstream in `buildTrajectory`.
|
|
10
|
+
*
|
|
11
|
+
* Terminal-coded per the agents-cli brand (#0a0a0a bg, #a3e635 lime accent,
|
|
12
|
+
* JetBrains Mono), light theme under `prefers-color-scheme: light`, with an
|
|
13
|
+
* in-page toggle — the same shell `share-html.ts` ships.
|
|
14
|
+
*/
|
|
15
|
+
import { formatDuration, formatTokenCount } from './render.js';
|
|
16
|
+
import { escapeHtml } from './share-html.js';
|
|
17
|
+
/** Color a tool bar by family; an error outcome overrides to red. */
|
|
18
|
+
function toolColor(step) {
|
|
19
|
+
if (step.outcome === 'error')
|
|
20
|
+
return '#f87171';
|
|
21
|
+
if (step.kind === 'thinking')
|
|
22
|
+
return '#3a3a55';
|
|
23
|
+
const tool = (step.tool ?? '').toLowerCase();
|
|
24
|
+
if (tool === 'bash' || tool === 'shell' || tool.includes('exec') || tool === 'run_command')
|
|
25
|
+
return '#e0b341';
|
|
26
|
+
if (tool === 'read' || tool === 'grep' || tool === 'glob' || tool === 'search' || tool === 'codebase_search')
|
|
27
|
+
return '#4a9eff';
|
|
28
|
+
if (tool === 'edit' || tool === 'write' || tool === 'notebookedit' || tool === 'multiedit')
|
|
29
|
+
return '#7ee787';
|
|
30
|
+
if (tool === 'task' || tool === 'agent')
|
|
31
|
+
return '#b98cff';
|
|
32
|
+
return '#8b98a5';
|
|
33
|
+
}
|
|
34
|
+
/** Precise per-step duration for the waterfall: "8m04s", "1.6s", "320ms". */
|
|
35
|
+
function formatStepDuration(ms) {
|
|
36
|
+
if (ms <= 0)
|
|
37
|
+
return '0s';
|
|
38
|
+
if (ms < 1000)
|
|
39
|
+
return `${Math.round(ms)}ms`;
|
|
40
|
+
if (ms < 60_000)
|
|
41
|
+
return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`;
|
|
42
|
+
const totalSec = Math.round(ms / 1000);
|
|
43
|
+
const m = Math.floor(totalSec / 60);
|
|
44
|
+
const s = totalSec % 60;
|
|
45
|
+
return `${m}m${String(s).padStart(2, '0')}s`;
|
|
46
|
+
}
|
|
47
|
+
/** Axis tick labels in minutes across the span (used by the compare lanes). */
|
|
48
|
+
function axisTicks(spanMs, count = 4) {
|
|
49
|
+
if (spanMs <= 0)
|
|
50
|
+
return [{ frac: 0, label: '0m' }];
|
|
51
|
+
const ticks = [];
|
|
52
|
+
for (let i = 0; i <= count; i++) {
|
|
53
|
+
const frac = i / count;
|
|
54
|
+
const min = (spanMs * frac) / 60_000;
|
|
55
|
+
ticks.push({ frac, label: min >= 1 ? `${Math.round(min)}m` : `${Math.round((spanMs * frac) / 1000)}s` });
|
|
56
|
+
}
|
|
57
|
+
return ticks;
|
|
58
|
+
}
|
|
59
|
+
/** Lane geometry for the compare waterfall (px). */
|
|
60
|
+
const GEO = { labelW: 84, chartW: 620 };
|
|
61
|
+
function clipLabel(label) {
|
|
62
|
+
return label.length <= 46 ? label : `${label.slice(0, 45)}…`;
|
|
63
|
+
}
|
|
64
|
+
function renderTimeShare(model) {
|
|
65
|
+
// Program-keyed share straight off the model — the one source the text panel
|
|
66
|
+
// reads too (a Bash-heavy session reads as git/agents/gh, not "Bash").
|
|
67
|
+
const entries = Object.entries(model.programTimeShare).sort((a, b) => b[1] - a[1]).slice(0, 8);
|
|
68
|
+
if (entries.length === 0)
|
|
69
|
+
return '<p class="muted">No measured tool time.</p>';
|
|
70
|
+
return entries.map(([tag, share]) => {
|
|
71
|
+
const pct = Math.round(share * 100);
|
|
72
|
+
const color = toolColor({ ordinal: 0, kind: 'tool', tool: tag, program: tag, lane: tag, startMs: 0, durationMs: 0, durationEstimated: false, label: tag });
|
|
73
|
+
return `<div class="share-row"><span class="share-name">${escapeHtml(tag)}</span>` +
|
|
74
|
+
`<span class="share-bar"><span class="share-fill" style="width:${pct}%;background:${color}"></span></span>` +
|
|
75
|
+
`<span class="share-pct">${pct}%</span></div>`;
|
|
76
|
+
}).join('\n');
|
|
77
|
+
}
|
|
78
|
+
/** The badge label for a step — the effective program for a shell call, else the tool. */
|
|
79
|
+
function stepBadge(step) {
|
|
80
|
+
return step.program ?? step.tool ?? step.kind;
|
|
81
|
+
}
|
|
82
|
+
/** A duration heat class: fast (<1s), mid (<15s), slow, or error. */
|
|
83
|
+
function durClass(step) {
|
|
84
|
+
if (step.outcome === 'error')
|
|
85
|
+
return 'err';
|
|
86
|
+
if (step.durationMs < 1_000)
|
|
87
|
+
return 'fast';
|
|
88
|
+
if (step.durationMs < 15_000)
|
|
89
|
+
return 'mid';
|
|
90
|
+
return 'slow';
|
|
91
|
+
}
|
|
92
|
+
function renderStepDetail(model) {
|
|
93
|
+
// Gaps are interleaved between steps as dividers, keyed by the step they follow.
|
|
94
|
+
const gapsAfter = new Map();
|
|
95
|
+
for (const g of model.gaps)
|
|
96
|
+
gapsAfter.set(g.afterOrdinal, g.durationMs);
|
|
97
|
+
const parts = [];
|
|
98
|
+
const leading = gapsAfter.get(0);
|
|
99
|
+
if (leading)
|
|
100
|
+
parts.push(`<div class="gap-divider">··· idle ${escapeHtml(formatStepDuration(leading))} ···</div>`);
|
|
101
|
+
for (const step of model.steps) {
|
|
102
|
+
const color = toolColor(step);
|
|
103
|
+
const badge = escapeHtml(stepBadge(step));
|
|
104
|
+
const exit = step.exitCode !== undefined && step.exitCode !== 0
|
|
105
|
+
? `<span class="exit">exit ${step.exitCode}</span>` : '';
|
|
106
|
+
const mark = step.outcome === 'error' ? '<span class="x">✗</span>' : '';
|
|
107
|
+
const est = step.durationEstimated ? '<span class="est">~</span>' : '';
|
|
108
|
+
const delegation = step.delegation ? `<span class="tag">${escapeHtml(step.delegation)}</span>` : '';
|
|
109
|
+
const tokens = step.outputTokens && step.outputTokens > 1000 ? `<span class="tok">${formatTokenCount(step.outputTokens)}</span>` : '';
|
|
110
|
+
const hasDetail = Boolean(step.detail);
|
|
111
|
+
const head = `<span class="step-ord">${step.ordinal}</span>` +
|
|
112
|
+
`<span class="badge" style="background:${color}">${badge}</span>` +
|
|
113
|
+
`<span class="step-label">${escapeHtml(step.label)}</span>` +
|
|
114
|
+
`${tokens}${delegation}` +
|
|
115
|
+
`<span class="step-dur ${durClass(step)}">${escapeHtml(formatStepDuration(step.durationMs))}${est}${exit}${mark}</span>`;
|
|
116
|
+
if (hasDetail) {
|
|
117
|
+
parts.push(`<details class="step ${step.outcome === 'error' ? 'error' : ''}" id="step-${step.ordinal}"><summary>${head}</summary><pre class="detail">${escapeHtml(step.detail)}</pre></details>`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
parts.push(`<div class="step ${step.outcome === 'error' ? 'error' : ''}" id="step-${step.ordinal}"><div class="row">${head}</div></div>`);
|
|
121
|
+
}
|
|
122
|
+
const gap = gapsAfter.get(step.ordinal);
|
|
123
|
+
if (gap)
|
|
124
|
+
parts.push(`<div class="gap-divider">··· idle ${escapeHtml(formatStepDuration(gap))} ···</div>`);
|
|
125
|
+
}
|
|
126
|
+
return parts.join('\n');
|
|
127
|
+
}
|
|
128
|
+
/** The command/program mix — Bash broken down by program. Count-based histogram. */
|
|
129
|
+
function renderProgramMix(model) {
|
|
130
|
+
const counts = new Map();
|
|
131
|
+
for (const step of model.steps) {
|
|
132
|
+
if (step.kind !== 'tool')
|
|
133
|
+
continue;
|
|
134
|
+
const tag = stepBadge(step);
|
|
135
|
+
counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
136
|
+
}
|
|
137
|
+
const entries = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
138
|
+
if (entries.length === 0)
|
|
139
|
+
return '';
|
|
140
|
+
const max = entries[0][1];
|
|
141
|
+
return entries.map(([tag, n]) => {
|
|
142
|
+
const color = toolColor({ ordinal: 0, kind: 'tool', tool: tag, program: tag, lane: tag, startMs: 0, durationMs: 0, durationEstimated: false, label: tag });
|
|
143
|
+
return `<div class="mix-row"><span class="mix-name">${escapeHtml(tag)}</span>` +
|
|
144
|
+
`<span class="mix-bar"><span class="mix-fill" style="width:${Math.round(n / max * 100)}%;background:${color}"></span></span>` +
|
|
145
|
+
`<span class="mix-n">${n}</span></div>`;
|
|
146
|
+
}).join('\n');
|
|
147
|
+
}
|
|
148
|
+
/** The slowest steps — the debugging signal for "where did the time go". */
|
|
149
|
+
function renderSlowest(model) {
|
|
150
|
+
const slowest = [...model.steps].filter((s) => s.durationMs > 0).sort((a, b) => b.durationMs - a.durationMs).slice(0, 6);
|
|
151
|
+
if (slowest.length === 0)
|
|
152
|
+
return '';
|
|
153
|
+
return slowest.map((s) => {
|
|
154
|
+
const color = toolColor(s);
|
|
155
|
+
return `<div class="slow-row"><span class="slow-ord">${s.ordinal}</span>` +
|
|
156
|
+
`<span class="badge sm" style="background:${color}">${escapeHtml(stepBadge(s))}</span>` +
|
|
157
|
+
`<span class="slow-label">${escapeHtml(clipLabel(s.label))}</span>` +
|
|
158
|
+
`<span class="slow-dur ${durClass(s)}">${escapeHtml(formatStepDuration(s.durationMs))}</span></div>`;
|
|
159
|
+
}).join('\n');
|
|
160
|
+
}
|
|
161
|
+
/** Render one session's trajectory as a self-contained HTML page. */
|
|
162
|
+
export function renderTrajectoryHtml(model) {
|
|
163
|
+
const { session, stats } = model;
|
|
164
|
+
const title = `${session.agent} · ${session.shortId || session.id}`;
|
|
165
|
+
const model2 = session.model ? escapeHtml(session.model) : '';
|
|
166
|
+
const turns = stats.userTurns + stats.assistantTurns;
|
|
167
|
+
const metrics = [];
|
|
168
|
+
metrics.push(`${formatDuration(model.spanMs)}`);
|
|
169
|
+
metrics.push(`${stats.toolCount} tool${stats.toolCount === 1 ? '' : 's'}`);
|
|
170
|
+
if (model.errorCount > 0)
|
|
171
|
+
metrics.push(`${model.errorCount} error${model.errorCount === 1 ? '' : 's'}`);
|
|
172
|
+
if (stats.outputTokens > 0)
|
|
173
|
+
metrics.push(`${formatTokenCount(stats.outputTokens)} out`);
|
|
174
|
+
if (session.costUsd)
|
|
175
|
+
metrics.push(`$${session.costUsd.toFixed(2)}`);
|
|
176
|
+
metrics.push(`${turns} turn${turns === 1 ? '' : 's'}`);
|
|
177
|
+
const metricLine = metrics.map((m) => escapeHtml(m)).join(' · ');
|
|
178
|
+
const chips = [];
|
|
179
|
+
if (session.project)
|
|
180
|
+
chips.push(`<span class="chip"><span class="k">project</span>${escapeHtml(session.project)}</span>`);
|
|
181
|
+
if (session.mode)
|
|
182
|
+
chips.push(`<span class="chip"><span class="k">mode</span>${escapeHtml(session.mode)}</span>`);
|
|
183
|
+
if (session.gitBranch)
|
|
184
|
+
chips.push(`<span class="chip"><span class="k">branch</span>${escapeHtml(session.gitBranch)}</span>`);
|
|
185
|
+
if (session.ticketId)
|
|
186
|
+
chips.push(`<span class="chip"><span class="k">ticket</span>${escapeHtml(session.ticketId)}</span>`);
|
|
187
|
+
const date = (session.timestamp || '').slice(0, 10);
|
|
188
|
+
if (date)
|
|
189
|
+
chips.push(`<span class="chip"><span class="k">date</span>${escapeHtml(date)}</span>`);
|
|
190
|
+
const idleTotal = model.gaps.reduce((sum, g) => sum + g.durationMs, 0);
|
|
191
|
+
const longestGap = model.gaps.length > 0 ? Math.max(...model.gaps.map((g) => g.durationMs)) : 0;
|
|
192
|
+
const gapNote = model.gaps.length > 0
|
|
193
|
+
? `<p class="stall">${model.gaps.length} idle gap${model.gaps.length === 1 ? '' : 's'} — longest ${escapeHtml(formatStepDuration(longestGap))}.</p>`
|
|
194
|
+
: '';
|
|
195
|
+
const truncNote = model.truncatedSteps > 0
|
|
196
|
+
? `<p class="stall">Showing the first ${model.steps.length} steps; ${model.truncatedSteps} later step${model.truncatedSteps === 1 ? '' : 's'} collapsed.</p>`
|
|
197
|
+
: '';
|
|
198
|
+
return `<!DOCTYPE html>
|
|
199
|
+
<html lang="en" data-theme="auto">
|
|
200
|
+
<head>
|
|
201
|
+
<meta charset="utf-8" />
|
|
202
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
203
|
+
<meta name="robots" content="noindex" />
|
|
204
|
+
<title>${escapeHtml(title)} — trajectory</title>
|
|
205
|
+
<style>${BASE_STYLE}</style>
|
|
206
|
+
</head>
|
|
207
|
+
<body>
|
|
208
|
+
<header>
|
|
209
|
+
<div class="inner">
|
|
210
|
+
<button class="toggle" id="theme" title="Toggle light and dark">◙</button>
|
|
211
|
+
<div class="mark">agents session trace</div>
|
|
212
|
+
<h1>${escapeHtml(title)}${model2 ? ` <span class="muted">${model2}</span>` : ''}</h1>
|
|
213
|
+
<div class="metrics">${metricLine}</div>
|
|
214
|
+
<div class="chips">
|
|
215
|
+
${chips.join('\n ')}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</header>
|
|
219
|
+
<main>
|
|
220
|
+
<section class="analysis">
|
|
221
|
+
<div class="card">
|
|
222
|
+
<h3>Where the time went</h3>
|
|
223
|
+
${renderTimeShare(model)}
|
|
224
|
+
<h3 class="mt">Slowest steps</h3>
|
|
225
|
+
${renderSlowest(model) || '<p class="muted">No measured steps.</p>'}
|
|
226
|
+
</div>
|
|
227
|
+
<div class="card">
|
|
228
|
+
<h3>Command mix</h3>
|
|
229
|
+
${renderProgramMix(model) || '<p class="muted">No tool calls.</p>'}
|
|
230
|
+
<div class="kpis">
|
|
231
|
+
<div class="kpi"><div class="v ${model.errorCount ? 'bad' : ''}">${model.errorCount}</div><div class="l">errors</div></div>
|
|
232
|
+
<div class="kpi"><div class="v warn">${escapeHtml(idleTotal > 0 ? formatStepDuration(idleTotal) : '0')}</div><div class="l">idle total</div></div>
|
|
233
|
+
<div class="kpi"><div class="v">${escapeHtml(longestGap > 0 ? formatStepDuration(longestGap) : '0')}</div><div class="l">longest gap</div></div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</section>
|
|
237
|
+
<h2>Trajectory · ${model.steps.length} step${model.steps.length === 1 ? '' : 's'}, execution order</h2>
|
|
238
|
+
${gapNote}
|
|
239
|
+
${truncNote}
|
|
240
|
+
<div class="steps">
|
|
241
|
+
${renderStepDetail(model)}
|
|
242
|
+
</div>
|
|
243
|
+
</main>
|
|
244
|
+
<footer>
|
|
245
|
+
${model.truncatedSteps > 0 ? 'Truncated · ' : ''}${model.redacted ? 'Secret-redacted' : 'Unredacted (local only)'} trajectory rendered by agents-cli · <code>agents sessions trace</code>
|
|
246
|
+
</footer>
|
|
247
|
+
<script>${THEME_SCRIPT}</script>
|
|
248
|
+
</body>
|
|
249
|
+
</html>
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
const BASE_STYLE = `
|
|
253
|
+
:root {
|
|
254
|
+
--bg: #0a0a0a; --panel: #121212; --border: #262626; --fg: #e5e5e5;
|
|
255
|
+
--dim: #737373; --accent: #a3e635; --quote: #1a1a1a;
|
|
256
|
+
}
|
|
257
|
+
html[data-theme="light"] {
|
|
258
|
+
--bg: #fafafa; --panel: #ffffff; --border: #e5e5e5; --fg: #171717;
|
|
259
|
+
--dim: #737373; --accent: #4d7c0f; --quote: #f5f5f5;
|
|
260
|
+
}
|
|
261
|
+
@media (prefers-color-scheme: light) {
|
|
262
|
+
html[data-theme="auto"] {
|
|
263
|
+
--bg: #fafafa; --panel: #ffffff; --border: #e5e5e5; --fg: #171717;
|
|
264
|
+
--dim: #737373; --accent: #4d7c0f; --quote: #f5f5f5;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
* { box-sizing: border-box; }
|
|
268
|
+
body {
|
|
269
|
+
margin: 0; background: var(--bg); color: var(--fg);
|
|
270
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Inter, sans-serif;
|
|
271
|
+
font-size: 14px; line-height: 1.6;
|
|
272
|
+
}
|
|
273
|
+
header { border-bottom: 1px solid var(--border); padding: 24px 20px 18px; }
|
|
274
|
+
header .inner, main { max-width: 960px; margin: 0 auto; }
|
|
275
|
+
header .mark {
|
|
276
|
+
color: var(--accent); font-weight: 700; letter-spacing: .5px; font-size: 12px;
|
|
277
|
+
text-transform: uppercase; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
|
|
278
|
+
}
|
|
279
|
+
header h1 { font-size: 20px; line-height: 1.3; margin: 8px 0 8px; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; }
|
|
280
|
+
.metrics { color: var(--dim); font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 13px; margin-bottom: 12px; }
|
|
281
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
282
|
+
.chip {
|
|
283
|
+
font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 11px;
|
|
284
|
+
color: var(--fg); background: var(--panel); border: 1px solid var(--border);
|
|
285
|
+
border-radius: 10px; padding: 2px 9px;
|
|
286
|
+
}
|
|
287
|
+
.chip .k { color: var(--dim); margin-right: 6px; }
|
|
288
|
+
.toggle {
|
|
289
|
+
float: right; cursor: pointer; background: none; border: 1px solid var(--border);
|
|
290
|
+
color: var(--dim); border-radius: 6px; padding: 2px 8px; font-size: 14px;
|
|
291
|
+
}
|
|
292
|
+
main { padding: 20px 20px 64px; }
|
|
293
|
+
h2 {
|
|
294
|
+
font-size: 12px; color: var(--accent); border-bottom: 1px solid var(--border);
|
|
295
|
+
padding-bottom: 6px; margin: 32px 0 14px; text-transform: uppercase;
|
|
296
|
+
letter-spacing: 1px; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
|
|
297
|
+
}
|
|
298
|
+
.stall { color: #e0b341; font-size: 12.5px; margin: 6px 0; }
|
|
299
|
+
.muted { color: var(--dim); }
|
|
300
|
+
svg text { font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; }
|
|
301
|
+
svg .axis { stroke: var(--border); stroke-width: 1; }
|
|
302
|
+
svg .tick { fill: var(--dim); font-size: 8px; }
|
|
303
|
+
svg .lane { fill: var(--dim); font-size: 9px; }
|
|
304
|
+
svg .bar-label { fill: var(--dim); font-size: 8.5px; }
|
|
305
|
+
svg .bar-dur { fill: var(--fg); }
|
|
306
|
+
svg .gap { fill: #7a3030; fill-opacity: 0.12; }
|
|
307
|
+
svg .gap-label { fill: #c06a6a; font-size: 8px; }
|
|
308
|
+
svg a { cursor: pointer; }
|
|
309
|
+
.share-row { display: flex; align-items: center; gap: 10px; margin: 5px 0; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12px; }
|
|
310
|
+
.share-name { width: 120px; color: var(--fg); }
|
|
311
|
+
.share-bar { flex: 1; height: 9px; background: var(--panel); border: 1px solid var(--border); border-radius: 5px; overflow: hidden; }
|
|
312
|
+
.share-fill { display: block; height: 100%; }
|
|
313
|
+
.share-pct { width: 44px; text-align: right; color: var(--dim); }
|
|
314
|
+
.steps { display: flex; flex-direction: column; gap: 4px; }
|
|
315
|
+
.step { border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; background: var(--panel); scroll-margin-top: 16px; }
|
|
316
|
+
.step:target { border-color: var(--accent); }
|
|
317
|
+
.step-head { display: flex; align-items: center; gap: 8px; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12px; flex-wrap: wrap; }
|
|
318
|
+
.dot { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
|
|
319
|
+
.step-ord { color: var(--dim); width: 28px; }
|
|
320
|
+
.step-tool { color: var(--fg); font-weight: 600; }
|
|
321
|
+
.step-dur { color: var(--dim); }
|
|
322
|
+
.outcome { font-size: 11px; padding: 0 6px; border-radius: 8px; }
|
|
323
|
+
.outcome.ok { color: #7ee787; }
|
|
324
|
+
.outcome.error { color: #f87171; }
|
|
325
|
+
.est { color: #e0b341; font-size: 11px; }
|
|
326
|
+
.tag { color: #b98cff; font-size: 11px; }
|
|
327
|
+
.step-label { margin-top: 4px; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12px; color: var(--fg); word-break: break-word; }
|
|
328
|
+
pre.detail {
|
|
329
|
+
margin: 6px 0 0; padding: 8px 10px; background: var(--bg); border: 1px solid var(--border);
|
|
330
|
+
border-radius: 4px; overflow-x: auto; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
|
|
331
|
+
font-size: 11.5px; color: var(--dim); white-space: pre-wrap; word-break: break-word;
|
|
332
|
+
}
|
|
333
|
+
footer {
|
|
334
|
+
max-width: 960px; margin: 0 auto; padding: 0 20px 48px;
|
|
335
|
+
color: var(--dim); font-size: 12px;
|
|
336
|
+
font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
|
|
337
|
+
}
|
|
338
|
+
`;
|
|
339
|
+
/** Compare-only rules layered on top of {@link BASE_STYLE} — lanes, divergence marker, summary table. */
|
|
340
|
+
const COMPARE_STYLE = `
|
|
341
|
+
svg .diverge { stroke: #e0b341; stroke-width: 1.4; stroke-dasharray: 4 3; }
|
|
342
|
+
.diverge-note { color: #e0b341; font-size: 12.5px; margin: 6px 0; }
|
|
343
|
+
table.cmp-table { border-collapse: collapse; width: 100%; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12.5px; }
|
|
344
|
+
table.cmp-table th, table.cmp-table td { text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--border); }
|
|
345
|
+
table.cmp-table th { color: var(--dim); font-weight: 600; text-transform: uppercase; font-size: 10.5px; letter-spacing: .5px; }
|
|
346
|
+
.diff-cols { display: flex; gap: 24px; flex-wrap: wrap; }
|
|
347
|
+
.diff-col { flex: 1; min-width: 260px; }
|
|
348
|
+
.diff-col h3 { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: .5px; margin: 0 0 8px; }
|
|
349
|
+
.diff-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
350
|
+
.diff-list li {
|
|
351
|
+
font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12px;
|
|
352
|
+
border: 1px solid var(--border); border-radius: 5px; padding: 5px 8px; background: var(--panel);
|
|
353
|
+
}
|
|
354
|
+
/* trace v2 — analysis hero + program-aware step list */
|
|
355
|
+
main { padding: 20px; }
|
|
356
|
+
.analysis { display: grid; grid-template-columns: 1.25fr 1fr; gap: 20px; margin-bottom: 26px; }
|
|
357
|
+
@media (max-width: 720px) { .analysis { grid-template-columns: 1fr; } }
|
|
358
|
+
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; }
|
|
359
|
+
.card h3 { margin: 0 0 10px; font-size: 10.5px; color: var(--dim); text-transform: uppercase; letter-spacing: .1em; font-weight: 600; }
|
|
360
|
+
.card h3.mt { margin-top: 16px; }
|
|
361
|
+
.mono, .badge, .step-dur, .mix-row, .slow-row { font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; }
|
|
362
|
+
.mix-row { display: grid; grid-template-columns: 84px 1fr 34px; align-items: center; gap: 8px; margin: 4px 0; font-size: 12px; }
|
|
363
|
+
.mix-name { color: var(--dim); overflow: hidden; text-overflow: ellipsis; }
|
|
364
|
+
.mix-bar { height: 8px; background: var(--bg); border-radius: 4px; overflow: hidden; }
|
|
365
|
+
.mix-fill { display: block; height: 100%; min-width: 2px; }
|
|
366
|
+
.mix-n { text-align: right; color: var(--fg); font-size: 11px; }
|
|
367
|
+
.slow-row { display: grid; grid-template-columns: 30px 64px 1fr auto; align-items: center; gap: 8px; margin: 5px 0; font-size: 12px; }
|
|
368
|
+
.slow-ord { color: var(--dim); } .slow-label { color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
369
|
+
.kpis { display: flex; gap: 20px; margin-top: 14px; }
|
|
370
|
+
.kpi .v { font-size: 20px; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; }
|
|
371
|
+
.kpi .v.bad { color: #f87171; } .kpi .v.warn { color: #e0b341; }
|
|
372
|
+
.kpi .l { font-size: 9.5px; color: var(--dim); text-transform: uppercase; letter-spacing: .06em; }
|
|
373
|
+
.badge { color: #0a0a0a; font-weight: 600; font-size: 10px; text-align: center; padding: 2px 7px; border-radius: 4px; white-space: nowrap; display: inline-block; }
|
|
374
|
+
.badge.sm { font-size: 9.5px; padding: 1px 5px; }
|
|
375
|
+
.steps { display: flex; flex-direction: column; gap: 2px; }
|
|
376
|
+
details.step, .step { border: 0; border-radius: 7px; padding: 0; background: transparent; scroll-margin-top: 16px; }
|
|
377
|
+
.step .row, details.step > summary {
|
|
378
|
+
display: grid; grid-template-columns: 36px 78px minmax(0,1fr) auto 64px; gap: 10px; align-items: center;
|
|
379
|
+
padding: 6px 10px; border-radius: 7px; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12px;
|
|
380
|
+
}
|
|
381
|
+
details.step > summary { cursor: pointer; list-style: none; }
|
|
382
|
+
details.step > summary::-webkit-details-marker { display: none; }
|
|
383
|
+
.step .row:hover, details.step > summary:hover { background: var(--panel); }
|
|
384
|
+
.step.error > summary, .step.error .row { background: rgba(179,64,63,.14); border-left: 2px solid #b3403f; }
|
|
385
|
+
.step-ord { color: var(--dim); }
|
|
386
|
+
.step-label { color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
387
|
+
.step-dur { text-align: right; font-size: 12px; }
|
|
388
|
+
.step-dur.fast { color: var(--dim); } .step-dur.mid { color: #e0b341; } .step-dur.slow, .step-dur.err { color: #f87171; }
|
|
389
|
+
.exit { color: #f87171; margin-left: 6px; } .x { color: #f87171; margin-left: 6px; } .est { color: var(--dim); margin-left: 3px; }
|
|
390
|
+
.tok { color: var(--dim); font-size: 10px; }
|
|
391
|
+
.gap-divider { text-align: center; color: #7a5c3a; font-size: 11px; margin: 8px 0; letter-spacing: .06em; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; }
|
|
392
|
+
details.step .detail { white-space: pre-wrap; color: var(--dim); font-size: 11px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; margin: 2px 0 8px 54px; max-height: 220px; overflow: auto; }
|
|
393
|
+
`;
|
|
394
|
+
const THEME_SCRIPT = `
|
|
395
|
+
(function () {
|
|
396
|
+
var root = document.documentElement;
|
|
397
|
+
var saved = null;
|
|
398
|
+
try { saved = localStorage.getItem('agents-share-theme'); } catch (e) {}
|
|
399
|
+
if (saved) root.setAttribute('data-theme', saved);
|
|
400
|
+
var toggle = document.getElementById('theme');
|
|
401
|
+
if (toggle) toggle.addEventListener('click', function () {
|
|
402
|
+
var dark = getComputedStyle(root).getPropertyValue('--bg').trim() === '#0a0a0a';
|
|
403
|
+
var next = dark ? 'light' : 'dark';
|
|
404
|
+
root.setAttribute('data-theme', next);
|
|
405
|
+
try { localStorage.setItem('agents-share-theme', next); } catch (e) {}
|
|
406
|
+
});
|
|
407
|
+
})();
|
|
408
|
+
`;
|
|
409
|
+
function compareTitle(cmp) {
|
|
410
|
+
const a = cmp.a.session;
|
|
411
|
+
const b = cmp.b.session;
|
|
412
|
+
return `${a.agent} ${a.shortId || a.id} vs ${b.agent} ${b.shortId || b.id}`;
|
|
413
|
+
}
|
|
414
|
+
function renderCompareWaterfallSvg(cmp) {
|
|
415
|
+
const { a, b, divergence } = cmp;
|
|
416
|
+
const sharedSpan = Math.max(a.spanMs, b.spanMs, 1);
|
|
417
|
+
const rowH = 26;
|
|
418
|
+
const top = 34;
|
|
419
|
+
const width = GEO.labelW + GEO.chartW + 40;
|
|
420
|
+
const height = top + 2 * rowH + 16;
|
|
421
|
+
const parts = [];
|
|
422
|
+
parts.push(`<svg viewBox="0 0 ${width} ${height}" width="100%" role="img" aria-label="Compare waterfall: two sessions' tool calls on a shared time axis with a divergence marker" xmlns="http://www.w3.org/2000/svg">`);
|
|
423
|
+
const axisY = top - 10;
|
|
424
|
+
parts.push(`<line x1="${GEO.labelW}" y1="${axisY}" x2="${GEO.labelW + GEO.chartW}" y2="${axisY}" class="axis" />`);
|
|
425
|
+
for (const tick of axisTicks(sharedSpan)) {
|
|
426
|
+
const x = GEO.labelW + tick.frac * GEO.chartW;
|
|
427
|
+
parts.push(`<text x="${x.toFixed(1)}" y="${axisY - 3}" class="tick">${escapeHtml(tick.label)}</text>`);
|
|
428
|
+
}
|
|
429
|
+
const lanes = [
|
|
430
|
+
{ session: a.session, steps: a.steps.filter((s) => s.kind === 'tool') },
|
|
431
|
+
{ session: b.session, steps: b.steps.filter((s) => s.kind === 'tool') },
|
|
432
|
+
];
|
|
433
|
+
lanes.forEach((lane, li) => {
|
|
434
|
+
const y = top + li * rowH;
|
|
435
|
+
const barY = y + 5;
|
|
436
|
+
const barH = rowH - 12;
|
|
437
|
+
const label = `${lane.session.agent} ${lane.session.shortId || lane.session.id}`;
|
|
438
|
+
parts.push(`<text x="${GEO.labelW - 6}" y="${y + rowH / 2 + 3}" class="lane" text-anchor="end">${escapeHtml(label)}</text>`);
|
|
439
|
+
for (const step of lane.steps) {
|
|
440
|
+
const x = GEO.labelW + Math.min(1, Math.max(0, step.startMs / sharedSpan)) * GEO.chartW;
|
|
441
|
+
const w = Math.max(3, (step.durationMs / sharedSpan) * GEO.chartW);
|
|
442
|
+
const color = toolColor(step);
|
|
443
|
+
const dur = formatStepDuration(step.durationMs);
|
|
444
|
+
parts.push(`<rect x="${x.toFixed(1)}" y="${barY}" width="${w.toFixed(1)}" height="${barH}" rx="2" fill="${color}"><title>${escapeHtml(label)} · step ${step.ordinal} · ${escapeHtml(step.tool ?? step.kind)} · ${escapeHtml(dur)}${step.outcome === 'error' ? ' ✗' : ''}</title></rect>`);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
if (divergence) {
|
|
448
|
+
const dxA = GEO.labelW + Math.min(1, Math.max(0, divergence.startMsA / sharedSpan)) * GEO.chartW;
|
|
449
|
+
const dxB = GEO.labelW + Math.min(1, Math.max(0, divergence.startMsB / sharedSpan)) * GEO.chartW;
|
|
450
|
+
const dx = Math.min(dxA, dxB);
|
|
451
|
+
parts.push(`<line x1="${dx.toFixed(1)}" y1="${top - 6}" x2="${dx.toFixed(1)}" y2="${top + 2 * rowH}" class="diverge"><title>diverge: ${escapeHtml(divergence.detail)}</title></line>`);
|
|
452
|
+
}
|
|
453
|
+
parts.push('</svg>');
|
|
454
|
+
return parts.join('\n');
|
|
455
|
+
}
|
|
456
|
+
function renderCompareSummaryTable(cmp) {
|
|
457
|
+
const rows = [cmp.summaryA, cmp.summaryB].map((s) => {
|
|
458
|
+
const label = `${s.session.agent} ${s.session.shortId || s.session.id}`;
|
|
459
|
+
return `<tr><td>${escapeHtml(label)}</td><td>${s.toolCount}</td><td>${s.errorCount}</td><td>${escapeHtml(formatDuration(s.spanMs))}</td><td>${escapeHtml(formatTokenCount(s.outputTokens))}</td></tr>`;
|
|
460
|
+
}).join('\n');
|
|
461
|
+
return `<table class="cmp-table"><thead><tr><th>session</th><th>tools</th><th>errors</th><th>duration</th><th>tokens</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
462
|
+
}
|
|
463
|
+
function renderStepListItem(step) {
|
|
464
|
+
const dur = formatStepDuration(step.durationMs);
|
|
465
|
+
return `<li>${escapeHtml(step.tool ?? step.kind)} · ${escapeHtml(clipLabel(step.label))} <span class="muted">${escapeHtml(dur)}</span></li>`;
|
|
466
|
+
}
|
|
467
|
+
function renderCompareDiffLists(cmp) {
|
|
468
|
+
const aLabel = `${cmp.a.session.agent} ${cmp.a.session.shortId || cmp.a.session.id}`;
|
|
469
|
+
const bLabel = `${cmp.b.session.agent} ${cmp.b.session.shortId || cmp.b.session.id}`;
|
|
470
|
+
const removedItems = cmp.removed.length > 0
|
|
471
|
+
? cmp.removed.map(renderStepListItem).join('\n')
|
|
472
|
+
: '<li class="muted">none</li>';
|
|
473
|
+
const addedItems = cmp.added.length > 0
|
|
474
|
+
? cmp.added.map(renderStepListItem).join('\n')
|
|
475
|
+
: '<li class="muted">none</li>';
|
|
476
|
+
return `<div class="diff-cols">
|
|
477
|
+
<div class="diff-col">
|
|
478
|
+
<h3>Only in ${escapeHtml(aLabel)} (${cmp.removed.length})</h3>
|
|
479
|
+
<ul class="diff-list">${removedItems}</ul>
|
|
480
|
+
</div>
|
|
481
|
+
<div class="diff-col">
|
|
482
|
+
<h3>Only in ${escapeHtml(bLabel)} (${cmp.added.length})</h3>
|
|
483
|
+
<ul class="diff-list">${addedItems}</ul>
|
|
484
|
+
</div>
|
|
485
|
+
</div>`;
|
|
486
|
+
}
|
|
487
|
+
/** Render a two-session {@link TrajectoryComparison} as a self-contained HTML page. */
|
|
488
|
+
export function renderTrajectoryCompareHtml(cmp) {
|
|
489
|
+
const title = compareTitle(cmp);
|
|
490
|
+
const divergenceNote = cmp.divergence
|
|
491
|
+
? `<p class="diverge-note">◆ diverge after step ${cmp.divergence.afterOrdinalA}/${cmp.divergence.afterOrdinalB} — ${escapeHtml(cmp.divergence.detail)}</p>`
|
|
492
|
+
: '<p class="muted">No divergence — the two tool sequences match.</p>';
|
|
493
|
+
const truncNote = (cmp.truncatedA > 0 || cmp.truncatedB > 0)
|
|
494
|
+
? `<p class="stall">Diff capped — ${cmp.truncatedA} step${cmp.truncatedA === 1 ? '' : 's'} from the first and ${cmp.truncatedB} from the second were not compared.</p>`
|
|
495
|
+
: '';
|
|
496
|
+
return `<!DOCTYPE html>
|
|
497
|
+
<html lang="en" data-theme="auto">
|
|
498
|
+
<head>
|
|
499
|
+
<meta charset="utf-8" />
|
|
500
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
501
|
+
<meta name="robots" content="noindex" />
|
|
502
|
+
<title>${escapeHtml(title)} — compare</title>
|
|
503
|
+
<style>${BASE_STYLE}${COMPARE_STYLE}</style>
|
|
504
|
+
</head>
|
|
505
|
+
<body>
|
|
506
|
+
<header>
|
|
507
|
+
<div class="inner">
|
|
508
|
+
<button class="toggle" id="theme" title="Toggle light and dark">◙</button>
|
|
509
|
+
<div class="mark">agents session trace · compare</div>
|
|
510
|
+
<h1>${escapeHtml(title)}</h1>
|
|
511
|
+
</div>
|
|
512
|
+
</header>
|
|
513
|
+
<main>
|
|
514
|
+
<h2>Trajectory</h2>
|
|
515
|
+
${truncNote}
|
|
516
|
+
${renderCompareWaterfallSvg(cmp)}
|
|
517
|
+
${divergenceNote}
|
|
518
|
+
<h2>Summary</h2>
|
|
519
|
+
${renderCompareSummaryTable(cmp)}
|
|
520
|
+
<h2>Step diff</h2>
|
|
521
|
+
${renderCompareDiffLists(cmp)}
|
|
522
|
+
</main>
|
|
523
|
+
<footer>
|
|
524
|
+
Secret-redacted compare rendered by agents-cli · <code>agents sessions trace</code>
|
|
525
|
+
</footer>
|
|
526
|
+
<script>${THEME_SCRIPT}</script>
|
|
527
|
+
</body>
|
|
528
|
+
</html>
|
|
529
|
+
`;
|
|
530
|
+
}
|
|
531
|
+
/** Lineage-only rules layered on top of {@link BASE_STYLE} — the node graph and its cards. */
|
|
532
|
+
const LINEAGE_STYLE = `
|
|
533
|
+
.lineage-wrap { overflow-x: auto; }
|
|
534
|
+
svg .lnode { cursor: pointer; }
|
|
535
|
+
svg .lnode rect { fill: var(--panel); stroke-width: 1.3; }
|
|
536
|
+
svg .lnode.selected rect { stroke-width: 2.4; }
|
|
537
|
+
svg .lnode .n-id { font-size: 11px; }
|
|
538
|
+
svg .lnode .n-sub { fill: var(--dim); font-size: 9px; }
|
|
539
|
+
svg .ledge { stroke: #4a6b3a; stroke-width: 1.4; fill: none; }
|
|
540
|
+
.lcards { margin-top: 4px; }
|
|
541
|
+
.lcard { display: none; border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; background: var(--panel); }
|
|
542
|
+
.lcard.shown { display: block; }
|
|
543
|
+
.lcard h3 { margin: 0 0 8px; font-size: 13px; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; }
|
|
544
|
+
.lcard dl { display: grid; grid-template-columns: 110px 1fr; gap: 3px 12px; margin: 0; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12px; }
|
|
545
|
+
.lcard dt { color: var(--dim); }
|
|
546
|
+
.lcard dd { margin: 0; word-break: break-word; }
|
|
547
|
+
.legend { color: var(--dim); font-size: 11.5px; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; margin: 8px 0 0; }
|
|
548
|
+
`;
|
|
549
|
+
/** Node stroke by recency — never a success/failure claim (see LineageActivity). */
|
|
550
|
+
function lineageColor(node) {
|
|
551
|
+
if (node.activity === 'active')
|
|
552
|
+
return '#a3e635';
|
|
553
|
+
if (node.activity === 'idle')
|
|
554
|
+
return '#e0b341';
|
|
555
|
+
return '#6e7681';
|
|
556
|
+
}
|
|
557
|
+
const LNODE = { w: 210, h: 68, gapX: 22, levelH: 124, top: 30, marginX: 20 };
|
|
558
|
+
/**
|
|
559
|
+
* Characters that fit one node line at the given font size. The box is a fixed
|
|
560
|
+
* 210px and the face is monospace (~0.6em per glyph), so a line longer than this
|
|
561
|
+
* runs out past the border — clip it here rather than letting the SVG overflow.
|
|
562
|
+
*/
|
|
563
|
+
function fitNodeLine(text, fontSizePx) {
|
|
564
|
+
const max = Math.floor((LNODE.w - 24) / (fontSizePx * 0.62));
|
|
565
|
+
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
566
|
+
return oneLine.length <= max ? oneLine : `${oneLine.slice(0, max - 1)}…`;
|
|
567
|
+
}
|
|
568
|
+
/** Lay the graph out level by level: depth 0 on top, each level centered below it. */
|
|
569
|
+
function lineageLayout(lineage) {
|
|
570
|
+
const levels = new Map();
|
|
571
|
+
for (const node of lineage.nodes) {
|
|
572
|
+
(levels.get(node.depth) ?? levels.set(node.depth, []).get(node.depth)).push(node);
|
|
573
|
+
}
|
|
574
|
+
const widest = Math.max(1, ...[...levels.values()].map((l) => l.length));
|
|
575
|
+
const width = Math.max(920, LNODE.marginX * 2 + widest * LNODE.w + (widest - 1) * LNODE.gapX);
|
|
576
|
+
const depth = Math.max(...lineage.nodes.map((n) => n.depth));
|
|
577
|
+
const height = LNODE.top + (depth + 1) * LNODE.levelH;
|
|
578
|
+
const at = new Map();
|
|
579
|
+
for (const [level, row] of levels) {
|
|
580
|
+
const rowWidth = row.length * LNODE.w + (row.length - 1) * LNODE.gapX;
|
|
581
|
+
const startX = (width - rowWidth) / 2;
|
|
582
|
+
row.forEach((node, i) => {
|
|
583
|
+
at.set(node.id, { x: startX + i * (LNODE.w + LNODE.gapX), y: LNODE.top + level * LNODE.levelH });
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return { width, height, at };
|
|
587
|
+
}
|
|
588
|
+
/** The inline-SVG delegation graph: one box per session, edges parent → child. */
|
|
589
|
+
function renderLineageSvg(lineage) {
|
|
590
|
+
const { width, height, at } = lineageLayout(lineage);
|
|
591
|
+
const byId = new Map(lineage.nodes.map((n) => [n.id, n]));
|
|
592
|
+
const parts = [];
|
|
593
|
+
parts.push('<defs><marker id="lg" markerWidth="9" markerHeight="9" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#4a6b3a"/></marker></defs>');
|
|
594
|
+
for (const edge of lineage.edges) {
|
|
595
|
+
const from = at.get(edge.parent);
|
|
596
|
+
const to = at.get(edge.child);
|
|
597
|
+
if (!from || !to)
|
|
598
|
+
continue;
|
|
599
|
+
const x1 = from.x + LNODE.w / 2;
|
|
600
|
+
const y1 = from.y + LNODE.h;
|
|
601
|
+
const x2 = to.x + LNODE.w / 2;
|
|
602
|
+
const y2 = to.y - 8;
|
|
603
|
+
const mid = (y1 + y2) / 2;
|
|
604
|
+
parts.push(`<path d="M${x1.toFixed(1)} ${y1} C ${x1.toFixed(1)} ${mid.toFixed(1)}, ${x2.toFixed(1)} ${mid.toFixed(1)}, ${x2.toFixed(1)} ${y2.toFixed(1)}" class="ledge" marker-end="url(#lg)"><title>${escapeHtml(edge.source)}</title></path>`);
|
|
605
|
+
}
|
|
606
|
+
for (const node of byId.values()) {
|
|
607
|
+
const pos = at.get(node.id);
|
|
608
|
+
const color = lineageColor(node);
|
|
609
|
+
const name = node.handle && node.handle !== node.shortId ? `${node.handle} · ${node.shortId}` : node.shortId;
|
|
610
|
+
// Three lines, because one 210px row cannot hold a handle, an id, a harness,
|
|
611
|
+
// a role, a PR, and the counts without clipping the end off every node.
|
|
612
|
+
const who = [node.agent, node.role];
|
|
613
|
+
if (node.prNumber)
|
|
614
|
+
who.push(`PR #${node.prNumber}`);
|
|
615
|
+
const counts = [`${node.toolCount} tools`];
|
|
616
|
+
if (node.durationMs > 0)
|
|
617
|
+
counts.push(formatDuration(node.durationMs));
|
|
618
|
+
counts.push(node.activity);
|
|
619
|
+
parts.push(`<g class="lnode" data-id="${escapeHtml(node.id)}" tabindex="0">` +
|
|
620
|
+
`<rect x="${pos.x.toFixed(1)}" y="${pos.y}" width="${LNODE.w}" height="${LNODE.h}" rx="8" stroke="${color}"><title>${escapeHtml(`${name} · ${who.join(' · ')} · ${counts.join(' · ')}`)}</title></rect>` +
|
|
621
|
+
`<text x="${(pos.x + 12).toFixed(1)}" y="${pos.y + 22}" class="n-id" fill="${color}">${escapeHtml(fitNodeLine(name, 11))}</text>` +
|
|
622
|
+
`<text x="${(pos.x + 12).toFixed(1)}" y="${pos.y + 40}" class="n-sub">${escapeHtml(fitNodeLine(who.join(' · '), 9))}</text>` +
|
|
623
|
+
`<text x="${(pos.x + 12).toFixed(1)}" y="${pos.y + 56}" class="n-sub">${escapeHtml(fitNodeLine(counts.join(' · '), 9))}</text>` +
|
|
624
|
+
'</g>');
|
|
625
|
+
}
|
|
626
|
+
// Scale to the container and no further: a wide fan-out shrinks to fit rather
|
|
627
|
+
// than pushing the page into a horizontal scroll, and a narrow one is not blown up.
|
|
628
|
+
return `<div class="lineage-wrap"><svg viewBox="0 0 ${width} ${height}" style="width:100%;max-width:${width}px;height:auto" role="img" aria-label="Session lineage graph">${parts.join('')}</svg></div>`;
|
|
629
|
+
}
|
|
630
|
+
/** One detail card per node, revealed by clicking that node. */
|
|
631
|
+
function renderLineageCards(lineage) {
|
|
632
|
+
return lineage.nodes
|
|
633
|
+
.map((node) => {
|
|
634
|
+
const s = node.session;
|
|
635
|
+
const rows = [
|
|
636
|
+
['session', node.id],
|
|
637
|
+
['agent', node.agent],
|
|
638
|
+
['role', node.role],
|
|
639
|
+
['tools', String(node.toolCount)],
|
|
640
|
+
];
|
|
641
|
+
if (node.durationMs > 0)
|
|
642
|
+
rows.push(['span', formatDuration(node.durationMs)]);
|
|
643
|
+
rows.push(['activity', node.activity]);
|
|
644
|
+
if (node.team)
|
|
645
|
+
rows.push(['team', node.team]);
|
|
646
|
+
if (node.mode)
|
|
647
|
+
rows.push(['mode', node.mode]);
|
|
648
|
+
if (node.prNumber)
|
|
649
|
+
rows.push(['pr', `#${node.prNumber}`]);
|
|
650
|
+
if (s.project)
|
|
651
|
+
rows.push(['project', s.project]);
|
|
652
|
+
if (s.gitBranch)
|
|
653
|
+
rows.push(['branch', s.gitBranch]);
|
|
654
|
+
if (s.ticketId)
|
|
655
|
+
rows.push(['ticket', s.ticketId]);
|
|
656
|
+
if (s.outputTokens)
|
|
657
|
+
rows.push(['out tokens', formatTokenCount(s.outputTokens)]);
|
|
658
|
+
const date = (s.timestamp || '').slice(0, 10);
|
|
659
|
+
if (date)
|
|
660
|
+
rows.push(['started', date]);
|
|
661
|
+
const dl = rows
|
|
662
|
+
.map(([k, v]) => `<dt>${escapeHtml(k)}</dt><dd>${escapeHtml(v)}</dd>`)
|
|
663
|
+
.join('\n ');
|
|
664
|
+
const heading = node.handle && node.handle !== node.shortId ? `${node.handle} · ${node.shortId}` : node.shortId;
|
|
665
|
+
return `<div class="lcard${node.depth === 0 ? ' shown' : ''}" id="lcard-${escapeHtml(node.id)}">
|
|
666
|
+
<h3>${escapeHtml(heading)}</h3>
|
|
667
|
+
<dl>
|
|
668
|
+
${dl}
|
|
669
|
+
</dl>
|
|
670
|
+
<p class="muted" style="margin:8px 0 0;font-size:11.5px">Full trajectory: <code>agents sessions trace ${escapeHtml(node.shortId)}</code></p>
|
|
671
|
+
</div>`;
|
|
672
|
+
})
|
|
673
|
+
.join('\n ');
|
|
674
|
+
}
|
|
675
|
+
/** Wire node clicks to their cards. Self-contained, no CDN, no framework. */
|
|
676
|
+
const LINEAGE_SCRIPT = `
|
|
677
|
+
(function () {
|
|
678
|
+
var nodes = document.querySelectorAll('.lnode');
|
|
679
|
+
function select(id) {
|
|
680
|
+
document.querySelectorAll('.lcard').forEach(function (c) { c.classList.remove('shown'); });
|
|
681
|
+
var card = document.getElementById('lcard-' + id);
|
|
682
|
+
if (card) card.classList.add('shown');
|
|
683
|
+
nodes.forEach(function (n) { n.classList.toggle('selected', n.getAttribute('data-id') === id); });
|
|
684
|
+
}
|
|
685
|
+
nodes.forEach(function (n) {
|
|
686
|
+
var id = n.getAttribute('data-id');
|
|
687
|
+
n.addEventListener('click', function () { select(id); });
|
|
688
|
+
n.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); select(id); } });
|
|
689
|
+
});
|
|
690
|
+
if (nodes.length) select(nodes[0].getAttribute('data-id'));
|
|
691
|
+
})();
|
|
692
|
+
`;
|
|
693
|
+
/**
|
|
694
|
+
* Render a {@link SessionLineage} as ONE self-contained HTML page: the inline-SVG
|
|
695
|
+
* delegation graph (orchestrator on top, the sessions it spawned below, edges
|
|
696
|
+
* auto-discovered from the team records) plus a per-node summary card revealed
|
|
697
|
+
* by clicking a node. Same shell, redaction, and no-CDN rule as the other two
|
|
698
|
+
* layouts; local paths (`filePath`/`cwd`) are deliberately never rendered.
|
|
699
|
+
*/
|
|
700
|
+
export function renderLineageHtml(lineage) {
|
|
701
|
+
const root = lineage.nodes[0];
|
|
702
|
+
const title = root ? `${root.agent} · ${root.shortId}` : 'lineage';
|
|
703
|
+
const spawned = Math.max(0, lineage.nodes.length - 1);
|
|
704
|
+
const teamPart = lineage.teams.length > 0 ? ` · team ${lineage.teams.join(', ')}` : '';
|
|
705
|
+
const empty = !root
|
|
706
|
+
? '<p class="muted">No lineage — the selected session spawned nothing that is indexed.</p>'
|
|
707
|
+
: spawned === 0
|
|
708
|
+
? '<p class="muted">This session spawned no indexed teammate. Only the orchestrator is drawn.</p>'
|
|
709
|
+
: '';
|
|
710
|
+
const unresolved = lineage.unresolvedParentIds.length > 0
|
|
711
|
+
? `<p class="stall">${lineage.unresolvedParentIds.length} parent session${lineage.unresolvedParentIds.length === 1 ? '' : 's'} referenced but not in the scanned pool — widen with --limit or --since.</p>`
|
|
712
|
+
: '';
|
|
713
|
+
return `<!DOCTYPE html>
|
|
714
|
+
<html lang="en" data-theme="auto">
|
|
715
|
+
<head>
|
|
716
|
+
<meta charset="utf-8" />
|
|
717
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
718
|
+
<meta name="robots" content="noindex" />
|
|
719
|
+
<title>${escapeHtml(title)} — lineage</title>
|
|
720
|
+
<style>${BASE_STYLE}${LINEAGE_STYLE}</style>
|
|
721
|
+
</head>
|
|
722
|
+
<body>
|
|
723
|
+
<header>
|
|
724
|
+
<div class="inner">
|
|
725
|
+
<button class="toggle" id="theme" title="Toggle light and dark">◙</button>
|
|
726
|
+
<div class="mark">agents session trace · lineage</div>
|
|
727
|
+
<h1>${escapeHtml(title)}</h1>
|
|
728
|
+
<div class="metrics">${escapeHtml(`${spawned} spawned session${spawned === 1 ? '' : 's'}${teamPart}`)}</div>
|
|
729
|
+
</div>
|
|
730
|
+
</header>
|
|
731
|
+
<main>
|
|
732
|
+
<h2>Lineage</h2>
|
|
733
|
+
${empty}
|
|
734
|
+
${unresolved}
|
|
735
|
+
${root ? renderLineageSvg(lineage) : ''}
|
|
736
|
+
<p class="legend">node color = recency (lime active · amber idle · grey stale) · click a node for its summary · edges from teamOrigin.parentSessionId</p>
|
|
737
|
+
<h2>Session</h2>
|
|
738
|
+
<div class="lcards">
|
|
739
|
+
${root ? renderLineageCards(lineage) : ''}
|
|
740
|
+
</div>
|
|
741
|
+
</main>
|
|
742
|
+
<footer>
|
|
743
|
+
Secret-redacted lineage rendered by agents-cli · <code>agents sessions trace --tree</code>
|
|
744
|
+
</footer>
|
|
745
|
+
<script>${THEME_SCRIPT}</script>
|
|
746
|
+
<script>${LINEAGE_SCRIPT}</script>
|
|
747
|
+
</body>
|
|
748
|
+
</html>
|
|
749
|
+
`;
|
|
750
|
+
}
|