@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.
Files changed (129) hide show
  1. package/README.md +184 -59
  2. package/SETUP.md +23 -7
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/ask-user/index.ts +30 -14
  6. package/extensions/background-terminals/index.ts +30 -2
  7. package/extensions/background-terminals/src/domain.ts +2 -0
  8. package/extensions/background-terminals/src/manager.ts +486 -106
  9. package/extensions/background-terminals/src/output.ts +33 -0
  10. package/extensions/background-terminals/src/prompt.ts +14 -6
  11. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  12. package/extensions/background-terminals/src/ui/ps.ts +132 -129
  13. package/extensions/capabilities/index.ts +30 -42
  14. package/extensions/capabilities/src/ui.ts +93 -0
  15. package/extensions/clear-context/index.ts +83 -0
  16. package/extensions/context-pivot/index.ts +16 -6
  17. package/extensions/cron/schedule.ts +7 -1
  18. package/extensions/file-mutation-display/index.ts +34 -76
  19. package/extensions/file-mutation-display/render.ts +146 -87
  20. package/extensions/file-search/index.ts +8 -7
  21. package/extensions/file-search/src/binaries.ts +75 -59
  22. package/extensions/git-info/src/changed-files-view.ts +47 -14
  23. package/extensions/git-read/index.ts +328 -0
  24. package/extensions/git-read/src/args.ts +171 -0
  25. package/extensions/git-read/src/process.ts +81 -0
  26. package/extensions/git-read/src/prompt.ts +56 -0
  27. package/extensions/model-info/index.ts +21 -33
  28. package/extensions/model-info/session-metrics.ts +96 -0
  29. package/extensions/plan-mode/bash-policy.ts +54 -9
  30. package/extensions/plan-mode/index.ts +7 -2
  31. package/extensions/post-edit/index.ts +16 -6
  32. package/extensions/sessions/git-stats.ts +258 -72
  33. package/extensions/sessions/index.ts +222 -140
  34. package/extensions/sessions/preview-cache.ts +104 -0
  35. package/extensions/sessions/preview-loader.ts +856 -0
  36. package/extensions/sessions/sessions.ts +43 -4
  37. package/extensions/setup/index.ts +127 -131
  38. package/extensions/shared/activity-status.ts +36 -5
  39. package/extensions/shared/agent-session-page.ts +319 -0
  40. package/extensions/shared/agent-tool-renderer.ts +218 -0
  41. package/extensions/shared/agent-transcript.ts +524 -0
  42. package/extensions/shared/below-editor-navigation.ts +26 -0
  43. package/extensions/shared/capability-intent.ts +53 -0
  44. package/extensions/shared/child-session.ts +444 -22
  45. package/extensions/shared/result-budget.ts +134 -0
  46. package/extensions/shared/result-delivery.ts +34 -0
  47. package/extensions/shared/screen-chrome.ts +133 -0
  48. package/extensions/shared/setup-config.ts +97 -38
  49. package/extensions/shared/setup-episode-state.ts +1 -1
  50. package/extensions/shared/spinner.ts +28 -0
  51. package/extensions/shared/terminal-text.ts +110 -23
  52. package/extensions/shared/text-projection.ts +113 -0
  53. package/extensions/shared/tool-activity.ts +382 -0
  54. package/extensions/shared/tool-surface.ts +42 -8
  55. package/extensions/shared/transcript-viewport.ts +46 -0
  56. package/extensions/shared/web-observer-registry.ts +390 -0
  57. package/extensions/shared/worktree.ts +11 -0
  58. package/extensions/subagents/index.ts +461 -186
  59. package/extensions/subagents/navigation.ts +86 -28
  60. package/extensions/subagents/src/agent-types.ts +37 -15
  61. package/extensions/subagents/src/backend.ts +12 -1
  62. package/extensions/subagents/src/backends/pi.ts +375 -66
  63. package/extensions/subagents/src/domain.ts +5 -0
  64. package/extensions/subagents/src/id-sequence.ts +84 -0
  65. package/extensions/subagents/src/manager.ts +651 -536
  66. package/extensions/subagents/src/prompt.ts +185 -42
  67. package/extensions/subagents/src/result-artifact.ts +146 -0
  68. package/extensions/subagents/src/result-delivery.ts +7 -1
  69. package/extensions/subagents/src/runtime.ts +23 -6
  70. package/extensions/subagents/src/ui/takeover.ts +128 -337
  71. package/extensions/subagents/src/ui/transcript.ts +38 -501
  72. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  73. package/extensions/suggestions/src/ui.ts +10 -4
  74. package/extensions/tasks/index.ts +0 -3
  75. package/extensions/tasks/ui.ts +79 -62
  76. package/extensions/ui-customization/footer.ts +7 -44
  77. package/extensions/ui-customization/index.ts +0 -4
  78. package/extensions/user-input-fold/index.ts +185 -0
  79. package/extensions/web/index.ts +234 -0
  80. package/extensions/workflows/artifacts.ts +147 -22
  81. package/extensions/workflows/completion-projection.ts +457 -0
  82. package/extensions/workflows/controller.ts +14 -2
  83. package/extensions/workflows/coordinator.ts +62 -0
  84. package/extensions/workflows/dashboard.ts +458 -339
  85. package/extensions/workflows/handoff.ts +121 -25
  86. package/extensions/workflows/index.ts +1042 -492
  87. package/extensions/workflows/journal.ts +148 -13
  88. package/extensions/workflows/model.ts +131 -19
  89. package/extensions/workflows/navigation.ts +61 -18
  90. package/extensions/workflows/progress-projection.ts +306 -0
  91. package/extensions/workflows/prompt.ts +166 -10
  92. package/extensions/workflows/replay-safety.ts +58 -27
  93. package/extensions/workflows/result-delivery.ts +253 -0
  94. package/extensions/workflows/retention.ts +593 -0
  95. package/extensions/workflows/runner.ts +388 -279
  96. package/extensions/workflows/sandbox-child.cjs +36 -3
  97. package/extensions/workflows/sandbox.ts +62 -8
  98. package/extensions/workflows/serialization.ts +325 -17
  99. package/extensions/workflows/tool-renderer.ts +22 -0
  100. package/extensions/workflows/transcript.ts +149 -0
  101. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  102. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  103. package/package.json +28 -8
  104. package/skills/subagents/REFERENCE.md +189 -0
  105. package/skills/subagents/SKILL.md +2 -2
  106. package/skills/workflows/REFERENCE.md +10 -5
  107. package/skills/workflows/SKILL.md +53 -10
  108. package/web/adapter/pi-adapter.ts +661 -0
  109. package/web/host/browser-launcher.ts +20 -0
  110. package/web/host/static-assets.ts +4 -0
  111. package/web/host/terminal-status.ts +38 -0
  112. package/web/host/web-host.ts +789 -0
  113. package/web/http-dispatcher.ts +125 -0
  114. package/web/protocol/types.ts +462 -0
  115. package/web/runtime/pi-runtime.ts +991 -0
  116. package/web/runtime/types.ts +71 -0
  117. package/web/runtime/web-host-lease.ts +497 -0
  118. package/web/trace.ts +18 -0
  119. package/web/ui/app.js +1398 -0
  120. package/web/ui/index.html +139 -0
  121. package/web/ui/styles.css +598 -0
  122. package/web/vite.config.mjs +34 -0
  123. package/extensions/execution-convergence/active-evidence.ts +0 -129
  124. package/extensions/execution-convergence/index.ts +0 -442
  125. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  126. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  127. package/extensions/setup/intercom.ts +0 -603
  128. package/extensions/subagents/src/backends/stub.ts +0 -296
  129. package/extensions/subagents/src/format.ts +0 -48
