@tt-a1i/openpi 0.4.0 → 0.6.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 +116 -46
- package/SETUP.md +29 -7
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +155 -0
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +38 -3
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +484 -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 +47 -24
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +12 -2
- 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/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +65 -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 +82 -6
- 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 +138 -130
- 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 +457 -21
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +83 -34
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/structured-output.ts +154 -0
- 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 +313 -62
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +450 -70
- package/extensions/subagents/src/domain.ts +21 -1
- package/extensions/subagents/src/manager.ts +39 -2
- package/extensions/subagents/src/prompt.ts +49 -7
- package/extensions/subagents/src/result-artifact.ts +36 -0
- package/extensions/subagents/src/result-delivery.ts +39 -14
- 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 +16 -45
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +257 -0
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +459 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +175 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +501 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +79 -5
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +70 -16
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +214 -76
- package/extensions/workflows/retention.ts +599 -0
- package/extensions/workflows/runner.ts +389 -345
- 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 +34 -14
- package/skills/subagents/REFERENCE.md +190 -0
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +6 -4
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +664 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +1069 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +467 -0
- package/web/runtime/pi-runtime.ts +1206 -0
- package/web/runtime/types.ts +102 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1700 -0
- package/web/ui/index.html +142 -0
- package/web/ui/styles.css +680 -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
|
@@ -1,542 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
* Transcript rendering for the takeover view: turns a SubagentSnapshot's
|
|
3
|
-
* normalized transcript + live state into width-bounded TUI lines. The domain
|
|
4
|
-
* stream stays normalized and bounded; this renderer only formats its previews.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { AgentToolRenderer } from "../../../shared/agent-tool-renderer.ts";
|
|
8
2
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* Strip raw ANSI codes, expand tabs, and drop control chars. Terminal-expanded
|
|
32
|
-
* tabs (and stray escapes) make lines wider than the width we declare to the
|
|
33
|
-
* TUI, which desyncs the renderer and smears the overlay.
|
|
34
|
-
*/
|
|
35
|
-
export function sanitizeText(text: string): string {
|
|
36
|
-
return sanitizeTerminalText(text);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function singleLinePreview(text: string) {
|
|
40
|
-
// Keep meaningful whitespace inside parsed commands and paths; only fold
|
|
41
|
-
// physical line breaks so a preview remains one terminal row.
|
|
42
|
-
return sanitizeText(text).replace(/\r?\n/g, " ↵ ");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function compactPreview(text: string) {
|
|
46
|
-
return singleLinePreview(text).trim();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function stringField(value: Record<string, unknown>, field: string) {
|
|
50
|
-
const candidate = value[field];
|
|
51
|
-
return typeof candidate === "string" && candidate.length > 0
|
|
52
|
-
? singleLinePreview(candidate)
|
|
53
|
-
: undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function parsedArgs(preview: string) {
|
|
57
|
-
try {
|
|
58
|
-
const value: unknown = JSON.parse(preview);
|
|
59
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
60
|
-
? (value as Record<string, unknown>)
|
|
61
|
-
: undefined;
|
|
62
|
-
} catch {
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Turn common tool arguments into a useful, bounded summary without retaining raw args. */
|
|
68
|
-
export function summarizeToolArgs(
|
|
69
|
-
name: string,
|
|
70
|
-
argsPreview?: string,
|
|
71
|
-
cwd?: string,
|
|
72
|
-
) {
|
|
73
|
-
if (!argsPreview) return undefined;
|
|
74
|
-
|
|
75
|
-
const fallback = compactPreview(argsPreview);
|
|
76
|
-
if (!fallback || fallback === "{}") return undefined;
|
|
77
|
-
|
|
78
|
-
const args = parsedArgs(fallback);
|
|
79
|
-
if (!args) return fallback;
|
|
80
|
-
|
|
81
|
-
const path = (field: string) => {
|
|
82
|
-
const value = stringField(args, field);
|
|
83
|
-
return value ? relativeToCwd(value, cwd) : undefined;
|
|
3
|
+
buildPairingIndex,
|
|
4
|
+
type AgentTranscriptDocument,
|
|
5
|
+
type PairingIndex,
|
|
6
|
+
} from "../../../shared/agent-transcript.ts";
|
|
7
|
+
import type { SubagentSnapshot } from "../domain.ts";
|
|
8
|
+
|
|
9
|
+
/** Cached items + pairing, keyed on snapshot identity and transcriptVersion. */
|
|
10
|
+
interface StableTranscriptParts {
|
|
11
|
+
readonly version: number;
|
|
12
|
+
readonly items: ReadonlyArray<SubagentSnapshot["transcript"][number]>;
|
|
13
|
+
readonly pairing: PairingIndex;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const stableCache = new WeakMap<SubagentSnapshot, StableTranscriptParts>();
|
|
17
|
+
|
|
18
|
+
function stableParts(snap: SubagentSnapshot): StableTranscriptParts {
|
|
19
|
+
const cached = stableCache.get(snap);
|
|
20
|
+
if (cached && cached.version === snap.transcriptVersion) return cached;
|
|
21
|
+
const parts: StableTranscriptParts = {
|
|
22
|
+
version: snap.transcriptVersion,
|
|
23
|
+
items: snap.transcript,
|
|
24
|
+
pairing: buildPairingIndex(snap.transcript),
|
|
84
25
|
};
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (tool === "bash") return stringField(args, "command") ?? fallback;
|
|
88
|
-
if (tool === "read" || tool === "write" || tool === "edit") {
|
|
89
|
-
return path("path") ?? fallback;
|
|
90
|
-
}
|
|
91
|
-
if (tool === "rg" || tool === "fd") {
|
|
92
|
-
const pattern = stringField(args, "pattern");
|
|
93
|
-
const searchPath = path("path");
|
|
94
|
-
if (pattern && searchPath) return `${pattern} · ${searchPath}`;
|
|
95
|
-
return pattern ?? searchPath ?? fallback;
|
|
96
|
-
}
|
|
97
|
-
return fallback;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Absolute paths inside the child's own checkout read as noise; relativize. */
|
|
101
|
-
function relativeToCwd(path: string, cwd?: string) {
|
|
102
|
-
if (!cwd) return path;
|
|
103
|
-
if (path === cwd) return ".";
|
|
104
|
-
const prefix = cwd.endsWith("/") ? cwd : `${cwd}/`;
|
|
105
|
-
return path.startsWith(prefix) ? path.slice(prefix.length) : path;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function transcriptMarkdownTheme() {
|
|
109
|
-
const theme = getMarkdownTheme();
|
|
110
|
-
return {
|
|
111
|
-
...theme,
|
|
112
|
-
// Markdown normalizes unordered lists to "- "; use a display bullet so
|
|
113
|
-
// transcript list syntax is never confused with unrendered source.
|
|
114
|
-
listBullet: (text: string) =>
|
|
115
|
-
theme.listBullet(text.replace(/^(?:[-+*]) /, "• ")),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function renderMarkdown(
|
|
120
|
-
text: string,
|
|
121
|
-
width: number,
|
|
122
|
-
defaultTextStyle?: DefaultTextStyle,
|
|
123
|
-
options?: MarkdownOptions,
|
|
124
|
-
) {
|
|
125
|
-
const clean = sanitizeText(text).trim();
|
|
126
|
-
if (!clean) return [];
|
|
127
|
-
const markdown = new Markdown(
|
|
128
|
-
clean,
|
|
129
|
-
0,
|
|
130
|
-
0,
|
|
131
|
-
transcriptMarkdownTheme(),
|
|
132
|
-
defaultTextStyle,
|
|
133
|
-
options,
|
|
134
|
-
);
|
|
135
|
-
return markdown
|
|
136
|
-
.render(Math.max(1, width))
|
|
137
|
-
.map((line) => truncateToWidth(line, width));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function renderUserText(theme: Theme, text: string, width: number) {
|
|
141
|
-
const lines = renderMarkdown(
|
|
142
|
-
text,
|
|
143
|
-
Math.max(1, width - 2),
|
|
144
|
-
{ color: (content: string) => theme.fg("userMessageText", content) },
|
|
145
|
-
{ preserveOrderedListMarkers: true, preserveBackslashEscapes: true },
|
|
146
|
-
);
|
|
147
|
-
return lines.map((line, index) =>
|
|
148
|
-
truncateToWidth(
|
|
149
|
-
(index === 0 ? theme.fg("accent", "> ") : " ") + line,
|
|
150
|
-
width,
|
|
151
|
-
),
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function renderThinking(theme: Theme, text: string, width: number) {
|
|
156
|
-
const reasoning = sanitizeText(text).trim();
|
|
157
|
-
if (!reasoning) return [];
|
|
158
|
-
const out: string[] = [];
|
|
159
|
-
const prefix = theme.fg("dim", "~ ");
|
|
160
|
-
const defaultTextStyle = {
|
|
161
|
-
color: (content: string) => theme.fg("muted", content),
|
|
162
|
-
italic: true,
|
|
163
|
-
} satisfies DefaultTextStyle;
|
|
164
|
-
const lines = renderMarkdown(
|
|
165
|
-
reasoning,
|
|
166
|
-
Math.max(1, width - 2),
|
|
167
|
-
defaultTextStyle,
|
|
168
|
-
);
|
|
169
|
-
for (let i = 0; i < lines.length; i++) {
|
|
170
|
-
out.push(truncateToWidth((i === 0 ? prefix : " ") + lines[i], width));
|
|
171
|
-
}
|
|
172
|
-
return out;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function renderToolBody(
|
|
176
|
-
theme: Theme,
|
|
177
|
-
name: string,
|
|
178
|
-
argsPreview?: string,
|
|
179
|
-
cwd?: string,
|
|
180
|
-
) {
|
|
181
|
-
const toolName = sanitizeText(name);
|
|
182
|
-
const preview = summarizeToolArgs(toolName, argsPreview, cwd);
|
|
183
|
-
// The `$` form only earns its prompt when there is a command to show; a bare
|
|
184
|
-
// `$ ` would read as an empty shell line.
|
|
185
|
-
if (toolName === "bash" && preview) return theme.fg("dim", `$ ${preview}`);
|
|
186
|
-
return (
|
|
187
|
-
theme.fg("toolTitle", toolName) +
|
|
188
|
-
(preview ? theme.fg("dim", ` ${preview}`) : "")
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function firstOutputPreview(outputPreview?: string) {
|
|
193
|
-
return (
|
|
194
|
-
sanitizeText(outputPreview ?? "")
|
|
195
|
-
.split("\n")
|
|
196
|
-
.find((line) => line.trim()) ?? ""
|
|
197
|
-
);
|
|
26
|
+
stableCache.set(snap, parts);
|
|
27
|
+
return parts;
|
|
198
28
|
}
|
|
199
29
|
|
|
200
|
-
/**
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
function phaseGlyph(theme: Theme, phase: ToolPhase, now: number) {
|
|
208
|
-
switch (phase) {
|
|
209
|
-
case "live":
|
|
210
|
-
return theme.fg("warning", spinnerFrame(now));
|
|
211
|
-
case "ok":
|
|
212
|
-
return theme.fg("success", "✓");
|
|
213
|
-
case "error":
|
|
214
|
-
return theme.fg("error", "✗");
|
|
215
|
-
case "pending":
|
|
216
|
-
return theme.fg("dim", "·");
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Command line: `<glyph> $ cmd` for bash, `<glyph> name args` otherwise. */
|
|
221
|
-
function renderToolLine(
|
|
222
|
-
theme: Theme,
|
|
223
|
-
phase: ToolPhase,
|
|
224
|
-
name: string,
|
|
225
|
-
argsPreview: string | undefined,
|
|
226
|
-
width: number,
|
|
227
|
-
now: number,
|
|
228
|
-
cwd?: string,
|
|
229
|
-
) {
|
|
230
|
-
return truncateToWidth(
|
|
231
|
-
`${phaseGlyph(theme, phase, now)} ${renderToolBody(theme, name, argsPreview, cwd)}`,
|
|
232
|
-
width,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** Output line: indented under the command, no second glyph. */
|
|
237
|
-
function renderOutputLine(
|
|
238
|
-
theme: Theme,
|
|
239
|
-
isError: boolean,
|
|
240
|
-
outputPreview: string,
|
|
241
|
-
width: number,
|
|
242
|
-
cwd?: string,
|
|
243
|
-
) {
|
|
244
|
-
// Tool output echoes the absolute search path back (fd/rg print what they
|
|
245
|
-
// were given); inside the child's own checkout the relative form is enough.
|
|
246
|
-
const text = cwd ? outputPreview.split(`${cwd}/`).join("") : outputPreview;
|
|
247
|
-
const preview = text || "(no output)";
|
|
248
|
-
const content = isError
|
|
249
|
-
? theme.fg(text ? "error" : "dim", preview)
|
|
250
|
-
: theme.fg("dim", preview);
|
|
251
|
-
return truncateToWidth(` ${content}`, width);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function renderAssistantItem(
|
|
255
|
-
theme: Theme,
|
|
256
|
-
item: Extract<TranscriptItem, { kind: "assistant" }>,
|
|
257
|
-
width: number,
|
|
258
|
-
phases: ReadonlyMap<string, ToolPhase>,
|
|
259
|
-
now: number,
|
|
260
|
-
cwd?: string,
|
|
261
|
-
) {
|
|
262
|
-
const out: string[] = [];
|
|
263
|
-
for (const part of item.parts) {
|
|
264
|
-
if (part.type === "text") {
|
|
265
|
-
out.push(...renderMarkdown(part.text, width));
|
|
266
|
-
} else if (part.type === "thinking") {
|
|
267
|
-
out.push(
|
|
268
|
-
...renderThinking(
|
|
269
|
-
theme,
|
|
270
|
-
part.redacted ? "[redacted reasoning]" : part.text,
|
|
271
|
-
width,
|
|
272
|
-
),
|
|
273
|
-
);
|
|
274
|
-
} else if (part.type === "toolCall") {
|
|
275
|
-
const phase = phases.get(part.toolId) ?? "pending";
|
|
276
|
-
// A live tool is rendered by the live block, which owns the spinner and
|
|
277
|
-
// the streaming output; rendering the call here too would show the same
|
|
278
|
-
// command twice and make the block reflow when the tool settles.
|
|
279
|
-
if (phase === "live") continue;
|
|
280
|
-
out.push(
|
|
281
|
-
renderToolLine(
|
|
282
|
-
theme,
|
|
283
|
-
phase,
|
|
284
|
-
part.name,
|
|
285
|
-
part.argsPreview,
|
|
286
|
-
width,
|
|
287
|
-
now,
|
|
288
|
-
cwd,
|
|
289
|
-
),
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
return out;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function renderToolResultItem(
|
|
297
|
-
theme: Theme,
|
|
298
|
-
item: Extract<TranscriptItem, { kind: "toolResult" }>,
|
|
299
|
-
width: number,
|
|
300
|
-
paired: boolean,
|
|
301
|
-
now: number,
|
|
302
|
-
cwd?: string,
|
|
303
|
-
) {
|
|
304
|
-
const preview = firstOutputPreview(item.outputPreview);
|
|
305
|
-
// An orphan result (its call is not the previous item) still needs a glyph:
|
|
306
|
-
// there is no command line above it to carry one.
|
|
307
|
-
if (!paired) {
|
|
308
|
-
return [
|
|
309
|
-
renderToolLine(
|
|
310
|
-
theme,
|
|
311
|
-
item.isError ? "error" : "ok",
|
|
312
|
-
item.name,
|
|
313
|
-
undefined,
|
|
314
|
-
width,
|
|
315
|
-
now,
|
|
316
|
-
),
|
|
317
|
-
...(preview
|
|
318
|
-
? [renderOutputLine(theme, item.isError, preview, width, cwd)]
|
|
319
|
-
: []),
|
|
320
|
-
];
|
|
321
|
-
}
|
|
322
|
-
return [renderOutputLine(theme, item.isError, preview, width, cwd)];
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function isPairedToolResult(
|
|
326
|
-
previous: TranscriptItem | undefined,
|
|
327
|
-
current: TranscriptItem,
|
|
328
|
-
) {
|
|
329
|
-
if (
|
|
330
|
-
!previous ||
|
|
331
|
-
previous.kind !== "assistant" ||
|
|
332
|
-
current.kind !== "toolResult"
|
|
333
|
-
) {
|
|
334
|
-
return false;
|
|
335
|
-
}
|
|
336
|
-
const lastPart = previous.parts[previous.parts.length - 1];
|
|
337
|
-
return lastPart?.type === "toolCall" && lastPart.toolId === current.toolId;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function renderTranscriptItem(
|
|
341
|
-
theme: Theme,
|
|
342
|
-
item: TranscriptItem,
|
|
343
|
-
width: number,
|
|
344
|
-
context: ItemContext,
|
|
345
|
-
now: number,
|
|
346
|
-
cwd?: string,
|
|
347
|
-
) {
|
|
348
|
-
if (item.kind === "user") return renderUserText(theme, item.text, width);
|
|
349
|
-
if (item.kind === "assistant") {
|
|
350
|
-
return renderAssistantItem(theme, item, width, context.phases, now, cwd);
|
|
351
|
-
}
|
|
352
|
-
return renderToolResultItem(theme, item, width, context.paired, now, cwd);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
interface ItemContext {
|
|
356
|
-
readonly phases: ReadonlyMap<string, ToolPhase>;
|
|
357
|
-
readonly paired: boolean;
|
|
358
|
-
/** Cache discriminator: identity plus width is not enough on its own. */
|
|
359
|
-
readonly token: string;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* An item's rendering depends on its neighbours (does a call have its result
|
|
364
|
-
* yet?) and on live state (is the call still running?), so the cache key has to
|
|
365
|
-
* carry that context or a stale glyph would outlive the phase it described.
|
|
366
|
-
*/
|
|
367
|
-
function itemContext(
|
|
368
|
-
transcript: ReadonlyArray<TranscriptItem>,
|
|
369
|
-
index: number,
|
|
370
|
-
liveIds: ReadonlySet<string>,
|
|
371
|
-
): ItemContext {
|
|
372
|
-
const item = transcript[index]!;
|
|
373
|
-
if (item.kind === "user")
|
|
374
|
-
return { phases: new Map(), paired: false, token: "" };
|
|
375
|
-
if (item.kind === "toolResult") {
|
|
376
|
-
const paired = isPairedToolResult(transcript[index - 1], item);
|
|
377
|
-
return { phases: new Map(), paired, token: paired ? "p" : "o" };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const phases = new Map<string, ToolPhase>();
|
|
381
|
-
for (const part of item.parts) {
|
|
382
|
-
if (part.type !== "toolCall") continue;
|
|
383
|
-
if (liveIds.has(part.toolId)) {
|
|
384
|
-
phases.set(part.toolId, "live");
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
const result = findResult(transcript, index, part.toolId);
|
|
388
|
-
phases.set(
|
|
389
|
-
part.toolId,
|
|
390
|
-
result ? (result.isError ? "error" : "ok") : "pending",
|
|
391
|
-
);
|
|
392
|
-
}
|
|
30
|
+
/** Thin projection from Direct Subagent state to the shared UI document. */
|
|
31
|
+
export function subagentTranscriptDocument(
|
|
32
|
+
snap: SubagentSnapshot,
|
|
33
|
+
toolRenderer?: AgentToolRenderer,
|
|
34
|
+
): AgentTranscriptDocument {
|
|
35
|
+
const { items, pairing } = stableParts(snap);
|
|
393
36
|
return {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
37
|
+
items,
|
|
38
|
+
pairing,
|
|
39
|
+
cwd: snap.cwd,
|
|
40
|
+
...(toolRenderer ? { toolRenderer } : {}),
|
|
41
|
+
...(snap.liveAssistant ? { liveAssistant: snap.liveAssistant } : {}),
|
|
42
|
+
...(snap.liveTools.length > 0 ? { liveTools: snap.liveTools } : {}),
|
|
43
|
+
...(snap.queued.length > 0 ? { queued: snap.queued } : {}),
|
|
397
44
|
};
|
|
398
45
|
}
|
|
399
|
-
|
|
400
|
-
/** The result for a call, if it has already landed later in the transcript. */
|
|
401
|
-
function findResult(
|
|
402
|
-
transcript: ReadonlyArray<TranscriptItem>,
|
|
403
|
-
callIndex: number,
|
|
404
|
-
toolId: string,
|
|
405
|
-
) {
|
|
406
|
-
for (let index = callIndex + 1; index < transcript.length; index++) {
|
|
407
|
-
const candidate = transcript[index];
|
|
408
|
-
if (candidate?.kind === "toolResult" && candidate.toolId === toolId) {
|
|
409
|
-
return candidate;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
return undefined;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Caches finalized transcript items by identity and width. Live state remains
|
|
417
|
-
* uncached because it changes on every stream tick; callers clear this cache
|
|
418
|
-
* from their component's invalidate() when Pi changes theme.
|
|
419
|
-
*/
|
|
420
|
-
export class TranscriptRenderer {
|
|
421
|
-
private itemCache = new WeakMap<TranscriptItem, Map<string, string[]>>();
|
|
422
|
-
|
|
423
|
-
render(
|
|
424
|
-
snap: SubagentSnapshot,
|
|
425
|
-
width: number,
|
|
426
|
-
theme: Theme,
|
|
427
|
-
options?: { readonly now?: number },
|
|
428
|
-
) {
|
|
429
|
-
const out: string[] = [];
|
|
430
|
-
const now = options?.now ?? Date.now();
|
|
431
|
-
const liveIds = new Set(snap.liveTools.map((tool) => tool.toolId));
|
|
432
|
-
|
|
433
|
-
for (let index = 0; index < snap.transcript.length; index++) {
|
|
434
|
-
const item = snap.transcript[index];
|
|
435
|
-
const context = itemContext(snap.transcript, index, liveIds);
|
|
436
|
-
const key = `${width}|${context.token}`;
|
|
437
|
-
const cached = this.itemCache.get(item)?.get(key);
|
|
438
|
-
const lines =
|
|
439
|
-
cached ??
|
|
440
|
-
renderTranscriptItem(theme, item, width, context, now, snap.cwd);
|
|
441
|
-
if (!cached) {
|
|
442
|
-
const widths = this.itemCache.get(item) ?? new Map<string, string[]>();
|
|
443
|
-
if (widths.size >= MAX_CACHED_WIDTHS_PER_ITEM) {
|
|
444
|
-
const oldestWidth = widths.keys().next().value;
|
|
445
|
-
if (oldestWidth !== undefined) widths.delete(oldestWidth);
|
|
446
|
-
}
|
|
447
|
-
widths.set(key, lines);
|
|
448
|
-
this.itemCache.set(item, widths);
|
|
449
|
-
}
|
|
450
|
-
if (lines.length > 0) {
|
|
451
|
-
if (
|
|
452
|
-
out.length > 0 &&
|
|
453
|
-
!isPairedToolResult(snap.transcript[index - 1], item)
|
|
454
|
-
) {
|
|
455
|
-
out.push("");
|
|
456
|
-
}
|
|
457
|
-
out.push(...lines);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
while (out.length > 0 && out[out.length - 1] === "") out.pop();
|
|
461
|
-
|
|
462
|
-
// Live streaming assistant buffers (cleared when the finalized message lands).
|
|
463
|
-
if (snap.liveAssistant) {
|
|
464
|
-
const { thinking, text } = snap.liveAssistant;
|
|
465
|
-
const before = out.length;
|
|
466
|
-
if (out.length > 0) out.push("");
|
|
467
|
-
if (thinking.trim()) out.push(...renderThinking(theme, thinking, width));
|
|
468
|
-
if (text.trim()) out.push(...renderMarkdown(text, width));
|
|
469
|
-
if (out.length === before + 1) out.pop();
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Live tool executions. The manager drops a live entry when its ToolEnd
|
|
473
|
-
// lands, and the transcript's call line then takes over with the settled
|
|
474
|
-
// glyph in the same column, so the block never reflows.
|
|
475
|
-
for (const tool of snap.liveTools) {
|
|
476
|
-
if (out.length > 0) out.push("");
|
|
477
|
-
const phase: ToolPhase = tool.done
|
|
478
|
-
? tool.isError
|
|
479
|
-
? "error"
|
|
480
|
-
: "ok"
|
|
481
|
-
: "live";
|
|
482
|
-
out.push(
|
|
483
|
-
renderToolLine(
|
|
484
|
-
theme,
|
|
485
|
-
phase,
|
|
486
|
-
tool.name,
|
|
487
|
-
tool.argsPreview,
|
|
488
|
-
width,
|
|
489
|
-
now,
|
|
490
|
-
snap.cwd,
|
|
491
|
-
),
|
|
492
|
-
);
|
|
493
|
-
const preview = firstOutputPreview(tool.outputPreview);
|
|
494
|
-
if (preview)
|
|
495
|
-
out.push(
|
|
496
|
-
renderOutputLine(theme, !!tool.isError, preview, width, snap.cwd),
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Queued steering/follow-up messages: show them immediately so Enter
|
|
501
|
-
// visibly acknowledges the user's input instead of appearing to do nothing.
|
|
502
|
-
for (const message of snap.queued) {
|
|
503
|
-
if (out.length > 0) out.push("");
|
|
504
|
-
const prefix = theme.fg("warning", `> [queued ${message.kind}] `);
|
|
505
|
-
const wrapped = wrapTextWithAnsi(
|
|
506
|
-
sanitizeText(message.text),
|
|
507
|
-
Math.max(1, width - visibleWidth(prefix)),
|
|
508
|
-
);
|
|
509
|
-
for (let i = 0; i < wrapped.length; i++) {
|
|
510
|
-
out.push(
|
|
511
|
-
truncateToWidth(
|
|
512
|
-
(i === 0 ? prefix : " ".repeat(visibleWidth(prefix))) +
|
|
513
|
-
theme.fg("muted", wrapped[i]),
|
|
514
|
-
width,
|
|
515
|
-
),
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
return out;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
invalidate() {
|
|
524
|
-
this.itemCache = new WeakMap();
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/** Render a subagent's conversation as width-bounded lines. */
|
|
529
|
-
export function buildTranscriptLines(
|
|
530
|
-
snap: SubagentSnapshot,
|
|
531
|
-
width: number,
|
|
532
|
-
theme: Theme,
|
|
533
|
-
renderer?: TranscriptRenderer,
|
|
534
|
-
options?: { readonly now?: number },
|
|
535
|
-
) {
|
|
536
|
-
return (renderer ?? new TranscriptRenderer()).render(
|
|
537
|
-
snap,
|
|
538
|
-
width,
|
|
539
|
-
theme,
|
|
540
|
-
options,
|
|
541
|
-
);
|
|
542
|
-
}
|