@agi-fans/dsh-tui 0.9.0 → 0.11.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/README.md +4 -4
- package/lib/CHANGELOG.md +40 -4
- package/lib/chrome/welcome-tips.js +1 -0
- package/lib/chrome/welcome-tips.js.map +1 -1
- package/lib/chrome/width.js +36 -0
- package/lib/chrome/width.js.map +1 -1
- package/lib/commands/export.js +11 -6
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/model.js +111 -4
- package/lib/commands/model.js.map +1 -1
- package/lib/commands/session.js +79 -36
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/trajectory.d.ts +5 -0
- package/lib/commands/trajectory.js +20 -0
- package/lib/commands/trajectory.js.map +1 -0
- package/lib/definition.d.ts +12 -0
- package/lib/input/keybindings-config.d.ts +1 -1
- package/lib/input/keybindings-config.js +7 -1
- package/lib/input/keybindings-config.js.map +1 -1
- package/lib/runtime/provider-local.d.ts +1 -0
- package/lib/runtime/provider-local.js +115 -20
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/terminal-notifications.d.ts +24 -0
- package/lib/runtime/terminal-notifications.js +61 -0
- package/lib/runtime/terminal-notifications.js.map +1 -0
- package/lib/session/context-diagnostics.d.ts +8 -0
- package/lib/session/context-diagnostics.js +50 -0
- package/lib/session/context-diagnostics.js.map +1 -0
- package/lib/session/model-favorites.d.ts +11 -0
- package/lib/session/model-favorites.js +74 -0
- package/lib/session/model-favorites.js.map +1 -0
- package/lib/session/session-controller.d.ts +13 -1
- package/lib/session/session-controller.js +49 -7
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-library.d.ts +8 -0
- package/lib/session/session-library.js +43 -0
- package/lib/session/session-library.js.map +1 -0
- package/lib/session/subagent-roster.js +11 -1
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/tui-settings.d.ts +2 -0
- package/lib/session/tui-settings.js +2 -0
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/views/agent-hub.d.ts +29 -0
- package/lib/views/agent-hub.js +165 -0
- package/lib/views/agent-hub.js.map +1 -0
- package/lib/views/event-views.d.ts +6 -0
- package/lib/views/event-views.js +18 -0
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/hotkeys.js +3 -0
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/prompt-selector.js +2 -1
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/settings-list.d.ts +2 -0
- package/lib/views/settings-list.js +21 -1
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/trajectory.d.ts +64 -0
- package/lib/views/trajectory.js +550 -0
- package/lib/views/trajectory.js.map +1 -0
- package/lib/views/transcript-export.d.ts +2 -0
- package/lib/views/transcript-export.js +34 -0
- package/lib/views/transcript-export.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Keyboard-first Trajectory ledger projected directly from durable session events. */
|
|
2
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
3
|
+
import type { KeyEvent } from '../input/keys.ts';
|
|
4
|
+
import type { Theme } from '../chrome/theme.ts';
|
|
5
|
+
export type TrajectoryKind = 'system' | 'user' | 'context' | 'assistant' | 'tool' | 'subtool' | 'compaction' | 'error';
|
|
6
|
+
export type TrajectoryDetailTab = 'summary' | 'payload' | 'result' | 'schema' | 'timing';
|
|
7
|
+
export interface TrajectoryRecord {
|
|
8
|
+
id: string;
|
|
9
|
+
index: number;
|
|
10
|
+
seq: number;
|
|
11
|
+
type: string;
|
|
12
|
+
kind: TrajectoryKind;
|
|
13
|
+
turn: number | null;
|
|
14
|
+
step: number | null;
|
|
15
|
+
label: string;
|
|
16
|
+
summary: string;
|
|
17
|
+
payload: string;
|
|
18
|
+
result: string;
|
|
19
|
+
schema: string;
|
|
20
|
+
status: 'running' | 'ok' | 'error' | 'retry';
|
|
21
|
+
startedAt: number | null;
|
|
22
|
+
durationMs: number | null;
|
|
23
|
+
ttftMs: number | null;
|
|
24
|
+
inputTokens: number | null;
|
|
25
|
+
outputTokens: number | null;
|
|
26
|
+
parentCallId: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** Incremental projection. Opening a long session is O(events); live updates are O(1). */
|
|
29
|
+
export declare class TrajectoryLedger {
|
|
30
|
+
#private;
|
|
31
|
+
readonly records: TrajectoryRecord[];
|
|
32
|
+
constructor(events?: readonly SessionEvent[]);
|
|
33
|
+
append(event: SessionEvent): void;
|
|
34
|
+
}
|
|
35
|
+
export interface TrajectoryState {
|
|
36
|
+
ledger: TrajectoryLedger;
|
|
37
|
+
selectedId: string | null;
|
|
38
|
+
query: string;
|
|
39
|
+
searching: boolean;
|
|
40
|
+
details: boolean;
|
|
41
|
+
detailTab: TrajectoryDetailTab;
|
|
42
|
+
detailScroll: number;
|
|
43
|
+
collapsedTurns: ReadonlySet<number>;
|
|
44
|
+
callsCollapsed: boolean;
|
|
45
|
+
following: boolean;
|
|
46
|
+
}
|
|
47
|
+
export declare function createTrajectory(events: readonly SessionEvent[]): TrajectoryState;
|
|
48
|
+
export declare function appendTrajectoryEvent(state: TrajectoryState, event: SessionEvent): TrajectoryState;
|
|
49
|
+
export declare function trajectoryVisibleRecords(state: TrajectoryState): TrajectoryRecord[];
|
|
50
|
+
export type TrajectoryCommand = {
|
|
51
|
+
state: TrajectoryState;
|
|
52
|
+
} | {
|
|
53
|
+
close: true;
|
|
54
|
+
};
|
|
55
|
+
export declare function applyTrajectoryEvent(state: TrajectoryState, event: KeyEvent): TrajectoryCommand;
|
|
56
|
+
/** Render the full-screen ledger without touching the ordinary transcript viewport. */
|
|
57
|
+
export declare function renderTrajectory(state: TrajectoryState, theme: Theme, width: number, height: number): {
|
|
58
|
+
lines: string[];
|
|
59
|
+
cursor: {
|
|
60
|
+
row: number;
|
|
61
|
+
column: number;
|
|
62
|
+
};
|
|
63
|
+
cursorVisible: boolean;
|
|
64
|
+
};
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/** Keyboard-first Trajectory ledger projected directly from durable session events. */
|
|
2
|
+
import { padToWidth, truncateToWidth, visibleWidth, wrapText } from "../chrome/width.js";
|
|
3
|
+
function object(value) {
|
|
4
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
5
|
+
? value
|
|
6
|
+
: undefined;
|
|
7
|
+
}
|
|
8
|
+
function number(value) {
|
|
9
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
function string(value) {
|
|
12
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
function compact(text, fallback) {
|
|
15
|
+
const normalized = text.replace(/\s+/gu, ' ').trim();
|
|
16
|
+
return normalized === '' ? fallback : normalized;
|
|
17
|
+
}
|
|
18
|
+
function json(value) {
|
|
19
|
+
try {
|
|
20
|
+
const encoded = JSON.stringify(value, null, 2);
|
|
21
|
+
return encoded === undefined ? String(value) : encoded;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return String(value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function contentText(value, wanted) {
|
|
28
|
+
if (typeof value === 'string')
|
|
29
|
+
return wanted === 'reasoning' ? '' : value;
|
|
30
|
+
if (Array.isArray(value))
|
|
31
|
+
return value.map(item => contentText(item, wanted)).filter(Boolean).join('\n');
|
|
32
|
+
const row = object(value);
|
|
33
|
+
if (row === undefined)
|
|
34
|
+
return '';
|
|
35
|
+
const type = string(row['type']);
|
|
36
|
+
if (wanted === 'reasoning' && type !== undefined && !type.includes('reason'))
|
|
37
|
+
return '';
|
|
38
|
+
if (wanted === 'text' && type !== undefined && type.includes('reason'))
|
|
39
|
+
return '';
|
|
40
|
+
for (const key of ['text', 'content', 'reasoning', 'output', 'result']) {
|
|
41
|
+
const found = row[key];
|
|
42
|
+
if (typeof found === 'string')
|
|
43
|
+
return found;
|
|
44
|
+
if (Array.isArray(found))
|
|
45
|
+
return contentText(found, wanted);
|
|
46
|
+
}
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
function location(data, fallbackTurn, fallbackStep) {
|
|
50
|
+
const nested = object(data['location']);
|
|
51
|
+
return {
|
|
52
|
+
turn: number(data['turn']) ?? number(nested?.['turn']) ?? fallbackTurn,
|
|
53
|
+
step: number(data['step']) ?? number(nested?.['step']) ?? fallbackStep,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function stepKey(turn, step) {
|
|
57
|
+
return `${turn ?? 'between'}:${step ?? 0}`;
|
|
58
|
+
}
|
|
59
|
+
function tokenValue(source, ...keys) {
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
const value = number(source?.[key]);
|
|
62
|
+
if (value !== undefined)
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/** Incremental projection. Opening a long session is O(events); live updates are O(1). */
|
|
68
|
+
export class TrajectoryLedger {
|
|
69
|
+
records = [];
|
|
70
|
+
#turn = null;
|
|
71
|
+
#step = null;
|
|
72
|
+
#stepStarted = new Map();
|
|
73
|
+
#stepFirstToken = new Map();
|
|
74
|
+
#assistantByStep = new Map();
|
|
75
|
+
#toolByCallId = new Map();
|
|
76
|
+
#compactionById = new Map();
|
|
77
|
+
#requestByStep = new Map();
|
|
78
|
+
#schemas = new Map();
|
|
79
|
+
constructor(events = []) {
|
|
80
|
+
for (const event of events)
|
|
81
|
+
this.append(event);
|
|
82
|
+
}
|
|
83
|
+
#push(record) {
|
|
84
|
+
const index = this.records.length;
|
|
85
|
+
this.records.push({ ...record, index: index + 1 });
|
|
86
|
+
return index;
|
|
87
|
+
}
|
|
88
|
+
#base(event, kind, label, summary, data) {
|
|
89
|
+
const place = location(data, this.#turn, this.#step);
|
|
90
|
+
return {
|
|
91
|
+
id: `${event.seq}:${event.type}`,
|
|
92
|
+
seq: event.seq,
|
|
93
|
+
type: event.type,
|
|
94
|
+
kind,
|
|
95
|
+
turn: place.turn,
|
|
96
|
+
step: place.step,
|
|
97
|
+
label,
|
|
98
|
+
summary,
|
|
99
|
+
payload: json(event.data),
|
|
100
|
+
result: '',
|
|
101
|
+
schema: '',
|
|
102
|
+
status: 'ok',
|
|
103
|
+
startedAt: event.time,
|
|
104
|
+
durationMs: null,
|
|
105
|
+
ttftMs: null,
|
|
106
|
+
inputTokens: null,
|
|
107
|
+
outputTokens: null,
|
|
108
|
+
parentCallId: null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
append(event) {
|
|
112
|
+
const eventType = event.type;
|
|
113
|
+
const data = object(event.data) ?? {};
|
|
114
|
+
const place = location(data, this.#turn, this.#step);
|
|
115
|
+
const key = stepKey(place.turn, place.step);
|
|
116
|
+
if (eventType === 'turn/start') {
|
|
117
|
+
this.#turn = place.turn ?? (this.#turn ?? 0) + 1;
|
|
118
|
+
this.#step = null;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (eventType === 'step/start') {
|
|
122
|
+
this.#turn = place.turn;
|
|
123
|
+
this.#step = place.step;
|
|
124
|
+
this.#stepStarted.set(key, event.time);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (eventType === 'request/header') {
|
|
128
|
+
this.#requestByStep.set(key, json(event.data));
|
|
129
|
+
const prompt = object(data['prompt']);
|
|
130
|
+
const tools = prompt?.['tools'];
|
|
131
|
+
if (Array.isArray(tools)) {
|
|
132
|
+
for (const tool of tools) {
|
|
133
|
+
const row = object(tool);
|
|
134
|
+
const name = string(row?.['name']);
|
|
135
|
+
if (name !== undefined)
|
|
136
|
+
this.#schemas.set(name, json(tool));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (eventType === 'user/message') {
|
|
142
|
+
const source = object(data['source']);
|
|
143
|
+
const isUser = source?.['kind'] === 'user';
|
|
144
|
+
const text = contentText(data['content']);
|
|
145
|
+
this.#push(this.#base(event, isUser ? 'user' : 'context', isUser ? 'USER' : 'CONTEXT', compact(text, 'Injected context'), data));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (eventType === 'assistant/chunk') {
|
|
149
|
+
const chunk = object(data['chunk']);
|
|
150
|
+
const chunkType = string(chunk?.['type']);
|
|
151
|
+
if (chunkType !== 'text-delta' && chunkType !== 'reasoning-delta')
|
|
152
|
+
return;
|
|
153
|
+
const delta = string(chunk?.['text']) ?? '';
|
|
154
|
+
if (!this.#stepFirstToken.has(key))
|
|
155
|
+
this.#stepFirstToken.set(key, event.time);
|
|
156
|
+
const existing = this.#assistantByStep.get(key);
|
|
157
|
+
if (existing === undefined) {
|
|
158
|
+
const startedAt = this.#stepStarted.get(key) ?? event.time;
|
|
159
|
+
const record = this.#base(event, 'assistant', 'ASSISTANT', compact(delta, chunkType === 'reasoning-delta' ? 'Thinking…' : 'Streaming…'), data);
|
|
160
|
+
record.id = `assistant:${key}`;
|
|
161
|
+
record.status = 'running';
|
|
162
|
+
record.startedAt = startedAt;
|
|
163
|
+
record.ttftMs = event.time - startedAt;
|
|
164
|
+
record.payload = this.#requestByStep.get(key) ?? record.payload;
|
|
165
|
+
this.#assistantByStep.set(key, this.#push(record));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const record = this.records[existing];
|
|
169
|
+
if (record !== undefined && delta !== '')
|
|
170
|
+
record.summary = compact(`${record.summary} ${delta}`, record.summary);
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (eventType === 'assistant/message') {
|
|
175
|
+
const message = object(data['message']);
|
|
176
|
+
const text = contentText(message?.['content'] ?? data['content'], 'text');
|
|
177
|
+
const reasoning = contentText(message?.['content'] ?? data['content'], 'reasoning');
|
|
178
|
+
const usage = object(message?.['usage'] ?? data['usage']);
|
|
179
|
+
const startedAt = this.#stepStarted.get(key) ?? event.time;
|
|
180
|
+
const existing = this.#assistantByStep.get(key);
|
|
181
|
+
const result = reasoning === '' ? text : `Thinking\n${reasoning}\n\nAnswer\n${text}`;
|
|
182
|
+
if (existing === undefined) {
|
|
183
|
+
const record = this.#base(event, 'assistant', 'ASSISTANT', compact(text, 'Assistant response'), data);
|
|
184
|
+
record.id = `assistant:${key}`;
|
|
185
|
+
record.payload = this.#requestByStep.get(key) ?? record.payload;
|
|
186
|
+
record.result = result;
|
|
187
|
+
record.startedAt = startedAt;
|
|
188
|
+
record.durationMs = Math.max(0, event.time - startedAt);
|
|
189
|
+
const first = this.#stepFirstToken.get(key);
|
|
190
|
+
record.ttftMs = first === undefined ? null : Math.max(0, first - startedAt);
|
|
191
|
+
record.inputTokens = tokenValue(usage, 'inputTokens', 'input_tokens', 'promptTokens', 'prompt_tokens');
|
|
192
|
+
record.outputTokens = tokenValue(usage, 'outputTokens', 'output_tokens', 'completionTokens', 'completion_tokens');
|
|
193
|
+
this.#assistantByStep.set(key, this.#push(record));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const record = this.records[existing];
|
|
197
|
+
if (record !== undefined) {
|
|
198
|
+
record.seq = event.seq;
|
|
199
|
+
record.summary = compact(text, record.summary);
|
|
200
|
+
record.result = result;
|
|
201
|
+
record.status = data['interrupted'] === true ? 'error' : 'ok';
|
|
202
|
+
record.durationMs = Math.max(0, event.time - startedAt);
|
|
203
|
+
record.inputTokens = tokenValue(usage, 'inputTokens', 'input_tokens', 'promptTokens', 'prompt_tokens');
|
|
204
|
+
record.outputTokens = tokenValue(usage, 'outputTokens', 'output_tokens', 'completionTokens', 'completion_tokens');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (eventType === 'tool/call' || eventType === 'tool/code-dispatch-start') {
|
|
210
|
+
const callId = string(data['callId']) ?? string(data['id']) ?? `${event.seq}`;
|
|
211
|
+
const parentCallId = string(data['parentCallId']) ?? string(data['parentId']) ?? null;
|
|
212
|
+
const name = string(data['name']) ?? string(data['toolName']) ?? 'tool';
|
|
213
|
+
const args = data['arguments'] ?? data['input'] ?? data;
|
|
214
|
+
const record = this.#base(event, parentCallId === null ? 'tool' : 'subtool', name, `${name} ${compact(json(args), '')}`.trim(), data);
|
|
215
|
+
record.id = `tool:${callId}`;
|
|
216
|
+
record.label = parentCallId === null ? 'TOOL' : 'SUBTOOL';
|
|
217
|
+
record.status = 'running';
|
|
218
|
+
record.parentCallId = parentCallId;
|
|
219
|
+
record.schema = this.#schemas.get(name) ?? '';
|
|
220
|
+
this.#toolByCallId.set(callId, this.#push(record));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (eventType === 'tool/result' || eventType === 'tool/code-dispatch') {
|
|
224
|
+
const message = object(data['message']);
|
|
225
|
+
const callId = string(message?.['callId']) ?? string(data['callId']) ?? string(data['id']);
|
|
226
|
+
if (callId === undefined)
|
|
227
|
+
return;
|
|
228
|
+
const index = this.#toolByCallId.get(callId);
|
|
229
|
+
if (index === undefined)
|
|
230
|
+
return;
|
|
231
|
+
const record = this.records[index];
|
|
232
|
+
if (record === undefined)
|
|
233
|
+
return;
|
|
234
|
+
record.result = contentText(message?.['content'] ?? data['result'] ?? data['output']) || json(message ?? data);
|
|
235
|
+
record.status = data['error'] === true || object(data['error']) !== undefined ? 'error' : 'ok';
|
|
236
|
+
record.durationMs = record.startedAt === null ? null : Math.max(0, event.time - record.startedAt);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (eventType === 'llm/retry') {
|
|
240
|
+
const record = this.#base(event, 'error', 'RETRY', compact(json(data), 'Model request retry'), data);
|
|
241
|
+
record.status = 'retry';
|
|
242
|
+
this.#push(record);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (eventType === 'compaction/start') {
|
|
246
|
+
const id = string(data['compactId']) ?? string(data['id']) ?? `${event.seq}`;
|
|
247
|
+
const record = this.#base(event, 'compaction', 'COMPACT', 'Compaction started', data);
|
|
248
|
+
record.id = `compaction:${id}`;
|
|
249
|
+
record.status = 'running';
|
|
250
|
+
this.#compactionById.set(id, this.#push(record));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (eventType === 'compaction/summary' || eventType === 'compaction/end') {
|
|
254
|
+
const id = string(data['compactId']) ?? string(data['id']);
|
|
255
|
+
const index = id === undefined ? [...this.#compactionById.values()].at(-1) : this.#compactionById.get(id);
|
|
256
|
+
if (index === undefined)
|
|
257
|
+
return;
|
|
258
|
+
const record = this.records[index];
|
|
259
|
+
if (record === undefined)
|
|
260
|
+
return;
|
|
261
|
+
if (eventType === 'compaction/summary')
|
|
262
|
+
record.result = contentText(data['summary']) || json(data['summary']);
|
|
263
|
+
else {
|
|
264
|
+
record.status = data['error'] === undefined ? 'ok' : 'error';
|
|
265
|
+
record.durationMs = record.startedAt === null ? null : Math.max(0, event.time - record.startedAt);
|
|
266
|
+
record.summary = record.status === 'ok' ? 'Compaction completed' : 'Compaction failed';
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (eventType === 'step/end') {
|
|
271
|
+
const index = this.#assistantByStep.get(key);
|
|
272
|
+
const record = index === undefined ? undefined : this.records[index];
|
|
273
|
+
const reason = object(data['reason']);
|
|
274
|
+
if (record !== undefined) {
|
|
275
|
+
if (reason?.['kind'] === 'error' || data['error'] !== undefined)
|
|
276
|
+
record.status = 'error';
|
|
277
|
+
if (record.durationMs === null && record.startedAt !== null)
|
|
278
|
+
record.durationMs = Math.max(0, event.time - record.startedAt);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (eventType === 'turn/end') {
|
|
283
|
+
const reason = object(data['reason']);
|
|
284
|
+
if (reason?.['kind'] !== 'error' && reason?.['kind'] !== 'aborted')
|
|
285
|
+
return;
|
|
286
|
+
const record = this.#base(event, 'error', 'TURN', compact(json(reason), 'Turn interrupted'), data);
|
|
287
|
+
record.status = 'error';
|
|
288
|
+
this.#push(record);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export function createTrajectory(events) {
|
|
293
|
+
const ledger = new TrajectoryLedger(events);
|
|
294
|
+
return {
|
|
295
|
+
ledger,
|
|
296
|
+
selectedId: ledger.records.at(-1)?.id ?? null,
|
|
297
|
+
query: '',
|
|
298
|
+
searching: false,
|
|
299
|
+
details: false,
|
|
300
|
+
detailTab: 'summary',
|
|
301
|
+
detailScroll: 0,
|
|
302
|
+
collapsedTurns: new Set(),
|
|
303
|
+
callsCollapsed: false,
|
|
304
|
+
following: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
export function appendTrajectoryEvent(state, event) {
|
|
308
|
+
state.ledger.append(event);
|
|
309
|
+
return state.following
|
|
310
|
+
? { ...state, selectedId: trajectoryVisibleRecords(state).at(-1)?.id ?? state.selectedId }
|
|
311
|
+
: state;
|
|
312
|
+
}
|
|
313
|
+
export function trajectoryVisibleRecords(state) {
|
|
314
|
+
const query = state.query.trim().toLocaleLowerCase();
|
|
315
|
+
const firstInCollapsed = new Set();
|
|
316
|
+
return state.ledger.records.filter((record) => {
|
|
317
|
+
if (state.callsCollapsed && record.kind === 'subtool')
|
|
318
|
+
return false;
|
|
319
|
+
if (record.turn !== null && state.collapsedTurns.has(record.turn)) {
|
|
320
|
+
if (firstInCollapsed.has(record.turn))
|
|
321
|
+
return false;
|
|
322
|
+
firstInCollapsed.add(record.turn);
|
|
323
|
+
}
|
|
324
|
+
if (query === '')
|
|
325
|
+
return true;
|
|
326
|
+
return `${record.type}\n${record.label}\n${record.summary}\n${record.payload}\n${record.result}\n${record.schema}`
|
|
327
|
+
.toLocaleLowerCase().includes(query);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function move(state, delta) {
|
|
331
|
+
const visible = trajectoryVisibleRecords(state);
|
|
332
|
+
if (visible.length === 0)
|
|
333
|
+
return state;
|
|
334
|
+
const current = Math.max(0, visible.findIndex(record => record.id === state.selectedId));
|
|
335
|
+
const index = Math.max(0, Math.min(visible.length - 1, current + delta));
|
|
336
|
+
return { ...state, selectedId: visible[index]?.id ?? null, detailScroll: 0, following: index === visible.length - 1 };
|
|
337
|
+
}
|
|
338
|
+
const DETAIL_TABS = ['summary', 'payload', 'result', 'schema', 'timing'];
|
|
339
|
+
function moveTab(state, delta) {
|
|
340
|
+
const current = DETAIL_TABS.indexOf(state.detailTab);
|
|
341
|
+
const next = (current + delta + DETAIL_TABS.length) % DETAIL_TABS.length;
|
|
342
|
+
return { ...state, detailTab: DETAIL_TABS[next] ?? 'summary', detailScroll: 0 };
|
|
343
|
+
}
|
|
344
|
+
export function applyTrajectoryEvent(state, event) {
|
|
345
|
+
if (state.searching) {
|
|
346
|
+
if (event.type === 'text')
|
|
347
|
+
return { state: { ...state, query: state.query + event.value, following: false } };
|
|
348
|
+
if (event.type !== 'key')
|
|
349
|
+
return { state };
|
|
350
|
+
if (event.id === 'backspace')
|
|
351
|
+
return { state: { ...state, query: state.query.slice(0, -1) } };
|
|
352
|
+
if (event.id === 'enter')
|
|
353
|
+
return { state: { ...state, searching: false } };
|
|
354
|
+
if (event.id === 'escape' || event.id === 'ctrl+c')
|
|
355
|
+
return { state: { ...state, searching: false } };
|
|
356
|
+
return { state };
|
|
357
|
+
}
|
|
358
|
+
if (event.type === 'text') {
|
|
359
|
+
if (event.value === '/')
|
|
360
|
+
return { state: { ...state, searching: true, following: false } };
|
|
361
|
+
if (event.value.toLowerCase() === 'c')
|
|
362
|
+
return { state: { ...state, callsCollapsed: !state.callsCollapsed } };
|
|
363
|
+
if (event.value.toLowerCase() === 't') {
|
|
364
|
+
const selected = state.ledger.records.find(record => record.id === state.selectedId);
|
|
365
|
+
if (selected?.turn === null || selected?.turn === undefined)
|
|
366
|
+
return { state };
|
|
367
|
+
const collapsed = new Set(state.collapsedTurns);
|
|
368
|
+
if (collapsed.has(selected.turn))
|
|
369
|
+
collapsed.delete(selected.turn);
|
|
370
|
+
else
|
|
371
|
+
collapsed.add(selected.turn);
|
|
372
|
+
const first = state.ledger.records.find(record => record.turn === selected.turn);
|
|
373
|
+
return { state: { ...state, collapsedTurns: collapsed, selectedId: first?.id ?? state.selectedId } };
|
|
374
|
+
}
|
|
375
|
+
return { state };
|
|
376
|
+
}
|
|
377
|
+
if (event.type !== 'key')
|
|
378
|
+
return { state };
|
|
379
|
+
if (event.id === 'escape' || event.id === 'ctrl+c') {
|
|
380
|
+
if (state.details)
|
|
381
|
+
return { state: { ...state, details: false, detailScroll: 0 } };
|
|
382
|
+
return { close: true };
|
|
383
|
+
}
|
|
384
|
+
if (event.id === 'up')
|
|
385
|
+
return { state: move(state, -1) };
|
|
386
|
+
if (event.id === 'down')
|
|
387
|
+
return { state: move(state, 1) };
|
|
388
|
+
if (event.id === 'pageUp') {
|
|
389
|
+
if (state.details)
|
|
390
|
+
return { state: { ...state, detailScroll: Math.max(0, state.detailScroll - 10) } };
|
|
391
|
+
return { state: move(state, -10) };
|
|
392
|
+
}
|
|
393
|
+
if (event.id === 'pageDown') {
|
|
394
|
+
if (state.details)
|
|
395
|
+
return { state: { ...state, detailScroll: state.detailScroll + 10 } };
|
|
396
|
+
return { state: move(state, 10) };
|
|
397
|
+
}
|
|
398
|
+
if (event.id === 'home') {
|
|
399
|
+
const first = trajectoryVisibleRecords(state)[0];
|
|
400
|
+
return { state: { ...state, selectedId: first?.id ?? null, following: false } };
|
|
401
|
+
}
|
|
402
|
+
if (event.id === 'end') {
|
|
403
|
+
const last = trajectoryVisibleRecords(state).at(-1);
|
|
404
|
+
return { state: { ...state, selectedId: last?.id ?? null, following: true } };
|
|
405
|
+
}
|
|
406
|
+
if (event.id === 'enter')
|
|
407
|
+
return { state: { ...state, details: !state.details, detailScroll: 0 } };
|
|
408
|
+
if (event.id === 'tab' || event.id === 'right')
|
|
409
|
+
return { state: moveTab(state, 1) };
|
|
410
|
+
if (event.id === 'shift+tab' || event.id === 'left')
|
|
411
|
+
return { state: moveTab(state, -1) };
|
|
412
|
+
return { state };
|
|
413
|
+
}
|
|
414
|
+
function duration(record) {
|
|
415
|
+
if (record.durationMs === null)
|
|
416
|
+
return record.status === 'running' ? 'live' : '—';
|
|
417
|
+
if (record.durationMs < 1_000)
|
|
418
|
+
return `${Math.round(record.durationMs)}ms`;
|
|
419
|
+
return `${(record.durationMs / 1_000).toFixed(record.durationMs < 10_000 ? 1 : 0)}s`;
|
|
420
|
+
}
|
|
421
|
+
function kindColor(kind) {
|
|
422
|
+
if (kind === 'user')
|
|
423
|
+
return 'accent';
|
|
424
|
+
if (kind === 'assistant')
|
|
425
|
+
return 'text';
|
|
426
|
+
if (kind === 'tool' || kind === 'subtool')
|
|
427
|
+
return 'success';
|
|
428
|
+
if (kind === 'error')
|
|
429
|
+
return 'error';
|
|
430
|
+
if (kind === 'compaction')
|
|
431
|
+
return 'warning';
|
|
432
|
+
return 'dim';
|
|
433
|
+
}
|
|
434
|
+
function recordLine(record, selected, theme, width) {
|
|
435
|
+
const marker = selected ? theme.fg('accent', '›') : ' ';
|
|
436
|
+
const locationLabel = record.turn === null ? ' — ' : `T${record.turn}${record.step === null ? '' : `·${record.step}`}`.padEnd(5);
|
|
437
|
+
const kind = theme.fg(kindColor(record.kind), record.label.padEnd(9).slice(0, 9));
|
|
438
|
+
const elapsed = duration(record).padStart(6);
|
|
439
|
+
const prefix = `${marker} ${String(record.index).padStart(4)} ${locationLabel} ${kind} ${elapsed} `;
|
|
440
|
+
return prefix + truncateToWidth(record.summary, Math.max(0, width - visibleWidth(prefix)));
|
|
441
|
+
}
|
|
442
|
+
function selectedRecord(state) {
|
|
443
|
+
return state.ledger.records.find(record => record.id === state.selectedId);
|
|
444
|
+
}
|
|
445
|
+
function timingText(record) {
|
|
446
|
+
return [
|
|
447
|
+
`Event: ${record.type} (#${record.seq})`,
|
|
448
|
+
`Started: ${record.startedAt === null ? '—' : new Date(record.startedAt).toISOString()}`,
|
|
449
|
+
`Duration: ${record.durationMs === null ? '—' : `${Math.round(record.durationMs)} ms`}`,
|
|
450
|
+
`TTFT: ${record.ttftMs === null ? '—' : `${Math.round(record.ttftMs)} ms`}`,
|
|
451
|
+
`Tokens: ${record.inputTokens ?? '—'} in · ${record.outputTokens ?? '—'} out`,
|
|
452
|
+
`Status: ${record.status}`,
|
|
453
|
+
].join('\n');
|
|
454
|
+
}
|
|
455
|
+
function detailText(state, record) {
|
|
456
|
+
if (record === undefined)
|
|
457
|
+
return 'No trajectory record selected.';
|
|
458
|
+
if (state.detailTab === 'payload')
|
|
459
|
+
return record.payload || 'No payload recorded.';
|
|
460
|
+
if (state.detailTab === 'result')
|
|
461
|
+
return record.result || 'No result recorded.';
|
|
462
|
+
if (state.detailTab === 'schema')
|
|
463
|
+
return record.schema || 'No tool schema recorded.';
|
|
464
|
+
if (state.detailTab === 'timing')
|
|
465
|
+
return timingText(record);
|
|
466
|
+
return [
|
|
467
|
+
`${record.label} · ${record.type}`,
|
|
468
|
+
`Turn ${record.turn ?? 'between'} · Step ${record.step ?? '—'} · ${duration(record)}`,
|
|
469
|
+
'',
|
|
470
|
+
record.summary,
|
|
471
|
+
].join('\n');
|
|
472
|
+
}
|
|
473
|
+
function ledgerRows(state, theme, width, height) {
|
|
474
|
+
const visible = trajectoryVisibleRecords(state);
|
|
475
|
+
if (visible.length === 0)
|
|
476
|
+
return [theme.fg('dim', state.query === '' ? ' No trajectory records.' : ' No matching trajectory records.')];
|
|
477
|
+
const lines = [];
|
|
478
|
+
let previousTurn;
|
|
479
|
+
for (const record of visible) {
|
|
480
|
+
if (record.turn !== previousTurn) {
|
|
481
|
+
previousTurn = record.turn;
|
|
482
|
+
const label = record.turn === null ? 'Between turns' : `Turn ${record.turn}`;
|
|
483
|
+
const collapsed = record.turn !== null && state.collapsedTurns.has(record.turn) ? ' · collapsed' : '';
|
|
484
|
+
lines.push({ id: `turn:${record.turn ?? 'between'}`, text: theme.fg('accent', `── ${label}${collapsed} `) });
|
|
485
|
+
}
|
|
486
|
+
lines.push({ id: record.id, text: recordLine(record, record.id === state.selectedId, theme, width) });
|
|
487
|
+
}
|
|
488
|
+
const selectedLine = Math.max(0, lines.findIndex(line => line.id === state.selectedId));
|
|
489
|
+
const start = Math.max(0, Math.min(lines.length - height, selectedLine - Math.floor(height / 2)));
|
|
490
|
+
return lines.slice(start, start + height).map(line => truncateToWidth(line.text, width));
|
|
491
|
+
}
|
|
492
|
+
function detailRows(state, theme, width, height) {
|
|
493
|
+
const tabs = DETAIL_TABS.map(tab => tab === state.detailTab ? theme.fg('accent', `[${tab}]`) : theme.fg('dim', tab)).join(' ');
|
|
494
|
+
const body = wrapText(detailText(state, selectedRecord(state)), Math.max(1, width - 2));
|
|
495
|
+
const viewportHeight = Math.max(0, height - 2);
|
|
496
|
+
const maxScroll = Math.max(0, body.length - viewportHeight);
|
|
497
|
+
const start = Math.min(state.detailScroll, maxScroll);
|
|
498
|
+
const end = Math.min(body.length, start + viewportHeight);
|
|
499
|
+
const position = body.length > viewportHeight
|
|
500
|
+
? theme.fg('dim', ` ${start + 1}-${end} / ${body.length} lines · PgUp/PgDn scroll`)
|
|
501
|
+
: '';
|
|
502
|
+
return [truncateToWidth(tabs, width), truncateToWidth(position, width), ...body.slice(start, end)];
|
|
503
|
+
}
|
|
504
|
+
/** Render the full-screen ledger without touching the ordinary transcript viewport. */
|
|
505
|
+
export function renderTrajectory(state, theme, width, height) {
|
|
506
|
+
const safeWidth = Math.max(1, width);
|
|
507
|
+
const safeHeight = Math.max(1, height);
|
|
508
|
+
const records = state.ledger.records;
|
|
509
|
+
const errors = records.filter(record => record.status === 'error').length;
|
|
510
|
+
const requests = records.filter(record => record.kind === 'assistant').length;
|
|
511
|
+
const tools = records.filter(record => record.kind === 'tool' || record.kind === 'subtool').length;
|
|
512
|
+
const header = truncateToWidth(theme.bold(' Trajectory ') + theme.fg('dim', `${records.length} records · ${requests} requests · ${tools} calls · ${errors} errors`), safeWidth);
|
|
513
|
+
const toolbar = truncateToWidth(theme.fg('dim', state.details
|
|
514
|
+
? ' ↑↓ records · PgUp/PgDn detail · Tab/←→ section · Esc back'
|
|
515
|
+
: ` ↑↓ navigate · Enter details · / search · t turn · c calls · End follow${state.following ? ' ●' : ''}`), safeWidth);
|
|
516
|
+
const divider = theme.fg('border', '─'.repeat(safeWidth));
|
|
517
|
+
const bodyHeight = Math.max(0, safeHeight - 4);
|
|
518
|
+
const wideDetails = state.details && safeWidth >= 96;
|
|
519
|
+
let body;
|
|
520
|
+
if (wideDetails) {
|
|
521
|
+
const leftWidth = Math.max(40, Math.floor(safeWidth * 0.58));
|
|
522
|
+
const rightWidth = Math.max(1, safeWidth - leftWidth - 1);
|
|
523
|
+
const left = ledgerRows(state, theme, leftWidth, bodyHeight);
|
|
524
|
+
const right = detailRows(state, theme, rightWidth, bodyHeight);
|
|
525
|
+
body = Array.from({ length: bodyHeight }, (_, index) => padToWidth(left[index] ?? '', leftWidth) + theme.fg('border', '│') + truncateToWidth(right[index] ?? '', rightWidth));
|
|
526
|
+
}
|
|
527
|
+
else if (state.details) {
|
|
528
|
+
body = detailRows(state, theme, safeWidth, bodyHeight);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
body = ledgerRows(state, theme, safeWidth, bodyHeight);
|
|
532
|
+
}
|
|
533
|
+
while (body.length < bodyHeight)
|
|
534
|
+
body.push('');
|
|
535
|
+
const footerText = state.searching
|
|
536
|
+
? ` Search: ${state.query}`
|
|
537
|
+
: state.details
|
|
538
|
+
? ' PgUp/PgDn scroll · Tab/←→ section · Esc back'
|
|
539
|
+
: ' Esc close';
|
|
540
|
+
const footer = truncateToWidth(theme.fg(state.searching ? 'accent' : 'dim', footerText), safeWidth);
|
|
541
|
+
const lines = [header, toolbar, divider, ...body.slice(0, bodyHeight), footer].slice(0, safeHeight);
|
|
542
|
+
while (lines.length < safeHeight)
|
|
543
|
+
lines.push('');
|
|
544
|
+
return {
|
|
545
|
+
lines,
|
|
546
|
+
cursor: { row: safeHeight - 1, column: Math.min(safeWidth, visibleWidth(` Search: ${state.query}`)) },
|
|
547
|
+
cursorVisible: state.searching,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
//# sourceMappingURL=trajectory.js.map
|