@tt-a1i/openpi 0.3.1 → 0.5.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 +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Takeover UI for subagents (ported from v1, rendering from the synchronous
|
|
3
3
|
* SubagentReadModel instead of live pi sessions):
|
|
4
|
-
* - SubagentDashboard:
|
|
5
|
-
* - TakeoverView: full
|
|
6
|
-
*
|
|
4
|
+
* - SubagentDashboard: compact picker docked above the input, listing all subagents.
|
|
5
|
+
* - TakeoverView: full read-only view of one subagent; steering remains owned
|
|
6
|
+
* by the parent model through the subagent tools.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type {
|
|
@@ -12,17 +12,15 @@ import type {
|
|
|
12
12
|
Theme,
|
|
13
13
|
} from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import type { Component, Focusable, TUI } from "@earendil-works/pi-tui";
|
|
15
|
-
import {
|
|
15
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
16
|
+
import { AgentSessionPage } from "../../../shared/agent-session-page.ts";
|
|
17
|
+
import { hintLine, panelFrame } from "../../../shared/screen-chrome.ts";
|
|
16
18
|
import { sanitizeTerminalText } from "../../../shared/terminal-text.ts";
|
|
17
19
|
import { formatElapsed, type SubagentSnapshot } from "../domain.ts";
|
|
18
|
-
import { formatContextUtilization } from "
|
|
20
|
+
import { formatContextUtilization } from "../../../shared/context-utilization.ts";
|
|
21
|
+
import { SPINNER_INTERVAL_MS, spinnerFrame } from "../../../shared/spinner.ts";
|
|
19
22
|
import type { SubagentReadModel } from "../manager.ts";
|
|
20
|
-
import {
|
|
21
|
-
SPINNER_INTERVAL_MS,
|
|
22
|
-
TranscriptRenderer,
|
|
23
|
-
buildTranscriptLines,
|
|
24
|
-
spinnerFrame,
|
|
25
|
-
} from "./transcript.ts";
|
|
23
|
+
import { subagentTranscriptDocument } from "./transcript.ts";
|
|
26
24
|
|
|
27
25
|
export function sanitizeSubagentDisplayLine(value: string) {
|
|
28
26
|
return sanitizeTerminalText(value).replace(/\s+/g, " ").trim();
|
|
@@ -38,58 +36,34 @@ function configuredKeys(
|
|
|
38
36
|
/**
|
|
39
37
|
* One spinner definition for the whole subagent UI: the dashboard glyph, the
|
|
40
38
|
* takeover header, and the transcript's live tools must animate in step, so the
|
|
41
|
-
* frames and their cadence live in `
|
|
39
|
+
* frames and their cadence live in `shared/spinner.ts` and are imported here.
|
|
42
40
|
*/
|
|
43
41
|
function statusGlyph(
|
|
44
42
|
snap: SubagentSnapshot,
|
|
45
43
|
theme: Theme,
|
|
46
44
|
now = Date.now(),
|
|
45
|
+
selected = false,
|
|
47
46
|
): string {
|
|
47
|
+
// A selected row keeps its state glyph and borrows the accent tone, so the
|
|
48
|
+
// list never hides what is still running behind a selection marker.
|
|
49
|
+
const tone = (color: "warning" | "success" | "error") =>
|
|
50
|
+
selected ? ("accent" as const) : color;
|
|
48
51
|
switch (snap.status) {
|
|
49
52
|
case "running":
|
|
50
|
-
return theme.fg("warning", spinnerFrame(now));
|
|
53
|
+
return theme.fg(tone("warning"), spinnerFrame(now));
|
|
51
54
|
case "done":
|
|
52
|
-
return theme.fg("success", "✓");
|
|
55
|
+
return theme.fg(tone("success"), "✓");
|
|
53
56
|
case "error":
|
|
54
|
-
return theme.fg("error", "✗");
|
|
57
|
+
return theme.fg(tone("error"), "✗");
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
/**
|
|
59
|
-
* Tool names come from the child's own tool-call events, so they are as
|
|
60
|
-
* untrusted as their arguments and must be sanitized before reaching the
|
|
61
|
-
* terminal: `truncateToWidth` only ignores escape sequences while measuring.
|
|
62
|
-
*/
|
|
63
|
-
function runningActivity(snap: SubagentSnapshot) {
|
|
64
|
-
if (snap.status !== "running") return "";
|
|
65
|
-
const liveTool = snap.liveTools.at(-1);
|
|
66
|
-
if (liveTool) {
|
|
67
|
-
const name = sanitizeSubagentDisplayLine(liveTool.name);
|
|
68
|
-
const args = truncateToWidth(
|
|
69
|
-
sanitizeSubagentDisplayLine(liveTool.argsPreview ?? ""),
|
|
70
|
-
48,
|
|
71
|
-
);
|
|
72
|
-
return args ? `${name} · ${args}` : name;
|
|
73
|
-
}
|
|
74
|
-
for (const item of [...snap.transcript].reverse()) {
|
|
75
|
-
if (item.kind === "toolResult") {
|
|
76
|
-
return sanitizeSubagentDisplayLine(item.name);
|
|
77
|
-
}
|
|
78
|
-
if (item.kind !== "assistant") continue;
|
|
79
|
-
const tool = [...item.parts]
|
|
80
|
-
.reverse()
|
|
81
|
-
.find((part) => part.type === "toolCall");
|
|
82
|
-
if (tool?.type === "toolCall") {
|
|
83
|
-
return sanitizeSubagentDisplayLine(tool.name);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return "";
|
|
87
|
-
}
|
|
88
|
-
|
|
89
61
|
// --- Entry points --------------------------------------------------------------
|
|
90
62
|
|
|
91
63
|
export interface TakeoverOptions {
|
|
92
64
|
readonly badge?: string;
|
|
65
|
+
/** Captured from the parent UI when this child page opens. */
|
|
66
|
+
readonly toolsExpanded?: boolean;
|
|
93
67
|
}
|
|
94
68
|
|
|
95
69
|
export async function openSubagentTakeover(
|
|
@@ -99,12 +73,24 @@ export async function openSubagentTakeover(
|
|
|
99
73
|
options?: TakeoverOptions,
|
|
100
74
|
) {
|
|
101
75
|
if (!view.get(id)) return;
|
|
76
|
+
const takeoverOptions: TakeoverOptions = {
|
|
77
|
+
...options,
|
|
78
|
+
toolsExpanded: ctx.ui.getToolsExpanded(),
|
|
79
|
+
};
|
|
102
80
|
await ctx.ui.custom<null>(
|
|
103
81
|
(tui, theme, keybindings, done) =>
|
|
104
|
-
new TakeoverView(
|
|
82
|
+
new TakeoverView(
|
|
83
|
+
tui,
|
|
84
|
+
theme,
|
|
85
|
+
keybindings,
|
|
86
|
+
id,
|
|
87
|
+
view,
|
|
88
|
+
done,
|
|
89
|
+
takeoverOptions,
|
|
90
|
+
),
|
|
105
91
|
{
|
|
106
92
|
overlay: true,
|
|
107
|
-
overlayOptions: { anchor: "
|
|
93
|
+
overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" },
|
|
108
94
|
},
|
|
109
95
|
);
|
|
110
96
|
}
|
|
@@ -127,7 +113,14 @@ export async function openSubagentPicker(
|
|
|
127
113
|
new SubagentDashboard(tui, theme, keybindings, view, selection, done),
|
|
128
114
|
{
|
|
129
115
|
overlay: true,
|
|
130
|
-
|
|
116
|
+
// Dock the picker just above the editor (editor + strip + footer ≈ 5
|
|
117
|
+
// rows) like a command palette, instead of covering the conversation.
|
|
118
|
+
overlayOptions: {
|
|
119
|
+
anchor: "bottom-center",
|
|
120
|
+
width: "100%",
|
|
121
|
+
maxHeight: "60%",
|
|
122
|
+
margin: { bottom: 5 },
|
|
123
|
+
},
|
|
131
124
|
},
|
|
132
125
|
);
|
|
133
126
|
|
|
@@ -139,7 +132,10 @@ export async function openSubagentPicker(
|
|
|
139
132
|
}
|
|
140
133
|
}
|
|
141
134
|
|
|
142
|
-
// --- Dashboard (
|
|
135
|
+
// --- Dashboard (picker docked above the input) ---------------------------------
|
|
136
|
+
|
|
137
|
+
/** A picker is a glance, not a workspace: cap the list window and scroll. */
|
|
138
|
+
const MAX_PICKER_ROWS = 10;
|
|
143
139
|
|
|
144
140
|
export interface DashboardSelection {
|
|
145
141
|
id?: string;
|
|
@@ -258,24 +254,6 @@ export class SubagentDashboard implements Component {
|
|
|
258
254
|
}
|
|
259
255
|
}
|
|
260
256
|
|
|
261
|
-
private pad(text: string, width: number): string {
|
|
262
|
-
const truncated = truncateToWidth(text, width);
|
|
263
|
-
return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private borderSegment(width: number, title: string): string {
|
|
267
|
-
const theme = this.theme;
|
|
268
|
-
const label = title
|
|
269
|
-
? ` ${truncateToWidth(title, Math.max(0, width - 3))} `
|
|
270
|
-
: "";
|
|
271
|
-
const labelWidth = visibleWidth(label);
|
|
272
|
-
return (
|
|
273
|
-
theme.fg("border", "─") +
|
|
274
|
-
(label ? theme.fg("text", label) : "") +
|
|
275
|
-
theme.fg("border", "─".repeat(Math.max(0, width - 1 - labelWidth)))
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
257
|
render(width: number): string[] {
|
|
280
258
|
const theme = this.theme;
|
|
281
259
|
const subs = this.subs();
|
|
@@ -283,13 +261,14 @@ export class SubagentDashboard implements Component {
|
|
|
283
261
|
|
|
284
262
|
// One timestamp per frame so every row's spinner shows the same frame.
|
|
285
263
|
const now = Date.now();
|
|
264
|
+
// Docked above the editor, the panel borrows conversation space: cap the
|
|
265
|
+
// list window and scroll instead of growing toward the top of the screen.
|
|
286
266
|
const rows = this.tui.terminal.rows || 30;
|
|
287
|
-
const maxBodyHeight = Math.max(1, rows - 5);
|
|
267
|
+
const maxBodyHeight = Math.min(Math.max(1, rows - 5), MAX_PICKER_ROWS);
|
|
288
268
|
const bodyHeight =
|
|
289
269
|
subs.length > maxBodyHeight ? maxBodyHeight : Math.max(1, subs.length);
|
|
290
270
|
const innerWidth = Math.max(0, width - 2);
|
|
291
271
|
|
|
292
|
-
const lines: string[] = [];
|
|
293
272
|
const running = subs.filter((snap) => snap.status === "running").length;
|
|
294
273
|
const done = subs.filter((snap) => snap.status === "done").length;
|
|
295
274
|
const failed = subs.filter((snap) => snap.status === "error").length;
|
|
@@ -301,37 +280,32 @@ export class SubagentDashboard implements Component {
|
|
|
301
280
|
]
|
|
302
281
|
.filter(Boolean)
|
|
303
282
|
.join(" · ") || "no agents";
|
|
304
|
-
lines.push(
|
|
305
|
-
theme.fg("border", "╭") +
|
|
306
|
-
this.borderSegment(innerWidth, `Subagents · ${summary}`) +
|
|
307
|
-
theme.fg("border", "╮"),
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
const divider = theme.fg("border", "│");
|
|
311
283
|
const rowLines = this.renderRows(subs, innerWidth, bodyHeight, now);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
284
|
+
const keys = (binding: Parameters<KeybindingsManager["getKeys"]>[0]) =>
|
|
285
|
+
configuredKeys(this.keybindings, binding);
|
|
286
|
+
// Shared chrome, so this panel and its hints read the same as /ps and
|
|
287
|
+
// /workflows. The frame is padded to the rows it was given, which keeps
|
|
288
|
+
// this view's content-fit height rather than reintroducing a fixed one.
|
|
289
|
+
return [
|
|
290
|
+
// One empty row of air between the conversation and the docked panel.
|
|
291
|
+
"",
|
|
292
|
+
...panelFrame(theme, {
|
|
293
|
+
label: `Subagents · ${summary}`,
|
|
294
|
+
rows: rowLines,
|
|
295
|
+
width,
|
|
296
|
+
height: rowLines.length + 2,
|
|
297
|
+
}),
|
|
298
|
+
hintLine(
|
|
299
|
+
theme,
|
|
300
|
+
[
|
|
301
|
+
[`${keys("tui.select.up")}/${keys("tui.select.down")}/jk`, "select"],
|
|
302
|
+
[keys("tui.select.confirm"), "take over"],
|
|
303
|
+
["x", "abort"],
|
|
304
|
+
[keys("tui.select.cancel"), "close"],
|
|
305
|
+
],
|
|
330
306
|
width,
|
|
331
307
|
),
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return lines;
|
|
308
|
+
];
|
|
335
309
|
}
|
|
336
310
|
|
|
337
311
|
private renderRows(
|
|
@@ -362,13 +336,16 @@ export class SubagentDashboard implements Component {
|
|
|
362
336
|
const index = start + i;
|
|
363
337
|
const isSelected = index === this.selection.index;
|
|
364
338
|
|
|
365
|
-
|
|
339
|
+
// One glyph column: a selected row tints its status glyph with the
|
|
340
|
+
// accent tone instead of stacking a second marker. The live tool name
|
|
341
|
+
// is deliberately absent: it flickers with every tool call and says
|
|
342
|
+
// nothing the user acts on.
|
|
343
|
+
const glyph = statusGlyph(snap, theme, now, isSelected);
|
|
366
344
|
const safeTitle = sanitizeSubagentDisplayLine(snap.title) || snap.id;
|
|
367
345
|
const title = isSelected
|
|
368
346
|
? theme.fg("accent", safeTitle)
|
|
369
347
|
: theme.fg("text", safeTitle);
|
|
370
|
-
const
|
|
371
|
-
const prefix = ` ${marker} ${statusGlyph(snap, theme, now)} `;
|
|
348
|
+
const prefix = ` ${glyph} `;
|
|
372
349
|
|
|
373
350
|
const utilization = formatContextUtilization(snap.usage);
|
|
374
351
|
const metadata = [
|
|
@@ -392,20 +369,9 @@ export class SubagentDashboard implements Component {
|
|
|
392
369
|
const right = metadata.join(dot);
|
|
393
370
|
const rightWidth = visibleWidth(right);
|
|
394
371
|
const leftMax = Math.max(0, width - rightWidth - (right ? 2 : 0));
|
|
395
|
-
const activityLabel = activity ? theme.fg("muted", ` · ${activity}`) : "";
|
|
396
372
|
const left =
|
|
397
373
|
prefix +
|
|
398
|
-
truncateToWidth(
|
|
399
|
-
title,
|
|
400
|
-
Math.max(
|
|
401
|
-
0,
|
|
402
|
-
leftMax - visibleWidth(prefix) - visibleWidth(activityLabel),
|
|
403
|
-
),
|
|
404
|
-
) +
|
|
405
|
-
truncateToWidth(
|
|
406
|
-
activityLabel,
|
|
407
|
-
Math.max(0, leftMax - visibleWidth(prefix)),
|
|
408
|
-
);
|
|
374
|
+
truncateToWidth(title, Math.max(0, leftMax - visibleWidth(prefix)));
|
|
409
375
|
const gap = right
|
|
410
376
|
? Math.max(1, width - visibleWidth(left) - rightWidth)
|
|
411
377
|
: 0;
|
|
@@ -428,33 +394,18 @@ export class SubagentDashboard implements Component {
|
|
|
428
394
|
|
|
429
395
|
// --- Takeover view ------------------------------------------------------------
|
|
430
396
|
|
|
431
|
-
const TRANSCRIPT_SCROLL_STEP = 6;
|
|
432
|
-
|
|
433
397
|
export class TakeoverView implements Component, Focusable {
|
|
434
|
-
private
|
|
435
|
-
private theme: Theme;
|
|
436
|
-
private keybindings: KeybindingsManager;
|
|
437
|
-
private id: string;
|
|
438
|
-
private view: SubagentReadModel;
|
|
439
|
-
private done: (value: null) => void;
|
|
440
|
-
private options?: TakeoverOptions;
|
|
441
|
-
|
|
442
|
-
private input = new Input();
|
|
443
|
-
private transcriptRenderer = new TranscriptRenderer();
|
|
444
|
-
/** Scroll offset in lines from the bottom of the transcript. 0 = pinned to bottom. */
|
|
445
|
-
private scrollOffset = 0;
|
|
398
|
+
private page: AgentSessionPage;
|
|
446
399
|
private unsubscribe: () => void;
|
|
447
400
|
private renderTimer?: ReturnType<typeof setTimeout>;
|
|
448
401
|
private ticker?: ReturnType<typeof setInterval>;
|
|
449
402
|
private closed = false;
|
|
450
403
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
return this._focused;
|
|
404
|
+
get focused() {
|
|
405
|
+
return this.page.focused;
|
|
454
406
|
}
|
|
455
407
|
set focused(value: boolean) {
|
|
456
|
-
this.
|
|
457
|
-
this.input.focused = value;
|
|
408
|
+
this.page.focused = value;
|
|
458
409
|
}
|
|
459
410
|
|
|
460
411
|
constructor(
|
|
@@ -466,46 +417,58 @@ export class TakeoverView implements Component, Focusable {
|
|
|
466
417
|
done: (value: null) => void,
|
|
467
418
|
options?: TakeoverOptions,
|
|
468
419
|
) {
|
|
469
|
-
this.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
420
|
+
this.page = new AgentSessionPage(
|
|
421
|
+
tui,
|
|
422
|
+
theme,
|
|
423
|
+
keybindings,
|
|
424
|
+
{
|
|
425
|
+
getState: () => {
|
|
426
|
+
const snap = view.get(id);
|
|
427
|
+
if (!snap) return undefined;
|
|
428
|
+
return {
|
|
429
|
+
id: snap.id,
|
|
430
|
+
title: snap.title,
|
|
431
|
+
status: snap.status,
|
|
432
|
+
document: subagentTranscriptDocument(
|
|
433
|
+
snap,
|
|
434
|
+
view.getToolRenderer?.(id),
|
|
435
|
+
),
|
|
436
|
+
metadata: [
|
|
437
|
+
options?.badge,
|
|
438
|
+
snap.meta.modelLabel,
|
|
439
|
+
formatContextUtilization(snap.usage),
|
|
440
|
+
formatElapsed(snap),
|
|
441
|
+
],
|
|
442
|
+
errorText: snap.errorText,
|
|
443
|
+
};
|
|
444
|
+
},
|
|
445
|
+
close: () => this.close(done),
|
|
446
|
+
abort: () => view.requestAbort(id),
|
|
447
|
+
},
|
|
448
|
+
{ toolsExpanded: options?.toolsExpanded },
|
|
449
|
+
);
|
|
476
450
|
this.unsubscribe = view.subscribeTo(id, () => {
|
|
477
|
-
this.refreshTicker();
|
|
478
|
-
this.scheduleRender();
|
|
451
|
+
this.refreshTicker(view.get(id), tui);
|
|
452
|
+
this.scheduleRender(tui);
|
|
479
453
|
});
|
|
480
|
-
this.refreshTicker();
|
|
481
|
-
this.input.onSubmit = (value: string) => {
|
|
482
|
-
const text = value.trim();
|
|
483
|
-
if (!text) return;
|
|
484
|
-
this.input.setValue("");
|
|
485
|
-
this.view.requestSend(this.id, text);
|
|
486
|
-
this.scrollOffset = 0;
|
|
487
|
-
this.tui.requestRender();
|
|
488
|
-
};
|
|
454
|
+
this.refreshTicker(view.get(id), tui);
|
|
489
455
|
}
|
|
490
456
|
|
|
491
|
-
private snap
|
|
492
|
-
return this.view.get(this.id);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
private refreshTicker() {
|
|
496
|
-
const interval =
|
|
497
|
-
this.snap()?.status === "running" ? SPINNER_INTERVAL_MS : 1000;
|
|
457
|
+
private refreshTicker(snap: SubagentSnapshot | undefined, tui: TUI) {
|
|
498
458
|
if (this.ticker) clearInterval(this.ticker);
|
|
499
|
-
this.ticker =
|
|
459
|
+
this.ticker = undefined;
|
|
460
|
+
if (snap?.status === "running") {
|
|
461
|
+
this.ticker = setInterval(() => tui.requestRender(), SPINNER_INTERVAL_MS);
|
|
462
|
+
}
|
|
500
463
|
}
|
|
501
464
|
|
|
502
|
-
private scheduleRender() {
|
|
465
|
+
private scheduleRender(tui: TUI) {
|
|
503
466
|
if (this.renderTimer) return;
|
|
504
467
|
// Streaming can emit an event per token. Limit terminal repaints so this
|
|
505
468
|
// view cannot starve input handling or make the child look frozen.
|
|
506
469
|
this.renderTimer = setTimeout(() => {
|
|
507
470
|
this.renderTimer = undefined;
|
|
508
|
-
if (!this.closed)
|
|
471
|
+
if (!this.closed) tui.requestRender();
|
|
509
472
|
}, 50);
|
|
510
473
|
}
|
|
511
474
|
|
|
@@ -519,8 +482,8 @@ export class TakeoverView implements Component, Focusable {
|
|
|
519
482
|
return true;
|
|
520
483
|
}
|
|
521
484
|
|
|
522
|
-
private close() {
|
|
523
|
-
if (this.cleanup())
|
|
485
|
+
private close(done: (value: null) => void) {
|
|
486
|
+
if (this.cleanup()) done(null);
|
|
524
487
|
}
|
|
525
488
|
|
|
526
489
|
dispose(): void {
|
|
@@ -528,186 +491,14 @@ export class TakeoverView implements Component, Focusable {
|
|
|
528
491
|
}
|
|
529
492
|
|
|
530
493
|
handleInput(data: string): void {
|
|
531
|
-
|
|
532
|
-
const snap = this.snap();
|
|
533
|
-
if (snap?.status === "running") this.view.requestAbort(this.id);
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
if (
|
|
537
|
-
this.keybindings.matches(data, "app.interrupt") ||
|
|
538
|
-
this.keybindings.matches(data, "tui.select.cancel")
|
|
539
|
-
) {
|
|
540
|
-
this.close();
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
if (this.keybindings.matches(data, "tui.editor.cursorUp")) {
|
|
544
|
-
this.scrollOffset += TRANSCRIPT_SCROLL_STEP;
|
|
545
|
-
this.tui.requestRender();
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
if (this.keybindings.matches(data, "tui.editor.cursorDown")) {
|
|
549
|
-
this.scrollOffset = Math.max(
|
|
550
|
-
0,
|
|
551
|
-
this.scrollOffset - TRANSCRIPT_SCROLL_STEP,
|
|
552
|
-
);
|
|
553
|
-
this.tui.requestRender();
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
if (this.keybindings.matches(data, "tui.editor.pageUp")) {
|
|
557
|
-
this.scrollOffset += this.viewportHeight();
|
|
558
|
-
this.tui.requestRender();
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
if (this.keybindings.matches(data, "tui.editor.pageDown")) {
|
|
562
|
-
this.scrollOffset = Math.max(
|
|
563
|
-
0,
|
|
564
|
-
this.scrollOffset - this.viewportHeight(),
|
|
565
|
-
);
|
|
566
|
-
this.tui.requestRender();
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
this.input.handleInput(data);
|
|
570
|
-
this.tui.requestRender();
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
private viewportHeight(): number {
|
|
574
|
-
const rows = this.tui.terminal.rows || 30;
|
|
575
|
-
// Top rule, transcript rule, input, key hints, and bottom rule are five
|
|
576
|
-
// chrome rows. The overlay leaves Pi's final footer row visible.
|
|
577
|
-
return Math.max(1, rows - 6);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
private rule(width: number, left = "", right = "") {
|
|
581
|
-
const fill = "─";
|
|
582
|
-
const available = Math.max(1, width);
|
|
583
|
-
const leftWidth = visibleWidth(left);
|
|
584
|
-
const rightWidth = visibleWidth(right);
|
|
585
|
-
if (!right || leftWidth + rightWidth + 2 > available) {
|
|
586
|
-
return truncateToWidth(
|
|
587
|
-
left + fill.repeat(Math.max(0, available - leftWidth)),
|
|
588
|
-
available,
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
return (
|
|
592
|
-
left +
|
|
593
|
-
fill.repeat(Math.max(1, available - leftWidth - rightWidth)) +
|
|
594
|
-
right
|
|
595
|
-
);
|
|
494
|
+
this.page.handleInput(data);
|
|
596
495
|
}
|
|
597
496
|
|
|
598
497
|
render(width: number): string[] {
|
|
599
|
-
|
|
600
|
-
const now = Date.now();
|
|
601
|
-
const lines: string[] = [];
|
|
602
|
-
const snap = this.snap();
|
|
603
|
-
|
|
604
|
-
if (!snap) {
|
|
605
|
-
const border = theme.fg("borderAccent", "─".repeat(Math.max(1, width)));
|
|
606
|
-
lines.push(
|
|
607
|
-
border,
|
|
608
|
-
theme.fg("dim", `${this.id} is no longer tracked`),
|
|
609
|
-
border,
|
|
610
|
-
);
|
|
611
|
-
return lines;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const title = sanitizeSubagentDisplayLine(snap.title) || snap.id;
|
|
615
|
-
const headerLeft =
|
|
616
|
-
theme.fg("borderAccent", "─ ") +
|
|
617
|
-
statusGlyph(snap, theme, now) +
|
|
618
|
-
" " +
|
|
619
|
-
theme.fg("accent", theme.bold(title)) +
|
|
620
|
-
theme.fg("borderAccent", " ");
|
|
621
|
-
const utilization = formatContextUtilization(snap.usage);
|
|
622
|
-
const metadata = [
|
|
623
|
-
...(this.options?.badge
|
|
624
|
-
? [theme.fg("muted", sanitizeSubagentDisplayLine(this.options.badge))]
|
|
625
|
-
: []),
|
|
626
|
-
theme.fg(
|
|
627
|
-
"muted",
|
|
628
|
-
sanitizeSubagentDisplayLine(snap.meta.modelLabel ?? "?") || "?",
|
|
629
|
-
),
|
|
630
|
-
...(utilization ? [theme.fg("muted", utilization)] : []),
|
|
631
|
-
theme.fg("muted", formatElapsed(snap)),
|
|
632
|
-
];
|
|
633
|
-
const dot = theme.fg("dim", " · ");
|
|
634
|
-
while (
|
|
635
|
-
metadata.length > 1 &&
|
|
636
|
-
visibleWidth(headerLeft) + visibleWidth(metadata.join(dot)) + 2 > width
|
|
637
|
-
) {
|
|
638
|
-
metadata.shift();
|
|
639
|
-
}
|
|
640
|
-
lines.push(
|
|
641
|
-
this.rule(
|
|
642
|
-
width,
|
|
643
|
-
truncateToWidth(
|
|
644
|
-
headerLeft,
|
|
645
|
-
Math.max(1, width - visibleWidth(metadata.join(dot)) - 2),
|
|
646
|
-
),
|
|
647
|
-
metadata.join(dot),
|
|
648
|
-
),
|
|
649
|
-
);
|
|
650
|
-
|
|
651
|
-
// Fixed-height transcript viewport. Errors consume a row, but scroll state
|
|
652
|
-
// is represented by the following rule so its height never changes.
|
|
653
|
-
// `now` is shared with the header glyph so both spinners show one frame.
|
|
654
|
-
const transcript = buildTranscriptLines(
|
|
655
|
-
snap,
|
|
656
|
-
width,
|
|
657
|
-
theme,
|
|
658
|
-
this.transcriptRenderer,
|
|
659
|
-
{ now },
|
|
660
|
-
);
|
|
661
|
-
const viewport = this.viewportHeight();
|
|
662
|
-
const errorRows = snap.errorText ? 1 : 0;
|
|
663
|
-
const transcriptCapacity = Math.max(1, viewport - errorRows);
|
|
664
|
-
const maxOffset = Math.max(0, transcript.length - transcriptCapacity);
|
|
665
|
-
if (this.scrollOffset > maxOffset) this.scrollOffset = maxOffset;
|
|
666
|
-
|
|
667
|
-
const body: string[] = [];
|
|
668
|
-
if (snap.errorText) {
|
|
669
|
-
body.push(
|
|
670
|
-
truncateToWidth(
|
|
671
|
-
theme.fg(
|
|
672
|
-
"error",
|
|
673
|
-
`error: ${sanitizeSubagentDisplayLine(snap.errorText)}`,
|
|
674
|
-
),
|
|
675
|
-
width,
|
|
676
|
-
),
|
|
677
|
-
);
|
|
678
|
-
}
|
|
679
|
-
const end = transcript.length - this.scrollOffset;
|
|
680
|
-
const visible = transcript.slice(
|
|
681
|
-
Math.max(0, end - Math.max(1, viewport - body.length)),
|
|
682
|
-
end,
|
|
683
|
-
);
|
|
684
|
-
if (visible.length === 0) body.push(theme.fg("dim", "waiting for output…"));
|
|
685
|
-
else body.push(...visible);
|
|
686
|
-
while (body.length < viewport) body.push("");
|
|
687
|
-
lines.push(...body.slice(0, viewport));
|
|
688
|
-
|
|
689
|
-
lines.push(
|
|
690
|
-
this.rule(
|
|
691
|
-
width,
|
|
692
|
-
theme.fg("borderAccent", "─"),
|
|
693
|
-
this.scrollOffset > 0 ? theme.fg("dim", `↓ ${this.scrollOffset}`) : "",
|
|
694
|
-
),
|
|
695
|
-
);
|
|
696
|
-
lines.push(...this.input.render(width));
|
|
697
|
-
const hints = `${configuredKeys(this.keybindings, "tui.input.submit")} send · ${configuredKeys(this.keybindings, "app.interrupt")} back · ${configuredKeys(this.keybindings, "app.clear")} abort run · ${configuredKeys(this.keybindings, "tui.editor.cursorUp")}/${configuredKeys(this.keybindings, "tui.editor.cursorDown")} scroll · ${configuredKeys(this.keybindings, "tui.editor.pageUp")}/${configuredKeys(this.keybindings, "tui.editor.pageDown")} page`;
|
|
698
|
-
const compactHints = `${configuredKeys(this.keybindings, "tui.input.submit")} send · ${configuredKeys(this.keybindings, "app.interrupt")} back · ${configuredKeys(this.keybindings, "app.clear")} abort run · ${configuredKeys(this.keybindings, "tui.editor.cursorUp")}/${configuredKeys(this.keybindings, "tui.editor.cursorDown")} scroll`;
|
|
699
|
-
lines.push(
|
|
700
|
-
truncateToWidth(
|
|
701
|
-
theme.fg("dim", visibleWidth(hints) <= width ? hints : compactHints),
|
|
702
|
-
width,
|
|
703
|
-
),
|
|
704
|
-
);
|
|
705
|
-
lines.push(this.rule(width, theme.fg("borderAccent", "─")));
|
|
706
|
-
return lines;
|
|
498
|
+
return this.page.render(width);
|
|
707
499
|
}
|
|
708
500
|
|
|
709
501
|
invalidate(): void {
|
|
710
|
-
this.
|
|
711
|
-
this.transcriptRenderer.invalidate();
|
|
502
|
+
this.page.invalidate();
|
|
712
503
|
}
|
|
713
504
|
}
|