@@ -16,7 +16,8 @@
16
16
  * settle. `/subagents` opens a picker + full interactive takeover view.
17
17
  *
18
18
  * Agent types (`src/agent-types.ts`) are optional named presets that fix a
19
- * child's system prompt, model, and tool allowlist; see `docs/agent-types.md`.
19
+ * child's system prompt, model, and tool allowlist; see
20
+ * `skills/subagents/REFERENCE.md`.
20
21
  *
21
22
  * Architecture: Effect v4 generators throughout (backend -> manager ->
22
23
  * runtime); this file is the async boundary where tool handlers run effects
@@ -25,7 +26,6 @@
25
26
 
26
27
  import * as fs from "node:fs";
27
28
  import * as path from "node:path";
28
- import { StringEnum } from "@earendil-works/pi-ai";
29
29
  import type {
30
30
  ExtensionAPI,
31
31
  ExtensionCommandContext,
@@ -37,7 +37,6 @@ import {
37
37
  DEFAULT_MAX_BYTES,
38
38
  DEFAULT_MAX_LINES,
39
39
  defineTool,
40
- formatSize,
41
40
  getAgentDir,
42
41
  getMarkdownTheme,
43
42
  keyHint,
@@ -45,42 +44,83 @@ import {
45
44
  } from "@earendil-works/pi-coding-agent";
46
45
  import { Markdown, Text } from "@earendil-works/pi-tui";
47
46
  import { Type } from "typebox";
47
+ import {
48
+ createStatusWriter,
49
+ formatActivityStatus,
50
+ hasActivity,
51
+ unreadActivityCounts,
52
+ } from "../shared/activity-status.ts";
53
+ import { sanitizeText } from "../shared/agent-transcript.ts";
54
+ import {
55
+ BelowEditorNavigationEditor,
56
+ BelowEditorStripState,
57
+ } from "../shared/below-editor-navigation.ts";
58
+ import {
59
+ effectiveChildToolAllowlist,
60
+ resolveStandaloneChildProjectTrust,
61
+ } from "../shared/child-session.ts";
62
+ import { formatContextUtilization } from "../shared/context-utilization.ts";
63
+ import {
64
+ registerEditorLayer,
65
+ removeEditorLayer,
66
+ } from "../shared/editor-layers.ts";
67
+ import {
68
+ PLAN_MODE_CHANNEL,
69
+ type PlanModeState,
70
+ planModeAllowsDeclaredTools,
71
+ planModeChildTools,
72
+ } from "../shared/plan-mode-state.ts";
73
+ import {
74
+ allocateResultBudgets,
75
+ type ParentContextUsage,
76
+ } from "../shared/result-budget.ts";
77
+ import { type DetailDisplay, loadSetupConfig } from "../shared/setup-config.ts";
78
+ import {
79
+ OPENPI_TOOL_SURFACE,
80
+ patchOwnedTools,
81
+ } from "../shared/tool-surface.ts";
82
+ import {
83
+ projectSubagentCapability,
84
+ registerWebCapability,
85
+ } from "../shared/web-observer-registry.ts";
86
+ import {
87
+ createWorktree,
88
+ formatWorktreeCleanupWarning,
89
+ reclaimWorktree,
90
+ type Worktree,
91
+ } from "../shared/worktree.ts";
92
+ import {
93
+ normalizeSubagentTitle,
94
+ SubagentStripWidget,
95
+ selectSubagentStripEntry,
96
+ subagentStripEntryKey,
97
+ } from "./navigation.ts";
48
98
  import {
49
99
  formatAgentTypeDiagnostics,
50
100
  loadAgentTypes,
51
101
  roleModelForAgentType,
52
102
  selectSubagentModel,
53
- type AgentType,
54
103
  } from "./src/agent-types.ts";
55
104
  import { deriveBtwTitle, isModelVisible } from "./src/by-the-way.ts";
56
105
  import {
57
106
  BACKEND_NAMES,
58
107
  formatElapsed,
59
108
  latestText,
60
- REASONING_EFFORTS,
61
109
  type SubagentSnapshot,
62
110
  } from "./src/domain.ts";
63
111
  import {
64
- formatActivityStatus,
65
- hasActivity,
66
- unreadActivityCounts,
67
- } from "../shared/activity-status.ts";
68
- import {
69
- OPENPI_TOOL_SURFACE,
70
- patchOwnedTools,
71
- } from "../shared/tool-surface.ts";
72
- import {
73
- registerEditorLayer,
74
- removeEditorLayer,
75
- } from "../shared/editor-layers.ts";
76
- import { formatContextUtilization } from "./src/format.ts";
112
+ restoreSubagentIdCounters,
113
+ SUBAGENT_ID_WATERMARK_ENTRY_TYPE,
114
+ type SubagentIdCounters,
115
+ subagentIdWatermark,
116
+ } from "./src/id-sequence.ts";
77
117
  import { SubagentManager, type SubagentManagerShape } from "./src/manager.ts";
78
118
  import {
119
+ buildSubagentResultDisplayMessage,
79
120
  buildSubagentResultMessage,
80
- createAgentTypeParameterSchema,
81
121
  buildSubagentSendResult,
82
122
  buildSubagentSpawnResult,
83
- buildSubagentSpawnToolDescription,
123
+ createSubagentSpawnToolSurface,
84
124
  SUBAGENT_CANCEL_PARAMETER_DESCRIPTIONS,
85
125
  SUBAGENT_CANCEL_TOOL_DESCRIPTION,
86
126
  SUBAGENT_CHECK_PARAMETER_DESCRIPTIONS,
@@ -88,54 +128,40 @@ import {
88
128
  SUBAGENT_LIST_TOOL_DESCRIPTION,
89
129
  SUBAGENT_SEND_PARAMETER_DESCRIPTIONS,
90
130
  SUBAGENT_SEND_TOOL_DESCRIPTION,
91
- SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS,
92
131
  SUBAGENT_SPAWN_PROMPT_GUIDELINES,
93
132
  SUBAGENT_SPAWN_PROMPT_SNIPPET,
94
- SUBAGENT_SPAWN_TOOL_DESCRIPTION,
95
133
  SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS,
96
134
  SUBAGENT_WAIT_TOOL_DESCRIPTION,
135
+ stripSubagentResultTransportInstruction,
97
136
  } from "./src/prompt.ts";
98
- import { createSubagentResultDelivery } from "./src/result-delivery.ts";
99
137
  import {
100
- effectiveChildToolAllowlist,
101
- resolveStandaloneChildProjectTrust,
102
- } from "../shared/child-session.ts";
103
- import {
104
- BelowEditorNavigationEditor,
105
- BelowEditorStripState,
106
- } from "../shared/below-editor-navigation.ts";
107
- import { loadSetupConfig } from "../shared/setup-config.ts";
108
- import {
109
- PLAN_MODE_CHANNEL,
110
- planModeAllowsDeclaredTools,
111
- planModeChildTools,
112
- type PlanModeState,
113
- } from "../shared/plan-mode-state.ts";
114
- import {
115
- createWorktree,
116
- reclaimWorktree,
117
- type Worktree,
118
- } from "../shared/worktree.ts";
138
+ persistResultArtifact,
139
+ projectResult,
140
+ type ResultProjection,
141
+ } from "./src/result-artifact.ts";
142
+ import { createSubagentResultDelivery } from "./src/result-delivery.ts";
119
143
  import {
120
144
  createSubagentRuntime,
121
145
  runTool,
122
146
  type SubagentRuntime,
123
147
  } from "./src/runtime.ts";
124
- import {
125
- normalizeSubagentTitle,
126
- selectSubagentStripEntry,
127
- SubagentStripWidget,
128
- } from "./navigation.ts";
129
148
  import { openSubagentPicker, openSubagentTakeover } from "./src/ui/takeover.ts";
130
149
  import {
131
- buildWaitResultPreview,
132
150
  renderWaitResult,
151
+ renderWaitResultPreview,
133
152
  type WaitResultDetails,
134
153
  } from "./src/ui/wait-result.ts";
135
154
 
136
155
  const SUBAGENT_OUTPUT_MAX_BYTES = 24 * 1024;
156
+ const AUTOMATIC_OUTPUT_MAX_BYTES = 48 * 1024;
157
+ const AUTOMATIC_MIN_RESULT_BYTES = 2 * 1024;
137
158
  const WAIT_OUTPUT_MAX_BYTES = 48 * 1024;
138
159
  const WAIT_PER_AGENT_MAX_BYTES = 16 * 1024;
160
+ const WAIT_MIN_RESULT_BYTES = 512;
161
+ const RESULT_HEADROOM_SHARE = 0.5;
162
+ const ESTIMATED_BYTES_PER_TOKEN = 4;
163
+ const AUTOMATIC_BATCH_TRUNCATION_NOTICE =
164
+ "\n\n[Automatic subagent result batch truncated at the 48 KiB total limit.]";
139
165
 
140
166
  interface SpawnResultDetails {
141
167
  readonly id?: string;
@@ -156,12 +182,24 @@ interface SubagentResultDetails {
156
182
  readonly id?: string;
157
183
  readonly title?: string;
158
184
  readonly status?: SubagentSnapshot["status"];
185
+ readonly outcome?: SubagentSnapshot["outcome"];
186
+ readonly worktreeBranch?: string;
187
+ readonly elapsed?: string;
188
+ readonly artifactSaveFailed?: boolean;
189
+ readonly fullResultSaved?: boolean;
159
190
  readonly count?: number;
160
191
  readonly results?: ReadonlyArray<{
161
192
  readonly id: string;
162
193
  readonly title: string;
163
194
  readonly status: SubagentSnapshot["status"];
195
+ readonly outcome?: SubagentSnapshot["outcome"];
196
+ readonly worktreeBranch?: string;
197
+ readonly elapsed?: string;
198
+ readonly artifactSaveFailed?: boolean;
199
+ readonly fullResultSaved?: boolean;
164
200
  }>;
201
+ /** Display-only projection for the custom message renderer. */
202
+ readonly displayContent?: string;
165
203
  }
166
204
 
167
205
  interface SubagentResultEntryData {
@@ -189,56 +227,171 @@ function describeSubagent(snap: SubagentSnapshot) {
189
227
  return `${snap.id} [${snap.status}] "${snap.title}" (${details.join(", ")})`;
190
228
  }
191
229
 
192
- function truncatedOutput(
230
+ export function truncatedOutput(
193
231
  snap: SubagentSnapshot,
194
232
  maxBytes = SUBAGENT_OUTPUT_MAX_BYTES,
233
+ writeArtifact: (content: string) => string = (content) =>
234
+ persistResultArtifact(getAgentDir(), content),
195
235
  ): string {
196
236
  const output = snap.finalText || "(no output)";
197
- const truncation = truncateHead(output, {
237
+ return projectResult(output, {
238
+ maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
239
+ maxLines: Math.min(600, DEFAULT_MAX_LINES),
240
+ writeArtifact,
241
+ }).text;
242
+ }
243
+
244
+ function projectSubagentOutput(
245
+ snap: SubagentSnapshot,
246
+ maxBytes: number,
247
+ ): ResultProjection {
248
+ const output = snap.finalText || "(no output)";
249
+ return projectResult(output, {
198
250
  maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
199
251
  maxLines: Math.min(600, DEFAULT_MAX_LINES),
252
+ writeArtifact: (content) => persistResultArtifact(getAgentDir(), content),
200
253
  });
201
- let text = truncation.content;
202
- if (truncation.truncated) {
203
- text += `\n\n[Output truncated: ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)} shown. Full transcript in session file: ${snap.meta.sessionFilePath ?? "?"}]`;
204
- }
205
- return text;
254
+ }
255
+
256
+ type OutputProjection = Pick<
257
+ ResultProjection,
258
+ "text" | "artifactPath" | "artifactSaveFailed"
259
+ >;
260
+
261
+ function normalizeProjection(
262
+ output: string | OutputProjection,
263
+ ): OutputProjection {
264
+ return typeof output === "string" ? { text: output } : output;
265
+ }
266
+
267
+ function boundAutomaticResultBatch(content: string) {
268
+ const probe = truncateHead(content, {
269
+ maxBytes: AUTOMATIC_OUTPUT_MAX_BYTES,
270
+ maxLines: Number.MAX_SAFE_INTEGER,
271
+ });
272
+ if (!probe.truncated) return content;
273
+
274
+ const noticeBytes = Buffer.byteLength(
275
+ AUTOMATIC_BATCH_TRUNCATION_NOTICE,
276
+ "utf8",
277
+ );
278
+ const bounded = truncateHead(content, {
279
+ maxBytes: Math.max(0, AUTOMATIC_OUTPUT_MAX_BYTES - noticeBytes),
280
+ maxLines: Number.MAX_SAFE_INTEGER,
281
+ });
282
+ return `${bounded.content}${AUTOMATIC_BATCH_TRUNCATION_NOTICE}`;
206
283
  }
207
284
 
208
285
  export function createSubagentResultDispatcher(
209
286
  pi: ExtensionAPI,
210
- outputFor: (snap: SubagentSnapshot) => string = truncatedOutput,
287
+ outputFor: (
288
+ snap: SubagentSnapshot,
289
+ maxBytes: number,
290
+ ) => string | OutputProjection = projectSubagentOutput,
291
+ getContextUsage: () => ParentContextUsage | undefined = () => undefined,
211
292
  ) {
212
293
  return (snaps: readonly SubagentSnapshot[]) => {
213
294
  if (snaps.length === 0) return;
214
- const content = snaps
215
- .map((snap) =>
216
- buildSubagentResultMessage({
217
- id: snap.id,
218
- title: snap.title,
219
- status: snap.status,
220
- errorText: snap.errorText,
221
- output: outputFor(snap),
222
- }),
223
- )
224
- .join("\n\n");
295
+ const emptyMessages = snaps.map((snap) =>
296
+ buildSubagentResultMessage({
297
+ id: snap.id,
298
+ title: snap.title,
299
+ status: snap.status,
300
+ errorText: snap.errorText,
301
+ output: "",
302
+ }),
303
+ );
304
+ const wrapperBytes =
305
+ emptyMessages.reduce(
306
+ (sum, message) => sum + Buffer.byteLength(message, "utf8"),
307
+ 0,
308
+ ) +
309
+ Math.max(0, snaps.length - 1) * 2;
310
+ const projectionBatchBytes = Math.max(
311
+ 0,
312
+ AUTOMATIC_OUTPUT_MAX_BYTES - wrapperBytes,
313
+ );
314
+ const allocation = allocateResultBudgets(
315
+ snaps.map((snap) =>
316
+ Buffer.byteLength(snap.finalText || "(no output)", "utf8"),
317
+ ),
318
+ getContextUsage(),
319
+ {
320
+ maxBatchBytes: projectionBatchBytes,
321
+ maxResultBytes: SUBAGENT_OUTPUT_MAX_BYTES,
322
+ minResultBytes: AUTOMATIC_MIN_RESULT_BYTES,
323
+ headroomShare: RESULT_HEADROOM_SHARE,
324
+ estimatedBytesPerToken: ESTIMATED_BYTES_PER_TOKEN,
325
+ fixedBytes: wrapperBytes,
326
+ },
327
+ );
328
+ const projections = snaps.map((snap, index) =>
329
+ normalizeProjection(outputFor(snap, allocation.budgets[index]!)),
330
+ );
331
+ const outputs = projections.map((projection) => projection.text);
332
+ const displayContent = boundAutomaticResultBatch(
333
+ snaps
334
+ .map((snap, index) =>
335
+ buildSubagentResultDisplayMessage({
336
+ id: snap.id,
337
+ title: snap.title,
338
+ status: snap.status,
339
+ errorText: snap.errorText,
340
+ output: outputs[index]!,
341
+ }),
342
+ )
343
+ .join("\n\n"),
344
+ );
345
+ const content = boundAutomaticResultBatch(
346
+ snaps
347
+ .map((snap, index) =>
348
+ buildSubagentResultMessage({
349
+ id: snap.id,
350
+ title: snap.title,
351
+ status: snap.status,
352
+ errorText: snap.errorText,
353
+ output: outputs[index]!,
354
+ }),
355
+ )
356
+ .join("\n\n"),
357
+ );
225
358
  const details: SubagentResultDetails =
226
359
  snaps.length === 1
227
360
  ? {
228
361
  id: snaps[0]!.id,
229
362
  title: snaps[0]!.title,
230
363
  status: snaps[0]!.status,
364
+ ...(snaps[0]!.outcome ? { outcome: snaps[0]!.outcome } : {}),
365
+ ...(snaps[0]!.worktreeBranch
366
+ ? { worktreeBranch: snaps[0]!.worktreeBranch }
367
+ : {}),
368
+ elapsed: formatElapsed(snaps[0]!),
369
+ ...(projections[0]!.artifactPath ? { fullResultSaved: true } : {}),
370
+ ...(projections[0]!.artifactSaveFailed
371
+ ? { artifactSaveFailed: true }
372
+ : {}),
231
373
  }
232
374
  : {
233
375
  count: snaps.length,
234
- results: snaps.map((snap) => ({
376
+ results: snaps.map((snap, index) => ({
235
377
  id: snap.id,
236
378
  title: snap.title,
237
379
  status: snap.status,
380
+ ...(snap.outcome ? { outcome: snap.outcome } : {}),
381
+ ...(snap.worktreeBranch
382
+ ? { worktreeBranch: snap.worktreeBranch }
383
+ : {}),
384
+ elapsed: formatElapsed(snap),
385
+ ...(projections[index]!.artifactPath
386
+ ? { fullResultSaved: true }
387
+ : {}),
388
+ ...(projections[index]!.artifactSaveFailed
389
+ ? { artifactSaveFailed: true }
390
+ : {}),
238
391
  })),
239
392
  };
240
393
  pi.appendEntry<SubagentResultEntryData>("subagent-result", {
241
- content,
394
+ content: displayContent,
242
395
  details,
243
396
  });
244
397
  pi.sendMessage(
@@ -246,7 +399,7 @@ export function createSubagentResultDispatcher(
246
399
  customType: "subagent-result",
247
400
  content,
248
401
  display: false,
249
- details,
402
+ details: { ...details, displayContent },
250
403
  },
251
404
  { deliverAs: "followUp", triggerTurn: true },
252
405
  );
@@ -259,36 +412,52 @@ function renderSubagentResult(
259
412
  content: string,
260
413
  details: SubagentResultDetails,
261
414
  expanded: boolean,
415
+ resultDisplay: DetailDisplay,
262
416
  theme: SubagentResultTheme,
263
417
  ) {
264
- if (!expanded && loadSetupConfig().ui.subagentResultDisplay === "compact") {
265
- const results = details.results?.length
266
- ? details.results
267
- : details.id
268
- ? [
269
- {
270
- id: details.id,
271
- title: details.title,
272
- status: details.status,
273
- },
274
- ]
275
- : [];
276
- return new Text(buildWaitResultPreview(content, { results }, theme), 0, 0);
418
+ const displayContent = sanitizeText(
419
+ details.displayContent ?? stripSubagentResultTransportInstruction(content),
420
+ );
421
+ const results = details.results?.length
422
+ ? details.results
423
+ : details.id
424
+ ? [
425
+ {
426
+ id: details.id,
427
+ title: details.title,
428
+ status: details.status,
429
+ outcome: details.outcome,
430
+ worktreeBranch: details.worktreeBranch,
431
+ elapsed: details.elapsed,
432
+ artifactSaveFailed: details.artifactSaveFailed,
433
+ fullResultSaved: details.fullResultSaved,
434
+ },
435
+ ]
436
+ : [];
437
+ if (!expanded && resultDisplay === "compact") {
438
+ return renderWaitResultPreview(displayContent, { results }, theme);
277
439
  }
278
440
 
279
- const failed = details.status === "error";
280
- const icon = failed ? theme.fg("error", "x") : theme.fg("success", "■");
281
- const header =
282
- `${icon} ` +
283
- theme.fg("accent", theme.bold(`subagent ${details.id ?? "?"}`)) +
284
- theme.fg(
285
- "muted",
286
- ` · ${details.title ?? ""} · ${failed ? "failed" : "finished"}`,
287
- );
441
+ const failed = results.some((result) => result.status === "error");
442
+ const batched = results.length > 1;
443
+ const icon = failed ? theme.fg("error", "x") : theme.fg("success", "✓");
444
+ const header = batched
445
+ ? `${icon} ${theme.fg("accent", theme.bold(`${results.length} subagents`))}${theme.fg("muted", ` · ${failed ? `${results.filter((result) => result.status === "error").length} failed` : "finished"}`)}`
446
+ : `${icon} ` +
447
+ theme.fg(
448
+ "accent",
449
+ theme.bold(`subagent ${sanitizeText(details.id ?? "?")}`),
450
+ ) +
451
+ theme.fg(
452
+ "muted",
453
+ ` · ${sanitizeText(details.title ?? "")} · ${failed ? "failed" : "finished"}${details.elapsed ? ` · ${sanitizeText(details.elapsed)}` : ""}`,
454
+ );
288
455
 
289
- // Remove only the summary line. The following Error line (when present)
290
- // is part of the actual result and must remain visible.
291
- const body = content.split("\n").slice(1).join("\n").trim();
456
+ // Remove only the single-result summary line. Error lines and batched result
457
+ // summaries are part of the display projection and must remain visible.
458
+ const body = batched
459
+ ? displayContent.trim()
460
+ : displayContent.split("\n").slice(1).join("\n").trim();
292
461
  const md = new Markdown(body, 0, 0, getMarkdownTheme());
293
462
  const container = new Text(header, 0, 0);
294
463
  return {
@@ -303,9 +472,23 @@ function renderSubagentResult(
303
472
  };
304
473
  }
305
474
 
306
- export default function (pi: ExtensionAPI) {
475
+ interface SubagentExtensionOptions {
476
+ readonly getResultDisplay?: () => DetailDisplay;
477
+ }
478
+
479
+ export default function (
480
+ pi: ExtensionAPI,
481
+ options: SubagentExtensionOptions = {},
482
+ ) {
483
+ const getResultDisplay =
484
+ options.getResultDisplay ??
485
+ (() => loadSetupConfig().ui.subagentResultDisplay);
307
486
  let runtime: SubagentRuntime | undefined;
308
487
  let managerPromise: Promise<SubagentManagerShape> | undefined;
488
+ let restoredIdCounters: SubagentIdCounters = {
489
+ modelCounter: 0,
490
+ btwCounter: 0,
491
+ };
309
492
  let sessionContext: ExtensionContext | undefined;
310
493
  let ui: ExtensionUIContext | undefined;
311
494
  let unsubStatus: (() => void) | undefined;
@@ -315,13 +498,20 @@ export default function (pi: ExtensionAPI) {
315
498
  */
316
499
  let settledAcknowledgedAt = 0;
317
500
  const stripState = new BelowEditorStripState();
501
+ const statusWriter = createStatusWriter("subagents");
318
502
  const widgetKey = "subagent-navigation";
319
503
  let navigationManager: SubagentManagerShape | undefined;
504
+ let unregisterWebCapability: (() => void) | undefined;
320
505
  let widgetVisible = false;
506
+ let widgetEntryKey: string | undefined;
321
507
  let requestWidgetRender: (() => void) | undefined;
322
508
  let navigationLayerRegistered = false;
323
509
  let dashboardOpen = false;
324
- const dispatchResults = createSubagentResultDispatcher(pi);
510
+ const dispatchResults = createSubagentResultDispatcher(
511
+ pi,
512
+ projectSubagentOutput,
513
+ () => sessionContext?.getContextUsage(),
514
+ );
325
515
  const resultDelivery = createSubagentResultDelivery<SubagentSnapshot>({
326
516
  isIdle: () => sessionContext?.isIdle() === true,
327
517
  // Every unconsumed fire-and-forget result must reach the parent. The
@@ -329,23 +519,36 @@ export default function (pi: ExtensionAPI) {
329
519
  deliver: dispatchResults,
330
520
  });
331
521
  pi.on("agent_settled", () => resultDelivery.parentSettled());
332
- const hideLifecycleTools = () =>
522
+ const registerStableToolFamily = () =>
333
523
  patchOwnedTools(pi, "subagents", {
334
- disable: OPENPI_TOOL_SURFACE.subagents.deferred,
335
- });
336
- const showLifecycleTools = () =>
337
- patchOwnedTools(pi, "subagents", {
338
- enable: OPENPI_TOOL_SURFACE.subagents.deferred,
524
+ enable: OPENPI_TOOL_SURFACE.subagents.entry,
339
525
  });
340
526
 
341
- const getRuntime = () => (runtime ??= createSubagentRuntime());
527
+ const getRuntime = () =>
528
+ (runtime ??= createSubagentRuntime({
529
+ initialModelCounter: restoredIdCounters.modelCounter,
530
+ initialBtwCounter: restoredIdCounters.btwCounter,
531
+ }));
532
+
533
+ const persistId = (id: string) =>
534
+ pi.appendEntry(SUBAGENT_ID_WATERMARK_ENTRY_TYPE, subagentIdWatermark(id));
342
535
 
343
536
  /** Resolve the manager service once per runtime and wire the extension hooks. */
344
537
  const getManager = () => {
538
+ const scope = sessionContext?.sessionManager;
345
539
  managerPromise ??= getRuntime()
346
540
  .runPromise(SubagentManager)
347
541
  .then((manager) => {
348
542
  navigationManager = manager;
543
+ unregisterWebCapability?.();
544
+ unregisterWebCapability =
545
+ scope && sessionContext?.sessionManager === scope
546
+ ? registerWebCapability(scope, {
547
+ kind: "subagents",
548
+ snapshot: () => projectSubagentCapability(manager.view.list()),
549
+ subscribe: (listener) => manager.view.subscribe(listener),
550
+ })
551
+ : undefined;
349
552
  manager.view.setOnSettled(onSettled);
350
553
  unsubStatus?.();
351
554
  unsubStatus = manager.view.subscribe(() => updateStatus(manager));
@@ -366,10 +569,19 @@ export default function (pi: ExtensionAPI) {
366
569
  const updateSubagentWidget = () => {
367
570
  const ctx = sessionContext;
368
571
  if (!ctx || ctx.mode !== "tui") return;
369
- const visible = Boolean(stripEntry());
370
- if (visible === widgetVisible) return;
572
+ const entry = stripEntry();
573
+ const visible = Boolean(entry);
574
+ const entryKey = subagentStripEntryKey(entry);
575
+ if (visible === widgetVisible) {
576
+ if (visible && entryKey !== widgetEntryKey) {
577
+ widgetEntryKey = entryKey;
578
+ requestWidgetRender?.();
579
+ }
580
+ return;
581
+ }
371
582
  if (!visible) {
372
583
  stripState.focused = false;
584
+ widgetEntryKey = undefined;
373
585
  requestWidgetRender = undefined;
374
586
  ctx.ui.setWidget(widgetKey, undefined);
375
587
  widgetVisible = false;
@@ -384,6 +596,7 @@ export default function (pi: ExtensionAPI) {
384
596
  { placement: "belowEditor" },
385
597
  );
386
598
  widgetVisible = true;
599
+ widgetEntryKey = entryKey;
387
600
  };
388
601
 
389
602
  const updateStatus = (manager: SubagentManagerShape) => {
@@ -392,9 +605,12 @@ export default function (pi: ExtensionAPI) {
392
605
  manager.view.list(),
393
606
  settledAcknowledgedAt,
394
607
  );
395
- ui.setStatus(
396
- "subagents",
397
- hasActivity(counts)
608
+ // In the TUI the below-editor strip already reports the same activity and
609
+ // carries the manage affordance, so a footer status line would repeat it.
610
+ const tui = sessionContext?.mode === "tui";
611
+ statusWriter.write(
612
+ ui,
613
+ !tui && hasActivity(counts)
398
614
  ? formatActivityStatus(ui.theme, "subagents", counts)
399
615
  : undefined,
400
616
  );
@@ -493,8 +709,11 @@ export default function (pi: ExtensionAPI) {
493
709
  };
494
710
 
495
711
  pi.on("session_start", (_event, ctx) => {
712
+ restoredIdCounters = restoreSubagentIdCounters(
713
+ ctx.sessionManager.getBranch(),
714
+ );
496
715
  refreshAgentTypes(ctx.cwd, ctx.isProjectTrusted());
497
- hideLifecycleTools();
716
+ registerStableToolFamily();
498
717
  sessionContext = ctx;
499
718
  settledAcknowledgedAt = 0;
500
719
  if (ctx.hasUI) ui = ctx.ui;
@@ -524,16 +743,20 @@ export default function (pi: ExtensionAPI) {
524
743
  resultDelivery.clear();
525
744
  unsubStatus?.();
526
745
  unsubStatus = undefined;
746
+ unregisterWebCapability?.();
747
+ unregisterWebCapability = undefined;
527
748
  try {
528
749
  ui?.setStatus("subagents", undefined);
529
750
  sessionContext?.ui.setWidget(widgetKey, undefined);
530
751
  } catch {
531
752
  // UI may already be disposed.
532
753
  }
754
+ statusWriter.reset();
533
755
  sessionContext = undefined;
534
756
  ui = undefined;
535
757
  navigationManager = undefined;
536
758
  widgetVisible = false;
759
+ widgetEntryKey = undefined;
537
760
  requestWidgetRender = undefined;
538
761
  stripState.focused = false;
539
762
  dashboardOpen = false;
@@ -592,57 +815,22 @@ export default function (pi: ExtensionAPI) {
592
815
  agentTypes = loaded.agentTypes;
593
816
  agentTypeDiagnostics = loaded.diagnostics;
594
817
  agentTypeList = [...agentTypes.values()];
595
- subagentSpawnTool.description =
596
- buildSubagentSpawnToolDescription(agentTypeList);
597
- subagentSpawnTool.parameters = createSubagentSpawnParameters();
818
+ const surface = createSubagentSpawnToolSurface(agentTypeList);
819
+ subagentSpawnTool.description = surface.description;
820
+ subagentSpawnTool.parameters = surface.parameters;
598
821
  registerSubagentSpawnTool();
599
822
  };
600
823
 
601
824
  // --- Tools -------------------------------------------------------------
602
825
 
603
- const createSubagentSpawnParameters = () =>
604
- Type.Object({
605
- agent_type: createAgentTypeParameterSchema(agentTypeList),
606
- prompt: Type.String({
607
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.prompt,
608
- }),
609
- name: Type.String({
610
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.name,
611
- }),
612
- harness: Type.Optional(
613
- StringEnum(BACKEND_NAMES, {
614
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.harness,
615
- }),
616
- ),
617
- working_dir: Type.Optional(
618
- Type.String({
619
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.workingDir,
620
- }),
621
- ),
622
- isolation: Type.Optional(
623
- StringEnum(["worktree"] as const, {
624
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.isolation,
625
- }),
626
- ),
627
- model: Type.Optional(
628
- Type.String({
629
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.model,
630
- }),
631
- ),
632
- reasoning_effort: Type.Optional(
633
- StringEnum(REASONING_EFFORTS, {
634
- description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort,
635
- }),
636
- ),
637
- });
826
+ const initialSpawnSurface = createSubagentSpawnToolSurface(agentTypeList);
638
827
 
639
828
  const subagentSpawnTool = defineTool({
640
829
  name: "subagent_spawn",
641
830
  label: "Spawn Subagent",
642
- description: buildSubagentSpawnToolDescription(agentTypeList),
831
+ ...initialSpawnSurface,
643
832
  promptSnippet: SUBAGENT_SPAWN_PROMPT_SNIPPET,
644
833
  promptGuidelines: SUBAGENT_SPAWN_PROMPT_GUIDELINES,
645
- parameters: createSubagentSpawnParameters(),
646
834
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
647
835
  // Only one backend exists; harness is optional and defaults to it.
648
836
  const harness = params.harness ?? BACKEND_NAMES[0];
@@ -764,11 +952,40 @@ export default function (pi: ExtensionAPI) {
764
952
  } catch (error) {
765
953
  // The session scope owns reclamation, but it never opened, so this
766
954
  // worktree would otherwise be orphaned on disk.
767
- if (worktree) await reclaimWorktree(cwd, worktree).catch(() => {});
955
+ if (worktree) {
956
+ const spawnError =
957
+ error instanceof Error ? error.message : String(error);
958
+ let cleanupWarning: string | undefined;
959
+ let cleanupError: unknown;
960
+ try {
961
+ const cleanup = await reclaimWorktree(cwd, worktree);
962
+ cleanupWarning = formatWorktreeCleanupWarning(
963
+ cleanup,
964
+ worktree.path,
965
+ );
966
+ } catch (failure) {
967
+ cleanupError = failure;
968
+ }
969
+ if (cleanupError) {
970
+ const reason =
971
+ cleanupError instanceof Error
972
+ ? cleanupError.message
973
+ : String(cleanupError);
974
+ throw new Error(
975
+ `${spawnError}; worktree cleanup failed: ${reason}; checkout preserved at ${worktree.path}`,
976
+ { cause: error },
977
+ );
978
+ }
979
+ if (cleanupWarning) {
980
+ throw new Error(
981
+ `${spawnError}; worktree cleanup warning: ${cleanupWarning}`,
982
+ { cause: error },
983
+ );
984
+ }
985
+ }
768
986
  throw error;
769
987
  }
770
-
771
- showLifecycleTools();
988
+ persistId(snap.id);
772
989
 
773
990
  return {
774
991
  content: [
@@ -813,8 +1030,11 @@ export default function (pi: ExtensionAPI) {
813
1030
  const meta = [details.harness, details.model]
814
1031
  .filter(Boolean)
815
1032
  .join(" \u00b7 ");
1033
+ // A spawn is an event, not a state, so it speaks in the activity rows'
1034
+ // verb language ("Wrote" / "Ran" / "Spawned") rather than a glyph; the
1035
+ // strip's spinner carries the running state from here on.
816
1036
  return new Text(
817
- `${theme.fg("success", "\u25cf")} ${theme.bold(details.title ?? details.id)} ${theme.fg("dim", meta)}`,
1037
+ `${theme.fg("toolTitle", "Spawned")} ${theme.bold(details.title ?? details.id)} ${theme.fg("dim", meta)}`,
818
1038
  0,
819
1039
  0,
820
1040
  );
@@ -834,7 +1054,7 @@ export default function (pi: ExtensionAPI) {
834
1054
  description: SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS.ids,
835
1055
  }),
836
1056
  }),
837
- async execute(_toolCallId, params, signal, onUpdate) {
1057
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
838
1058
  const manager = await getManager();
839
1059
  const ids = [...new Set(params.ids)];
840
1060
  if (ids.length === 0)
@@ -870,33 +1090,71 @@ export default function (pi: ExtensionAPI) {
870
1090
  // deferred automatic delivery now that the tool is returning the result.
871
1091
  resultDelivery.consume(ids);
872
1092
 
873
- const sections: string[] = [];
874
- let remainingBytes = WAIT_OUTPUT_MAX_BYTES;
875
- for (const id of ids) {
1093
+ const entries: Array<
1094
+ | { readonly id: string; readonly section: string }
1095
+ | {
1096
+ readonly id: string;
1097
+ readonly snap: SubagentSnapshot;
1098
+ readonly header: string;
1099
+ }
1100
+ > = ids.map((id) => {
876
1101
  const snap = manager.view.get(id);
877
- if (!snap) {
878
- sections.push(`## ${id}\n\n(no longer tracked)`);
879
- continue;
880
- }
1102
+ if (!snap) return { id, section: `## ${id}\n\n(no longer tracked)` };
881
1103
  const verb = snap.status === "error" ? "failed" : "finished";
882
- let section = `## ${snap.id} "${snap.title}" ${verb}`;
883
- if (snap.errorText) section += `\nError: ${snap.errorText}`;
884
- const headerBytes = Buffer.byteLength(section, "utf8") + 2;
885
- const outputBudget = Math.max(
886
- 512,
887
- Math.min(WAIT_PER_AGENT_MAX_BYTES, remainingBytes - headerBytes),
1104
+ let header = `## ${snap.id} "${snap.title}" ${verb}`;
1105
+ if (snap.errorText) header += `\nError: ${snap.errorText}`;
1106
+ return { id, snap, header };
1107
+ });
1108
+ const separatorsBytes = Math.max(0, entries.length - 1) * 7;
1109
+ const fixedBytes =
1110
+ separatorsBytes +
1111
+ entries.reduce(
1112
+ (sum, entry) =>
1113
+ sum +
1114
+ Buffer.byteLength(
1115
+ "section" in entry ? entry.section : `${entry.header}\n\n`,
1116
+ "utf8",
1117
+ ),
1118
+ 0,
888
1119
  );
889
- section += `\n\n${truncatedOutput(snap, outputBudget)}`;
890
- const sectionBytes = Buffer.byteLength(section, "utf8");
891
- if (sectionBytes > remainingBytes) {
892
- sections.push(
893
- `## ${snap.id} "${snap.title}"\n\n[omitted: total wait output limit reached]`,
894
- );
895
- break;
896
- }
897
- sections.push(section);
898
- remainingBytes -= sectionBytes;
899
- }
1120
+ const resultEntries = entries.filter(
1121
+ (
1122
+ entry,
1123
+ ): entry is {
1124
+ readonly id: string;
1125
+ readonly snap: SubagentSnapshot;
1126
+ readonly header: string;
1127
+ } => "snap" in entry,
1128
+ );
1129
+ const projectionBatchBytes = Math.max(
1130
+ WAIT_MIN_RESULT_BYTES * resultEntries.length,
1131
+ WAIT_OUTPUT_MAX_BYTES - fixedBytes,
1132
+ );
1133
+ const allocation = allocateResultBudgets(
1134
+ resultEntries.map(({ snap }) =>
1135
+ Buffer.byteLength(snap.finalText || "(no output)", "utf8"),
1136
+ ),
1137
+ ctx.getContextUsage(),
1138
+ {
1139
+ maxBatchBytes: projectionBatchBytes,
1140
+ maxResultBytes: WAIT_PER_AGENT_MAX_BYTES,
1141
+ minResultBytes: WAIT_MIN_RESULT_BYTES,
1142
+ headroomShare: RESULT_HEADROOM_SHARE,
1143
+ estimatedBytesPerToken: ESTIMATED_BYTES_PER_TOKEN,
1144
+ fixedBytes,
1145
+ },
1146
+ );
1147
+ let resultIndex = 0;
1148
+ const artifactSaveFailures = new Set<string>();
1149
+ const fullResultsSaved = new Set<string>();
1150
+ const sections = entries.map((entry) => {
1151
+ if ("section" in entry) return entry.section;
1152
+ const outputBudget = allocation.budgets[resultIndex++]!;
1153
+ const projection = projectSubagentOutput(entry.snap, outputBudget);
1154
+ if (projection.artifactSaveFailed) artifactSaveFailures.add(entry.id);
1155
+ if (projection.artifactPath) fullResultsSaved.add(entry.id);
1156
+ return `${entry.header}\n\n${projection.text}`;
1157
+ });
900
1158
 
901
1159
  const combined = sections.join("\n\n---\n\n");
902
1160
  const bounded = truncateHead(combined, {
@@ -911,7 +1169,20 @@ export default function (pi: ExtensionAPI) {
911
1169
  details: {
912
1170
  results: ids.map((id) => {
913
1171
  const snap = manager.view.get(id);
914
- return { id, title: snap?.title, status: snap?.status };
1172
+ return {
1173
+ id,
1174
+ title: snap?.title,
1175
+ status: snap?.status,
1176
+ ...(snap?.outcome ? { outcome: snap.outcome } : {}),
1177
+ ...(snap?.worktreeBranch
1178
+ ? { worktreeBranch: snap.worktreeBranch }
1179
+ : {}),
1180
+ ...(snap ? { elapsed: formatElapsed(snap) } : {}),
1181
+ ...(fullResultsSaved.has(id) ? { fullResultSaved: true } : {}),
1182
+ ...(artifactSaveFailures.has(id)
1183
+ ? { artifactSaveFailed: true }
1184
+ : {}),
1185
+ };
915
1186
  }),
916
1187
  },
917
1188
  };
@@ -936,7 +1207,7 @@ export default function (pi: ExtensionAPI) {
936
1207
  return renderWaitResult(
937
1208
  content,
938
1209
  result.details as WaitResultDetails | undefined,
939
- expanded || loadSetupConfig().ui.subagentResultDisplay === "full",
1210
+ expanded || getResultDisplay() === "full",
940
1211
  theme,
941
1212
  );
942
1213
  },
@@ -1131,6 +1402,7 @@ export default function (pi: ExtensionAPI) {
1131
1402
  content,
1132
1403
  (message.details ?? {}) as SubagentResultDetails,
1133
1404
  expanded,
1405
+ getResultDisplay(),
1134
1406
  theme,
1135
1407
  );
1136
1408
  },
@@ -1143,6 +1415,7 @@ export default function (pi: ExtensionAPI) {
1143
1415
  entry.data?.content ?? "",
1144
1416
  entry.data?.details ?? {},
1145
1417
  expanded,
1418
+ getResultDisplay(),
1146
1419
  theme,
1147
1420
  ),
1148
1421
  );
@@ -1152,11 +1425,12 @@ export default function (pi: ExtensionAPI) {
1152
1425
  (entry, _options, theme) => {
1153
1426
  const data = entry.data;
1154
1427
  const failed = data?.status === "error";
1428
+ const icon = failed ? theme.fg("error", "x") : theme.fg("success", "✓");
1155
1429
  return new Text(
1156
- `${theme.fg(failed ? "error" : "success", "\u25cf")} ` +
1430
+ `${icon} ${theme.fg("accent", data?.title ?? "?")}` +
1157
1431
  theme.fg(
1158
- "muted",
1159
- `Agent "${data?.title ?? "?"}" ${failed ? "failed" : "finished"} \u00b7 ${data?.elapsed ?? "?"}`,
1432
+ "dim",
1433
+ ` ${failed ? "failed" : "finished"} · ${data?.elapsed ?? "?"}`,
1160
1434
  ),
1161
1435
  1,
1162
1436
  0,
@@ -1169,7 +1443,7 @@ export default function (pi: ExtensionAPI) {
1169
1443
  (entry, { expanded }, theme) => {
1170
1444
  const data = entry.data;
1171
1445
  const failed = data?.status === "error";
1172
- const icon = failed ? theme.fg("error", "x") : theme.fg("success", "");
1446
+ const icon = failed ? theme.fg("error", "x") : theme.fg("success", "");
1173
1447
  const header =
1174
1448
  `${icon} ` +
1175
1449
  theme.fg("accent", theme.bold(`by the way · ${data?.title ?? "?"}`)) +
@@ -1184,7 +1458,7 @@ export default function (pi: ExtensionAPI) {
1184
1458
  .filter(Boolean)
1185
1459
  .join("\n\n");
1186
1460
 
1187
- if (expanded || loadSetupConfig().ui.subagentResultDisplay === "full") {
1461
+ if (expanded || getResultDisplay() === "full") {
1188
1462
  const md = new Markdown(body, 0, 0, getMarkdownTheme());
1189
1463
  const container = new Text(header, 0, 0);
1190
1464
  return {
@@ -1253,6 +1527,7 @@ export default function (pi: ExtensionAPI) {
1253
1527
  );
1254
1528
  return;
1255
1529
  }
1530
+ persistId(snap.id);
1256
1531
 
1257
1532
  await openSubagentTakeover(ctx, manager.view, snap.id, {
1258
1533
  badge: "by the way",