@superjackfruitlabs/salaam 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/src/acp-desk.js +140 -0
- package/dist/src/acp-outcome.js +23 -19
- package/dist/src/acp.js +258 -11
- package/dist/src/approval-rules.js +287 -0
- package/dist/src/attach.js +3 -1
- package/dist/src/autonomy.js +80 -9
- package/dist/src/cli-help.js +34 -4
- package/dist/src/cli.js +55 -3
- package/dist/src/command-danger.js +797 -0
- package/dist/src/command-sandbox.js +116 -6
- package/dist/src/custom-commands.js +159 -0
- package/dist/src/danger-classifier.js +286 -0
- package/dist/src/git-helpers.js +160 -0
- package/dist/src/hooks.js +351 -0
- package/dist/src/host.js +96 -4
- package/dist/src/house-extension.js +133 -7
- package/dist/src/local-coding.js +553 -58
- package/dist/src/local-edits.js +168 -6
- package/dist/src/mentions.js +277 -0
- package/dist/src/pi-sessions.js +5 -1
- package/dist/src/project-instructions.js +195 -0
- package/dist/src/protocol.js +4 -1
- package/dist/src/service.js +342 -5
- package/dist/src/session-pick.js +77 -0
- package/dist/src/shell-passthrough.js +39 -0
- package/dist/src/store.js +51 -4
- package/dist/src/supervisor.js +233 -22
- package/dist/src/task-context.js +8 -2
- package/dist/src/terminal-status.js +88 -0
- package/dist/src/tui-activity.js +99 -0
- package/dist/src/tui-classifier.js +25 -0
- package/dist/src/tui-client.js +186 -5
- package/dist/src/tui-commands.js +23 -5
- package/dist/src/tui-composer.js +113 -0
- package/dist/src/tui-keymap.js +116 -0
- package/dist/src/tui-mentions.js +38 -0
- package/dist/src/tui-modals.js +157 -34
- package/dist/src/tui-notify.js +62 -0
- package/dist/src/tui-review.js +55 -0
- package/dist/src/tui-turn-keys.js +52 -0
- package/dist/src/tui-view.js +243 -69
- package/dist/src/tui.js +875 -118
- package/dist/src/turn-checkpoints.js +221 -0
- package/dist/src/turn-rewind.js +168 -0
- package/dist/src/web-fetch.js +114 -0
- package/dist/src/web-guard.js +163 -0
- package/dist/src/web-search.js +302 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/acp-desk.ts +155 -0
- package/src/acp-outcome.ts +25 -21
- package/src/acp.ts +338 -28
- package/src/approval-rules.ts +360 -0
- package/src/attach.ts +4 -1
- package/src/autonomy.ts +111 -10
- package/src/cli-help.ts +33 -4
- package/src/cli.ts +66 -2
- package/src/command-danger.ts +886 -0
- package/src/command-sandbox.ts +128 -8
- package/src/custom-commands.ts +175 -0
- package/src/danger-classifier.ts +321 -0
- package/src/git-helpers.ts +177 -0
- package/src/hooks.ts +425 -0
- package/src/host.ts +122 -1
- package/src/house-extension.ts +142 -7
- package/src/local-coding.ts +674 -75
- package/src/local-edits.ts +191 -8
- package/src/mentions.ts +304 -0
- package/src/pi-sessions.ts +9 -1
- package/src/project-instructions.ts +229 -0
- package/src/protocol.ts +19 -2
- package/src/service.ts +488 -5
- package/src/session-pick.ts +102 -0
- package/src/shell-passthrough.ts +38 -0
- package/src/store.ts +52 -3
- package/src/supervisor.ts +262 -24
- package/src/task-context.ts +9 -2
- package/src/terminal-status.ts +103 -0
- package/src/tui-activity.ts +133 -0
- package/src/tui-classifier.ts +38 -0
- package/src/tui-client.ts +239 -8
- package/src/tui-commands.ts +49 -10
- package/src/tui-composer.ts +124 -0
- package/src/tui-keymap.ts +152 -0
- package/src/tui-mentions.ts +43 -0
- package/src/tui-modals.ts +173 -43
- package/src/tui-notify.ts +77 -0
- package/src/tui-review.ts +76 -0
- package/src/tui-turn-keys.ts +77 -0
- package/src/tui-view.ts +341 -88
- package/src/tui.ts +1047 -137
- package/src/turn-checkpoints.ts +253 -0
- package/src/turn-rewind.ts +258 -0
- package/src/web-fetch.ts +140 -0
- package/src/web-guard.ts +195 -0
- package/src/web-search.ts +402 -0
package/src/tui-view.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { Markdown, wrapTextWithAnsi, visibleWidth, type Component } from '@earendil-works/pi-tui';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { SalaamTheme, safeText, shortId, humanTool } from './tui-theme.js';
|
|
4
|
-
import
|
|
4
|
+
import { typedText } from './shell-passthrough.js';
|
|
5
|
+
import {
|
|
6
|
+
instructionsLabel,
|
|
7
|
+
sessionFailure,
|
|
8
|
+
usageCost,
|
|
9
|
+
usageInput,
|
|
10
|
+
type OperatorClient,
|
|
11
|
+
} from './tui-client.js';
|
|
5
12
|
import type { WorkspaceChange } from './workspace-diff.js';
|
|
6
13
|
import { workspaceDiff } from './workspace-diff.js';
|
|
7
14
|
import { commandTerminal } from './coding.js';
|
|
15
|
+
import { interactiveCoding, runningAttempt } from './tui-turn-keys.js';
|
|
16
|
+
import { backgroundNote, changeCounts } from './tui-review.js';
|
|
17
|
+
import { activityVerb, progressLine, toolLine, type ToolLine } from './tui-activity.js';
|
|
8
18
|
|
|
9
19
|
export type ViewTab = 'conversation' | 'changes' | 'commands' | 'context';
|
|
10
20
|
export interface DiffPage {
|
|
@@ -24,6 +34,57 @@ export interface ViewModel {
|
|
|
24
34
|
log?: { id: string; text: string; offset: number; complete: boolean };
|
|
25
35
|
busy: boolean;
|
|
26
36
|
continuation?: string;
|
|
37
|
+
/** The Changes tab of a local coding session (tui-review.ts). */
|
|
38
|
+
review?: import('./tui-review.js').ReviewView;
|
|
39
|
+
/** Phase 2b: the session's mode, its access (full access or YOLO) and where commands run. */
|
|
40
|
+
mode?: { mode: string; access?: 'full' | 'yolo'; sandbox?: string };
|
|
41
|
+
/** Trial polish #9: every tool call with its full recorded output, not the compact lines. */
|
|
42
|
+
transcript?: boolean;
|
|
43
|
+
/** When the running turn was first seen, for the progress line's elapsed seconds. */
|
|
44
|
+
turn?: { attempt: string; since: number };
|
|
45
|
+
}
|
|
46
|
+
/** The session's tool calls, oldest first, each as one compact line. */
|
|
47
|
+
export function toolLines(
|
|
48
|
+
view: ViewModel,
|
|
49
|
+
): (ToolLine & { parent?: string; key: string; output?: string })[] {
|
|
50
|
+
const calls = new Map<
|
|
51
|
+
string,
|
|
52
|
+
{
|
|
53
|
+
tool: string;
|
|
54
|
+
input?: string;
|
|
55
|
+
finished?: { failed?: boolean; output?: string };
|
|
56
|
+
parent?: string;
|
|
57
|
+
}
|
|
58
|
+
>();
|
|
59
|
+
for (const event of view.client.activity) {
|
|
60
|
+
const p = event.payload;
|
|
61
|
+
const key = p.attempt_id + ':' + p.tool_call_id;
|
|
62
|
+
const before = calls.get(key);
|
|
63
|
+
calls.set(key, {
|
|
64
|
+
tool: p.tool,
|
|
65
|
+
input: event.kind === 'tool.started' ? p.input : before?.input,
|
|
66
|
+
finished:
|
|
67
|
+
event.kind === 'tool.finished' ? { failed: p.failed, output: p.output } : before?.finished,
|
|
68
|
+
parent: p.parent_tool_call_id ? p.attempt_id + ':' + p.parent_tool_call_id : before?.parent,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return [...calls.entries()].map(([key, c]) => {
|
|
72
|
+
const line = toolLine(c.tool, c.input, c.finished);
|
|
73
|
+
// A command's recorded result is JSON; the transcript shows its output as text.
|
|
74
|
+
return { key, parent: c.parent, output: line.full ?? c.finished?.output, ...line };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** How the status line names a mode. */
|
|
78
|
+
export function modeLabel(mode: string): string {
|
|
79
|
+
return (
|
|
80
|
+
{
|
|
81
|
+
review: 'review',
|
|
82
|
+
'accept-edits': 'accept edits',
|
|
83
|
+
auto: 'auto',
|
|
84
|
+
inspect: 'inspect only',
|
|
85
|
+
'scoped-edit': 'scoped edits',
|
|
86
|
+
}[mode] ?? mode
|
|
87
|
+
);
|
|
27
88
|
}
|
|
28
89
|
export function modelLabel(client: OperatorClient): string {
|
|
29
90
|
const route = client.snapshot?.provider_route ?? client.health?.provider_route;
|
|
@@ -38,6 +99,12 @@ export function modelLabel(client: OperatorClient): string {
|
|
|
38
99
|
route.model.replace('claude-sonnet-', 'Claude Sonnet ').replace(/(\d)-(\d)$/, '$1.$2') + warning
|
|
39
100
|
);
|
|
40
101
|
}
|
|
102
|
+
/** 950 → "950", 12_400 → "12.4k", 2_300_000 → "2.3M". */
|
|
103
|
+
export function compactTokens(n: number): string {
|
|
104
|
+
if (n < 1000) return String(n);
|
|
105
|
+
if (n < 1_000_000) return (n / 1000).toFixed(n < 10_000 ? 1 : 0).replace(/\.0$/, '') + 'k';
|
|
106
|
+
return (n / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
|
|
107
|
+
}
|
|
41
108
|
export class Dynamic implements Component {
|
|
42
109
|
constructor(private readonly content: (width: number) => string[]) {}
|
|
43
110
|
render(width: number): string[] {
|
|
@@ -99,19 +166,65 @@ export function status(view: ViewModel, width: number): string[] {
|
|
|
99
166
|
const active = c.snapshot?.attempts.find((a) =>
|
|
100
167
|
['accepted', 'dispatching', 'running'].includes(a.state),
|
|
101
168
|
);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
169
|
+
// The turn is timed from when this terminal first saw it running (attempts carry no start time).
|
|
170
|
+
if (active && view.turn?.attempt !== active.id)
|
|
171
|
+
view.turn = { attempt: active.id, since: Date.now() };
|
|
172
|
+
if (!active) view.turn = undefined;
|
|
173
|
+
const turn = active ? view.turn : undefined;
|
|
174
|
+
const running = toolLines(view)
|
|
175
|
+
.filter((l) => l.state === 'running')
|
|
176
|
+
.at(-1);
|
|
177
|
+
const state =
|
|
178
|
+
turn && !pending
|
|
179
|
+
? progressLine(
|
|
180
|
+
activityVerb(running),
|
|
181
|
+
turn.since,
|
|
182
|
+
Date.now(),
|
|
183
|
+
c.usage?.observations
|
|
184
|
+
? compactTokens(c.usage.known_input + c.usage.known_output)
|
|
185
|
+
: undefined,
|
|
186
|
+
)
|
|
187
|
+
: view.busy
|
|
188
|
+
? 'Working…'
|
|
189
|
+
: pending
|
|
190
|
+
? `${pending} approval${pending === 1 ? '' : 's'} waiting · ctrl+o to review`
|
|
191
|
+
: sessionFailure(c.snapshot) ||
|
|
192
|
+
c.notice ||
|
|
193
|
+
(!c.snapshot
|
|
194
|
+
? 'ctrl+n to start · ctrl+p to return to a session'
|
|
195
|
+
: active
|
|
196
|
+
? `Agent ${active.state} · detach anytime`
|
|
197
|
+
: c.controlling
|
|
198
|
+
? 'Ready when you are.'
|
|
199
|
+
: 'Observing · ctrl+k → Take control');
|
|
200
|
+
const tone = pending
|
|
201
|
+
? 'amber'
|
|
202
|
+
: sessionFailure(c.snapshot) && !view.busy
|
|
203
|
+
? 'red'
|
|
204
|
+
: turn
|
|
205
|
+
? 'jade'
|
|
206
|
+
: c.connection === 'reconnecting'
|
|
207
|
+
? 'amber'
|
|
208
|
+
: 'muted';
|
|
209
|
+
const guidance = c.snapshot?.project_instructions?.files.length
|
|
210
|
+
? ' Using ' + instructionsLabel(c.snapshot.project_instructions)
|
|
211
|
+
: '';
|
|
212
|
+
const spend = c.usage?.observations
|
|
213
|
+
? ' ' +
|
|
214
|
+
compactTokens(usageInput(c.usage) + c.usage.known_output) +
|
|
215
|
+
' tokens · $' +
|
|
216
|
+
usageCost(c.usage).toFixed(2)
|
|
217
|
+
: '';
|
|
218
|
+
const mode = view.mode
|
|
219
|
+
? ' ' +
|
|
220
|
+
t.ink('mode ' + modeLabel(view.mode.mode), 'muted') +
|
|
221
|
+
(view.mode.access === 'yolo'
|
|
222
|
+
? ' ' + t.ink('YOLO', 'red')
|
|
223
|
+
: view.mode.access === 'full'
|
|
224
|
+
? ' ' + t.ink('FULL ACCESS', 'amber')
|
|
225
|
+
: '') +
|
|
226
|
+
t.ink(' · shift+tab', 'muted')
|
|
227
|
+
: '';
|
|
115
228
|
const identity = c.snapshot
|
|
116
229
|
? ' session ' +
|
|
117
230
|
shortId(c.snapshot.id) +
|
|
@@ -119,17 +232,28 @@ export function status(view: ViewModel, width: number): string[] {
|
|
|
119
232
|
: '';
|
|
120
233
|
return [
|
|
121
234
|
t.line(
|
|
122
|
-
t.ink(safeText(state).replace(/\bPi\b/g, 'Worker'), tone) +
|
|
235
|
+
t.ink(safeText(state).replace(/\bPi\b/g, 'Worker'), tone) +
|
|
236
|
+
mode +
|
|
237
|
+
t.ink(safeText(guidance) + spend + identity, 'muted'),
|
|
123
238
|
width,
|
|
124
239
|
),
|
|
125
240
|
t.line(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
241
|
+
(c.snapshot && interactiveCoding(c.snapshot) && runningAttempt(c.snapshot)
|
|
242
|
+
? [
|
|
243
|
+
t.key('esc', 'interrupt'),
|
|
244
|
+
t.key('enter', 'steer'),
|
|
245
|
+
t.key('tab', 'queue'),
|
|
246
|
+
t.key('alt+enter', 'newline'),
|
|
247
|
+
t.key('ctrl+c ×2', 'leave'),
|
|
248
|
+
]
|
|
249
|
+
: [
|
|
250
|
+
t.key('enter', 'send'),
|
|
251
|
+
t.key('alt+enter', 'newline'),
|
|
252
|
+
t.key('ctrl+k', 'actions'),
|
|
253
|
+
t.key('ctrl+p', 'sessions'),
|
|
254
|
+
t.key('ctrl+c ×2', 'leave'),
|
|
255
|
+
]
|
|
256
|
+
).join(' '),
|
|
133
257
|
width,
|
|
134
258
|
),
|
|
135
259
|
];
|
|
@@ -154,6 +278,80 @@ export class DocumentView implements Component {
|
|
|
154
278
|
}
|
|
155
279
|
return cached.markdown.render(Math.max(1, width));
|
|
156
280
|
}
|
|
281
|
+
/** Per-file review of what changed in the folder since the review base. */
|
|
282
|
+
private reviewLines(width: number): string[] {
|
|
283
|
+
const t = this.view.theme,
|
|
284
|
+
review = this.view.review,
|
|
285
|
+
lines = this.title('CHANGES SINCE REVIEW', width);
|
|
286
|
+
if (!review) return [...lines, ...this.text('Loading changes…', width)];
|
|
287
|
+
if (!review.data)
|
|
288
|
+
return [
|
|
289
|
+
...lines,
|
|
290
|
+
...this.text(
|
|
291
|
+
'Changes appear here after the first turn starts; each turn is checkpointed.',
|
|
292
|
+
width,
|
|
293
|
+
),
|
|
294
|
+
];
|
|
295
|
+
const note = backgroundNote(review.data.background_running);
|
|
296
|
+
if (note) lines.push(...this.text(note, width));
|
|
297
|
+
if (!review.data.files.length)
|
|
298
|
+
return [
|
|
299
|
+
...lines,
|
|
300
|
+
...this.text('Nothing to review: the folder matches what you accepted.', width),
|
|
301
|
+
];
|
|
302
|
+
review.data.files.forEach((change, i) => {
|
|
303
|
+
const mark = i === review.file ? '▸ ' : ' ';
|
|
304
|
+
lines.push(
|
|
305
|
+
t.line(
|
|
306
|
+
mark +
|
|
307
|
+
safeText(change.path) +
|
|
308
|
+
' ' +
|
|
309
|
+
t.ink(change.status + ' · ' + changeCounts(change), 'muted'),
|
|
310
|
+
width,
|
|
311
|
+
),
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
lines.push(t.fill('', width));
|
|
315
|
+
const file = review.data.files[review.file]!;
|
|
316
|
+
const hunks = review.hunks ?? [];
|
|
317
|
+
lines.push(
|
|
318
|
+
...this.title(
|
|
319
|
+
safeText(file.path) +
|
|
320
|
+
(review.hunk >= 0 ? ` · hunk ${review.hunk + 1} of ${hunks.length}` : ' · whole file'),
|
|
321
|
+
width,
|
|
322
|
+
),
|
|
323
|
+
);
|
|
324
|
+
if (!hunks.length) lines.push(...this.text('No text diff to show for this file.', width));
|
|
325
|
+
hunks.forEach((hunk, i) => {
|
|
326
|
+
const selected = i === review.hunk;
|
|
327
|
+
lines.push(
|
|
328
|
+
...t
|
|
329
|
+
.highlight(safeText(hunk.text).replace(/\n$/, ''), 'diff')
|
|
330
|
+
.flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width - 4)))
|
|
331
|
+
.map((line) => t.line((selected ? '┃ ' : review.hunk >= 0 ? ' ' : '') + line, width)),
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
lines.push(
|
|
335
|
+
t.fill('', width),
|
|
336
|
+
t.line(
|
|
337
|
+
t.key('alt+←/→', 'file') +
|
|
338
|
+
' ' +
|
|
339
|
+
t.key('alt+↑/↓', 'hunk') +
|
|
340
|
+
' ' +
|
|
341
|
+
t.key('a', 'accept') +
|
|
342
|
+
' ' +
|
|
343
|
+
t.key('r', 'revert') +
|
|
344
|
+
' ' +
|
|
345
|
+
t.key('i', 'instruct'),
|
|
346
|
+
width,
|
|
347
|
+
),
|
|
348
|
+
...this.text(
|
|
349
|
+
'a/r/i work while the message box is empty. Accept marks a change reviewed; revert writes the reviewed base back. /rewind returns to before any turn.',
|
|
350
|
+
width,
|
|
351
|
+
),
|
|
352
|
+
);
|
|
353
|
+
return lines;
|
|
354
|
+
}
|
|
157
355
|
private title(label: string, width: number, role: 'jade' | 'amber' | 'muted' = 'jade'): string[] {
|
|
158
356
|
const t = this.view.theme;
|
|
159
357
|
return [t.line(t.ink(label, role, true), width)];
|
|
@@ -191,7 +389,7 @@ export class DocumentView implements Component {
|
|
|
191
389
|
),
|
|
192
390
|
t.fill('', width),
|
|
193
391
|
t.line(t.key('ctrl+n', 'new session') + ' ' + t.key('ctrl+p', 'recent sessions'), width),
|
|
194
|
-
t.line(t.key('ctrl+k', 'all actions') + ' ' + t.key('
|
|
392
|
+
t.line(t.key('ctrl+k', 'all actions') + ' ' + t.key('alt+g', 'review changes'), width),
|
|
195
393
|
...(c.snapshot
|
|
196
394
|
? [
|
|
197
395
|
t.fill('', width),
|
|
@@ -201,7 +399,10 @@ export class DocumentView implements Component {
|
|
|
201
399
|
? 'Owned coding workspace · guarded edits'
|
|
202
400
|
: c.snapshot.capabilities.includes('local-workspace-read')
|
|
203
401
|
? c.snapshot.capabilities.includes('local-coding-reviewed')
|
|
204
|
-
? 'Local coding · reviewed edits · commands
|
|
402
|
+
? 'Local coding · reviewed edits · commands ' +
|
|
403
|
+
(this.view.mode?.sandbox
|
|
404
|
+
? '· ' + this.view.mode.sandbox
|
|
405
|
+
: 'run as your user')
|
|
205
406
|
: 'Local workspace · read, list and search · /coding for edits and commands'
|
|
206
407
|
: 'Conversation session · coding tools are off',
|
|
207
408
|
'muted',
|
|
@@ -212,6 +413,51 @@ export class DocumentView implements Component {
|
|
|
212
413
|
: []),
|
|
213
414
|
];
|
|
214
415
|
}
|
|
416
|
+
/** Compact lines for tool calls (with their output in the full transcript), children indented. */
|
|
417
|
+
private toolBlock(
|
|
418
|
+
calls: ReturnType<typeof toolLines>,
|
|
419
|
+
byKey: Map<string, ReturnType<typeof toolLines>[number]>,
|
|
420
|
+
width: number,
|
|
421
|
+
): string[] {
|
|
422
|
+
const t = this.view.theme,
|
|
423
|
+
lines: string[] = [],
|
|
424
|
+
shown = this.view.transcript ? calls : calls.slice(-8);
|
|
425
|
+
if (shown.length < calls.length)
|
|
426
|
+
lines.push(
|
|
427
|
+
t.line(
|
|
428
|
+
t.ink(` … ${calls.length - shown.length} earlier calls · ctrl+x shows all`, 'muted'),
|
|
429
|
+
width,
|
|
430
|
+
),
|
|
431
|
+
);
|
|
432
|
+
for (const call of shown) {
|
|
433
|
+
let depth = 0,
|
|
434
|
+
parent = call.parent;
|
|
435
|
+
const visited = new Set<string>();
|
|
436
|
+
while (parent && !visited.has(parent) && depth < 4) {
|
|
437
|
+
visited.add(parent);
|
|
438
|
+
depth++;
|
|
439
|
+
parent = byKey.get(parent)?.parent;
|
|
440
|
+
}
|
|
441
|
+
const indent = depth ? ' '.repeat(depth) + '└─ ' : '';
|
|
442
|
+
lines.push(
|
|
443
|
+
t.line(
|
|
444
|
+
t.ink(
|
|
445
|
+
call.state === 'running' ? '◌ ' : call.state === 'failed' ? '× ' : '✓ ',
|
|
446
|
+
call.state === 'failed' ? 'red' : call.state === 'running' ? 'amber' : 'jade',
|
|
447
|
+
) + t.ink(indent + call.label),
|
|
448
|
+
width,
|
|
449
|
+
),
|
|
450
|
+
);
|
|
451
|
+
const detail = this.view.transcript
|
|
452
|
+
? call.output
|
|
453
|
+
? safeText(call.output).split('\n').slice(0, 40)
|
|
454
|
+
: []
|
|
455
|
+
: call.tail;
|
|
456
|
+
for (const tail of detail)
|
|
457
|
+
lines.push(...this.text(' ' + ' '.repeat(indent.length) + '│ ' + tail, width));
|
|
458
|
+
}
|
|
459
|
+
return lines;
|
|
460
|
+
}
|
|
215
461
|
render(width: number): string[] {
|
|
216
462
|
const { client: c, theme: t, tab } = this.view;
|
|
217
463
|
if (!c.snapshot) {
|
|
@@ -245,7 +491,37 @@ export class DocumentView implements Component {
|
|
|
245
491
|
t.fill('', width),
|
|
246
492
|
);
|
|
247
493
|
const ids = new Set(c.snapshot.messages.map((m) => m.entry_id));
|
|
494
|
+
// Each turn's tool calls sit where they ran: before the message that followed them.
|
|
495
|
+
const calls = toolLines(this.view),
|
|
496
|
+
byKey = new Map(calls.map((l) => [l.key, l])),
|
|
497
|
+
groups = new Map<string, typeof calls>(),
|
|
498
|
+
rootOf = new Map<string, string>(),
|
|
499
|
+
placed = new Set<string>();
|
|
500
|
+
for (const call of calls) {
|
|
501
|
+
let root = call.key;
|
|
502
|
+
const visited = new Set<string>();
|
|
503
|
+
while (byKey.get(root)?.parent && !visited.has(root) && visited.size < 8) {
|
|
504
|
+
visited.add(root);
|
|
505
|
+
root = byKey.get(root)!.parent!;
|
|
506
|
+
}
|
|
507
|
+
groups.set(root, [...(groups.get(root) ?? []), call]);
|
|
508
|
+
rootOf.set(root.slice(root.indexOf(':') + 1), root);
|
|
509
|
+
}
|
|
248
510
|
for (const message of c.snapshot.messages) {
|
|
511
|
+
const before = (message.tool_calls ?? [])
|
|
512
|
+
.map((id) => rootOf.get(id))
|
|
513
|
+
.filter((root): root is string => !!root && !placed.has(root));
|
|
514
|
+
if (before.length) {
|
|
515
|
+
for (const root of before) placed.add(root);
|
|
516
|
+
lines.push(
|
|
517
|
+
...this.toolBlock(
|
|
518
|
+
before.flatMap((root) => groups.get(root)!),
|
|
519
|
+
byKey,
|
|
520
|
+
width,
|
|
521
|
+
),
|
|
522
|
+
t.fill('', width),
|
|
523
|
+
);
|
|
524
|
+
}
|
|
249
525
|
lines.push(
|
|
250
526
|
...this.title(
|
|
251
527
|
message.role === 'user' ? 'YOU' : 'SALAAM',
|
|
@@ -254,9 +530,11 @@ export class DocumentView implements Component {
|
|
|
254
530
|
),
|
|
255
531
|
);
|
|
256
532
|
lines.push(
|
|
257
|
-
...this.prose(
|
|
258
|
-
|
|
259
|
-
|
|
533
|
+
...this.prose(
|
|
534
|
+
message.entry_id,
|
|
535
|
+
message.role === 'user' ? typedText(message.text) : message.text,
|
|
536
|
+
width - 4,
|
|
537
|
+
).map((line) => t.line(line, width)),
|
|
260
538
|
...(message.truncated
|
|
261
539
|
? this.text(
|
|
262
540
|
'Message preview truncated. Read its full history entry to inspect all content.',
|
|
@@ -275,54 +553,28 @@ export class DocumentView implements Component {
|
|
|
275
553
|
width,
|
|
276
554
|
'muted',
|
|
277
555
|
),
|
|
278
|
-
...this.text(submission.text, width),
|
|
556
|
+
...this.text(typedText(submission.text), width),
|
|
279
557
|
t.fill('', width),
|
|
280
558
|
);
|
|
281
559
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
if (tools.size) {
|
|
303
|
-
lines.push(...this.title('TOOL ACTIVITY', width, 'muted'));
|
|
304
|
-
for (const tool of [...tools.values()].slice(-6)) {
|
|
305
|
-
let depth = 0,
|
|
306
|
-
parent = tool.parent;
|
|
307
|
-
const visited = new Set<string>();
|
|
308
|
-
while (parent && !visited.has(parent) && depth < 4) {
|
|
309
|
-
visited.add(parent);
|
|
310
|
-
depth++;
|
|
311
|
-
parent = tools.get(parent)?.parent;
|
|
312
|
-
}
|
|
313
|
-
lines.push(
|
|
314
|
-
t.line(
|
|
315
|
-
t.ink(
|
|
316
|
-
tool.state === 'running' ? '◌ ' : tool.failed ? '× ' : '✓ ',
|
|
317
|
-
tool.failed ? 'red' : tool.state === 'running' ? 'amber' : 'jade',
|
|
318
|
-
) +
|
|
319
|
-
t.ink((depth ? ' '.repeat(depth) + '└─ ' : '') + tool.label) +
|
|
320
|
-
t.ink(' ' + tool.state, 'muted'),
|
|
321
|
-
width,
|
|
322
|
-
),
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
lines.push(t.fill('', width));
|
|
560
|
+
// Calls not placed before a message: the turn still running, or ones history no longer shows.
|
|
561
|
+
const rest = [...groups.entries()].filter(([root]) => !placed.has(root));
|
|
562
|
+
if (rest.length) {
|
|
563
|
+
lines.push(
|
|
564
|
+
...this.title(
|
|
565
|
+
this.view.transcript
|
|
566
|
+
? 'TRANSCRIPT · every tool call'
|
|
567
|
+
: 'TOOLS · ctrl+x for the full transcript',
|
|
568
|
+
width,
|
|
569
|
+
'muted',
|
|
570
|
+
),
|
|
571
|
+
...this.toolBlock(
|
|
572
|
+
rest.flatMap(([, group]) => group),
|
|
573
|
+
byKey,
|
|
574
|
+
width,
|
|
575
|
+
),
|
|
576
|
+
t.fill('', width),
|
|
577
|
+
);
|
|
326
578
|
}
|
|
327
579
|
if (c.preview)
|
|
328
580
|
lines.push(
|
|
@@ -359,23 +611,20 @@ export class DocumentView implements Component {
|
|
|
359
611
|
);
|
|
360
612
|
for (const key of this.cache.keys())
|
|
361
613
|
if (key !== 'live-preview' && !ids.has(key)) this.cache.delete(key);
|
|
362
|
-
} else if (
|
|
363
|
-
(
|
|
364
|
-
|
|
365
|
-
) {
|
|
366
|
-
const prefix = tab === 'changes' ? 'Local edit:' : 'Local command:';
|
|
614
|
+
} else if (tab === 'changes' && c.snapshot.capabilities.includes('local-coding-reviewed')) {
|
|
615
|
+
lines.push(...this.reviewLines(width));
|
|
616
|
+
} else if (tab === 'commands' && c.snapshot.capabilities.includes('local-coding-reviewed')) {
|
|
367
617
|
const operations = c.snapshot.operations.filter(
|
|
368
|
-
(op) => op.kind === 'workspace' && op.target.startsWith(
|
|
369
|
-
);
|
|
370
|
-
lines.push(
|
|
371
|
-
...this.title(tab === 'changes' ? 'REVIEWED LOCAL EDITS' : 'LOCAL COMMAND RESULTS', width),
|
|
618
|
+
(op) => op.kind === 'workspace' && op.target.startsWith('Local command:'),
|
|
372
619
|
);
|
|
620
|
+
lines.push(...this.title('LOCAL COMMAND RESULTS', width));
|
|
373
621
|
if (!operations.length)
|
|
374
622
|
lines.push(
|
|
375
623
|
...this.text(
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
624
|
+
'Commands appear here with their recorded exit status and output. ' +
|
|
625
|
+
(this.view.mode?.sandbox
|
|
626
|
+
? 'Where commands run: ' + this.view.mode.sandbox + '.'
|
|
627
|
+
: 'Commands run as your local user.'),
|
|
379
628
|
width,
|
|
380
629
|
),
|
|
381
630
|
);
|
|
@@ -400,6 +649,8 @@ export class DocumentView implements Component {
|
|
|
400
649
|
),
|
|
401
650
|
);
|
|
402
651
|
} else if (tab === 'changes') {
|
|
652
|
+
const failed = sessionFailure(c.snapshot);
|
|
653
|
+
if (failed && !c.workspace) return this.text(failed, width);
|
|
403
654
|
if (!c.workspace)
|
|
404
655
|
return this.text(
|
|
405
656
|
'This session has no coding workspace. Create a coding session to review guarded edits.',
|
|
@@ -810,7 +1061,7 @@ export class DocumentView implements Component {
|
|
|
810
1061
|
width,
|
|
811
1062
|
),
|
|
812
1063
|
...this.text(
|
|
813
|
-
`Recorded tokens: ${c.usage
|
|
1064
|
+
`Recorded tokens: ${usageInput(c.usage)} input (${c.usage.known_cache_read ?? 0} cache reads, ${c.usage.known_cache_write ?? 0} cache writes) · ${c.usage.known_output} output`,
|
|
814
1065
|
width,
|
|
815
1066
|
),
|
|
816
1067
|
...this.text(
|
|
@@ -881,12 +1132,14 @@ export function sidebar(view: ViewModel, width: number): string[] {
|
|
|
881
1132
|
if (c.usage) {
|
|
882
1133
|
label('USAGE');
|
|
883
1134
|
row(
|
|
884
|
-
c.usage.
|
|
1135
|
+
usageInput(c.usage).toLocaleString() +
|
|
885
1136
|
' in · ' +
|
|
886
1137
|
c.usage.known_output.toLocaleString() +
|
|
887
1138
|
' out',
|
|
888
1139
|
);
|
|
889
|
-
|
|
1140
|
+
if (c.usage.known_cache_read)
|
|
1141
|
+
row(c.usage.known_cache_read.toLocaleString() + ' from cache', 'muted');
|
|
1142
|
+
row((c.usage.estimated_observations ? 'Est. $' : '$') + usageCost(c.usage).toFixed(4), 'muted');
|
|
890
1143
|
if (c.usage.unknown_observations)
|
|
891
1144
|
row(c.usage.unknown_observations + ' unknown entries', 'amber');
|
|
892
1145
|
}
|
|
@@ -903,7 +1156,7 @@ export function sidebar(view: ViewModel, width: number): string[] {
|
|
|
903
1156
|
);
|
|
904
1157
|
row(
|
|
905
1158
|
c.snapshot?.capabilities.includes('local-coding-reviewed')
|
|
906
|
-
? 'Commands · local user'
|
|
1159
|
+
? 'Commands · ' + (view.mode?.sandbox ?? 'local user')
|
|
907
1160
|
: c.workspace && c.health?.execution?.commands
|
|
908
1161
|
? 'Commands · isolated'
|
|
909
1162
|
: 'Commands · off',
|