@agi-fans/dsh-tui 0.22.0 → 0.23.0
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/LICENSE +26 -0
- package/lib/CHANGELOG.md +199 -1
- package/lib/chrome/box.d.ts +1 -18
- package/lib/chrome/box.js +2 -45
- package/lib/chrome/box.js.map +1 -1
- package/lib/chrome/code-highlight.js +4 -4
- package/lib/chrome/code-highlight.js.map +1 -1
- package/lib/chrome/command-output.d.ts +0 -1
- package/lib/chrome/command-output.js +11 -5
- package/lib/chrome/command-output.js.map +1 -1
- package/lib/chrome/goal-bar.js +1 -1
- package/lib/chrome/goal-bar.js.map +1 -1
- package/lib/chrome/main-screen-renderer.js +4 -3
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/markdown.d.ts +3 -3
- package/lib/chrome/markdown.js +85 -36
- package/lib/chrome/markdown.js.map +1 -1
- package/lib/chrome/md-style.d.ts +5 -2
- package/lib/chrome/md-style.js +13 -4
- package/lib/chrome/md-style.js.map +1 -1
- package/lib/chrome/renderer.d.ts +37 -1
- package/lib/chrome/renderer.js.map +1 -1
- package/lib/chrome/status-line.d.ts +3 -3
- package/lib/chrome/status-line.js +5 -5
- package/lib/chrome/status-line.js.map +1 -1
- package/lib/chrome/theme.d.ts +8 -3
- package/lib/chrome/theme.js +51 -153
- package/lib/chrome/theme.js.map +1 -1
- package/lib/commands/auth-account.d.ts +9 -0
- package/lib/commands/auth-account.js +202 -0
- package/lib/commands/auth-account.js.map +1 -0
- package/lib/commands/auth.js +238 -414
- package/lib/commands/auth.js.map +1 -1
- package/lib/commands/diff.js +22 -16
- package/lib/commands/diff.js.map +1 -1
- package/lib/commands/export.d.ts +0 -1
- package/lib/commands/export.js +97 -32
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/files.js +2 -8
- package/lib/commands/files.js.map +1 -1
- package/lib/commands/integrations.d.ts +12 -1
- package/lib/commands/integrations.js +120 -23
- package/lib/commands/integrations.js.map +1 -1
- package/lib/commands/jobs-browser.d.ts +3 -0
- package/lib/commands/jobs-browser.js +152 -0
- package/lib/commands/jobs-browser.js.map +1 -0
- package/lib/commands/jobs.d.ts +3 -14
- package/lib/commands/jobs.js +26 -18
- package/lib/commands/jobs.js.map +1 -1
- package/lib/commands/model.d.ts +3 -0
- package/lib/commands/model.js +197 -120
- package/lib/commands/model.js.map +1 -1
- package/lib/commands/open-external.d.ts +2 -0
- package/lib/commands/open-external.js +19 -0
- package/lib/commands/open-external.js.map +1 -0
- package/lib/commands/permission.js +65 -54
- package/lib/commands/permission.js.map +1 -1
- package/lib/commands/plugins.js +381 -139
- package/lib/commands/plugins.js.map +1 -1
- package/lib/commands/schedule-catalog.d.ts +12 -0
- package/lib/commands/schedule-catalog.js +41 -0
- package/lib/commands/schedule-catalog.js.map +1 -0
- package/lib/commands/schedule-history.d.ts +4 -0
- package/lib/commands/schedule-history.js +62 -0
- package/lib/commands/schedule-history.js.map +1 -0
- package/lib/commands/schedule.d.ts +12 -15
- package/lib/commands/schedule.js +220 -187
- package/lib/commands/schedule.js.map +1 -1
- package/lib/commands/session-configuration.d.ts +3 -0
- package/lib/commands/session-configuration.js +62 -28
- package/lib/commands/session-configuration.js.map +1 -1
- package/lib/commands/session.js +111 -66
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/terminal.js +14 -8
- package/lib/commands/terminal.js.map +1 -1
- package/lib/definition.d.ts +112 -15
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/input/actions.d.ts +1 -1
- package/lib/input/actions.js +5 -2
- package/lib/input/actions.js.map +1 -1
- package/lib/input/composer-files.d.ts +2 -0
- package/lib/input/composer-files.js +13 -3
- package/lib/input/composer-files.js.map +1 -1
- package/lib/input/image-paste.d.ts +5 -12
- package/lib/input/image-paste.js +32 -28
- package/lib/input/image-paste.js.map +1 -1
- package/lib/input/key-test.d.ts +5 -0
- package/lib/input/key-test.js +19 -0
- package/lib/input/key-test.js.map +1 -0
- package/lib/input/keys.d.ts +2 -0
- package/lib/input/keys.js +4 -1
- package/lib/input/keys.js.map +1 -1
- package/lib/locale/index.d.ts +6 -0
- package/lib/locale/index.js +32 -0
- package/lib/locale/index.js.map +1 -0
- package/lib/locale/prompt.d.ts +4 -0
- package/lib/locale/prompt.js +24 -0
- package/lib/locale/prompt.js.map +1 -0
- package/lib/locale/zh.d.ts +1114 -0
- package/lib/locale/zh.js +1115 -0
- package/lib/locale/zh.js.map +1 -0
- package/lib/prompt-intents.d.ts +4 -0
- package/lib/prompt-intents.js +4 -0
- package/lib/prompt-intents.js.map +1 -1
- package/lib/runtime/account-settings.d.ts +18 -0
- package/lib/runtime/account-settings.js +241 -0
- package/lib/runtime/account-settings.js.map +1 -0
- package/lib/runtime/agent-profile.d.ts +3 -3
- package/lib/runtime/agent-profile.js +4 -4
- package/lib/runtime/agent-profile.js.map +1 -1
- package/lib/runtime/btw-policy.js +2 -1
- package/lib/runtime/btw-policy.js.map +1 -1
- package/lib/runtime/btw.js +1 -2
- package/lib/runtime/btw.js.map +1 -1
- package/lib/runtime/composer-images.d.ts +2 -2
- package/lib/runtime/composer-images.js +4 -4
- package/lib/runtime/composer-images.js.map +1 -1
- package/lib/runtime/composer-shell.js +27 -1
- package/lib/runtime/composer-shell.js.map +1 -1
- package/lib/runtime/file-browser.d.ts +19 -0
- package/lib/runtime/file-browser.js +291 -0
- package/lib/runtime/file-browser.js.map +1 -0
- package/lib/runtime/file-review.d.ts +6 -9
- package/lib/runtime/file-review.js +91 -7
- package/lib/runtime/file-review.js.map +1 -1
- package/lib/runtime/help-reader.d.ts +12 -0
- package/lib/runtime/help-reader.js +50 -0
- package/lib/runtime/help-reader.js.map +1 -0
- package/lib/runtime/human-interaction.js +1 -1
- package/lib/runtime/human-interaction.js.map +1 -1
- package/lib/runtime/model-catalog-editor.d.ts +12 -0
- package/lib/runtime/model-catalog-editor.js +215 -0
- package/lib/runtime/model-catalog-editor.js.map +1 -0
- package/lib/runtime/model-settings.d.ts +11 -0
- package/lib/runtime/model-settings.js +404 -0
- package/lib/runtime/model-settings.js.map +1 -0
- package/lib/runtime/plugin-settings.d.ts +4 -1
- package/lib/runtime/plugin-settings.js +164 -88
- package/lib/runtime/plugin-settings.js.map +1 -1
- package/lib/runtime/prompt-browser.d.ts +9 -0
- package/lib/runtime/prompt-browser.js +19 -0
- package/lib/runtime/prompt-browser.js.map +1 -0
- package/lib/runtime/provider-local.d.ts +27 -27
- package/lib/runtime/provider-local.js +1264 -308
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/reference-reader.d.ts +15 -0
- package/lib/runtime/reference-reader.js +78 -0
- package/lib/runtime/reference-reader.js.map +1 -0
- package/lib/runtime/runner.js +38 -29
- package/lib/runtime/runner.js.map +1 -1
- package/lib/runtime/session-runtime.js +1 -1
- package/lib/runtime/session-runtime.js.map +1 -1
- package/lib/runtime/settings-draft.d.ts +21 -0
- package/lib/runtime/settings-draft.js +166 -0
- package/lib/runtime/settings-draft.js.map +1 -0
- package/lib/runtime/settings-preferences.js +1 -1
- package/lib/runtime/settings-preferences.js.map +1 -1
- package/lib/runtime/startup-notices.d.ts +0 -1
- package/lib/runtime/startup-notices.js +35 -6
- package/lib/runtime/startup-notices.js.map +1 -1
- package/lib/runtime/subagent-model-selection.js +2 -1
- package/lib/runtime/subagent-model-selection.js.map +1 -1
- package/lib/runtime/subagent-settings.d.ts +14 -0
- package/lib/runtime/subagent-settings.js +288 -0
- package/lib/runtime/subagent-settings.js.map +1 -0
- package/lib/runtime/terminal-notifications.d.ts +3 -0
- package/lib/runtime/terminal-notifications.js +13 -8
- package/lib/runtime/terminal-notifications.js.map +1 -1
- package/lib/runtime/terminal-probe.d.ts +2 -0
- package/lib/runtime/terminal-probe.js +9 -0
- package/lib/runtime/terminal-probe.js.map +1 -1
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/runtime/tool-reader.d.ts +5 -0
- package/lib/runtime/tool-reader.js +27 -0
- package/lib/runtime/tool-reader.js.map +1 -0
- package/lib/runtime/web-settings.d.ts +14 -0
- package/lib/runtime/web-settings.js +339 -0
- package/lib/runtime/web-settings.js.map +1 -0
- package/lib/session/command-attachments.d.ts +17 -0
- package/lib/session/command-attachments.js +50 -0
- package/lib/session/command-attachments.js.map +1 -0
- package/lib/session/context-diagnostics.d.ts +16 -0
- package/lib/session/context-diagnostics.js +15 -0
- package/lib/session/context-diagnostics.js.map +1 -1
- package/lib/session/external-agents.d.ts +3 -1
- package/lib/session/external-agents.js +35 -10
- package/lib/session/external-agents.js.map +1 -1
- package/lib/session/generation-continuation.d.ts +11 -0
- package/lib/session/generation-continuation.js +34 -0
- package/lib/session/generation-continuation.js.map +1 -0
- package/lib/session/goal-controls.d.ts +5 -0
- package/lib/session/goal-controls.js +81 -0
- package/lib/session/goal-controls.js.map +1 -0
- package/lib/session/interaction-adapter.d.ts +7 -3
- package/lib/session/interaction-adapter.js +121 -49
- package/lib/session/interaction-adapter.js.map +1 -1
- package/lib/session/message-queue.d.ts +2 -0
- package/lib/session/message-queue.js +102 -17
- package/lib/session/message-queue.js.map +1 -1
- package/lib/session/pending-inputs.d.ts +30 -0
- package/lib/session/pending-inputs.js +183 -0
- package/lib/session/pending-inputs.js.map +1 -0
- package/lib/session/session-configuration.js +1 -1
- package/lib/session/session-configuration.js.map +1 -1
- package/lib/session/session-controller.d.ts +32 -13
- package/lib/session/session-controller.js +444 -98
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/subagent-roster.d.ts +1 -0
- package/lib/session/subagent-roster.js +16 -5
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/subagent-tracker.d.ts +2 -0
- package/lib/session/subagent-tracker.js +18 -0
- package/lib/session/subagent-tracker.js.map +1 -1
- package/lib/session/tui-settings.d.ts +9 -5
- package/lib/session/tui-settings.js +3 -1
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/session/workflow-projection.d.ts +2 -1
- package/lib/session/workflow-projection.js +34 -4
- package/lib/session/workflow-projection.js.map +1 -1
- package/lib/views/autocomplete.d.ts +10 -7
- package/lib/views/autocomplete.js +44 -37
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/context-overview.d.ts +4 -0
- package/lib/views/context-overview.js +43 -0
- package/lib/views/context-overview.js.map +1 -0
- package/lib/views/copy-targets.d.ts +6 -2
- package/lib/views/copy-targets.js +19 -14
- package/lib/views/copy-targets.js.map +1 -1
- package/lib/views/document-reader.d.ts +10 -1
- package/lib/views/document-reader.js +59 -5
- package/lib/views/document-reader.js.map +1 -1
- package/lib/views/event-views.js +29 -11
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/file-actions.js +21 -0
- package/lib/views/file-actions.js.map +1 -1
- package/lib/views/file-browser.d.ts +98 -0
- package/lib/views/file-browser.js +120 -0
- package/lib/views/file-browser.js.map +1 -0
- package/lib/views/history-search.js +4 -4
- package/lib/views/history-search.js.map +1 -1
- package/lib/views/hotkey-format.d.ts +3 -2
- package/lib/views/hotkey-format.js +4 -3
- package/lib/views/hotkey-format.js.map +1 -1
- package/lib/views/hotkeys.js +10 -12
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/list-view.js +16 -11
- package/lib/views/list-view.js.map +1 -1
- package/lib/views/prompt-selector.d.ts +23 -1
- package/lib/views/prompt-selector.js +123 -60
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/settings-list.d.ts +6 -1
- package/lib/views/settings-list.js +69 -46
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/tool-actions.d.ts +16 -0
- package/lib/views/tool-actions.js +29 -0
- package/lib/views/tool-actions.js.map +1 -0
- package/lib/views/trajectory-timeline.d.ts +5 -6
- package/lib/views/trajectory-timeline.js +25 -15
- package/lib/views/trajectory-timeline.js.map +1 -1
- package/lib/views/trajectory.d.ts +23 -2
- package/lib/views/trajectory.js +404 -186
- package/lib/views/trajectory.js.map +1 -1
- package/lib/views/transcript-render.d.ts +39 -15
- package/lib/views/transcript-render.js +406 -208
- package/lib/views/transcript-render.js.map +1 -1
- package/lib/views/transcript-types.d.ts +3 -2
- package/lib/views/transcript-types.js.map +1 -1
- package/lib/views/workflow.d.ts +16 -0
- package/lib/views/workflow.js +42 -0
- package/lib/views/workflow.js.map +1 -0
- package/package.json +61 -58
- package/lib/chrome/welcome-tips.d.ts +0 -23
- package/lib/chrome/welcome-tips.js +0 -24
- package/lib/chrome/welcome-tips.js.map +0 -1
- package/lib/session/steering.d.ts +0 -3
- package/lib/session/steering.js +0 -9
- package/lib/session/steering.js.map +0 -1
package/lib/views/trajectory.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** Keyboard-first Trajectory ledger projected directly from durable session events. */
|
|
2
|
-
import { padToWidth, truncateToWidth, visibleWidth, wrapText } from "../chrome/width.js";
|
|
2
|
+
import { expandTabs, padToWidth, truncateToWidth, visibleWidth, wrapCode, wrapText } from "../chrome/width.js";
|
|
3
3
|
import { moveGraphemeLeft, moveGraphemeRight } from "../chrome/grapheme.js";
|
|
4
4
|
import { firstVisibleStreamTime } from "./stream-time.js";
|
|
5
|
-
import { formatOverlayHint } from "./hotkey-format.js";
|
|
6
5
|
import { listNavigation } from "./list-navigation.js";
|
|
7
|
-
import { highlightCodeLines
|
|
6
|
+
import { highlightCodeLines } from "../chrome/code-highlight.js";
|
|
7
|
+
import { toolArgSubject } from "../chrome/tool-renderers.js";
|
|
8
|
+
import { toolArgsObject } from "../chrome/tool-args.js";
|
|
9
|
+
import { paintSurface } from "../chrome/theme.js";
|
|
8
10
|
import { renderMarkdown } from "../chrome/markdown.js";
|
|
9
11
|
import { renderTimelineOverview, trajectoryTimeline } from "./trajectory-timeline.js";
|
|
10
12
|
import { formatDuration, formatTokens } from "../chrome/status-line.js";
|
|
@@ -72,6 +74,120 @@ function tokenValue(source, ...keys) {
|
|
|
72
74
|
}
|
|
73
75
|
return null;
|
|
74
76
|
}
|
|
77
|
+
const TAB_LABELS = {
|
|
78
|
+
summary: 'Overview', input: 'Input', result: 'Result', payload: 'Raw', schema: 'Schema',
|
|
79
|
+
};
|
|
80
|
+
function rawPayload(record) {
|
|
81
|
+
return [record.payload, record.rawResult].filter(value => value !== undefined && value !== '').join('\n\n');
|
|
82
|
+
}
|
|
83
|
+
/** Decode only the outer argument object; strings keep their whitespace and newlines. */
|
|
84
|
+
function argumentRows(record, theme, width) {
|
|
85
|
+
const raw = record.input ?? '';
|
|
86
|
+
const args = toolArgsObject(raw);
|
|
87
|
+
if (args === undefined)
|
|
88
|
+
return codeRows(raw, undefined, theme, width);
|
|
89
|
+
const rows = [];
|
|
90
|
+
for (const [key, value] of Object.entries(args)) {
|
|
91
|
+
if (rows.length > 0)
|
|
92
|
+
rows.push('');
|
|
93
|
+
rows.push(theme.fg('dim', key));
|
|
94
|
+
rows.push(...codeRows(typeof value === 'string' ? value : json(value), typeof value === 'string' ? undefined : 'json', theme, width));
|
|
95
|
+
}
|
|
96
|
+
return rows.length === 0 ? [theme.fg('dim', 'No arguments.')] : rows;
|
|
97
|
+
}
|
|
98
|
+
/** The same split geometry drives drawing, pointer hit tests and wheel routing. */
|
|
99
|
+
export function trajectoryLeftWidth(width) {
|
|
100
|
+
return Math.max(32, Math.min(64, Math.floor(width * 0.38)));
|
|
101
|
+
}
|
|
102
|
+
const groupCache = new WeakMap();
|
|
103
|
+
function recordGroups(ledger) {
|
|
104
|
+
const cached = groupCache.get(ledger);
|
|
105
|
+
if (cached?.version === ledger.version)
|
|
106
|
+
return cached.groups;
|
|
107
|
+
const contexts = new Map();
|
|
108
|
+
const owner = new Map();
|
|
109
|
+
const children = new Map();
|
|
110
|
+
let previous, contextHead;
|
|
111
|
+
for (const record of ledger.records) {
|
|
112
|
+
if (record.kind === 'context') {
|
|
113
|
+
if (previous?.kind !== 'context' || previous.turn !== record.turn || previous.step !== record.step)
|
|
114
|
+
contextHead = record.id;
|
|
115
|
+
const head = contextHead;
|
|
116
|
+
const group = contexts.get(head) ?? [];
|
|
117
|
+
group.push(record);
|
|
118
|
+
contexts.set(head, group);
|
|
119
|
+
owner.set(record.id, head);
|
|
120
|
+
}
|
|
121
|
+
if (record.parentCallId !== null) {
|
|
122
|
+
const parent = `tool:${record.parentCallId}`;
|
|
123
|
+
const group = children.get(parent) ?? [];
|
|
124
|
+
group.push(record);
|
|
125
|
+
children.set(parent, group);
|
|
126
|
+
}
|
|
127
|
+
previous = record;
|
|
128
|
+
}
|
|
129
|
+
const groups = { contexts, owner, children };
|
|
130
|
+
groupCache.set(ledger, { version: ledger.version, groups });
|
|
131
|
+
return groups;
|
|
132
|
+
}
|
|
133
|
+
function toggleGroup(state) {
|
|
134
|
+
const record = selectedRecord(state);
|
|
135
|
+
if (record === undefined)
|
|
136
|
+
return state;
|
|
137
|
+
const groups = recordGroups(state.ledger);
|
|
138
|
+
const id = groups.owner.get(record.id) ?? record.id;
|
|
139
|
+
if (!groups.contexts.has(id) && !groups.children.has(id))
|
|
140
|
+
return state;
|
|
141
|
+
const expanded = new Set(state.expandedGroups);
|
|
142
|
+
if (expanded.has(id))
|
|
143
|
+
expanded.delete(id);
|
|
144
|
+
else
|
|
145
|
+
expanded.add(id);
|
|
146
|
+
return { ...state, expandedGroups: expanded, selectedId: id, following: false };
|
|
147
|
+
}
|
|
148
|
+
/** Clicks are committed on release by the terminal owner, after excluding drags. */
|
|
149
|
+
export function trajectoryClick(state, width, height, row, column) {
|
|
150
|
+
if (row < 3 || row >= height - 1 || column < 0 || column >= width)
|
|
151
|
+
return state;
|
|
152
|
+
const split = state.details && !state.fullDetail && width >= 96;
|
|
153
|
+
const inDetail = state.details && (!split || column > trajectoryLeftWidth(width));
|
|
154
|
+
if (inDetail) {
|
|
155
|
+
const next = { ...state, focus: 'detail', following: false };
|
|
156
|
+
if (row === 4) {
|
|
157
|
+
const tabs = detailTabsFor(selectedRecord(state));
|
|
158
|
+
let at = (split ? trajectoryLeftWidth(width) + 1 : 0) + 1;
|
|
159
|
+
for (const tab of tabs) {
|
|
160
|
+
const cells = visibleWidth(TAB_LABELS[tab]) + 4;
|
|
161
|
+
if (column >= at && column < at + cells)
|
|
162
|
+
return { ...next, detailTab: tab, detailScroll: 0 };
|
|
163
|
+
at += cells;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return next;
|
|
167
|
+
}
|
|
168
|
+
const overview = trajectoryOverviewHeight(state, width);
|
|
169
|
+
const body = Math.max(0, height - 4 - overview);
|
|
170
|
+
const offset = row - 3 - overview;
|
|
171
|
+
if (offset < 0 || offset >= body)
|
|
172
|
+
return state;
|
|
173
|
+
const rows = trajectoryRows(state);
|
|
174
|
+
const index = Math.max(0, rows.findIndex(item => item.kind === 'record' && item.id === state.selectedId));
|
|
175
|
+
const start = Math.max(0, Math.min(Math.max(0, rows.length - body), index - Math.floor(body / 2)));
|
|
176
|
+
const target = rows[start + offset];
|
|
177
|
+
if (target?.kind === 'turn' && target.stats.turn !== null) {
|
|
178
|
+
const collapsed = new Set(state.collapsedTurns);
|
|
179
|
+
if (collapsed.has(target.stats.turn))
|
|
180
|
+
collapsed.delete(target.stats.turn);
|
|
181
|
+
else
|
|
182
|
+
collapsed.add(target.stats.turn);
|
|
183
|
+
const first = state.ledger.records.find(record => record.turn === target.stats.turn);
|
|
184
|
+
return retainVisibleSelection({ ...state, collapsedTurns: collapsed, selectedId: first?.id ?? state.selectedId, focus: 'list', following: false });
|
|
185
|
+
}
|
|
186
|
+
if (target?.kind !== 'record')
|
|
187
|
+
return state;
|
|
188
|
+
const next = { ...state, selectedId: target.id, details: true, focus: 'list', following: false, detailScroll: 0, notice: '' };
|
|
189
|
+
return column < 4 ? toggleGroup({ ...next, details: state.details }) : next;
|
|
190
|
+
}
|
|
75
191
|
/** Incremental projection. Opening a long session is O(events); live updates are O(1). */
|
|
76
192
|
export class TrajectoryLedger {
|
|
77
193
|
records = [];
|
|
@@ -85,7 +201,7 @@ export class TrajectoryLedger {
|
|
|
85
201
|
#assistantByStep = new Map();
|
|
86
202
|
#toolByCallId = new Map();
|
|
87
203
|
#compactionById = new Map();
|
|
88
|
-
#
|
|
204
|
+
#requestHeader = '';
|
|
89
205
|
#schemas = new Map();
|
|
90
206
|
#version = 0;
|
|
91
207
|
constructor(events = []) {
|
|
@@ -97,7 +213,7 @@ export class TrajectoryLedger {
|
|
|
97
213
|
const cached = this.#fieldCache.get(record);
|
|
98
214
|
if (cached !== undefined)
|
|
99
215
|
return cached;
|
|
100
|
-
const fields =
|
|
216
|
+
const fields = SEARCH_FIELDS.map(field => record[field] ?? '')
|
|
101
217
|
.map(field => field.toLocaleLowerCase());
|
|
102
218
|
this.#fieldCache.set(record, fields);
|
|
103
219
|
return fields;
|
|
@@ -176,8 +292,9 @@ export class TrajectoryLedger {
|
|
|
176
292
|
return;
|
|
177
293
|
}
|
|
178
294
|
if (eventType === 'request/header') {
|
|
179
|
-
this.#
|
|
180
|
-
|
|
295
|
+
this.#requestHeader = json(event.data);
|
|
296
|
+
this.#schemas.clear();
|
|
297
|
+
const prompt = object(data['header']) ?? object(data['prompt']);
|
|
181
298
|
const tools = prompt?.['tools'];
|
|
182
299
|
if (Array.isArray(tools)) {
|
|
183
300
|
for (const tool of tools) {
|
|
@@ -189,11 +306,15 @@ export class TrajectoryLedger {
|
|
|
189
306
|
}
|
|
190
307
|
return;
|
|
191
308
|
}
|
|
192
|
-
if (eventType === 'user/message') {
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
309
|
+
if (eventType === 'user/message' || eventType === 'developer/message' || eventType === 'system/message') {
|
|
310
|
+
const message = object(data['message']) ?? data;
|
|
311
|
+
const source = object(message['source']);
|
|
312
|
+
const isUser = source?.['kind'] === 'user' || source?.['kind'] === 'user-question-reply';
|
|
313
|
+
const text = contentText(message['content']);
|
|
314
|
+
const system = eventType === 'system/message';
|
|
315
|
+
const record = this.#base(event, isUser ? 'user' : system ? 'system' : 'context', isUser ? 'USER' : system ? 'SYSTEM' : 'CONTEXT', compact(text, system ? 'System prompt' : 'Injected context'), data);
|
|
316
|
+
record.result = text;
|
|
317
|
+
this.#push(record);
|
|
197
318
|
return;
|
|
198
319
|
}
|
|
199
320
|
if (eventType === 'assistant/message') {
|
|
@@ -201,7 +322,7 @@ export class TrajectoryLedger {
|
|
|
201
322
|
const text = contentText(message?.['content'] ?? data['content'], 'text');
|
|
202
323
|
const reasoning = contentText(message?.['content'] ?? data['content'], 'reasoning');
|
|
203
324
|
const usage = object(message?.['usage'] ?? data['usage']);
|
|
204
|
-
const startedAt = this.#stepStarted.get(key) ??
|
|
325
|
+
const startedAt = this.#stepStarted.get(key) ?? null;
|
|
205
326
|
const existing = this.#assistantByStep.get(key);
|
|
206
327
|
// Format-v2 messages embed the exact timed stream; derive the first
|
|
207
328
|
// visible token time from it instead of live chunk events.
|
|
@@ -214,15 +335,16 @@ export class TrajectoryLedger {
|
|
|
214
335
|
this.#stepFirstToken.set(key, firstTime);
|
|
215
336
|
}
|
|
216
337
|
if (existing === undefined) {
|
|
217
|
-
const record = this.#base(event, 'assistant', 'ASSISTANT', compact(text, '
|
|
338
|
+
const record = this.#base(event, 'assistant', 'ASSISTANT', compact(text || reasoning, 'Model response'), data);
|
|
218
339
|
record.id = `assistant:${key}`;
|
|
219
|
-
record.payload = this.#
|
|
340
|
+
record.payload = this.#requestHeader || record.payload;
|
|
220
341
|
record.result = text;
|
|
221
342
|
record.thinking = reasoning;
|
|
343
|
+
record.status = data['interrupted'] === true ? 'error' : 'ok';
|
|
222
344
|
record.startedAt = startedAt;
|
|
223
|
-
record.durationMs = Math.max(0, event.time - startedAt);
|
|
345
|
+
record.durationMs = startedAt === null ? null : Math.max(0, event.time - startedAt);
|
|
224
346
|
const first = this.#stepFirstToken.get(key);
|
|
225
|
-
record.ttftMs = first === undefined ? null : Math.max(0, first - startedAt);
|
|
347
|
+
record.ttftMs = first === undefined || startedAt === null ? null : Math.max(0, first - startedAt);
|
|
226
348
|
record.inputTokens = tokenValue(usage, 'inputTokens', 'input_tokens', 'promptTokens', 'prompt_tokens');
|
|
227
349
|
record.outputTokens = tokenValue(usage, 'outputTokens', 'output_tokens', 'completionTokens', 'completion_tokens');
|
|
228
350
|
this.#assistantByStep.set(key, this.#push(record));
|
|
@@ -232,11 +354,11 @@ export class TrajectoryLedger {
|
|
|
232
354
|
if (record !== undefined) {
|
|
233
355
|
this.#invalidateText(record);
|
|
234
356
|
record.seq = event.seq;
|
|
235
|
-
record.summary = compact(text, record.summary);
|
|
357
|
+
record.summary = compact(text || reasoning, record.summary);
|
|
236
358
|
record.result = text;
|
|
237
359
|
record.thinking = reasoning;
|
|
238
360
|
record.status = data['interrupted'] === true ? 'error' : 'ok';
|
|
239
|
-
record.durationMs = Math.max(0, event.time - startedAt);
|
|
361
|
+
record.durationMs = startedAt === null ? null : Math.max(0, event.time - startedAt);
|
|
240
362
|
record.inputTokens = tokenValue(usage, 'inputTokens', 'input_tokens', 'promptTokens', 'prompt_tokens');
|
|
241
363
|
record.outputTokens = tokenValue(usage, 'outputTokens', 'output_tokens', 'completionTokens', 'completion_tokens');
|
|
242
364
|
}
|
|
@@ -244,11 +366,15 @@ export class TrajectoryLedger {
|
|
|
244
366
|
return;
|
|
245
367
|
}
|
|
246
368
|
if (eventType === 'tool/call' || eventType === 'tool/ptc-dispatch-start') {
|
|
247
|
-
const callId = string(data['callId']) ?? string(data['id']) ?? `${event.seq}`;
|
|
369
|
+
const callId = string(data['subCallId']) ?? string(data['callId']) ?? string(data['id']) ?? `${event.seq}`;
|
|
248
370
|
const parentCallId = string(data['parentCallId']) ?? string(data['parentId']) ?? null;
|
|
249
371
|
const name = string(data['name']) ?? string(data['toolName']) ?? 'tool';
|
|
250
372
|
const args = data['arguments'] ?? data['input'] ?? data;
|
|
251
|
-
const
|
|
373
|
+
const input = typeof args === 'string' ? args : json(args);
|
|
374
|
+
const decoded = toolArgsObject(input);
|
|
375
|
+
const subject = string(decoded?.['description']) ?? toolArgSubject(input, false);
|
|
376
|
+
const record = this.#base(event, parentCallId === null ? 'tool' : 'subtool', name, compact(subject, name), data);
|
|
377
|
+
record.input = input;
|
|
252
378
|
record.id = `tool:${callId}`;
|
|
253
379
|
record.label = parentCallId === null ? 'TOOL' : 'SUBTOOL';
|
|
254
380
|
record.name = name;
|
|
@@ -260,18 +386,39 @@ export class TrajectoryLedger {
|
|
|
260
386
|
}
|
|
261
387
|
if (eventType === 'tool/result' || eventType === 'tool/ptc-dispatch') {
|
|
262
388
|
const message = object(data['message']);
|
|
263
|
-
const
|
|
389
|
+
const source = object(message?.['source']);
|
|
390
|
+
const callId = string(data['subCallId']) ?? string(message?.['toolCallId']) ?? string(source?.['callId'])
|
|
391
|
+
?? string(message?.['callId']) ?? string(data['callId']) ?? string(data['id']);
|
|
264
392
|
if (callId === undefined)
|
|
265
393
|
return;
|
|
266
|
-
|
|
267
|
-
if (index === undefined)
|
|
268
|
-
|
|
394
|
+
let index = this.#toolByCallId.get(callId);
|
|
395
|
+
if (index === undefined) {
|
|
396
|
+
// Partial history can contain a result without its call. Keep the result
|
|
397
|
+
// inspectable, without inferring its input, start time or duration.
|
|
398
|
+
const parent = string(data['parentCallId']) ?? null;
|
|
399
|
+
const name = string(data['name']) ?? 'tool';
|
|
400
|
+
const orphan = this.#base(event, parent === null ? 'tool' : 'subtool', parent === null ? 'TOOL' : 'SUBTOOL', 'Call not loaded', data);
|
|
401
|
+
orphan.id = `tool:${callId}`;
|
|
402
|
+
orphan.name = name;
|
|
403
|
+
orphan.parentCallId = parent;
|
|
404
|
+
orphan.startedAt = null;
|
|
405
|
+
orphan.payload = '';
|
|
406
|
+
index = this.#push(orphan);
|
|
407
|
+
this.#toolByCallId.set(callId, index);
|
|
408
|
+
}
|
|
269
409
|
const record = this.records[index];
|
|
270
410
|
if (record === undefined)
|
|
271
411
|
return;
|
|
272
412
|
this.#invalidateText(record);
|
|
273
|
-
|
|
274
|
-
|
|
413
|
+
const content = message?.['content'] ?? data['content'] ?? data['result'] ?? data['output'];
|
|
414
|
+
const error = object(data['error']);
|
|
415
|
+
record.result = contentText(content);
|
|
416
|
+
record.rawResult = json(event.data);
|
|
417
|
+
record.resultRecorded = true;
|
|
418
|
+
record.status = message?.['isError'] === true || data['isError'] === true || data['error'] === true || error !== undefined ? 'error' : 'ok';
|
|
419
|
+
if (record.result === '' && record.status === 'error') {
|
|
420
|
+
record.result = [string(error?.['name']), string(error?.['code']), string(error?.['reason'])].filter(Boolean).join(' · ') || 'Tool failed';
|
|
421
|
+
}
|
|
275
422
|
record.durationMs = record.startedAt === null ? null : Math.max(0, event.time - record.startedAt);
|
|
276
423
|
return;
|
|
277
424
|
}
|
|
@@ -331,8 +478,10 @@ export class TrajectoryLedger {
|
|
|
331
478
|
const record = index === undefined ? undefined : this.records[index];
|
|
332
479
|
const reason = object(data['reason']);
|
|
333
480
|
if (record !== undefined) {
|
|
334
|
-
if (reason?.['kind'] === 'error' || data['error'] !== undefined)
|
|
481
|
+
if (reason?.['kind'] === 'error' || data['error'] !== undefined) {
|
|
482
|
+
this.#invalidateText(record);
|
|
335
483
|
record.status = 'error';
|
|
484
|
+
}
|
|
336
485
|
if (record.durationMs === null && record.startedAt !== null)
|
|
337
486
|
record.durationMs = Math.max(0, event.time - record.startedAt);
|
|
338
487
|
}
|
|
@@ -343,6 +492,12 @@ export class TrajectoryLedger {
|
|
|
343
492
|
const reasonKind = string(reason?.['kind']);
|
|
344
493
|
if (reasonKind === undefined || reasonKind === 'completed')
|
|
345
494
|
return;
|
|
495
|
+
for (const call of this.records) {
|
|
496
|
+
if (isCall(call) && call.turn === place.turn && call.status === 'running') {
|
|
497
|
+
this.#invalidateText(call);
|
|
498
|
+
call.status = 'warning';
|
|
499
|
+
}
|
|
500
|
+
}
|
|
346
501
|
const isError = reasonKind === 'error' || reasonKind === 'aborted';
|
|
347
502
|
const summary = reasonKind === 'max-tokens'
|
|
348
503
|
? 'Output token limit reached'
|
|
@@ -359,23 +514,26 @@ export class TrajectoryLedger {
|
|
|
359
514
|
}
|
|
360
515
|
}
|
|
361
516
|
}
|
|
362
|
-
const SEARCH_FIELDS = ['type', 'label', 'summary', 'payload', 'result', 'schema', 'thinking'];
|
|
517
|
+
const SEARCH_FIELDS = ['type', 'label', 'summary', 'payload', 'result', 'schema', 'thinking', 'input', 'rawResult'];
|
|
363
518
|
export function createTrajectory(events) {
|
|
364
519
|
const ledger = new TrajectoryLedger(events);
|
|
365
|
-
|
|
520
|
+
const state = {
|
|
366
521
|
ledger,
|
|
367
|
-
selectedId:
|
|
522
|
+
selectedId: null,
|
|
368
523
|
query: '',
|
|
369
524
|
searching: false,
|
|
370
525
|
details: false,
|
|
371
526
|
detailTab: 'summary',
|
|
372
527
|
detailScroll: 0,
|
|
528
|
+
focus: 'list',
|
|
529
|
+
expandedGroups: new Set(),
|
|
373
530
|
collapsedTurns: new Set(),
|
|
374
531
|
callsCollapsed: false,
|
|
375
532
|
following: true,
|
|
376
533
|
searchFocus: null,
|
|
377
534
|
followNotice: 0,
|
|
378
535
|
};
|
|
536
|
+
return { ...state, selectedId: trajectoryVisibleRecords(state).at(-1)?.id ?? null };
|
|
379
537
|
}
|
|
380
538
|
export function appendTrajectoryEvent(state, event) {
|
|
381
539
|
const beforeCount = state.ledger.records.length;
|
|
@@ -449,7 +607,13 @@ function isCall(record) {
|
|
|
449
607
|
export function trajectoryVisibleRecords(state) {
|
|
450
608
|
const query = state.query.trim().toLocaleLowerCase();
|
|
451
609
|
const firstInCollapsed = new Set();
|
|
610
|
+
const groups = recordGroups(state.ledger);
|
|
611
|
+
const byId = new Map(state.ledger.records.map(record => [record.id, record]));
|
|
452
612
|
return state.ledger.records.filter((record) => {
|
|
613
|
+
if (query !== '')
|
|
614
|
+
return state.ledger.recordMatches(record, query);
|
|
615
|
+
if (state.errorsOnly)
|
|
616
|
+
return ['error', 'warning', 'retry'].includes(record.status);
|
|
453
617
|
if (state.callsCollapsed && isCall(record))
|
|
454
618
|
return false;
|
|
455
619
|
if (record.turn !== null && state.collapsedTurns.has(record.turn)) {
|
|
@@ -457,9 +621,19 @@ export function trajectoryVisibleRecords(state) {
|
|
|
457
621
|
return false;
|
|
458
622
|
firstInCollapsed.add(record.turn);
|
|
459
623
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
624
|
+
const head = groups.owner.get(record.id);
|
|
625
|
+
if (head !== undefined && head !== record.id && !state.expandedGroups?.has(head))
|
|
626
|
+
return false;
|
|
627
|
+
let parent = record.parentCallId === null ? undefined : `tool:${record.parentCallId}`;
|
|
628
|
+
const seen = new Set();
|
|
629
|
+
while (parent !== undefined && byId.has(parent) && !seen.has(parent)) {
|
|
630
|
+
if (!state.expandedGroups?.has(parent))
|
|
631
|
+
return false;
|
|
632
|
+
seen.add(parent);
|
|
633
|
+
const next = byId.get(parent)?.parentCallId;
|
|
634
|
+
parent = next == null ? undefined : `tool:${next}`;
|
|
635
|
+
}
|
|
636
|
+
return true;
|
|
463
637
|
});
|
|
464
638
|
}
|
|
465
639
|
/**
|
|
@@ -502,7 +676,7 @@ function trajectoryIndex(ledger) {
|
|
|
502
676
|
continue;
|
|
503
677
|
const callKey = `${key}:${record.step ?? 'none'}`;
|
|
504
678
|
const counts = calls.get(callKey) ?? new Map();
|
|
505
|
-
const name = record.
|
|
679
|
+
const name = record.name || record.label;
|
|
506
680
|
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
507
681
|
calls.set(callKey, counts);
|
|
508
682
|
}
|
|
@@ -524,16 +698,7 @@ function trajectoryIndex(ledger) {
|
|
|
524
698
|
turnStatsCache.set(ledger, entry);
|
|
525
699
|
return entry;
|
|
526
700
|
}
|
|
527
|
-
/**
|
|
528
|
-
* The ledger as three levels, the shape the desktop client settled on.
|
|
529
|
-
*
|
|
530
|
-
* A turn is the question, a step is one model request and the calls it made, and
|
|
531
|
-
* a record is one event. The flat list this replaced gave a user message and a
|
|
532
|
-
* tool result the same weight, so answering "which step took sixteen minutes"
|
|
533
|
-
* meant reading a location column on every row and adding durations up by hand.
|
|
534
|
-
* Step headers also give the calls fold somewhere to report what it hid, which is
|
|
535
|
-
* why the histogram comes from the whole ledger and not from the visible rows.
|
|
536
|
-
*/
|
|
701
|
+
/** Turn and step boundaries remain visible; folded-call counts come from the full ledger. */
|
|
537
702
|
function trajectoryRows(state) {
|
|
538
703
|
const index = trajectoryIndex(state.ledger);
|
|
539
704
|
const rows = [];
|
|
@@ -611,7 +776,7 @@ export function trajectorySearch(state) {
|
|
|
611
776
|
const lower = lowerFields[fieldIndex] ?? '';
|
|
612
777
|
if (lower === '')
|
|
613
778
|
continue;
|
|
614
|
-
const original = record[field];
|
|
779
|
+
const original = record[field] ?? '';
|
|
615
780
|
// Case folding can expand code points (e.g. U+0130); only then are the
|
|
616
781
|
// lowercase offsets valid against the original text.
|
|
617
782
|
const offsetsExact = Array.from(lower).length === Array.from(original).length;
|
|
@@ -639,27 +804,42 @@ export function trajectorySearch(state) {
|
|
|
639
804
|
searchCache.set(state.ledger, { version: state.ledger.version, query, result });
|
|
640
805
|
return result;
|
|
641
806
|
}
|
|
807
|
+
/** Reveal the selected record without discarding unrelated folds. */
|
|
808
|
+
function revealRecord(state, record) {
|
|
809
|
+
const expanded = new Set(state.expandedGroups);
|
|
810
|
+
const groups = recordGroups(state.ledger);
|
|
811
|
+
const head = groups.owner.get(record.id);
|
|
812
|
+
if (head !== undefined)
|
|
813
|
+
expanded.add(head);
|
|
814
|
+
const byId = new Map(state.ledger.records.map(row => [row.id, row]));
|
|
815
|
+
let parent = record.parentCallId;
|
|
816
|
+
const seen = new Set();
|
|
817
|
+
while (parent !== null && !seen.has(parent)) {
|
|
818
|
+
seen.add(parent);
|
|
819
|
+
expanded.add(`tool:${parent}`);
|
|
820
|
+
parent = byId.get(`tool:${parent}`)?.parentCallId ?? null;
|
|
821
|
+
}
|
|
822
|
+
const turns = new Set(state.collapsedTurns);
|
|
823
|
+
if (record.turn !== null)
|
|
824
|
+
turns.delete(record.turn);
|
|
825
|
+
return { ...state, expandedGroups: expanded, collapsedTurns: turns, errorsOnly: false,
|
|
826
|
+
callsCollapsed: isCall(record) ? false : state.callsCollapsed, selectedId: record.id, following: false, detailScroll: 0 };
|
|
827
|
+
}
|
|
828
|
+
function retainVisibleSelection(state) {
|
|
829
|
+
const visible = trajectoryVisibleRecords(state);
|
|
830
|
+
if (visible.some(record => record.id === state.selectedId))
|
|
831
|
+
return state;
|
|
832
|
+
const selected = selectedRecord(state);
|
|
833
|
+
const nearest = visible.findLast(record => record.index <= (selected?.index ?? 0)) ?? visible[0];
|
|
834
|
+
return { ...state, selectedId: nearest?.id ?? null, detailScroll: 0 };
|
|
835
|
+
}
|
|
642
836
|
/** Locate one match: expand its turn/subtool and select the carrying record. */
|
|
643
837
|
function locateMatch(state, index) {
|
|
644
838
|
const match = trajectorySearch(state).matches[index];
|
|
645
839
|
if (match === undefined)
|
|
646
840
|
return state;
|
|
647
841
|
const record = match.record;
|
|
648
|
-
|
|
649
|
-
if (record.turn !== null && collapsedTurns.has(record.turn)) {
|
|
650
|
-
const next = new Set(collapsedTurns);
|
|
651
|
-
next.delete(record.turn);
|
|
652
|
-
collapsedTurns = next;
|
|
653
|
-
}
|
|
654
|
-
return {
|
|
655
|
-
...state,
|
|
656
|
-
searchFocus: index,
|
|
657
|
-
selectedId: record.id,
|
|
658
|
-
collapsedTurns,
|
|
659
|
-
callsCollapsed: state.callsCollapsed && !isCall(record),
|
|
660
|
-
detailScroll: 0,
|
|
661
|
-
following: false,
|
|
662
|
-
};
|
|
842
|
+
return { ...revealRecord(state, record), searchFocus: index };
|
|
663
843
|
}
|
|
664
844
|
function moveMatch(state, delta) {
|
|
665
845
|
const matches = trajectorySearch(state).matches;
|
|
@@ -682,7 +862,8 @@ function move(state, delta) {
|
|
|
682
862
|
...state,
|
|
683
863
|
selectedId: visible[index]?.id ?? null,
|
|
684
864
|
detailScroll: 0,
|
|
685
|
-
following,
|
|
865
|
+
following: state.details ? false : following,
|
|
866
|
+
notice: '',
|
|
686
867
|
...(following ? { followNotice: 0 } : {}),
|
|
687
868
|
};
|
|
688
869
|
}
|
|
@@ -691,24 +872,17 @@ function graphemes(text) {
|
|
|
691
872
|
trajectorySegmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
692
873
|
return trajectorySegmenter.segment(text);
|
|
693
874
|
}
|
|
694
|
-
/**
|
|
695
|
-
* Tabs a record can actually fill.
|
|
696
|
-
*
|
|
697
|
-
* The tabs used to be fixed, so most records offered four that said "No payload
|
|
698
|
-
* recorded." and the reader pressed `Tab` through them to find the one with
|
|
699
|
-
* content. The desktop client instead asks each record which sections it has,
|
|
700
|
-
* which is also why its tab row never shows a dead one. Timing was a section of
|
|
701
|
-
* its own until it became clear that the numbers a reader wants first belong in
|
|
702
|
-
* the summary, beside the record's identity.
|
|
703
|
-
*/
|
|
875
|
+
/** Offer only sections with recorded content; completed empty results still have a Result section. */
|
|
704
876
|
function detailTabsFor(record) {
|
|
705
877
|
if (record === undefined)
|
|
706
878
|
return ['summary'];
|
|
707
879
|
const tabs = ['summary'];
|
|
708
|
-
if (record.
|
|
709
|
-
tabs.push('
|
|
710
|
-
if (record.result !== '')
|
|
880
|
+
if (record.input !== undefined && record.input !== '')
|
|
881
|
+
tabs.push('input');
|
|
882
|
+
if (record.resultRecorded || record.result !== '' || record.thinking !== '')
|
|
711
883
|
tabs.push('result');
|
|
884
|
+
if (record.payload !== '' || record.rawResult !== undefined)
|
|
885
|
+
tabs.push('payload');
|
|
712
886
|
if (record.schema !== '')
|
|
713
887
|
tabs.push('schema');
|
|
714
888
|
return tabs;
|
|
@@ -733,6 +907,10 @@ function moveTab(state, delta) {
|
|
|
733
907
|
const DEFAULT_PAGE_SIZE = 10;
|
|
734
908
|
const DEFAULT_DETAIL_PAGE_LINES = 10;
|
|
735
909
|
export function applyTrajectoryEvent(state, event, options = {}) {
|
|
910
|
+
const detailFocused = state.details && (state.focus !== 'list' || state.fullDetail === true || (options.width !== undefined && options.width < 96));
|
|
911
|
+
const maxScroll = options.maxDetailScroll ?? Number.MAX_SAFE_INTEGER;
|
|
912
|
+
const scroll = (offset) => ({ ...state, detailScroll: Math.max(0, Math.min(maxScroll, offset)), following: false });
|
|
913
|
+
const currentScroll = Math.min(state.detailScroll, maxScroll);
|
|
736
914
|
const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
737
915
|
const detailPageLines = options.detailPageLines ?? DEFAULT_DETAIL_PAGE_LINES;
|
|
738
916
|
if (state.searching) {
|
|
@@ -759,6 +937,34 @@ export function applyTrajectoryEvent(state, event, options = {}) {
|
|
|
759
937
|
return { state: { ...state, searching: false } };
|
|
760
938
|
return { state };
|
|
761
939
|
}
|
|
940
|
+
if (event.type === 'text' && state.query === '') {
|
|
941
|
+
if (event.value === ' ')
|
|
942
|
+
return { state: toggleGroup(state) };
|
|
943
|
+
if (event.value === 'f' && state.details)
|
|
944
|
+
return { state: { ...state, fullDetail: !state.fullDetail, focus: 'detail', following: false } };
|
|
945
|
+
if (event.value === 'e') {
|
|
946
|
+
const next = { ...state, errorsOnly: !state.errorsOnly, following: false };
|
|
947
|
+
return { state: { ...next, selectedId: trajectoryVisibleRecords(next)[0]?.id ?? null } };
|
|
948
|
+
}
|
|
949
|
+
if (event.value === 's') {
|
|
950
|
+
const slow = state.ledger.records.filter(record => isCall(record) && record.durationMs !== null)
|
|
951
|
+
.sort((a, b) => b.durationMs - a.durationMs || a.index - b.index);
|
|
952
|
+
const at = slow.findIndex(record => record.id === state.selectedId);
|
|
953
|
+
const record = slow[(at + 1) % slow.length];
|
|
954
|
+
if (record !== undefined) {
|
|
955
|
+
return { state: { ...revealRecord(state, record), notice: 'Calls by duration · s next' } };
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (event.value === 'y') {
|
|
959
|
+
const record = selectedRecord(state);
|
|
960
|
+
if (record !== undefined) {
|
|
961
|
+
const tab = state.details ? activeTab(state) : 'summary';
|
|
962
|
+
const text = tab === 'input' ? record.input ?? '' : tab === 'payload' ? rawPayload(record) : tab === 'schema' ? record.schema
|
|
963
|
+
: tab === 'result' ? [record.thinking, record.result].filter(Boolean).join('\n\n') : [record.summary, record.input === undefined ? '' : `Input\n${record.input}`, record.thinking === '' ? '' : `Thinking\n${record.thinking}`, record.result === '' ? '' : `Result\n${record.result}`].filter(Boolean).join('\n\n');
|
|
964
|
+
return { copy: text, label: TAB_LABELS[tab] };
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
762
968
|
if (event.type === 'text') {
|
|
763
969
|
const lower = event.value.toLowerCase();
|
|
764
970
|
if (event.value === '/' && state.query === '') {
|
|
@@ -771,7 +977,7 @@ export function applyTrajectoryEvent(state, event, options = {}) {
|
|
|
771
977
|
if (lower === 'n' && state.query !== '')
|
|
772
978
|
return { state: moveMatch(state, event.value === 'N' ? -1 : 1) };
|
|
773
979
|
if (lower === 'c')
|
|
774
|
-
return { state: { ...state, callsCollapsed: !state.callsCollapsed } };
|
|
980
|
+
return { state: retainVisibleSelection({ ...state, callsCollapsed: !state.callsCollapsed, following: false }) };
|
|
775
981
|
if (lower === 't') {
|
|
776
982
|
const selected = state.ledger.records.find(record => record.id === state.selectedId);
|
|
777
983
|
if (selected?.turn === null || selected?.turn === undefined)
|
|
@@ -782,7 +988,7 @@ export function applyTrajectoryEvent(state, event, options = {}) {
|
|
|
782
988
|
else
|
|
783
989
|
collapsed.add(selected.turn);
|
|
784
990
|
const first = state.ledger.records.find(record => record.turn === selected.turn);
|
|
785
|
-
return { state: { ...state, collapsedTurns: collapsed, selectedId: first?.id ?? state.selectedId } };
|
|
991
|
+
return { state: retainVisibleSelection({ ...state, collapsedTurns: collapsed, selectedId: first?.id ?? state.selectedId, following: false }) };
|
|
786
992
|
}
|
|
787
993
|
if (state.query !== '') {
|
|
788
994
|
// Any other printable character in the result state starts a new query.
|
|
@@ -790,70 +996,60 @@ export function applyTrajectoryEvent(state, event, options = {}) {
|
|
|
790
996
|
}
|
|
791
997
|
return { state };
|
|
792
998
|
}
|
|
793
|
-
|
|
794
|
-
* A wheel gesture scrolls whatever the reader is reading.
|
|
795
|
-
*
|
|
796
|
-
* The view received wheel events already but ignored everything that was not a
|
|
797
|
-
* key, so a mouse reader found the only way to scroll the detail pane was
|
|
798
|
-
* `PgUp`/`PgDn` — while the wheel silently did nothing at all. It now scrolls
|
|
799
|
-
* the detail when one is open and moves the selection otherwise, which is what
|
|
800
|
-
* the same gesture does in the transcript.
|
|
801
|
-
*/
|
|
999
|
+
// Wheel routing follows the pointer, independently of keyboard focus.
|
|
802
1000
|
if (event.type === 'wheel') {
|
|
803
1001
|
const step = Math.max(1, Math.round(detailPageLines / 2));
|
|
804
|
-
|
|
1002
|
+
const overDetail = event.column === undefined ? detailFocused
|
|
1003
|
+
: state.fullDetail || (options.width ?? 0) < 96 || event.column > trajectoryLeftWidth(options.width);
|
|
1004
|
+
if (state.details && overDetail) {
|
|
805
1005
|
const delta = event.direction === 'up' ? -step : step;
|
|
806
|
-
return { state:
|
|
1006
|
+
return { state: scroll(currentScroll + delta) };
|
|
807
1007
|
}
|
|
808
1008
|
return { state: move(state, (event.direction === 'up' ? -1 : 1) * 3) };
|
|
809
1009
|
}
|
|
810
1010
|
if (event.type !== 'key')
|
|
811
1011
|
return { state };
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
* payload changed which record the pane described instead of scrolling: the
|
|
826
|
-
* content jumped out from under them and the pane snapped back to the top of a
|
|
827
|
-
* different record. `Tab` still switches tabs and `Esc` still closes the pane,
|
|
828
|
-
* so nothing is lost — the selection simply stops competing with the text.
|
|
829
|
-
*/
|
|
830
|
-
if (state.details && (event.id === 'up' || event.id === 'down')) {
|
|
1012
|
+
if (event.id === 'tab' || event.id === 'shift+tab') {
|
|
1013
|
+
if (!state.details)
|
|
1014
|
+
return { state };
|
|
1015
|
+
const detailOnly = state.fullDetail || (options.width !== undefined && options.width < 96);
|
|
1016
|
+
return { state: { ...state, focus: detailOnly ? 'detail' : state.focus === 'list' ? 'detail' : 'list' } };
|
|
1017
|
+
}
|
|
1018
|
+
if (event.id === 'right')
|
|
1019
|
+
return { state: { ...moveTab(state, 1), details: true, focus: 'detail', following: false } };
|
|
1020
|
+
if (event.id === 'left')
|
|
1021
|
+
return { state: { ...moveTab(state, -1), details: true, focus: 'detail', following: false } };
|
|
1022
|
+
if (event.id === 'ctrl+n' || event.id === 'ctrl+p')
|
|
1023
|
+
return { state: move(state, event.id === 'ctrl+n' ? 1 : -1) };
|
|
1024
|
+
if (detailFocused && (event.id === 'up' || event.id === 'down')) {
|
|
831
1025
|
const delta = event.id === 'up' ? -1 : 1;
|
|
832
|
-
return { state:
|
|
1026
|
+
return { state: scroll(currentScroll + delta) };
|
|
833
1027
|
}
|
|
834
1028
|
const navigation = listNavigation(event);
|
|
835
1029
|
if (navigation.kind === 'close') {
|
|
836
1030
|
// One layer per press: an open detail panel closes before the ledger does.
|
|
837
1031
|
if (state.details)
|
|
838
|
-
return { state: { ...state, details: false, detailScroll: 0 } };
|
|
1032
|
+
return { state: { ...state, details: false, fullDetail: false, focus: 'list', detailScroll: 0 } };
|
|
839
1033
|
return { close: true };
|
|
840
1034
|
}
|
|
841
1035
|
if (navigation.kind === 'move')
|
|
842
1036
|
return { state: move(state, navigation.delta) };
|
|
843
1037
|
if (event.id === 'pageUp') {
|
|
844
|
-
if (
|
|
845
|
-
return { state:
|
|
1038
|
+
if (detailFocused)
|
|
1039
|
+
return { state: scroll(currentScroll - detailPageLines) };
|
|
846
1040
|
if (pageSize === 0)
|
|
847
1041
|
return { state };
|
|
848
1042
|
return { state: move(state, -pageSize) };
|
|
849
1043
|
}
|
|
850
1044
|
if (event.id === 'pageDown') {
|
|
851
|
-
if (
|
|
852
|
-
return { state:
|
|
1045
|
+
if (detailFocused)
|
|
1046
|
+
return { state: scroll(currentScroll + detailPageLines) };
|
|
853
1047
|
if (pageSize === 0)
|
|
854
1048
|
return { state };
|
|
855
1049
|
return { state: move(state, pageSize) };
|
|
856
1050
|
}
|
|
1051
|
+
if (detailFocused && (event.id === 'home' || event.id === 'end'))
|
|
1052
|
+
return { state: scroll(event.id === 'home' ? 0 : maxScroll) };
|
|
857
1053
|
if (event.id === 'home') {
|
|
858
1054
|
const first = trajectoryVisibleRecords(state)[0];
|
|
859
1055
|
return { state: { ...state, selectedId: first?.id ?? null, following: false } };
|
|
@@ -863,16 +1059,16 @@ export function applyTrajectoryEvent(state, event, options = {}) {
|
|
|
863
1059
|
return { state: { ...state, selectedId: last?.id ?? null, following: true, followNotice: 0 } };
|
|
864
1060
|
}
|
|
865
1061
|
if (event.id === 'enter')
|
|
866
|
-
return { state: { ...state, details:
|
|
1062
|
+
return { state: { ...state, details: true, focus: 'detail', following: false, detailScroll: 0 } };
|
|
867
1063
|
return { state };
|
|
868
1064
|
}
|
|
869
1065
|
// Every key `applyTrajectoryEvent` accepts. Rows whose first word is the footer
|
|
870
1066
|
// label are also printed by the overlay's toolbar and footer.
|
|
871
1067
|
const HOTKEY_NAVIGATE = { keys: '↑↓', action: 'Navigate records, or scroll an open detail' };
|
|
872
|
-
const HOTKEY_EDGE = { keys: 'Home / End', action: '
|
|
1068
|
+
const HOTKEY_EDGE = { keys: 'Home / End', action: 'First / newest record; top / bottom in details' };
|
|
873
1069
|
const HOTKEY_PAGE = { keys: '↑↓ / PgUp / PgDn', action: 'Scroll the open detail, or page the record list' };
|
|
874
|
-
const HOTKEY_DETAILS = { keys: 'Enter', action: 'Details — open
|
|
875
|
-
const HOTKEY_SECTION = { keys: '
|
|
1070
|
+
const HOTKEY_DETAILS = { keys: 'Enter', action: 'Details — open and focus the detail panel' };
|
|
1071
|
+
const HOTKEY_SECTION = { keys: '←→', action: 'Section — switch the detail tab' };
|
|
876
1072
|
const HOTKEY_SEARCH = { keys: '/', action: 'Search the ledger' };
|
|
877
1073
|
const HOTKEY_MATCH = { keys: 'n/N', action: 'Match — step to the next or previous hit' };
|
|
878
1074
|
const HOTKEY_MATCH_EDIT = { keys: 'Ctrl+N / Ctrl+P', action: 'Match while typing — step to the next or previous hit' };
|
|
@@ -888,6 +1084,13 @@ export const TRAJECTORY_HOTKEYS = [
|
|
|
888
1084
|
HOTKEY_PAGE,
|
|
889
1085
|
HOTKEY_DETAILS,
|
|
890
1086
|
HOTKEY_SECTION,
|
|
1087
|
+
{ keys: 'Tab', action: 'Focus — switch between records and details' },
|
|
1088
|
+
{ keys: 'Ctrl+N / Ctrl+P', action: 'Record — inspect the next or previous record' },
|
|
1089
|
+
{ keys: 'Space', action: 'Group — expand context entries or subcalls' },
|
|
1090
|
+
{ keys: 'f', action: 'Full screen — enlarge the detail pane' },
|
|
1091
|
+
{ keys: 'e', action: 'Problems — filter failed, interrupted and retry records' },
|
|
1092
|
+
{ keys: 's', action: 'Slow calls — visit calls in descending duration order' },
|
|
1093
|
+
{ keys: 'y', action: 'Copy — copy the current section without display formatting' },
|
|
891
1094
|
HOTKEY_SEARCH,
|
|
892
1095
|
HOTKEY_MATCH,
|
|
893
1096
|
HOTKEY_MATCH_EDIT,
|
|
@@ -905,7 +1108,7 @@ export const TRAJECTORY_HOTKEYS = [
|
|
|
905
1108
|
* frame can show.
|
|
906
1109
|
*/
|
|
907
1110
|
export function trajectoryOverviewHeight(state, width = Number.POSITIVE_INFINITY) {
|
|
908
|
-
if (state.details || state.query !== '')
|
|
1111
|
+
if (state.details || state.query !== '' || state.errorsOnly)
|
|
909
1112
|
return 0;
|
|
910
1113
|
if (width < 24)
|
|
911
1114
|
return 0;
|
|
@@ -926,7 +1129,7 @@ export function trajectoryListMetrics(state, height, width = Number.POSITIVE_INF
|
|
|
926
1129
|
if (rows.length === 0)
|
|
927
1130
|
return { pageSize: 1 };
|
|
928
1131
|
const selected = rows.findIndex(row => row.kind === 'record' && row.id === state.selectedId);
|
|
929
|
-
const start = Math.max(0, Math.min(rows.length -
|
|
1132
|
+
const start = Math.max(0, Math.min(Math.max(0, rows.length - body), Math.max(0, selected) - Math.floor(body / 2)));
|
|
930
1133
|
// Count record rows inside the window; turn and step headers only occupy space.
|
|
931
1134
|
let count = 0;
|
|
932
1135
|
for (let index = start; index < rows.length && index - start < body; index += 1) {
|
|
@@ -990,7 +1193,7 @@ function highlightSummary(summary, spans, theme) {
|
|
|
990
1193
|
}
|
|
991
1194
|
return out + summary.slice(cursor);
|
|
992
1195
|
}
|
|
993
|
-
function recordLine(record, selected, theme, width, highlightSpans, matchCount, summaryOverride) {
|
|
1196
|
+
function recordLine(record, selected, theme, width, highlightSpans, matchCount, summaryOverride, disclosure) {
|
|
994
1197
|
const marker = selected ? theme.fg('accent', '›') : ' ';
|
|
995
1198
|
/*
|
|
996
1199
|
* No location column: the turn rule and the step header above already say
|
|
@@ -998,7 +1201,10 @@ function recordLine(record, selected, theme, width, highlightSpans, matchCount,
|
|
|
998
1201
|
* a form nobody reads. The right edge instead carries what the row cost:
|
|
999
1202
|
* tokens for a model request, duration for everything.
|
|
1000
1203
|
*/
|
|
1001
|
-
const
|
|
1204
|
+
const status = record.status === 'error' ? '✗' : record.status === 'warning' ? '!' : record.status === 'running' ? '◌' : isCall(record) ? '✓' : ' ';
|
|
1205
|
+
const label = isCall(record) && record.name !== '' ? record.name : record.kind === 'assistant' ? 'Model' : record.kind === 'context' ? 'Context' : record.label;
|
|
1206
|
+
const tone = record.status === 'error' ? 'error' : record.status === 'warning' ? 'warning' : kindColor(record.kind);
|
|
1207
|
+
const prefix = `${marker} ${disclosure === undefined ? '' : theme.fg('dim', disclosure) + ' '}${theme.fg(tone, status)} ${record.parentCallId === null ? '' : ' ↳ '}${theme.fg(isCall(record) ? 'text' : tone, truncateToWidth(label, 12))} `;
|
|
1002
1208
|
const badge = matchCount === undefined ? '' : ` ${theme.fg('dim', `×${matchCount}`)}`;
|
|
1003
1209
|
// A record with no recorded duration prints nothing rather than an em dash:
|
|
1004
1210
|
// a column of `—` where most rows have a time reads as missing data, when in
|
|
@@ -1014,11 +1220,12 @@ function recordLine(record, selected, theme, width, highlightSpans, matchCount,
|
|
|
1014
1220
|
const bodyWidth = Math.max(0, width - fixed - visibleWidth(columns) - 2);
|
|
1015
1221
|
const text = truncateToWidth(body, bodyWidth);
|
|
1016
1222
|
const gap = Math.max(1, width - fixed - visibleWidth(text) - visibleWidth(columns));
|
|
1017
|
-
|
|
1223
|
+
const row = truncateToWidth(prefix + text + ' '.repeat(Math.max(0, gap)) + columns + badge, width);
|
|
1224
|
+
return selected ? paintSurface(theme, 'userMessageBg', 'userMessageText', padToWidth(row, width)) : row;
|
|
1018
1225
|
}
|
|
1019
1226
|
/** Snippet of one record field around its first hit (non-summary fields). */
|
|
1020
1227
|
function fieldSnippet(record, field, highlightSpans, width, theme, focused) {
|
|
1021
|
-
const text = record[field];
|
|
1228
|
+
const text = record[field] ?? '';
|
|
1022
1229
|
if (text === '')
|
|
1023
1230
|
return highlightSummary(record.summary, highlightSpans, theme);
|
|
1024
1231
|
const span = focused ?? highlightSpans.find(match => match.field === field);
|
|
@@ -1051,16 +1258,12 @@ function clockTime(ms) {
|
|
|
1051
1258
|
const pad = (value) => String(value).padStart(2, '0');
|
|
1052
1259
|
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
1053
1260
|
}
|
|
1054
|
-
/**
|
|
1055
|
-
* Label/value pairs for one record, omitting what it does not have.
|
|
1056
|
-
*
|
|
1057
|
-
* The title already states where the record sits and how long it took, so this
|
|
1058
|
-
* block does not repeat either: it answers what the record is, when it happened,
|
|
1059
|
-
* and what it cost the model.
|
|
1060
|
-
*/
|
|
1261
|
+
/** Recorded timing and usage, with exceptional status shown explicitly. */
|
|
1061
1262
|
function summaryFields(record) {
|
|
1062
1263
|
const rows = [];
|
|
1063
1264
|
rows.push({ label: 'Event', value: `${record.type} · #${record.seq}` });
|
|
1265
|
+
if (record.durationMs !== null)
|
|
1266
|
+
rows.push({ label: 'Duration', value: formatDuration(record.durationMs) });
|
|
1064
1267
|
if (record.startedAt !== null)
|
|
1065
1268
|
rows.push({ label: 'Started', value: clockTime(record.startedAt) });
|
|
1066
1269
|
if (record.ttftMs !== null)
|
|
@@ -1072,14 +1275,14 @@ function summaryFields(record) {
|
|
|
1072
1275
|
// `ok` on every record is the kind of field that teaches nothing; the ones
|
|
1073
1276
|
// worth a line are the ones that went wrong.
|
|
1074
1277
|
if (record.status !== 'ok')
|
|
1075
|
-
rows.push({ label: 'Status', value: record.status });
|
|
1278
|
+
rows.push({ label: 'Status', value: record.status === 'warning' && isCall(record) ? 'Interrupted · no result recorded' : record.status });
|
|
1076
1279
|
return rows;
|
|
1077
1280
|
}
|
|
1078
1281
|
/** The assistant's reasoning and answer are two pieces of writing, not one blob. */
|
|
1079
1282
|
function assistantWriting(record, theme, width) {
|
|
1080
1283
|
const out = [];
|
|
1081
1284
|
if (record.thinking !== '') {
|
|
1082
|
-
out.push(theme.fg('dim', 'Thinking'), ...renderMarkdown(record.thinking, theme, width));
|
|
1285
|
+
out.push(theme.fg('dim', 'Thinking'), ...renderMarkdown(record.thinking, theme, width, { color: 'thinkingText', italic: true }));
|
|
1083
1286
|
if (record.result !== '')
|
|
1084
1287
|
out.push('', theme.fg('dim', 'Answer'));
|
|
1085
1288
|
}
|
|
@@ -1093,7 +1296,9 @@ function detailSection(tab, record) {
|
|
|
1093
1296
|
if (tab === 'summary')
|
|
1094
1297
|
return { kind: 'fields', rows: summaryFields(record) };
|
|
1095
1298
|
if (tab === 'payload')
|
|
1096
|
-
return { kind: 'code', text: record
|
|
1299
|
+
return { kind: 'code', text: rawPayload(record), language: 'json' };
|
|
1300
|
+
if (tab === 'input')
|
|
1301
|
+
return { kind: 'code', text: record.input ?? '', language: 'json' };
|
|
1097
1302
|
if (tab === 'schema')
|
|
1098
1303
|
return { kind: 'code', text: record.schema, language: 'json' };
|
|
1099
1304
|
// A model's answer is prose; a tool's output is whatever the tool spoke.
|
|
@@ -1102,14 +1307,14 @@ function detailSection(tab, record) {
|
|
|
1102
1307
|
return { kind: 'prose', text: record.result };
|
|
1103
1308
|
return {
|
|
1104
1309
|
kind: 'code',
|
|
1105
|
-
text: record.result,
|
|
1106
|
-
language:
|
|
1310
|
+
text: record.result || (record.resultRecorded ? 'No text output. Non-text fields are available in Raw.' : 'No result recorded.'),
|
|
1311
|
+
language: undefined,
|
|
1107
1312
|
};
|
|
1108
1313
|
}
|
|
1109
1314
|
function ledgerRows(state, theme, width, height) {
|
|
1110
1315
|
const rows = trajectoryRows(state);
|
|
1111
1316
|
if (!rows.some(row => row.kind === 'record')) {
|
|
1112
|
-
return [theme.fg('dim', state.query === '' ? ' No trajectory records.' : ' No matching trajectory records.')];
|
|
1317
|
+
return [theme.fg('dim', state.query === '' ? state.errorsOnly ? ' No problems recorded.' : ' No trajectory records.' : ' No matching trajectory records.')];
|
|
1113
1318
|
}
|
|
1114
1319
|
const { matches, counts } = trajectorySearch(state);
|
|
1115
1320
|
const focused = state.searchFocus === null ? undefined : matches[state.searchFocus];
|
|
@@ -1119,7 +1324,10 @@ function ledgerRows(state, theme, width, height) {
|
|
|
1119
1324
|
list.push(match);
|
|
1120
1325
|
highlighted.set(match.record.id, list);
|
|
1121
1326
|
}
|
|
1122
|
-
const
|
|
1327
|
+
const groups = recordGroups(state.ledger);
|
|
1328
|
+
const selectedIndex = Math.max(0, rows.findIndex(row => row.kind === 'record' && row.id === state.selectedId));
|
|
1329
|
+
const start = Math.max(0, Math.min(Math.max(0, rows.length - height), selectedIndex - Math.floor(height / 2)));
|
|
1330
|
+
const lines = rows.slice(start, start + height).map((row) => {
|
|
1123
1331
|
if (row.kind === 'turn')
|
|
1124
1332
|
return turnLine(row.stats, row.collapsed, theme, width);
|
|
1125
1333
|
if (row.kind === 'step')
|
|
@@ -1129,42 +1337,60 @@ function ledgerRows(state, theme, width, height) {
|
|
|
1129
1337
|
const focusedForRecord = focused?.record === record ? focused : undefined;
|
|
1130
1338
|
const headField = focusedForRecord !== undefined ? focusedForRecord.field : recordMatches[0]?.field;
|
|
1131
1339
|
const headSpans = recordMatches.filter(match => match.field === headField);
|
|
1132
|
-
|
|
1340
|
+
let summary = headField === undefined || headField === 'summary'
|
|
1133
1341
|
? highlightSummary(record.summary, headSpans, theme)
|
|
1134
1342
|
: fieldSnippet(record, headField, headSpans, Math.max(0, width - 24), theme, focusedForRecord);
|
|
1135
|
-
|
|
1343
|
+
let disclosure;
|
|
1344
|
+
if (state.query === '' && !state.errorsOnly) {
|
|
1345
|
+
const contexts = groups.contexts.get(record.id);
|
|
1346
|
+
const children = groups.children.get(record.id);
|
|
1347
|
+
if ((contexts?.length ?? 0) > 1 || children !== undefined)
|
|
1348
|
+
disclosure = state.expandedGroups?.has(record.id) ? '▾' : '▸';
|
|
1349
|
+
if (contexts !== undefined && contexts.length > 1 && !state.expandedGroups?.has(record.id))
|
|
1350
|
+
summary = `${contexts.length} injected entries · Space to expand`;
|
|
1351
|
+
else if (children !== undefined)
|
|
1352
|
+
summary += ` · ${children.length} subcalls`;
|
|
1353
|
+
}
|
|
1354
|
+
return recordLine(record, record.id === state.selectedId, theme, width, headField === 'summary' ? headSpans : [], state.query === '' ? undefined : counts.get(record.id), summary, disclosure);
|
|
1136
1355
|
});
|
|
1137
|
-
|
|
1138
|
-
const start = Math.max(0, Math.min(rows.length - 1, selectedIndex - Math.floor(height / 2)));
|
|
1139
|
-
return lines.slice(start, start + height).map(line => truncateToWidth(line, width));
|
|
1356
|
+
return lines.map(line => truncateToWidth(line, width));
|
|
1140
1357
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
* Two rows of header — the record's identity and its section names — then the
|
|
1145
|
-
* section itself. Position and scroll hint ride on the section row rather than
|
|
1146
|
-
* taking a line of their own, and the sections are separated from the content by
|
|
1147
|
-
* whitespace instead of a rule, because in the split view a rule would sit one
|
|
1148
|
-
* column from the pane's own border.
|
|
1149
|
-
*/
|
|
1150
|
-
function detailRows(state, theme, width, height) {
|
|
1358
|
+
const detailBodyCache = new WeakMap();
|
|
1359
|
+
/** Wrapped source is reused while scrolling the same section. */
|
|
1360
|
+
function detailBody(state, theme, width) {
|
|
1151
1361
|
const record = selectedRecord(state);
|
|
1152
1362
|
const tab = activeTab(state);
|
|
1153
|
-
const
|
|
1363
|
+
const cached = detailBodyCache.get(state.ledger);
|
|
1364
|
+
if (cached?.version === state.ledger.version && cached.record === record && cached.tab === tab && cached.width === width && cached.theme === theme.key)
|
|
1365
|
+
return cached.rows;
|
|
1154
1366
|
const section = detailSection(tab, record);
|
|
1155
1367
|
const inner = Math.max(1, width - 2);
|
|
1156
|
-
const
|
|
1368
|
+
const rows = tab === 'input' && record !== undefined ? argumentRows(record, theme, inner) : section.kind === 'fields'
|
|
1157
1369
|
? summaryBlock(section.rows, record, theme, inner)
|
|
1158
1370
|
: section.kind === 'prose'
|
|
1159
1371
|
? (record !== undefined && record.kind === 'assistant' && tab === 'result'
|
|
1160
1372
|
? assistantWriting(record, theme, inner)
|
|
1161
1373
|
: renderMarkdown(section.text, theme, inner))
|
|
1162
1374
|
: codeRows(section.text, section.language, theme, inner);
|
|
1375
|
+
detailBodyCache.set(state.ledger, { version: state.ledger.version, record, tab, width, theme: theme.key, rows });
|
|
1376
|
+
return rows;
|
|
1377
|
+
}
|
|
1378
|
+
export function trajectoryDetailScrollLimit(state, theme, width, height) {
|
|
1379
|
+
if (!state.details)
|
|
1380
|
+
return 0;
|
|
1381
|
+
const paneWidth = !state.fullDetail && width >= 96 ? width - trajectoryLeftWidth(width) - 1 : width;
|
|
1382
|
+
return Math.max(0, detailBody(state, theme, Math.max(1, paneWidth)).length - Math.max(0, height - 7));
|
|
1383
|
+
}
|
|
1384
|
+
function detailRows(state, theme, width, height) {
|
|
1385
|
+
const record = selectedRecord(state);
|
|
1386
|
+
const tab = activeTab(state);
|
|
1387
|
+
const tabs = detailTabsFor(record);
|
|
1388
|
+
const body = detailBody(state, theme, width);
|
|
1163
1389
|
const viewport = Math.max(0, height - 3);
|
|
1164
1390
|
const maxScroll = Math.max(0, body.length - viewport);
|
|
1165
1391
|
const start = Math.min(state.detailScroll, maxScroll);
|
|
1166
1392
|
const end = Math.min(body.length, start + viewport);
|
|
1167
|
-
const first = truncateToWidth(titleLine(record, theme, width), width);
|
|
1393
|
+
const first = truncateToWidth(' ' + titleLine(record, theme, Math.max(1, width - 2)), width);
|
|
1168
1394
|
/*
|
|
1169
1395
|
* The active section is marked, not only coloured. Every style helper is a
|
|
1170
1396
|
* no-op when the terminal has no colour, so colour alone would leave a reader
|
|
@@ -1172,11 +1398,11 @@ function detailRows(state, theme, width, height) {
|
|
|
1172
1398
|
* the same "this one" the ledger uses for its selected row.
|
|
1173
1399
|
*/
|
|
1174
1400
|
const tabsText = tabs.map(candidate => candidate === tab
|
|
1175
|
-
? theme.bold(theme.fg('accent', `› ${candidate}`))
|
|
1176
|
-
: theme.fg('dim', ` ${candidate}`)).join(' ');
|
|
1401
|
+
? theme.bold(theme.fg('accent', `› ${TAB_LABELS[candidate]}`))
|
|
1402
|
+
: theme.fg('dim', ` ${TAB_LABELS[candidate]}`)).join(' ');
|
|
1177
1403
|
const position = body.length > viewport ? theme.fg('dim', `${start + 1}-${end} / ${body.length}`) : '';
|
|
1178
1404
|
const gap = Math.max(1, width - visibleWidth(tabsText) - visibleWidth(position));
|
|
1179
|
-
const second = truncateToWidth(tabsText + ' '.repeat(gap) + position, width);
|
|
1405
|
+
const second = truncateToWidth(' ' + tabsText + ' '.repeat(Math.max(1, gap - 2)) + position, width);
|
|
1180
1406
|
return [first, second, '', ...body.slice(start, end).map(line => truncateToWidth(' ' + line, width))];
|
|
1181
1407
|
}
|
|
1182
1408
|
/** `#3 TOOL` — which record this pane is about, and what it cost. */
|
|
@@ -1191,19 +1417,18 @@ function titleLine(record, theme, width) {
|
|
|
1191
1417
|
const gap = Math.max(1, width - visibleWidth(left) - visibleWidth(right));
|
|
1192
1418
|
return left + ' '.repeat(gap) + right;
|
|
1193
1419
|
}
|
|
1194
|
-
/**
|
|
1195
|
-
* The facts, then the record's own text when no other section holds it.
|
|
1196
|
-
*
|
|
1197
|
-
* A record whose text has a section of its own — an answer in `result`, a call in
|
|
1198
|
-
* `payload` — would only see that text flattened into one row here, which is what
|
|
1199
|
-
* made the pane read as a wall of code. A record with nowhere else to put it (a
|
|
1200
|
-
* turn that failed, a message the user sent) gets it as writing, below the facts.
|
|
1201
|
-
*/
|
|
1420
|
+
/** Overview combines recorded facts with readable input, reasoning and output. */
|
|
1202
1421
|
function summaryBlock(rows, record, theme, width) {
|
|
1203
1422
|
const facts = fieldRows(rows, theme, width);
|
|
1204
|
-
if (record === undefined
|
|
1423
|
+
if (record === undefined)
|
|
1205
1424
|
return facts;
|
|
1206
|
-
|
|
1425
|
+
if (isCall(record) && record.input !== undefined) {
|
|
1426
|
+
return [...facts, '', theme.bold('Input'), ...argumentRows(record, theme, width), '', theme.bold('Result'),
|
|
1427
|
+
...codeRows(record.resultRecorded ? record.result || 'No text output. Non-text fields are available in Raw.' : record.status === 'running' ? 'Running…' : 'No result recorded.', undefined, theme, width)];
|
|
1428
|
+
}
|
|
1429
|
+
if (record.kind === 'assistant')
|
|
1430
|
+
return [...facts, '', ...assistantWriting(record, theme, width)];
|
|
1431
|
+
return [...facts, '', ...renderMarkdown(record.result || record.summary, theme, width)];
|
|
1207
1432
|
}
|
|
1208
1433
|
/** Label/value rows with the label column as wide as the widest label needs. */
|
|
1209
1434
|
function fieldRows(rows, theme, width) {
|
|
@@ -1219,29 +1444,22 @@ function fieldRows(rows, theme, width) {
|
|
|
1219
1444
|
}
|
|
1220
1445
|
return out;
|
|
1221
1446
|
}
|
|
1222
|
-
/**
|
|
1223
|
-
* Code, numbered only once it wraps.
|
|
1224
|
-
*
|
|
1225
|
-
* The gutter used to number every wrapped row, so a JSON line spread over four
|
|
1226
|
-
* rows read as four source lines and nothing lined up. Numbers now belong to
|
|
1227
|
-
* source lines, a wrapped continuation leaves the column blank, and the gutter
|
|
1228
|
-
* appears at all only when some line is too long to fit — a short payload reads
|
|
1229
|
-
* better as the block it is.
|
|
1230
|
-
*/
|
|
1447
|
+
/** Preserve source indentation; only wrapped structured data receives a line-number gutter. */
|
|
1231
1448
|
function codeRows(text, language, theme, width) {
|
|
1232
1449
|
if (text === '')
|
|
1233
1450
|
return [theme.fg('dim', 'Nothing recorded.')];
|
|
1234
|
-
const source = text.split('\n');
|
|
1451
|
+
const source = text.split('\n').map(line => expandTabs(line, 8, 0));
|
|
1235
1452
|
const digits = String(source.length).length;
|
|
1236
1453
|
const gutter = digits + 1;
|
|
1237
|
-
|
|
1454
|
+
// Tool output may already contain source line numbers; only number structured raw data.
|
|
1455
|
+
const unwrapped = language === undefined || source.every(line => visibleWidth(line) <= width);
|
|
1238
1456
|
const room = Math.max(1, width - (unwrapped ? 0 : gutter));
|
|
1239
1457
|
const painted = language === undefined
|
|
1240
1458
|
? source
|
|
1241
1459
|
: highlightCodeLines(source, language, theme);
|
|
1242
1460
|
const out = [];
|
|
1243
1461
|
for (const [index, line] of painted.entries()) {
|
|
1244
|
-
const rows =
|
|
1462
|
+
const rows = wrapCode(line, room);
|
|
1245
1463
|
out.push(unwrapped ? (rows[0] ?? '') : theme.fg('dim', String(index + 1).padStart(digits) + ' ') + (rows[0] ?? ''));
|
|
1246
1464
|
for (const rest of rows.slice(1))
|
|
1247
1465
|
out.push(' '.repeat(unwrapped ? 0 : gutter) + rest);
|
|
@@ -1336,8 +1554,8 @@ export function renderTrajectory(state, theme, width, height) {
|
|
|
1336
1554
|
const searchInfo = state.query === '' ? '' : ` · ${searchPosition(state)}`;
|
|
1337
1555
|
const followInfo = state.following ? '' : state.followNotice > 0 ? ` · End: follow · +${state.followNotice} new` : '';
|
|
1338
1556
|
const toolbar = truncateToWidth(theme.fg('dim', state.details
|
|
1339
|
-
?
|
|
1340
|
-
:
|
|
1557
|
+
? ` ${state.focus === 'list' && !state.fullDetail && safeWidth >= 96 ? 'Records' : 'Details'} · Tab focus · ←→ section · Ctrl+N/P record · f ${state.fullDetail ? 'split' : 'full screen'}`
|
|
1558
|
+
: ` / Search · Space group · t Turn · c Calls · e ${state.errorsOnly ? 'All records' : 'Problems'} · s Slow calls${state.following ? ' · Following' : ''}${searchInfo}${followInfo}`), safeWidth);
|
|
1341
1559
|
const divider = theme.fg('border', '─'.repeat(safeWidth));
|
|
1342
1560
|
/*
|
|
1343
1561
|
* The overview answers "where did the time go" before the reader has to open a
|
|
@@ -1353,10 +1571,10 @@ export function renderTrajectory(state, theme, width, height) {
|
|
|
1353
1571
|
*/
|
|
1354
1572
|
const overview = trajectoryOverviewHeight(state, safeWidth) === 0 ? undefined : timelineRows(state, theme, safeWidth);
|
|
1355
1573
|
const bodyHeight = Math.max(0, safeHeight - 4 - (overview?.length ?? 0));
|
|
1356
|
-
const wideDetails = state.details && safeWidth >= 96;
|
|
1574
|
+
const wideDetails = state.details && !state.fullDetail && safeWidth >= 96;
|
|
1357
1575
|
let body;
|
|
1358
1576
|
if (wideDetails) {
|
|
1359
|
-
const leftWidth =
|
|
1577
|
+
const leftWidth = trajectoryLeftWidth(safeWidth);
|
|
1360
1578
|
const rightWidth = Math.max(1, safeWidth - leftWidth - 1);
|
|
1361
1579
|
const left = ledgerRows(state, theme, leftWidth, bodyHeight);
|
|
1362
1580
|
const right = detailRows(state, theme, rightWidth, bodyHeight);
|
|
@@ -1373,8 +1591,8 @@ export function renderTrajectory(state, theme, width, height) {
|
|
|
1373
1591
|
const footerText = state.searching
|
|
1374
1592
|
? ` Search: ${state.query}`
|
|
1375
1593
|
: state.details
|
|
1376
|
-
? ' ' +
|
|
1377
|
-
: ' ' +
|
|
1594
|
+
? ` ↑↓ / PgUp PgDn scroll · y Copy section · Esc Back${state.notice ? ' · ' + state.notice : ''}`
|
|
1595
|
+
: ` ↑↓ Select · Enter Inspect · End Follow · Esc Close${state.notice ? ' · ' + state.notice : ''}`;
|
|
1378
1596
|
const footer = truncateToWidth(theme.fg(state.searching ? 'accent' : 'dim', footerText), safeWidth);
|
|
1379
1597
|
const lines = [header, toolbar, divider, ...(overview ?? []), ...body.slice(0, bodyHeight), footer].slice(0, safeHeight);
|
|
1380
1598
|
while (lines.length < safeHeight)
|