@tt-a1i/openpi 0.4.0 → 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 +102 -40
- package/SETUP.md +22 -6
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- 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 +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- 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/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- 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 +153 -86
- 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 +123 -127
- package/extensions/shared/activity-status.ts +30 -0
- 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/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +437 -21
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +73 -33
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- 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 +270 -59
- package/extensions/subagents/navigation.ts +34 -5
- 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/manager.ts +34 -2
- package/extensions/subagents/src/prompt.ts +32 -4
- package/extensions/subagents/src/result-artifact.ts +4 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- 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/ui-customization/footer.ts +0 -40
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +1 -1
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +167 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +488 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +74 -4
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +66 -6
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +128 -64
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +25 -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 +1 -1
- package/skills/workflows/REFERENCE.md +4 -2
- 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 -303
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
Stream,
|
|
28
28
|
} from "effect";
|
|
29
29
|
import type { SubagentBackend, SubagentSession } from "./backend.ts";
|
|
30
|
+
import type { AgentToolRenderer } from "../../shared/agent-tool-renderer.ts";
|
|
30
31
|
import { BackendRegistry } from "./backend.ts";
|
|
31
32
|
import type {
|
|
32
33
|
BackendName,
|
|
@@ -63,7 +64,7 @@ const ENTRY_CLOSE_TIMEOUT_MS = 10_000;
|
|
|
63
64
|
* First-response watchdog: a run whose provider accepts the request but
|
|
64
65
|
* never emits an assistant event is settled as a failure so it cannot
|
|
65
66
|
* occupy a concurrency slot forever. Matches the workflow runner's
|
|
66
|
-
*
|
|
67
|
+
* MODEL_PROGRESS_TIMEOUT_MS (extensions/workflows/runner.ts).
|
|
67
68
|
*/
|
|
68
69
|
export const FIRST_RESPONSE_TIMEOUT_MS = 45_000;
|
|
69
70
|
const ERROR_TEXT_MAX_LENGTH = 4_096;
|
|
@@ -92,6 +93,9 @@ function appendTranscript(snapshot: MutableSnapshot, item: TranscriptItem) {
|
|
|
92
93
|
snapshot.transcript.length - MAX_TRANSCRIPT_ITEMS,
|
|
93
94
|
);
|
|
94
95
|
}
|
|
96
|
+
// Monotonic version so UI projections can detect transcript changes by
|
|
97
|
+
// O(1) comparison instead of scanning the mutable array.
|
|
98
|
+
snapshot.transcriptVersion++;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
// --- Internal state -----------------------------------------------------------
|
|
@@ -105,12 +109,16 @@ interface MutableSnapshot {
|
|
|
105
109
|
prompt: string;
|
|
106
110
|
cwd: string;
|
|
107
111
|
status: SubagentStatus;
|
|
112
|
+
outcome?: SubagentSnapshot["outcome"];
|
|
113
|
+
worktreeBranch?: string;
|
|
108
114
|
createdAt: number;
|
|
109
115
|
settledAt?: number;
|
|
110
116
|
errorText?: string;
|
|
111
117
|
meta: SubagentMeta;
|
|
112
118
|
usage: { tokens?: number; contextWindow?: number };
|
|
113
119
|
transcript: TranscriptItem[];
|
|
120
|
+
/** Bumped on every transcript mutation; UI caches key on this. */
|
|
121
|
+
transcriptVersion: number;
|
|
114
122
|
liveAssistant?: { text: string; thinking: string };
|
|
115
123
|
liveTools: LiveToolState[];
|
|
116
124
|
queued: SubagentSnapshot["queued"];
|
|
@@ -137,6 +145,8 @@ interface Entry {
|
|
|
137
145
|
export interface SubagentReadModel {
|
|
138
146
|
list(): ReadonlyArray<SubagentSnapshot>;
|
|
139
147
|
get(id: string): SubagentSnapshot | undefined;
|
|
148
|
+
/** Native tool projection retained by the live child session, when present. */
|
|
149
|
+
getToolRenderer?(id: string): AgentToolRenderer | undefined;
|
|
140
150
|
size(): number;
|
|
141
151
|
/** Any-change notification (footer status, dashboard). */
|
|
142
152
|
subscribe(listener: () => void): () => void;
|
|
@@ -299,7 +309,20 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
299
309
|
};
|
|
300
310
|
|
|
301
311
|
const closeEntryScope = (entry: Entry) =>
|
|
302
|
-
Scope.close(entry.scope, Exit.void).pipe(
|
|
312
|
+
Scope.close(entry.scope, Exit.void).pipe(
|
|
313
|
+
Effect.tap(() =>
|
|
314
|
+
Effect.sync(() => {
|
|
315
|
+
const receipt = entry.session.cleanupReceipt?.();
|
|
316
|
+
if (!receipt?.uncertain) return;
|
|
317
|
+
const current = entry.snapshot.errorText;
|
|
318
|
+
entry.snapshot.errorText = current
|
|
319
|
+
? `${current}; ${receipt.message}`
|
|
320
|
+
: receipt.message;
|
|
321
|
+
notify(entry.snapshot.id);
|
|
322
|
+
}),
|
|
323
|
+
),
|
|
324
|
+
Effect.ignore,
|
|
325
|
+
);
|
|
303
326
|
|
|
304
327
|
const pruneSettled = () => {
|
|
305
328
|
if (entries.size <= MAX_TRACKED) return;
|
|
@@ -337,11 +360,13 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
337
360
|
switch (outcome._tag) {
|
|
338
361
|
case "Completed":
|
|
339
362
|
s.status = "done";
|
|
363
|
+
s.outcome = "completed";
|
|
340
364
|
s.errorText = undefined;
|
|
341
365
|
s.finalText = outcome.finalText.slice(0, FINAL_TEXT_MAX_LENGTH);
|
|
342
366
|
break;
|
|
343
367
|
case "Failed":
|
|
344
368
|
s.status = "error";
|
|
369
|
+
s.outcome = "failed";
|
|
345
370
|
s.errorText = bounded(outcome.errorText);
|
|
346
371
|
// Never let a failed run report the previous run's successful output.
|
|
347
372
|
s.finalText = (outcome.partialText ?? "").slice(
|
|
@@ -351,6 +376,7 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
351
376
|
break;
|
|
352
377
|
case "Interrupted":
|
|
353
378
|
s.status = "error";
|
|
379
|
+
s.outcome = "interrupted";
|
|
354
380
|
s.errorText = "Run was aborted";
|
|
355
381
|
s.finalText = (outcome.partialText ?? "").slice(
|
|
356
382
|
0,
|
|
@@ -417,6 +443,7 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
417
443
|
case "RunStarted":
|
|
418
444
|
entry.restarting = false;
|
|
419
445
|
s.status = "running";
|
|
446
|
+
s.outcome = undefined;
|
|
420
447
|
s.settledAt = undefined;
|
|
421
448
|
s.errorText = undefined;
|
|
422
449
|
armWatchdog(entry);
|
|
@@ -578,10 +605,14 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
578
605
|
prompt: task.prompt,
|
|
579
606
|
cwd: task.cwd,
|
|
580
607
|
status: "running",
|
|
608
|
+
...(task.worktree
|
|
609
|
+
? { worktreeBranch: task.worktree.branch }
|
|
610
|
+
: {}),
|
|
581
611
|
createdAt: Date.now(),
|
|
582
612
|
meta,
|
|
583
613
|
usage: { contextWindow: meta.contextWindow },
|
|
584
614
|
transcript: [],
|
|
615
|
+
transcriptVersion: 0,
|
|
585
616
|
liveTools: [],
|
|
586
617
|
queued: [],
|
|
587
618
|
finalText: "",
|
|
@@ -792,6 +823,7 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
792
823
|
const view: SubagentReadModel = {
|
|
793
824
|
list: () => [...entries.values()].map((entry) => entry.snapshot),
|
|
794
825
|
get: (id) => entries.get(id)?.snapshot,
|
|
826
|
+
getToolRenderer: (id) => entries.get(id)?.session.toolRenderer,
|
|
795
827
|
size: () => entries.size,
|
|
796
828
|
subscribe: (listener) => {
|
|
797
829
|
listeners.add(listener);
|
|
@@ -285,8 +285,11 @@ export const SUBAGENT_CHECK_PARAMETER_DESCRIPTIONS = {
|
|
|
285
285
|
export const SUBAGENT_LIST_TOOL_DESCRIPTION =
|
|
286
286
|
"List all subagents (running and finished) with their status.";
|
|
287
287
|
|
|
288
|
-
|
|
289
|
-
|
|
288
|
+
const SUBAGENT_RESULT_TRANSPORT_INSTRUCTION =
|
|
289
|
+
"(This result is already shown to the user. Act on it and relay only the decisions or next steps — do not repeat it verbatim.)";
|
|
290
|
+
|
|
291
|
+
/** Builds the user-visible child completion/failure projection. */
|
|
292
|
+
export function buildSubagentResultDisplayMessage(options: {
|
|
290
293
|
id: string;
|
|
291
294
|
title: string;
|
|
292
295
|
status: "running" | "done" | "error";
|
|
@@ -297,9 +300,34 @@ export function buildSubagentResultMessage(options: {
|
|
|
297
300
|
let text = `Subagent ${options.id} "${options.title}" ${verb}.`;
|
|
298
301
|
if (options.errorText) text += `\nError: ${options.errorText}`;
|
|
299
302
|
text += `\n\n${options.output}`;
|
|
303
|
+
return text;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Builds the child completion/failure wrapper injected into the parent model's context. */
|
|
307
|
+
export function buildSubagentResultMessage(options: {
|
|
308
|
+
id: string;
|
|
309
|
+
title: string;
|
|
310
|
+
status: "running" | "done" | "error";
|
|
311
|
+
errorText?: string;
|
|
312
|
+
output: string;
|
|
313
|
+
}) {
|
|
314
|
+
let text = buildSubagentResultDisplayMessage(options);
|
|
300
315
|
// This message is already displayed to the user, so tell the parent to act on
|
|
301
316
|
// it rather than reprint it verbatim.
|
|
302
|
-
text +=
|
|
303
|
-
"\n\n(This result is already shown to the user. Act on it and relay only the decisions or next steps — do not repeat it verbatim.)";
|
|
317
|
+
text += `\n\n${SUBAGENT_RESULT_TRANSPORT_INSTRUCTION}`;
|
|
304
318
|
return text;
|
|
305
319
|
}
|
|
320
|
+
|
|
321
|
+
/** Remove the transport-only suffix from results persisted before the split. */
|
|
322
|
+
export function stripSubagentResultTransportInstruction(content: string) {
|
|
323
|
+
const separator = `\n\n${SUBAGENT_RESULT_TRANSPORT_INSTRUCTION}`;
|
|
324
|
+
const withoutBatchedSeparators = content.replaceAll(
|
|
325
|
+
`${separator}\n\nSubagent `,
|
|
326
|
+
"\n\nSubagent ",
|
|
327
|
+
);
|
|
328
|
+
return (
|
|
329
|
+
withoutBatchedSeparators.endsWith(separator)
|
|
330
|
+
? withoutBatchedSeparators.slice(0, -separator.length)
|
|
331
|
+
: withoutBatchedSeparators
|
|
332
|
+
).trimEnd();
|
|
333
|
+
}
|
|
@@ -20,6 +20,7 @@ export interface ResultProjection {
|
|
|
20
20
|
readonly text: string;
|
|
21
21
|
readonly truncated: boolean;
|
|
22
22
|
readonly artifactPath?: string;
|
|
23
|
+
readonly artifactSaveFailed?: boolean;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
function sliceStartToUtf8Bytes(content: string, maxBytes: number) {
|
|
@@ -96,11 +97,13 @@ export function projectResult(
|
|
|
96
97
|
const tailLines = Math.max(1, options.maxLines - headLines);
|
|
97
98
|
|
|
98
99
|
let artifactPath: string | undefined;
|
|
100
|
+
let artifactSaveFailed = false;
|
|
99
101
|
try {
|
|
100
102
|
artifactPath = options.writeArtifact(content);
|
|
101
103
|
} catch {
|
|
102
104
|
// Delivery is more important than the optional recovery cache. The footer
|
|
103
105
|
// below stays explicit so a failed write never advertises a false path.
|
|
106
|
+
artifactSaveFailed = true;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
let bodyBudget = options.maxBytes;
|
|
@@ -138,5 +141,6 @@ export function projectResult(
|
|
|
138
141
|
text,
|
|
139
142
|
truncated: true,
|
|
140
143
|
...(artifactPath ? { artifactPath } : {}),
|
|
144
|
+
...(artifactSaveFailed ? { artifactSaveFailed: true } : {}),
|
|
141
145
|
};
|
|
142
146
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ConsumableResultDeliveryQueue } from "../../shared/result-delivery.ts";
|
|
2
|
+
|
|
1
3
|
export interface SubagentResultDeliveryOptions<T> {
|
|
2
4
|
/** True only when the parent has no run or queued continuation in flight. */
|
|
3
5
|
readonly isIdle: () => boolean;
|
|
@@ -46,7 +48,7 @@ export function createSubagentResultDelivery<T extends { id: string }>(
|
|
|
46
48
|
}
|
|
47
49
|
};
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
const queue = {
|
|
50
52
|
defer(result: T) {
|
|
51
53
|
pending.set(result.id, result);
|
|
52
54
|
if (options.isIdle()) flush();
|
|
@@ -61,5 +63,9 @@ export function createSubagentResultDelivery<T extends { id: string }>(
|
|
|
61
63
|
clear() {
|
|
62
64
|
pending.clear();
|
|
63
65
|
},
|
|
66
|
+
size() {
|
|
67
|
+
return pending.size;
|
|
68
|
+
},
|
|
64
69
|
};
|
|
70
|
+
return queue satisfies ConsumableResultDeliveryQueue<T>;
|
|
65
71
|
}
|
|
@@ -11,8 +11,22 @@ import { BackendRegistry, type SubagentBackend } from "./backend.ts";
|
|
|
11
11
|
import { piBackend } from "./backends/pi.ts";
|
|
12
12
|
import type { BackendName } from "./domain.ts";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Test-only injection seam for extension-level tests that drive real spawns
|
|
16
|
+
* without a child pi session: production never sets it. The underscore-prefixed
|
|
17
|
+
* setter name makes any accidental production use self-evidently wrong.
|
|
18
|
+
*/
|
|
19
|
+
let testBackends: readonly SubagentBackend[] | undefined;
|
|
20
|
+
|
|
21
|
+
/** Test-only: replace the backends the manager can spawn against. */
|
|
22
|
+
export function __setSubagentTestBackends(
|
|
23
|
+
backends: readonly SubagentBackend[] | undefined,
|
|
24
|
+
) {
|
|
25
|
+
testBackends = backends;
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
const BackendRegistryLive = Layer.sync(BackendRegistry, () => {
|
|
15
|
-
const backends: SubagentBackend[] = [piBackend];
|
|
29
|
+
const backends: readonly SubagentBackend[] = testBackends ?? [piBackend];
|
|
16
30
|
return new Map<BackendName, SubagentBackend>(
|
|
17
31
|
backends.map((backend) => [backend.name, backend]),
|
|
18
32
|
);
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Takeover UI for subagents (ported from v1, rendering from the synchronous
|
|
3
3
|
* SubagentReadModel instead of live pi sessions):
|
|
4
4
|
* - SubagentDashboard: compact picker docked above the input, listing all subagents.
|
|
5
|
-
* - TakeoverView: full
|
|
6
|
-
*
|
|
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,22 +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 {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
panelFrame,
|
|
19
|
-
type ScreenHint,
|
|
20
|
-
} from "../../../shared/screen-chrome.ts";
|
|
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";
|
|
21
18
|
import { sanitizeTerminalText } from "../../../shared/terminal-text.ts";
|
|
22
19
|
import { formatElapsed, type SubagentSnapshot } from "../domain.ts";
|
|
23
20
|
import { formatContextUtilization } from "../../../shared/context-utilization.ts";
|
|
21
|
+
import { SPINNER_INTERVAL_MS, spinnerFrame } from "../../../shared/spinner.ts";
|
|
24
22
|
import type { SubagentReadModel } from "../manager.ts";
|
|
25
|
-
import {
|
|
26
|
-
buildTranscriptLines,
|
|
27
|
-
SPINNER_INTERVAL_MS,
|
|
28
|
-
spinnerFrame,
|
|
29
|
-
TranscriptRenderer,
|
|
30
|
-
} from "./transcript.ts";
|
|
23
|
+
import { subagentTranscriptDocument } from "./transcript.ts";
|
|
31
24
|
|
|
32
25
|
export function sanitizeSubagentDisplayLine(value: string) {
|
|
33
26
|
return sanitizeTerminalText(value).replace(/\s+/g, " ").trim();
|
|
@@ -43,7 +36,7 @@ function configuredKeys(
|
|
|
43
36
|
/**
|
|
44
37
|
* One spinner definition for the whole subagent UI: the dashboard glyph, the
|
|
45
38
|
* takeover header, and the transcript's live tools must animate in step, so the
|
|
46
|
-
* frames and their cadence live in `
|
|
39
|
+
* frames and their cadence live in `shared/spinner.ts` and are imported here.
|
|
47
40
|
*/
|
|
48
41
|
function statusGlyph(
|
|
49
42
|
snap: SubagentSnapshot,
|
|
@@ -69,6 +62,8 @@ function statusGlyph(
|
|
|
69
62
|
|
|
70
63
|
export interface TakeoverOptions {
|
|
71
64
|
readonly badge?: string;
|
|
65
|
+
/** Captured from the parent UI when this child page opens. */
|
|
66
|
+
readonly toolsExpanded?: boolean;
|
|
72
67
|
}
|
|
73
68
|
|
|
74
69
|
export async function openSubagentTakeover(
|
|
@@ -78,12 +73,24 @@ export async function openSubagentTakeover(
|
|
|
78
73
|
options?: TakeoverOptions,
|
|
79
74
|
) {
|
|
80
75
|
if (!view.get(id)) return;
|
|
76
|
+
const takeoverOptions: TakeoverOptions = {
|
|
77
|
+
...options,
|
|
78
|
+
toolsExpanded: ctx.ui.getToolsExpanded(),
|
|
79
|
+
};
|
|
81
80
|
await ctx.ui.custom<null>(
|
|
82
81
|
(tui, theme, keybindings, done) =>
|
|
83
|
-
new TakeoverView(
|
|
82
|
+
new TakeoverView(
|
|
83
|
+
tui,
|
|
84
|
+
theme,
|
|
85
|
+
keybindings,
|
|
86
|
+
id,
|
|
87
|
+
view,
|
|
88
|
+
done,
|
|
89
|
+
takeoverOptions,
|
|
90
|
+
),
|
|
84
91
|
{
|
|
85
92
|
overlay: true,
|
|
86
|
-
overlayOptions: { anchor: "
|
|
93
|
+
overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" },
|
|
87
94
|
},
|
|
88
95
|
);
|
|
89
96
|
}
|
|
@@ -387,33 +394,18 @@ export class SubagentDashboard implements Component {
|
|
|
387
394
|
|
|
388
395
|
// --- Takeover view ------------------------------------------------------------
|
|
389
396
|
|
|
390
|
-
const TRANSCRIPT_SCROLL_STEP = 6;
|
|
391
|
-
|
|
392
397
|
export class TakeoverView implements Component, Focusable {
|
|
393
|
-
private
|
|
394
|
-
private theme: Theme;
|
|
395
|
-
private keybindings: KeybindingsManager;
|
|
396
|
-
private id: string;
|
|
397
|
-
private view: SubagentReadModel;
|
|
398
|
-
private done: (value: null) => void;
|
|
399
|
-
private options?: TakeoverOptions;
|
|
400
|
-
|
|
401
|
-
private input = new Input();
|
|
402
|
-
private transcriptRenderer = new TranscriptRenderer();
|
|
403
|
-
/** Scroll offset in lines from the bottom of the transcript. 0 = pinned to bottom. */
|
|
404
|
-
private scrollOffset = 0;
|
|
398
|
+
private page: AgentSessionPage;
|
|
405
399
|
private unsubscribe: () => void;
|
|
406
400
|
private renderTimer?: ReturnType<typeof setTimeout>;
|
|
407
401
|
private ticker?: ReturnType<typeof setInterval>;
|
|
408
402
|
private closed = false;
|
|
409
403
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
return this._focused;
|
|
404
|
+
get focused() {
|
|
405
|
+
return this.page.focused;
|
|
413
406
|
}
|
|
414
407
|
set focused(value: boolean) {
|
|
415
|
-
this.
|
|
416
|
-
this.input.focused = value;
|
|
408
|
+
this.page.focused = value;
|
|
417
409
|
}
|
|
418
410
|
|
|
419
411
|
constructor(
|
|
@@ -425,46 +417,58 @@ export class TakeoverView implements Component, Focusable {
|
|
|
425
417
|
done: (value: null) => void,
|
|
426
418
|
options?: TakeoverOptions,
|
|
427
419
|
) {
|
|
428
|
-
this.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
+
);
|
|
435
450
|
this.unsubscribe = view.subscribeTo(id, () => {
|
|
436
|
-
this.refreshTicker();
|
|
437
|
-
this.scheduleRender();
|
|
451
|
+
this.refreshTicker(view.get(id), tui);
|
|
452
|
+
this.scheduleRender(tui);
|
|
438
453
|
});
|
|
439
|
-
this.refreshTicker();
|
|
440
|
-
this.input.onSubmit = (value: string) => {
|
|
441
|
-
const text = value.trim();
|
|
442
|
-
if (!text) return;
|
|
443
|
-
this.input.setValue("");
|
|
444
|
-
this.view.requestSend(this.id, text);
|
|
445
|
-
this.scrollOffset = 0;
|
|
446
|
-
this.tui.requestRender();
|
|
447
|
-
};
|
|
454
|
+
this.refreshTicker(view.get(id), tui);
|
|
448
455
|
}
|
|
449
456
|
|
|
450
|
-
private snap
|
|
451
|
-
return this.view.get(this.id);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
private refreshTicker() {
|
|
455
|
-
const interval =
|
|
456
|
-
this.snap()?.status === "running" ? SPINNER_INTERVAL_MS : 1000;
|
|
457
|
+
private refreshTicker(snap: SubagentSnapshot | undefined, tui: TUI) {
|
|
457
458
|
if (this.ticker) clearInterval(this.ticker);
|
|
458
|
-
this.ticker =
|
|
459
|
+
this.ticker = undefined;
|
|
460
|
+
if (snap?.status === "running") {
|
|
461
|
+
this.ticker = setInterval(() => tui.requestRender(), SPINNER_INTERVAL_MS);
|
|
462
|
+
}
|
|
459
463
|
}
|
|
460
464
|
|
|
461
|
-
private scheduleRender() {
|
|
465
|
+
private scheduleRender(tui: TUI) {
|
|
462
466
|
if (this.renderTimer) return;
|
|
463
467
|
// Streaming can emit an event per token. Limit terminal repaints so this
|
|
464
468
|
// view cannot starve input handling or make the child look frozen.
|
|
465
469
|
this.renderTimer = setTimeout(() => {
|
|
466
470
|
this.renderTimer = undefined;
|
|
467
|
-
if (!this.closed)
|
|
471
|
+
if (!this.closed) tui.requestRender();
|
|
468
472
|
}, 50);
|
|
469
473
|
}
|
|
470
474
|
|
|
@@ -478,8 +482,8 @@ export class TakeoverView implements Component, Focusable {
|
|
|
478
482
|
return true;
|
|
479
483
|
}
|
|
480
484
|
|
|
481
|
-
private close() {
|
|
482
|
-
if (this.cleanup())
|
|
485
|
+
private close(done: (value: null) => void) {
|
|
486
|
+
if (this.cleanup()) done(null);
|
|
483
487
|
}
|
|
484
488
|
|
|
485
489
|
dispose(): void {
|
|
@@ -487,202 +491,14 @@ export class TakeoverView implements Component, Focusable {
|
|
|
487
491
|
}
|
|
488
492
|
|
|
489
493
|
handleInput(data: string): void {
|
|
490
|
-
|
|
491
|
-
const snap = this.snap();
|
|
492
|
-
if (snap?.status === "running") this.view.requestAbort(this.id);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
if (
|
|
496
|
-
this.keybindings.matches(data, "app.interrupt") ||
|
|
497
|
-
this.keybindings.matches(data, "tui.select.cancel")
|
|
498
|
-
) {
|
|
499
|
-
this.close();
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
if (this.keybindings.matches(data, "tui.editor.cursorUp")) {
|
|
503
|
-
this.scrollOffset += TRANSCRIPT_SCROLL_STEP;
|
|
504
|
-
this.tui.requestRender();
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
if (this.keybindings.matches(data, "tui.editor.cursorDown")) {
|
|
508
|
-
this.scrollOffset = Math.max(
|
|
509
|
-
0,
|
|
510
|
-
this.scrollOffset - TRANSCRIPT_SCROLL_STEP,
|
|
511
|
-
);
|
|
512
|
-
this.tui.requestRender();
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
if (this.keybindings.matches(data, "tui.editor.pageUp")) {
|
|
516
|
-
this.scrollOffset += this.viewportHeight();
|
|
517
|
-
this.tui.requestRender();
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
if (this.keybindings.matches(data, "tui.editor.pageDown")) {
|
|
521
|
-
this.scrollOffset = Math.max(
|
|
522
|
-
0,
|
|
523
|
-
this.scrollOffset - this.viewportHeight(),
|
|
524
|
-
);
|
|
525
|
-
this.tui.requestRender();
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
this.input.handleInput(data);
|
|
529
|
-
this.tui.requestRender();
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
private viewportHeight(): number {
|
|
533
|
-
const rows = this.tui.terminal.rows || 30;
|
|
534
|
-
// Top rule, transcript rule, input, key hints, and bottom rule are five
|
|
535
|
-
// chrome rows. The overlay leaves Pi's final footer row visible.
|
|
536
|
-
return Math.max(1, rows - 6);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
private rule(width: number, left = "", right = "") {
|
|
540
|
-
const fill = "─";
|
|
541
|
-
const available = Math.max(1, width);
|
|
542
|
-
const leftWidth = visibleWidth(left);
|
|
543
|
-
const rightWidth = visibleWidth(right);
|
|
544
|
-
if (!right || leftWidth + rightWidth + 2 > available) {
|
|
545
|
-
return truncateToWidth(
|
|
546
|
-
left + fill.repeat(Math.max(0, available - leftWidth)),
|
|
547
|
-
available,
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
return (
|
|
551
|
-
left +
|
|
552
|
-
fill.repeat(Math.max(1, available - leftWidth - rightWidth)) +
|
|
553
|
-
right
|
|
554
|
-
);
|
|
494
|
+
this.page.handleInput(data);
|
|
555
495
|
}
|
|
556
496
|
|
|
557
497
|
render(width: number): string[] {
|
|
558
|
-
|
|
559
|
-
const now = Date.now();
|
|
560
|
-
const lines: string[] = [];
|
|
561
|
-
const snap = this.snap();
|
|
562
|
-
|
|
563
|
-
if (!snap) {
|
|
564
|
-
const border = theme.fg("borderAccent", "─".repeat(Math.max(1, width)));
|
|
565
|
-
lines.push(
|
|
566
|
-
border,
|
|
567
|
-
theme.fg("dim", `${this.id} is no longer tracked`),
|
|
568
|
-
border,
|
|
569
|
-
);
|
|
570
|
-
return lines;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const title = sanitizeSubagentDisplayLine(snap.title) || snap.id;
|
|
574
|
-
const headerLeft =
|
|
575
|
-
theme.fg("borderAccent", "─ ") +
|
|
576
|
-
statusGlyph(snap, theme, now) +
|
|
577
|
-
" " +
|
|
578
|
-
theme.fg("accent", theme.bold(title)) +
|
|
579
|
-
theme.fg("borderAccent", " ");
|
|
580
|
-
const utilization = formatContextUtilization(snap.usage);
|
|
581
|
-
const metadata = [
|
|
582
|
-
...(this.options?.badge
|
|
583
|
-
? [theme.fg("muted", sanitizeSubagentDisplayLine(this.options.badge))]
|
|
584
|
-
: []),
|
|
585
|
-
theme.fg(
|
|
586
|
-
"muted",
|
|
587
|
-
sanitizeSubagentDisplayLine(snap.meta.modelLabel ?? "?") || "?",
|
|
588
|
-
),
|
|
589
|
-
...(utilization ? [theme.fg("muted", utilization)] : []),
|
|
590
|
-
theme.fg("muted", formatElapsed(snap)),
|
|
591
|
-
];
|
|
592
|
-
const dot = theme.fg("dim", " · ");
|
|
593
|
-
while (
|
|
594
|
-
metadata.length > 1 &&
|
|
595
|
-
visibleWidth(headerLeft) + visibleWidth(metadata.join(dot)) + 2 > width
|
|
596
|
-
) {
|
|
597
|
-
metadata.shift();
|
|
598
|
-
}
|
|
599
|
-
lines.push(
|
|
600
|
-
this.rule(
|
|
601
|
-
width,
|
|
602
|
-
truncateToWidth(
|
|
603
|
-
headerLeft,
|
|
604
|
-
Math.max(1, width - visibleWidth(metadata.join(dot)) - 2),
|
|
605
|
-
),
|
|
606
|
-
metadata.join(dot),
|
|
607
|
-
),
|
|
608
|
-
);
|
|
609
|
-
|
|
610
|
-
// Fixed-height transcript viewport. Errors consume a row, but scroll state
|
|
611
|
-
// is represented by the following rule so its height never changes.
|
|
612
|
-
// `now` is shared with the header glyph so both spinners show one frame.
|
|
613
|
-
const transcript = buildTranscriptLines(
|
|
614
|
-
snap,
|
|
615
|
-
width,
|
|
616
|
-
theme,
|
|
617
|
-
this.transcriptRenderer,
|
|
618
|
-
{ now },
|
|
619
|
-
);
|
|
620
|
-
const viewport = this.viewportHeight();
|
|
621
|
-
const errorRows = snap.errorText ? 1 : 0;
|
|
622
|
-
const transcriptCapacity = Math.max(1, viewport - errorRows);
|
|
623
|
-
const maxOffset = Math.max(0, transcript.length - transcriptCapacity);
|
|
624
|
-
if (this.scrollOffset > maxOffset) this.scrollOffset = maxOffset;
|
|
625
|
-
|
|
626
|
-
const body: string[] = [];
|
|
627
|
-
if (snap.errorText) {
|
|
628
|
-
body.push(
|
|
629
|
-
truncateToWidth(
|
|
630
|
-
theme.fg(
|
|
631
|
-
"error",
|
|
632
|
-
`error: ${sanitizeSubagentDisplayLine(snap.errorText)}`,
|
|
633
|
-
),
|
|
634
|
-
width,
|
|
635
|
-
),
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
const end = transcript.length - this.scrollOffset;
|
|
639
|
-
const visible = transcript.slice(
|
|
640
|
-
Math.max(0, end - Math.max(1, viewport - body.length)),
|
|
641
|
-
end,
|
|
642
|
-
);
|
|
643
|
-
if (visible.length === 0) body.push(theme.fg("dim", "waiting for output…"));
|
|
644
|
-
else body.push(...visible);
|
|
645
|
-
while (body.length < viewport) body.push("");
|
|
646
|
-
lines.push(...body.slice(0, viewport));
|
|
647
|
-
|
|
648
|
-
lines.push(
|
|
649
|
-
this.rule(
|
|
650
|
-
width,
|
|
651
|
-
theme.fg("borderAccent", "─"),
|
|
652
|
-
this.scrollOffset > 0 ? theme.fg("dim", `↓ ${this.scrollOffset}`) : "",
|
|
653
|
-
),
|
|
654
|
-
);
|
|
655
|
-
lines.push(...this.input.render(width));
|
|
656
|
-
const keys = (binding: Parameters<KeybindingsManager["getKeys"]>[0]) =>
|
|
657
|
-
configuredKeys(this.keybindings, binding);
|
|
658
|
-
const editing: ScreenHint[] = [
|
|
659
|
-
[keys("tui.input.submit"), "send"],
|
|
660
|
-
[keys("app.interrupt"), "back"],
|
|
661
|
-
[keys("app.clear"), "abort run"],
|
|
662
|
-
[
|
|
663
|
-
`${keys("tui.editor.cursorUp")}/${keys("tui.editor.cursorDown")}`,
|
|
664
|
-
"scroll",
|
|
665
|
-
],
|
|
666
|
-
];
|
|
667
|
-
// Same drop-the-page-hint fallback as before, measured on the styled line
|
|
668
|
-
// so the keys-brighter-than-labels styling cannot change what fits.
|
|
669
|
-
const full = hintLine(
|
|
670
|
-
theme,
|
|
671
|
-
[
|
|
672
|
-
...editing,
|
|
673
|
-
[`${keys("tui.editor.pageUp")}/${keys("tui.editor.pageDown")}`, "page"],
|
|
674
|
-
],
|
|
675
|
-
width,
|
|
676
|
-
);
|
|
677
|
-
lines.push(
|
|
678
|
-
visibleWidth(full) <= width ? full : hintLine(theme, editing, width),
|
|
679
|
-
);
|
|
680
|
-
lines.push(this.rule(width, theme.fg("borderAccent", "─")));
|
|
681
|
-
return lines;
|
|
498
|
+
return this.page.render(width);
|
|
682
499
|
}
|
|
683
500
|
|
|
684
501
|
invalidate(): void {
|
|
685
|
-
this.
|
|
686
|
-
this.transcriptRenderer.invalidate();
|
|
502
|
+
this.page.invalidate();
|
|
687
503
|
}
|
|
688
504
|
}
|