@jwilger/pi-development-system 0.10.6 → 0.12.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 +9 -1
- package/agents/advisor.md +40 -0
- package/agents/architect.md +66 -0
- package/agents/coder.md +57 -0
- package/agents/implementer.md +47 -0
- package/agents/lens-cagan.md +54 -0
- package/agents/lens-perri.md +54 -0
- package/agents/lens-pichler.md +54 -0
- package/agents/lens-rumelt.md +54 -0
- package/agents/lens-torres.md +54 -0
- package/agents/researcher.md +70 -0
- package/agents/reviewer.md +57 -0
- package/agents/tasker.md +58 -0
- package/agents/writer.md +39 -0
- package/extensions/development-system.ts +4 -0
- package/package.json +5 -3
- package/skills/delegation/SKILL.md +66 -0
- package/src/core/models.ts +18 -0
- package/src/core/routing.ts +67 -0
- package/src/core/spawn-overrides.ts +59 -0
- package/src/jev/questions/route.ts +93 -0
- package/src/state/config.ts +2 -4
- package/src/state/route-task-tool.ts +74 -0
- package/src/subagents/VENDORED.md +40 -0
- package/src/subagents/index.ts +544 -0
- package/src/subagents/orch/inherited-tools.ts +85 -0
- package/src/subagents/orch/mailbox.ts +262 -0
- package/src/subagents/orch/manager.ts +778 -0
- package/src/subagents/orch/paths.ts +74 -0
- package/src/subagents/orch/prompt.ts +25 -0
- package/src/subagents/orch/runtime.ts +791 -0
- package/src/subagents/orch/tools.ts +177 -0
- package/src/subagents/orch/transcript.ts +187 -0
- package/src/subagents/prefs/agent-import.ts +246 -0
- package/src/subagents/prefs/config.ts +1029 -0
- package/src/subagents/prefs/import-discovery.ts +644 -0
- package/src/subagents/prefs/import-instructions.ts +157 -0
- package/src/subagents/prefs/models.ts +85 -0
- package/src/subagents/prefs/settings.ts +371 -0
- package/src/subagents/types.ts +209 -0
- package/src/subagents/ui/agent-navigation-editor.ts +210 -0
- package/src/subagents/ui/dialog.ts +651 -0
- package/src/subagents/ui/import-picker.ts +232 -0
- package/src/subagents/ui/live-agent-view.ts +572 -0
- package/src/subagents/ui/model-picker.ts +452 -0
- package/src/subagents/ui/settings-ui.ts +329 -0
- package/src/subagents/ui/status-ui.ts +545 -0
- package/src/subagents/ui/thread-tree.ts +135 -0
- package/src/subagents/ui/ui.ts +1507 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import {
|
|
5
|
+
Input,
|
|
6
|
+
Key,
|
|
7
|
+
Markdown,
|
|
8
|
+
matchesKey,
|
|
9
|
+
ScrollView,
|
|
10
|
+
stripTerminalSequences,
|
|
11
|
+
truncateToWidth,
|
|
12
|
+
visibleWidth,
|
|
13
|
+
wrapTextWithAnsi,
|
|
14
|
+
} from "@earendil-works/pi-tui";
|
|
15
|
+
import type { ThreadService, TranscriptSnapshot } from "../types.ts";
|
|
16
|
+
import { dialogText, type DialogHost } from "./dialog.ts";
|
|
17
|
+
import { AGENT_PROGRESS_INTERVAL, agentProgressIcon, agentTypeLabel } from "./ui.ts";
|
|
18
|
+
|
|
19
|
+
/** Transcript detail level cycled by the t key: full → preview → compact. */
|
|
20
|
+
export type TranscriptDetail = "full" | "preview" | "compact";
|
|
21
|
+
const DETAIL_CYCLE: TranscriptDetail[] = ["full", "preview", "compact"];
|
|
22
|
+
|
|
23
|
+
export interface AgentViewportState {
|
|
24
|
+
scrollTop: number;
|
|
25
|
+
follow: boolean;
|
|
26
|
+
showInherited?: boolean;
|
|
27
|
+
detail?: TranscriptDetail;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Fully cover main, including at very small sizes; only trusted renderers emit terminal sequences. */
|
|
31
|
+
export function fillViewport(lines: string[], width: number, height: number): string[] {
|
|
32
|
+
width = Math.max(0, Math.floor(width));
|
|
33
|
+
height = Math.max(1, Math.floor(height));
|
|
34
|
+
const result = lines.slice(0, height).map((line) => {
|
|
35
|
+
const clipped = truncateToWidth(line, width, "", true);
|
|
36
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
37
|
+
});
|
|
38
|
+
while (result.length < height) result.push(" ".repeat(width));
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function text(value: string): string {
|
|
43
|
+
return stripTerminalSequences(value)
|
|
44
|
+
.replace(/\r\n?/g, "\n")
|
|
45
|
+
.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function object(value: unknown): Record<string, unknown> | undefined {
|
|
49
|
+
return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function json(value: unknown): string {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.stringify(value, null, 2) ?? "";
|
|
55
|
+
} catch {
|
|
56
|
+
return "[unrenderable data]";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Live transcript and manual steering. Never invoke tools' custom renderers or pass through raw controls. */
|
|
61
|
+
export class LiveAgentView {
|
|
62
|
+
private snapshot: TranscriptSnapshot | undefined;
|
|
63
|
+
private error: string | undefined;
|
|
64
|
+
private disposed = false;
|
|
65
|
+
private attachment = 0;
|
|
66
|
+
private unsubscribe: (() => void) | undefined;
|
|
67
|
+
private renderTimer: ReturnType<typeof setTimeout> | undefined;
|
|
68
|
+
private progressTimer: ReturnType<typeof setInterval> | undefined;
|
|
69
|
+
private cache = new WeakMap<object, { width: number; detail: TranscriptDetail; lines: string[] }>();
|
|
70
|
+
private scroll: ScrollView;
|
|
71
|
+
private content: string[] = [];
|
|
72
|
+
private restoreViewport = true;
|
|
73
|
+
private input = new Input({ prompt: "Steer > " });
|
|
74
|
+
private editing = true;
|
|
75
|
+
private focusedState = true;
|
|
76
|
+
private pasting = false;
|
|
77
|
+
private pasteTail = "";
|
|
78
|
+
private sending = false;
|
|
79
|
+
private steerStatus: string | undefined;
|
|
80
|
+
private steerFailed = false;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
private host: DialogHost,
|
|
84
|
+
private theme: Theme,
|
|
85
|
+
private service: ThreadService,
|
|
86
|
+
readonly path: string,
|
|
87
|
+
private viewport: AgentViewportState,
|
|
88
|
+
private done: (result: "back" | "main") => void,
|
|
89
|
+
private nerdFontIcons = false,
|
|
90
|
+
) {
|
|
91
|
+
this.scroll = new ScrollView(
|
|
92
|
+
{ render: () => this.content, invalidate() {} },
|
|
93
|
+
{ follow: "end", overscroll: "contain", scrollbar: "hidden" },
|
|
94
|
+
);
|
|
95
|
+
this.input.focused = true;
|
|
96
|
+
this.input.onSubmit = (value) => void this.steer(value);
|
|
97
|
+
void this.attach();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get focused(): boolean {
|
|
101
|
+
return this.focusedState;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
set focused(value: boolean) {
|
|
105
|
+
this.focusedState = value;
|
|
106
|
+
this.input.focused = value && this.editing;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async steer(value: string): Promise<void> {
|
|
110
|
+
const message = dialogText(value);
|
|
111
|
+
if (this.disposed || this.sending || !message.trim()) return;
|
|
112
|
+
this.sending = true;
|
|
113
|
+
this.steerFailed = false;
|
|
114
|
+
this.steerStatus = "Sending steering…";
|
|
115
|
+
this.host.requestRender();
|
|
116
|
+
try {
|
|
117
|
+
// Same root-scoped service as the Actions menu: queues a running turn or resumes a settled one.
|
|
118
|
+
await this.service.steer(this.path, message);
|
|
119
|
+
if (this.disposed) return;
|
|
120
|
+
if (this.input.getValue() === message) this.input.setValue("");
|
|
121
|
+
this.steerStatus = "Steering sent";
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (this.disposed) return;
|
|
124
|
+
this.steerFailed = true;
|
|
125
|
+
this.steerStatus = `Steering failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
126
|
+
} finally {
|
|
127
|
+
this.sending = false;
|
|
128
|
+
if (!this.disposed) this.host.requestRender();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private editInput(data: string): void {
|
|
133
|
+
// Keep paste chunks (including embedded Esc, Tab and Enter) out of navigation shortcuts.
|
|
134
|
+
const pasteData = this.pasteTail + data;
|
|
135
|
+
if (pasteData.includes("\x1b[200~")) this.pasting = true;
|
|
136
|
+
if (pasteData.includes("\x1b[201~")) this.pasting = false;
|
|
137
|
+
this.pasteTail = this.pasting ? pasteData.slice(-5) : "";
|
|
138
|
+
const before = this.input.getValue();
|
|
139
|
+
this.input.handleInput(data);
|
|
140
|
+
const safe = dialogText(this.input.getValue());
|
|
141
|
+
if (safe !== this.input.getValue()) this.input.setValue(safe);
|
|
142
|
+
if (safe !== before && !this.sending) this.steerStatus = undefined;
|
|
143
|
+
this.host.requestRender();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async attach(): Promise<void> {
|
|
147
|
+
const attachment = ++this.attachment;
|
|
148
|
+
this.unsubscribe?.();
|
|
149
|
+
this.unsubscribe = undefined;
|
|
150
|
+
this.error = undefined;
|
|
151
|
+
this.scheduleRender();
|
|
152
|
+
try {
|
|
153
|
+
if (!this.service.observeTranscript)
|
|
154
|
+
throw new Error("Live observation is unavailable for this service.");
|
|
155
|
+
let latest: TranscriptSnapshot | undefined;
|
|
156
|
+
const observation = await this.service.observeTranscript(this.path, (snapshot) => {
|
|
157
|
+
latest = snapshot;
|
|
158
|
+
if (this.disposed || attachment !== this.attachment) return;
|
|
159
|
+
this.accept(snapshot);
|
|
160
|
+
});
|
|
161
|
+
if (this.disposed || attachment !== this.attachment) {
|
|
162
|
+
observation.unsubscribe();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.unsubscribe = observation.unsubscribe;
|
|
166
|
+
this.accept(latest ?? observation.snapshot);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (this.disposed || attachment !== this.attachment) return;
|
|
169
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
170
|
+
this.syncProgress();
|
|
171
|
+
this.scheduleRender();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private accept(snapshot: TranscriptSnapshot): void {
|
|
176
|
+
if (
|
|
177
|
+
this.snapshot &&
|
|
178
|
+
(snapshot.generation < this.snapshot.generation ||
|
|
179
|
+
(snapshot.generation === this.snapshot.generation &&
|
|
180
|
+
snapshot.revision < this.snapshot.revision))
|
|
181
|
+
)
|
|
182
|
+
return;
|
|
183
|
+
this.snapshot = snapshot;
|
|
184
|
+
this.error = snapshot.error;
|
|
185
|
+
this.syncProgress();
|
|
186
|
+
this.scheduleRender();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private syncProgress(): void {
|
|
190
|
+
const state = this.snapshot?.thread?.state;
|
|
191
|
+
const active = this.nerdFontIcons && !this.disposed && !this.error &&
|
|
192
|
+
(state === "starting" || state === "running");
|
|
193
|
+
if (!active) {
|
|
194
|
+
if (this.progressTimer !== undefined) clearInterval(this.progressTimer);
|
|
195
|
+
this.progressTimer = undefined;
|
|
196
|
+
} else if (this.progressTimer === undefined) {
|
|
197
|
+
this.progressTimer = setInterval(() => this.scheduleRender(), AGENT_PROGRESS_INTERVAL);
|
|
198
|
+
this.progressTimer.unref?.();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private scheduleRender(): void {
|
|
203
|
+
if (this.disposed || this.renderTimer) return;
|
|
204
|
+
this.renderTimer = setTimeout(() => {
|
|
205
|
+
this.renderTimer = undefined;
|
|
206
|
+
if (!this.disposed) this.host.requestRender();
|
|
207
|
+
}, 16);
|
|
208
|
+
this.renderTimer.unref?.();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
invalidate(): void {
|
|
212
|
+
this.cache = new WeakMap();
|
|
213
|
+
this.input.invalidate();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
dispose(): void {
|
|
217
|
+
if (this.disposed) return;
|
|
218
|
+
this.disposed = true;
|
|
219
|
+
++this.attachment;
|
|
220
|
+
this.unsubscribe?.();
|
|
221
|
+
this.unsubscribe = undefined;
|
|
222
|
+
if (this.renderTimer) clearTimeout(this.renderTimer);
|
|
223
|
+
this.renderTimer = undefined;
|
|
224
|
+
this.syncProgress();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
handleInput(data: string): void {
|
|
228
|
+
if (this.disposed) return;
|
|
229
|
+
if (this.pasting || (this.editing && data.includes("\x1b[200~"))) {
|
|
230
|
+
this.editInput(data);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (matchesKey(data, Key.escape)) return this.done("back");
|
|
234
|
+
if (matchesKey(data, Key.ctrl("q")) || matchesKey(data, Key.ctrl("c")))
|
|
235
|
+
return this.done("main");
|
|
236
|
+
if (matchesKey(data, Key.tab)) {
|
|
237
|
+
this.editing = !this.editing;
|
|
238
|
+
this.input.focused = this.focused && this.editing;
|
|
239
|
+
this.host.requestRender();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (this.error && (matchesKey(data, Key.ctrl("r")) || (!this.editing && data === "r"))) {
|
|
243
|
+
void this.attach();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// Transcript scrolling remains available while composing; Home/End edit the input.
|
|
247
|
+
if (matchesKey(data, Key.up)) this.scroll.scrollBy(-1);
|
|
248
|
+
else if (matchesKey(data, Key.down)) this.scroll.scrollBy(1);
|
|
249
|
+
else if (matchesKey(data, Key.pageUp))
|
|
250
|
+
this.scroll.scrollBy(-Math.max(1, this.scroll.viewportHeight));
|
|
251
|
+
else if (matchesKey(data, Key.pageDown))
|
|
252
|
+
this.scroll.scrollBy(Math.max(1, this.scroll.viewportHeight));
|
|
253
|
+
else if (this.editing) {
|
|
254
|
+
this.editInput(data);
|
|
255
|
+
return;
|
|
256
|
+
} else if (data === "c") {
|
|
257
|
+
this.viewport.showInherited = !this.viewport.showInherited;
|
|
258
|
+
this.invalidate();
|
|
259
|
+
} else if (data === "t") {
|
|
260
|
+
this.viewport.detail =
|
|
261
|
+
DETAIL_CYCLE[(DETAIL_CYCLE.indexOf(this.detail) + 1) % DETAIL_CYCLE.length];
|
|
262
|
+
this.invalidate();
|
|
263
|
+
} else if (matchesKey(data, Key.home)) this.scroll.scrollTo(0, { disableFollow: true });
|
|
264
|
+
else if (matchesKey(data, Key.end) || data === "l") this.scroll.scrollToEnd();
|
|
265
|
+
else return;
|
|
266
|
+
this.saveViewport();
|
|
267
|
+
this.host.requestRender();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private saveViewport(): void {
|
|
271
|
+
this.viewport.scrollTop = this.scroll.scrollTop;
|
|
272
|
+
this.viewport.follow = this.scroll.isFollowingEnd;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private get detail(): TranscriptDetail {
|
|
276
|
+
return this.viewport.detail ?? "preview";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private wrap(value: string, width: number): string[] {
|
|
280
|
+
return text(value)
|
|
281
|
+
.split("\n")
|
|
282
|
+
.flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width)));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Use the same themed Markdown (including code highlighting) as pi's main transcript. */
|
|
286
|
+
private markdown(value: string, width: number, thinking = false): string[] {
|
|
287
|
+
return new Markdown(
|
|
288
|
+
text(value),
|
|
289
|
+
0,
|
|
290
|
+
0,
|
|
291
|
+
getMarkdownTheme(),
|
|
292
|
+
thinking
|
|
293
|
+
? { color: (value) => this.theme.fg("thinkingText", value), italic: true }
|
|
294
|
+
: undefined,
|
|
295
|
+
).render(Math.max(1, width));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Tool/metadata previews are bounded by rendered rows, not just source newlines. */
|
|
299
|
+
private preview(content: unknown, width: number): string[] {
|
|
300
|
+
const values = Array.isArray(content) ? content : [content];
|
|
301
|
+
const lines: string[] = [];
|
|
302
|
+
for (const value of values) {
|
|
303
|
+
const block = object(value);
|
|
304
|
+
const valueText =
|
|
305
|
+
typeof value === "string"
|
|
306
|
+
? value
|
|
307
|
+
: block?.type === "text"
|
|
308
|
+
? String(block.text ?? "")
|
|
309
|
+
: block?.type === "image"
|
|
310
|
+
? "[Image — text-only observer]"
|
|
311
|
+
: json(value);
|
|
312
|
+
// Avoid wrapping megabytes of tool output just to display its first three rows.
|
|
313
|
+
const safe = text(valueText);
|
|
314
|
+
const bounded = safe.slice(0, 32768);
|
|
315
|
+
for (const line of bounded.split("\n")) {
|
|
316
|
+
lines.push(...wrapTextWithAnsi(line, Math.max(1, width)));
|
|
317
|
+
if (lines.length > 3) return [...lines.slice(0, 3), this.theme.fg("dim", "...")];
|
|
318
|
+
}
|
|
319
|
+
if (safe.length > bounded.length) return [...lines.slice(0, 3), this.theme.fg("dim", "...")];
|
|
320
|
+
}
|
|
321
|
+
return lines;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private blocks(content: unknown, width: number): string[] {
|
|
325
|
+
if (typeof content === "string") return this.markdown(content, width);
|
|
326
|
+
if (!Array.isArray(content)) return this.wrap(json(content), width);
|
|
327
|
+
return content.flatMap((value) => {
|
|
328
|
+
const block = object(value);
|
|
329
|
+
if (!block) return this.wrap(json(value), width);
|
|
330
|
+
if (block.type === "text") return this.markdown(String(block.text ?? ""), width);
|
|
331
|
+
if (block.type === "image") return ["[Image — text-only observer]"];
|
|
332
|
+
if (block.type === "thinking") {
|
|
333
|
+
if (this.detail === "compact")
|
|
334
|
+
return [this.theme.fg("dim", "[Thinking hidden · t cycles view]")];
|
|
335
|
+
// Thinking is model-authored Markdown; preview truncates the rendered lines, not the source.
|
|
336
|
+
const rendered = this.markdown(String(block.thinking ?? "[redacted thinking]"), width, true);
|
|
337
|
+
if (this.detail === "full" || rendered.length <= 3) return rendered;
|
|
338
|
+
return [...rendered.slice(0, 3), this.theme.fg("dim", "...")];
|
|
339
|
+
}
|
|
340
|
+
if (block.type === "toolCall")
|
|
341
|
+
return this.toolCall(String(block.name), block.arguments, width);
|
|
342
|
+
return this.preview(json(block), width);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private message(message: AgentMessage, width: number): string[] {
|
|
347
|
+
const cached = this.cache.get(message);
|
|
348
|
+
if (cached && cached.width === width && cached.detail === this.detail) return cached.lines;
|
|
349
|
+
const data = message as unknown as Record<string, unknown>;
|
|
350
|
+
const label =
|
|
351
|
+
message.role === "toolResult"
|
|
352
|
+
? `Tool result: ${String(data.toolName)}${data.isError ? " — error" : ""}`
|
|
353
|
+
: message.role;
|
|
354
|
+
const collapsed = !["user", "assistant", "custom"].includes(message.role);
|
|
355
|
+
const content = data.content ?? data.summary ?? data.output ?? data;
|
|
356
|
+
const lines = [
|
|
357
|
+
this.theme.fg(data.isError ? "error" : "accent", dialogText(label)),
|
|
358
|
+
...(collapsed ? this.preview(content, width) : this.blocks(content, width)),
|
|
359
|
+
...(data.errorMessage
|
|
360
|
+
? (collapsed
|
|
361
|
+
? this.preview(String(data.errorMessage), width)
|
|
362
|
+
: this.wrap(String(data.errorMessage), width)
|
|
363
|
+
).map((line) => this.theme.fg("error", line))
|
|
364
|
+
: []),
|
|
365
|
+
"",
|
|
366
|
+
];
|
|
367
|
+
this.cache.set(message, { width, detail: this.detail, lines });
|
|
368
|
+
return lines;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Pi-style tool call lines, mirroring the main session's built-in renderers (`$ command`,
|
|
373
|
+
* `read path:range`, `grep /pattern/ in path`, …) instead of bare JSON. All interpolated values
|
|
374
|
+
* are sanitized; unknown tools fall back to pretty-printed JSON. Shaped by the detail mode:
|
|
375
|
+
* compact keeps one truncated line, preview keeps three wrapped rows, full keeps everything
|
|
376
|
+
* (bounded by the preview byte cap for pathological payloads).
|
|
377
|
+
*/
|
|
378
|
+
private toolCall(name: string, args: unknown, width: number): string[] {
|
|
379
|
+
const data = object(args);
|
|
380
|
+
const str = (key: string): string | undefined => {
|
|
381
|
+
const value = data?.[key];
|
|
382
|
+
return typeof value === "string" ? text(value) : undefined;
|
|
383
|
+
};
|
|
384
|
+
const num = (key: string): number | undefined => {
|
|
385
|
+
const value = data?.[key];
|
|
386
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
387
|
+
};
|
|
388
|
+
const title = (value: string) => this.theme.fg("accent", value);
|
|
389
|
+
const detail = (value: string) => this.theme.fg("muted", value);
|
|
390
|
+
const path = str("file_path") ?? str("path");
|
|
391
|
+
let lines: string[];
|
|
392
|
+
switch (name) {
|
|
393
|
+
case "bash":
|
|
394
|
+
case "powershell": {
|
|
395
|
+
const prompt = name === "bash" ? "$" : ">";
|
|
396
|
+
const command = (str("command") ?? "...").split("\n");
|
|
397
|
+
const timeout = num("timeout");
|
|
398
|
+
lines = [
|
|
399
|
+
`${title(`${prompt} ${command[0]}`)}${timeout ? detail(` (timeout ${timeout}s)`) : ""}`,
|
|
400
|
+
...command.slice(1),
|
|
401
|
+
];
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case "read": {
|
|
405
|
+
const offset = num("offset");
|
|
406
|
+
const limit = num("limit");
|
|
407
|
+
const range =
|
|
408
|
+
offset == null && limit == null
|
|
409
|
+
? ""
|
|
410
|
+
: this.theme.fg(
|
|
411
|
+
"warning",
|
|
412
|
+
`:${offset ?? 1}${limit != null ? `-${(offset ?? 1) + limit - 1}` : ""}`,
|
|
413
|
+
);
|
|
414
|
+
lines = [`${title("read")} ${path ?? "?"}${range}`];
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case "edit":
|
|
418
|
+
lines = [`${title("edit")} ${path ?? "?"}`];
|
|
419
|
+
break;
|
|
420
|
+
case "write": {
|
|
421
|
+
lines = [`${title("write")} ${path ?? "?"}`];
|
|
422
|
+
const content = str("content");
|
|
423
|
+
if (content) {
|
|
424
|
+
const contentLines = content.split("\n");
|
|
425
|
+
while (contentLines.length && contentLines.at(-1) === "") contentLines.pop();
|
|
426
|
+
lines.push(...contentLines);
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case "grep": {
|
|
431
|
+
const pattern = str("pattern");
|
|
432
|
+
const glob = str("glob");
|
|
433
|
+
const limit = num("limit");
|
|
434
|
+
lines = [
|
|
435
|
+
`${title("grep")} ${this.theme.fg("accent", `/${pattern ?? ""}/`)}` +
|
|
436
|
+
`${path ? detail(` in ${path}`) : ""}${glob ? detail(` (${glob})`) : ""}` +
|
|
437
|
+
`${limit != null ? detail(` limit ${limit}`) : ""}`,
|
|
438
|
+
];
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
case "find": {
|
|
442
|
+
const pattern = str("pattern");
|
|
443
|
+
const limit = num("limit");
|
|
444
|
+
lines = [
|
|
445
|
+
`${title("find")} ${this.theme.fg("accent", pattern ?? "")}` +
|
|
446
|
+
`${path ? detail(` in ${path}`) : ""}${limit != null ? detail(` (limit ${limit})`) : ""}`,
|
|
447
|
+
];
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
case "ls": {
|
|
451
|
+
const limit = num("limit");
|
|
452
|
+
lines = [`${title("ls")} ${path ?? "."}${limit != null ? detail(` (limit ${limit})`) : ""}`];
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
default:
|
|
456
|
+
lines = [title(dialogText(name)), ...json(args).split("\n")];
|
|
457
|
+
}
|
|
458
|
+
if (this.detail === "compact")
|
|
459
|
+
return [truncateToWidth(lines[0] ?? "", Math.max(1, width), "…", true)];
|
|
460
|
+
const wrapped = lines.flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width)));
|
|
461
|
+
if (this.detail === "preview" && wrapped.length > 3)
|
|
462
|
+
return [...wrapped.slice(0, 3), this.theme.fg("dim", "...")];
|
|
463
|
+
const bounded: string[] = [];
|
|
464
|
+
let chars = 0;
|
|
465
|
+
for (const line of wrapped) {
|
|
466
|
+
if (chars > 32768) return [...bounded, this.theme.fg("dim", "...")];
|
|
467
|
+
bounded.push(line);
|
|
468
|
+
chars += line.length;
|
|
469
|
+
}
|
|
470
|
+
return bounded;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
render(width: number): string[] {
|
|
474
|
+
const height = Math.max(1, this.host.terminal?.rows ?? 24);
|
|
475
|
+
const columns = Math.max(1, width);
|
|
476
|
+
const snapshot = this.snapshot;
|
|
477
|
+
const thread = snapshot?.thread;
|
|
478
|
+
const header = this.theme.fg(
|
|
479
|
+
"accent",
|
|
480
|
+
dialogText(
|
|
481
|
+
`${this.path} · ${agentTypeLabel(thread?.type ?? "agent", thread ? agentProgressIcon(thread, this.nerdFontIcons) : undefined, this.nerdFontIcons)} · ${thread?.state ?? "attaching"} — Watching — main continues`,
|
|
482
|
+
),
|
|
483
|
+
);
|
|
484
|
+
const body: string[] = [];
|
|
485
|
+
if (this.error)
|
|
486
|
+
body.push(
|
|
487
|
+
this.theme.fg("error", dialogText(`Attachment error: ${this.error}`)),
|
|
488
|
+
"Ctrl+R Retry · Esc Back · Ctrl+Q Back to main",
|
|
489
|
+
);
|
|
490
|
+
if (!snapshot) {
|
|
491
|
+
if (!this.error) body.push("Attaching to retained live session…");
|
|
492
|
+
} else {
|
|
493
|
+
if (snapshot.inheritedCount)
|
|
494
|
+
body.push(
|
|
495
|
+
this.theme.fg(
|
|
496
|
+
"dim",
|
|
497
|
+
`${snapshot.inheritedCount} inherited messages ${this.viewport.showInherited ? "shown" : "collapsed"} · c toggle`,
|
|
498
|
+
),
|
|
499
|
+
);
|
|
500
|
+
const start = this.viewport.showInherited ? 0 : snapshot.inheritedCount;
|
|
501
|
+
for (const message of snapshot.messages.slice(start))
|
|
502
|
+
body.push(...this.message(message, columns));
|
|
503
|
+
if (snapshot.assistant)
|
|
504
|
+
body.push(
|
|
505
|
+
this.theme.fg("warning", "Assistant streaming…"),
|
|
506
|
+
...this.message(snapshot.assistant, columns),
|
|
507
|
+
);
|
|
508
|
+
const committedResults = new Set(
|
|
509
|
+
snapshot.messages
|
|
510
|
+
.filter((message) => message.role === "toolResult")
|
|
511
|
+
.map((message) => (message.role === "toolResult" ? message.toolCallId : "")),
|
|
512
|
+
);
|
|
513
|
+
for (const tool of snapshot.tools) {
|
|
514
|
+
if (committedResults.has(tool.toolCallId)) continue;
|
|
515
|
+
const call = this.toolCall(tool.toolName, tool.args, columns);
|
|
516
|
+
call[0] = `${call[0]} ${this.theme.fg(
|
|
517
|
+
tool.isError ? "error" : "warning",
|
|
518
|
+
dialogText(
|
|
519
|
+
`— ${tool.state}${tool.isError ? " (error)" : ""}${tool.parentToolCallId ? " (nested)" : ""}`,
|
|
520
|
+
),
|
|
521
|
+
)}`;
|
|
522
|
+
body.push(...call);
|
|
523
|
+
if (tool.result !== undefined && this.detail !== "compact")
|
|
524
|
+
body.push(...this.preview(object(tool.result)?.content ?? tool.result, columns));
|
|
525
|
+
body.push("");
|
|
526
|
+
}
|
|
527
|
+
if (!body.length)
|
|
528
|
+
body.push(
|
|
529
|
+
thread?.state === "starting"
|
|
530
|
+
? "Starting — waiting for transcript attachment…"
|
|
531
|
+
: "No retained messages yet.",
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
this.content = body;
|
|
535
|
+
const bodyHeight = Math.max(0, height - 4);
|
|
536
|
+
this.scroll.updateLayout(body.length, bodyHeight, () => this.scheduleRender());
|
|
537
|
+
if (this.restoreViewport) {
|
|
538
|
+
if (this.viewport.follow) this.scroll.scrollToEnd();
|
|
539
|
+
else this.scroll.scrollTo(this.viewport.scrollTop, { disableFollow: true });
|
|
540
|
+
this.restoreViewport = false;
|
|
541
|
+
}
|
|
542
|
+
const visible = this.scroll
|
|
543
|
+
.render(columns)
|
|
544
|
+
.slice(this.scroll.scrollTop, this.scroll.scrollTop + bodyHeight);
|
|
545
|
+
while (visible.length < bodyHeight) visible.push("");
|
|
546
|
+
this.saveViewport();
|
|
547
|
+
const footer = this.error
|
|
548
|
+
? "Ctrl+R Retry · Tab input/transcript · Esc tree · Ctrl+Q main"
|
|
549
|
+
: `${this.editing ? "Enter steer · Tab browse" : `Tab steer · Home/End scroll · c context · t view:${this.detail}`} · Esc tree · Ctrl+Q main · ↑↓/PgUp/PgDn scroll · ${this.viewport.follow ? "following" : "scrolled"}`;
|
|
550
|
+
const input = this.input.render(columns)[0]!;
|
|
551
|
+
const status = this.theme.fg(
|
|
552
|
+
this.steerStatus ? (this.steerFailed ? "error" : "muted") : this.error ? "error" : "muted",
|
|
553
|
+
dialogText(
|
|
554
|
+
this.steerStatus ??
|
|
555
|
+
(this.error
|
|
556
|
+
? `Attachment error: ${this.error}`
|
|
557
|
+
: thread
|
|
558
|
+
? `${thread.task} · ${thread.status}`
|
|
559
|
+
: "Live transcript · Enter to steer"),
|
|
560
|
+
),
|
|
561
|
+
);
|
|
562
|
+
const lines =
|
|
563
|
+
height === 1
|
|
564
|
+
? [input]
|
|
565
|
+
: height === 2
|
|
566
|
+
? [header, input]
|
|
567
|
+
: height === 3
|
|
568
|
+
? [header, this.theme.fg("dim", footer), input]
|
|
569
|
+
: [header, status, ...visible, this.theme.fg("dim", footer), input];
|
|
570
|
+
return fillViewport(lines, width, height);
|
|
571
|
+
}
|
|
572
|
+
}
|