@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.
Files changed (141) hide show
  1. package/README.md +116 -46
  2. package/SETUP.md +29 -7
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-launch-card-v1.webp +0 -0
  5. package/bin/openpi.js +155 -0
  6. package/extensions/ai-providers/LICENSE.upstream +23 -0
  7. package/extensions/ai-providers/README.md +59 -0
  8. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  9. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  10. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  11. package/extensions/ai-providers/antigravity/models.ts +84 -0
  12. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  13. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  14. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  15. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  16. package/extensions/ai-providers/cursor/constants.ts +5 -0
  17. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  18. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  19. package/extensions/ai-providers/cursor/input-images.ts +106 -0
  20. package/extensions/ai-providers/cursor/models.ts +45 -0
  21. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  22. package/extensions/ai-providers/cursor/proto.ts +1064 -0
  23. package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
  24. package/extensions/ai-providers/cursor/provider.ts +1175 -0
  25. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  26. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  27. package/extensions/ai-providers/index.ts +86 -0
  28. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  29. package/extensions/ai-providers/usage.ts +10 -0
  30. package/extensions/background-terminals/index.ts +38 -3
  31. package/extensions/background-terminals/src/domain.ts +2 -0
  32. package/extensions/background-terminals/src/manager.ts +484 -106
  33. package/extensions/background-terminals/src/output.ts +33 -0
  34. package/extensions/background-terminals/src/prompt.ts +13 -5
  35. package/extensions/background-terminals/src/result-delivery.ts +47 -24
  36. package/extensions/clear-context/index.ts +83 -0
  37. package/extensions/context-pivot/index.ts +16 -6
  38. package/extensions/cron/index.ts +68 -27
  39. package/extensions/cron/schedule.ts +12 -2
  40. package/extensions/file-mutation-display/render.ts +17 -257
  41. package/extensions/file-search/src/binaries.ts +57 -41
  42. package/extensions/git-read/index.ts +1 -3
  43. package/extensions/model-info/cache-diagnostics.ts +220 -0
  44. package/extensions/model-info/index.ts +65 -33
  45. package/extensions/model-info/session-metrics.ts +96 -0
  46. package/extensions/plan-mode/bash-policy.ts +54 -9
  47. package/extensions/plan-mode/index.ts +82 -6
  48. package/extensions/post-edit/index.ts +16 -6
  49. package/extensions/sessions/git-stats.ts +258 -72
  50. package/extensions/sessions/index.ts +153 -86
  51. package/extensions/sessions/preview-cache.ts +104 -0
  52. package/extensions/sessions/preview-loader.ts +856 -0
  53. package/extensions/sessions/sessions.ts +43 -4
  54. package/extensions/setup/index.ts +138 -130
  55. package/extensions/shared/activity-status.ts +30 -0
  56. package/extensions/shared/agent-session-page.ts +319 -0
  57. package/extensions/shared/agent-tool-renderer.ts +218 -0
  58. package/extensions/shared/agent-transcript.ts +524 -0
  59. package/extensions/shared/capability-intent.ts +1 -1
  60. package/extensions/shared/child-session.ts +457 -21
  61. package/extensions/shared/completion-inbox.ts +193 -0
  62. package/extensions/shared/result-delivery.ts +34 -0
  63. package/extensions/shared/setup-config.ts +83 -34
  64. package/extensions/shared/setup-episode-state.ts +1 -1
  65. package/extensions/shared/structured-output.ts +154 -0
  66. package/extensions/shared/terminal-text.ts +110 -23
  67. package/extensions/shared/text-projection.ts +72 -15
  68. package/extensions/shared/tool-activity.ts +382 -0
  69. package/extensions/shared/tool-surface.ts +29 -2
  70. package/extensions/shared/transcript-viewport.ts +46 -0
  71. package/extensions/shared/web-observer-registry.ts +390 -0
  72. package/extensions/shared/worktree.ts +11 -0
  73. package/extensions/subagents/index.ts +313 -62
  74. package/extensions/subagents/navigation.ts +34 -5
  75. package/extensions/subagents/src/backend.ts +12 -1
  76. package/extensions/subagents/src/backends/pi.ts +450 -70
  77. package/extensions/subagents/src/domain.ts +21 -1
  78. package/extensions/subagents/src/manager.ts +39 -2
  79. package/extensions/subagents/src/prompt.ts +49 -7
  80. package/extensions/subagents/src/result-artifact.ts +36 -0
  81. package/extensions/subagents/src/result-delivery.ts +39 -14
  82. package/extensions/subagents/src/runtime.ts +15 -1
  83. package/extensions/subagents/src/ui/takeover.ts +73 -257
  84. package/extensions/subagents/src/ui/transcript.ts +38 -535
  85. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  86. package/extensions/suggestions/src/ui.ts +10 -4
  87. package/extensions/tasks/index.ts +0 -3
  88. package/extensions/ui-customization/footer.ts +16 -45
  89. package/extensions/ui-customization/index.ts +0 -4
  90. package/extensions/user-input-fold/index.ts +42 -6
  91. package/extensions/web/index.ts +257 -0
  92. package/extensions/workflows/acceptance.ts +43 -19
  93. package/extensions/workflows/artifacts.ts +137 -47
  94. package/extensions/workflows/completion-projection.ts +459 -0
  95. package/extensions/workflows/coordinator.ts +8 -10
  96. package/extensions/workflows/dashboard.ts +175 -228
  97. package/extensions/workflows/handoff.ts +70 -16
  98. package/extensions/workflows/index.ts +501 -198
  99. package/extensions/workflows/journal.ts +148 -13
  100. package/extensions/workflows/model.ts +79 -5
  101. package/extensions/workflows/navigation.ts +32 -8
  102. package/extensions/workflows/progress-projection.ts +306 -0
  103. package/extensions/workflows/prompt.ts +70 -16
  104. package/extensions/workflows/replay-safety.ts +42 -21
  105. package/extensions/workflows/result-delivery.ts +214 -76
  106. package/extensions/workflows/retention.ts +599 -0
  107. package/extensions/workflows/runner.ts +389 -345
  108. package/extensions/workflows/sandbox-child.cjs +25 -3
  109. package/extensions/workflows/sandbox.ts +62 -8
  110. package/extensions/workflows/serialization.ts +325 -17
  111. package/extensions/workflows/tool-renderer.ts +22 -0
  112. package/extensions/workflows/transcript.ts +149 -0
  113. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  114. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  115. package/package.json +34 -14
  116. package/skills/subagents/REFERENCE.md +190 -0
  117. package/skills/subagents/SKILL.md +2 -1
  118. package/skills/workflows/REFERENCE.md +6 -4
  119. package/skills/workflows/SKILL.md +1 -1
  120. package/web/adapter/pi-adapter.ts +664 -0
  121. package/web/host/browser-launcher.ts +20 -0
  122. package/web/host/pi-coding-agent-entry.ts +162 -0
  123. package/web/host/static-assets.ts +4 -0
  124. package/web/host/terminal-status.ts +38 -0
  125. package/web/host/web-host.ts +1069 -0
  126. package/web/http-dispatcher.ts +125 -0
  127. package/web/protocol/types.ts +467 -0
  128. package/web/runtime/pi-runtime.ts +1206 -0
  129. package/web/runtime/types.ts +102 -0
  130. package/web/runtime/web-host-lease.ts +497 -0
  131. package/web/trace.ts +18 -0
  132. package/web/ui/app.js +1700 -0
  133. package/web/ui/index.html +142 -0
  134. package/web/ui/styles.css +680 -0
  135. package/web/vite.config.mjs +34 -0
  136. package/extensions/execution-convergence/active-evidence.ts +0 -129
  137. package/extensions/execution-convergence/index.ts +0 -442
  138. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  139. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  140. package/extensions/setup/intercom.ts +0 -603
  141. package/extensions/subagents/src/backends/stub.ts +0 -303
@@ -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
@@ -44,10 +45,12 @@ import {
44
45
  import { Markdown, Text } from "@earendil-works/pi-tui";
45
46
  import { Type } from "typebox";
46
47
  import {
48
+ createStatusWriter,
47
49
  formatActivityStatus,
48
50
  hasActivity,
49
51
  unreadActivityCounts,
50
52
  } from "../shared/activity-status.ts";
53
+ import { sanitizeText } from "../shared/agent-transcript.ts";
51
54
  import {
52
55
  BelowEditorNavigationEditor,
53
56
  BelowEditorStripState,
@@ -57,6 +60,7 @@ import {
57
60
  resolveStandaloneChildProjectTrust,
58
61
  } from "../shared/child-session.ts";
59
62
  import { formatContextUtilization } from "../shared/context-utilization.ts";
63
+ import { completionOwnerFor } from "../shared/completion-inbox.ts";
60
64
  import {
61
65
  registerEditorLayer,
62
66
  removeEditorLayer,
@@ -67,13 +71,22 @@ import {
67
71
  planModeAllowsDeclaredTools,
68
72
  planModeChildTools,
69
73
  } from "../shared/plan-mode-state.ts";
70
- import { loadSetupConfig } from "../shared/setup-config.ts";
74
+ import {
75
+ allocateResultBudgets,
76
+ type ParentContextUsage,
77
+ } from "../shared/result-budget.ts";
78
+ import { type DetailDisplay, loadSetupConfig } from "../shared/setup-config.ts";
71
79
  import {
72
80
  OPENPI_TOOL_SURFACE,
73
81
  patchOwnedTools,
74
82
  } from "../shared/tool-surface.ts";
83
+ import {
84
+ projectSubagentCapability,
85
+ registerWebCapability,
86
+ } from "../shared/web-observer-registry.ts";
75
87
  import {
76
88
  createWorktree,
89
+ formatWorktreeCleanupWarning,
77
90
  reclaimWorktree,
78
91
  type Worktree,
79
92
  } from "../shared/worktree.ts";
@@ -81,9 +94,9 @@ import {
81
94
  normalizeSubagentTitle,
82
95
  SubagentStripWidget,
83
96
  selectSubagentStripEntry,
97
+ subagentStripEntryKey,
84
98
  } from "./navigation.ts";
85
99
  import {
86
- type AgentType,
87
100
  formatAgentTypeDiagnostics,
88
101
  loadAgentTypes,
89
102
  roleModelForAgentType,
@@ -104,6 +117,7 @@ import {
104
117
  } from "./src/id-sequence.ts";
105
118
  import { SubagentManager, type SubagentManagerShape } from "./src/manager.ts";
106
119
  import {
120
+ buildSubagentResultDisplayMessage,
107
121
  buildSubagentResultMessage,
108
122
  buildSubagentSendResult,
109
123
  buildSubagentSpawnResult,
@@ -119,12 +133,13 @@ import {
119
133
  SUBAGENT_SPAWN_PROMPT_SNIPPET,
120
134
  SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS,
121
135
  SUBAGENT_WAIT_TOOL_DESCRIPTION,
136
+ stripSubagentResultTransportInstruction,
122
137
  } from "./src/prompt.ts";
123
- import { persistResultArtifact, projectResult } from "./src/result-artifact.ts";
124
138
  import {
125
- allocateResultBudgets,
126
- type ParentContextUsage,
127
- } from "../shared/result-budget.ts";
139
+ persistResultArtifact,
140
+ projectResult,
141
+ type ResultProjection,
142
+ } from "./src/result-artifact.ts";
128
143
  import { createSubagentResultDelivery } from "./src/result-delivery.ts";
129
144
  import {
130
145
  createSubagentRuntime,
@@ -133,8 +148,8 @@ import {
133
148
  } from "./src/runtime.ts";
134
149
  import { openSubagentPicker, openSubagentTakeover } from "./src/ui/takeover.ts";
135
150
  import {
136
- buildWaitResultPreview,
137
151
  renderWaitResult,
152
+ renderWaitResultPreview,
138
153
  type WaitResultDetails,
139
154
  } from "./src/ui/wait-result.ts";
140
155
 
@@ -146,6 +161,8 @@ const WAIT_PER_AGENT_MAX_BYTES = 16 * 1024;
146
161
  const WAIT_MIN_RESULT_BYTES = 512;
147
162
  const RESULT_HEADROOM_SHARE = 0.5;
148
163
  const ESTIMATED_BYTES_PER_TOKEN = 4;
164
+ const AUTOMATIC_BATCH_TRUNCATION_NOTICE =
165
+ "\n\n[Automatic subagent result batch truncated at the 48 KiB total limit.]";
149
166
 
150
167
  interface SpawnResultDetails {
151
168
  readonly id?: string;
@@ -153,6 +170,7 @@ interface SpawnResultDetails {
153
170
  readonly harness?: string;
154
171
  readonly model?: string;
155
172
  readonly agentType?: string;
173
+ readonly structured?: boolean;
156
174
  }
157
175
 
158
176
  interface SubagentFinishedData {
@@ -166,12 +184,28 @@ interface SubagentResultDetails {
166
184
  readonly id?: string;
167
185
  readonly title?: string;
168
186
  readonly status?: SubagentSnapshot["status"];
187
+ readonly outcome?: SubagentSnapshot["outcome"];
188
+ readonly worktreeBranch?: string;
189
+ readonly elapsed?: string;
190
+ readonly artifactSaveFailed?: boolean;
191
+ readonly fullResultSaved?: boolean;
192
+ readonly structured?: unknown;
193
+ readonly structuredArtifactPath?: string;
169
194
  readonly count?: number;
170
195
  readonly results?: ReadonlyArray<{
171
196
  readonly id: string;
172
197
  readonly title: string;
173
198
  readonly status: SubagentSnapshot["status"];
199
+ readonly outcome?: SubagentSnapshot["outcome"];
200
+ readonly worktreeBranch?: string;
201
+ readonly elapsed?: string;
202
+ readonly artifactSaveFailed?: boolean;
203
+ readonly fullResultSaved?: boolean;
204
+ readonly structured?: unknown;
205
+ readonly structuredArtifactPath?: string;
174
206
  }>;
207
+ /** Display-only projection for the custom message renderer. */
208
+ readonly displayContent?: string;
175
209
  }
176
210
 
177
211
  interface SubagentResultEntryData {
@@ -199,13 +233,17 @@ function describeSubagent(snap: SubagentSnapshot) {
199
233
  return `${snap.id} [${snap.status}] "${snap.title}" (${details.join(", ")})`;
200
234
  }
201
235
 
236
+ function subagentResultContent(snap: SubagentSnapshot) {
237
+ return snap.structuredResult?.json ?? (snap.finalText || "(no output)");
238
+ }
239
+
202
240
  export function truncatedOutput(
203
241
  snap: SubagentSnapshot,
204
242
  maxBytes = SUBAGENT_OUTPUT_MAX_BYTES,
205
243
  writeArtifact: (content: string) => string = (content) =>
206
244
  persistResultArtifact(getAgentDir(), content),
207
245
  ): string {
208
- const output = snap.finalText || "(no output)";
246
+ const output = subagentResultContent(snap);
209
247
  return projectResult(output, {
210
248
  maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
211
249
  maxLines: Math.min(600, DEFAULT_MAX_LINES),
@@ -213,12 +251,53 @@ export function truncatedOutput(
213
251
  }).text;
214
252
  }
215
253
 
254
+ function projectSubagentOutput(
255
+ snap: SubagentSnapshot,
256
+ maxBytes: number,
257
+ ): ResultProjection {
258
+ const output = subagentResultContent(snap);
259
+ return projectResult(output, {
260
+ maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
261
+ maxLines: Math.min(600, DEFAULT_MAX_LINES),
262
+ writeArtifact: (content) => persistResultArtifact(getAgentDir(), content),
263
+ });
264
+ }
265
+
266
+ type OutputProjection = Pick<
267
+ ResultProjection,
268
+ "text" | "artifactPath" | "artifactSaveFailed"
269
+ >;
270
+
271
+ function normalizeProjection(
272
+ output: string | OutputProjection,
273
+ ): OutputProjection {
274
+ return typeof output === "string" ? { text: output } : output;
275
+ }
276
+
277
+ function boundAutomaticResultBatch(content: string) {
278
+ const probe = truncateHead(content, {
279
+ maxBytes: AUTOMATIC_OUTPUT_MAX_BYTES,
280
+ maxLines: Number.MAX_SAFE_INTEGER,
281
+ });
282
+ if (!probe.truncated) return content;
283
+
284
+ const noticeBytes = Buffer.byteLength(
285
+ AUTOMATIC_BATCH_TRUNCATION_NOTICE,
286
+ "utf8",
287
+ );
288
+ const bounded = truncateHead(content, {
289
+ maxBytes: Math.max(0, AUTOMATIC_OUTPUT_MAX_BYTES - noticeBytes),
290
+ maxLines: Number.MAX_SAFE_INTEGER,
291
+ });
292
+ return `${bounded.content}${AUTOMATIC_BATCH_TRUNCATION_NOTICE}`;
293
+ }
294
+
216
295
  export function createSubagentResultDispatcher(
217
296
  pi: ExtensionAPI,
218
297
  outputFor: (
219
298
  snap: SubagentSnapshot,
220
299
  maxBytes: number,
221
- ) => string = truncatedOutput,
300
+ ) => string | OutputProjection = projectSubagentOutput,
222
301
  getContextUsage: () => ParentContextUsage | undefined = () => undefined,
223
302
  ) {
224
303
  return (snaps: readonly SubagentSnapshot[]) => {
@@ -239,12 +318,12 @@ export function createSubagentResultDispatcher(
239
318
  ) +
240
319
  Math.max(0, snaps.length - 1) * 2;
241
320
  const projectionBatchBytes = Math.max(
242
- AUTOMATIC_MIN_RESULT_BYTES * snaps.length,
321
+ 0,
243
322
  AUTOMATIC_OUTPUT_MAX_BYTES - wrapperBytes,
244
323
  );
245
324
  const allocation = allocateResultBudgets(
246
325
  snaps.map((snap) =>
247
- Buffer.byteLength(snap.finalText || "(no output)", "utf8"),
326
+ Buffer.byteLength(subagentResultContent(snap), "utf8"),
248
327
  ),
249
328
  getContextUsage(),
250
329
  {
@@ -256,34 +335,86 @@ export function createSubagentResultDispatcher(
256
335
  fixedBytes: wrapperBytes,
257
336
  },
258
337
  );
259
- const content = snaps
260
- .map((snap, index) =>
261
- buildSubagentResultMessage({
262
- id: snap.id,
263
- title: snap.title,
264
- status: snap.status,
265
- errorText: snap.errorText,
266
- output: outputFor(snap, allocation.budgets[index]!),
267
- }),
268
- )
269
- .join("\n\n");
338
+ const projections = snaps.map((snap, index) =>
339
+ normalizeProjection(outputFor(snap, allocation.budgets[index]!)),
340
+ );
341
+ const outputs = projections.map((projection) => projection.text);
342
+ const displayContent = boundAutomaticResultBatch(
343
+ snaps
344
+ .map((snap, index) =>
345
+ buildSubagentResultDisplayMessage({
346
+ id: snap.id,
347
+ title: snap.title,
348
+ status: snap.status,
349
+ errorText: snap.errorText,
350
+ output: outputs[index]!,
351
+ }),
352
+ )
353
+ .join("\n\n"),
354
+ );
355
+ const content = boundAutomaticResultBatch(
356
+ snaps
357
+ .map((snap, index) =>
358
+ buildSubagentResultMessage({
359
+ id: snap.id,
360
+ title: snap.title,
361
+ status: snap.status,
362
+ errorText: snap.errorText,
363
+ output: outputs[index]!,
364
+ }),
365
+ )
366
+ .join("\n\n"),
367
+ );
270
368
  const details: SubagentResultDetails =
271
369
  snaps.length === 1
272
370
  ? {
273
371
  id: snaps[0]!.id,
274
372
  title: snaps[0]!.title,
275
373
  status: snaps[0]!.status,
374
+ ...(snaps[0]!.outcome ? { outcome: snaps[0]!.outcome } : {}),
375
+ ...(snaps[0]!.worktreeBranch
376
+ ? { worktreeBranch: snaps[0]!.worktreeBranch }
377
+ : {}),
378
+ elapsed: formatElapsed(snaps[0]!),
379
+ ...(projections[0]!.artifactPath ? { fullResultSaved: true } : {}),
380
+ ...(projections[0]!.artifactSaveFailed
381
+ ? { artifactSaveFailed: true }
382
+ : {}),
383
+ ...(snaps[0]!.structuredResult
384
+ ? {
385
+ structured: snaps[0]!.structuredResult.value,
386
+ structuredArtifactPath:
387
+ snaps[0]!.structuredResult.artifactPath,
388
+ }
389
+ : {}),
276
390
  }
277
391
  : {
278
392
  count: snaps.length,
279
- results: snaps.map((snap) => ({
393
+ results: snaps.map((snap, index) => ({
280
394
  id: snap.id,
281
395
  title: snap.title,
282
396
  status: snap.status,
397
+ ...(snap.outcome ? { outcome: snap.outcome } : {}),
398
+ ...(snap.worktreeBranch
399
+ ? { worktreeBranch: snap.worktreeBranch }
400
+ : {}),
401
+ elapsed: formatElapsed(snap),
402
+ ...(projections[index]!.artifactPath
403
+ ? { fullResultSaved: true }
404
+ : {}),
405
+ ...(projections[index]!.artifactSaveFailed
406
+ ? { artifactSaveFailed: true }
407
+ : {}),
408
+ ...(snap.structuredResult
409
+ ? {
410
+ structured: snap.structuredResult.value,
411
+ structuredArtifactPath: snap.structuredResult.artifactPath,
412
+ }
413
+ : {}),
283
414
  })),
284
415
  };
285
416
  pi.appendEntry<SubagentResultEntryData>("subagent-result", {
286
- content,
417
+ content: displayContent,
287
418
  details,
288
419
  });
289
420
  pi.sendMessage(
@@ -291,7 +422,7 @@ export function createSubagentResultDispatcher(
291
422
  customType: "subagent-result",
292
423
  content,
293
424
  display: false,
294
- details,
425
+ details: { ...details, displayContent },
295
426
  },
296
427
  { deliverAs: "followUp", triggerTurn: true },
297
428
  );
@@ -304,36 +435,52 @@ function renderSubagentResult(
304
435
  content: string,
305
436
  details: SubagentResultDetails,
306
437
  expanded: boolean,
438
+ resultDisplay: DetailDisplay,
307
439
  theme: SubagentResultTheme,
308
440
  ) {
309
- if (!expanded && loadSetupConfig().ui.subagentResultDisplay === "compact") {
310
- const results = details.results?.length
311
- ? details.results
312
- : details.id
313
- ? [
314
- {
315
- id: details.id,
316
- title: details.title,
317
- status: details.status,
318
- },
319
- ]
320
- : [];
321
- return new Text(buildWaitResultPreview(content, { results }, theme), 0, 0);
441
+ const displayContent = sanitizeText(
442
+ details.displayContent ?? stripSubagentResultTransportInstruction(content),
443
+ );
444
+ const results = details.results?.length
445
+ ? details.results
446
+ : details.id
447
+ ? [
448
+ {
449
+ id: details.id,
450
+ title: details.title,
451
+ status: details.status,
452
+ outcome: details.outcome,
453
+ worktreeBranch: details.worktreeBranch,
454
+ elapsed: details.elapsed,
455
+ artifactSaveFailed: details.artifactSaveFailed,
456
+ fullResultSaved: details.fullResultSaved,
457
+ },
458
+ ]
459
+ : [];
460
+ if (!expanded && resultDisplay === "compact") {
461
+ return renderWaitResultPreview(displayContent, { results }, theme);
322
462
  }
323
463
 
324
- const failed = details.status === "error";
464
+ const failed = results.some((result) => result.status === "error");
465
+ const batched = results.length > 1;
325
466
  const icon = failed ? theme.fg("error", "x") : theme.fg("success", "✓");
326
- const header =
327
- `${icon} ` +
328
- theme.fg("accent", theme.bold(`subagent ${details.id ?? "?"}`)) +
329
- theme.fg(
330
- "muted",
331
- ` · ${details.title ?? ""} · ${failed ? "failed" : "finished"}`,
332
- );
467
+ const header = batched
468
+ ? `${icon} ${theme.fg("accent", theme.bold(`${results.length} subagents`))}${theme.fg("muted", ` · ${failed ? `${results.filter((result) => result.status === "error").length} failed` : "finished"}`)}`
469
+ : `${icon} ` +
470
+ theme.fg(
471
+ "accent",
472
+ theme.bold(`subagent ${sanitizeText(details.id ?? "?")}`),
473
+ ) +
474
+ theme.fg(
475
+ "muted",
476
+ ` · ${sanitizeText(details.title ?? "")} · ${failed ? "failed" : "finished"}${details.elapsed ? ` · ${sanitizeText(details.elapsed)}` : ""}`,
477
+ );
333
478
 
334
- // Remove only the summary line. The following Error line (when present)
335
- // is part of the actual result and must remain visible.
336
- const body = content.split("\n").slice(1).join("\n").trim();
479
+ // Remove only the single-result summary line. Error lines and batched result
480
+ // summaries are part of the display projection and must remain visible.
481
+ const body = batched
482
+ ? displayContent.trim()
483
+ : displayContent.split("\n").slice(1).join("\n").trim();
337
484
  const md = new Markdown(body, 0, 0, getMarkdownTheme());
338
485
  const container = new Text(header, 0, 0);
339
486
  return {
@@ -348,7 +495,17 @@ function renderSubagentResult(
348
495
  };
349
496
  }
350
497
 
351
- export default function (pi: ExtensionAPI) {
498
+ interface SubagentExtensionOptions {
499
+ readonly getResultDisplay?: () => DetailDisplay;
500
+ }
501
+
502
+ export default function (
503
+ pi: ExtensionAPI,
504
+ options: SubagentExtensionOptions = {},
505
+ ) {
506
+ const getResultDisplay =
507
+ options.getResultDisplay ??
508
+ (() => loadSetupConfig().ui.subagentResultDisplay);
352
509
  let runtime: SubagentRuntime | undefined;
353
510
  let managerPromise: Promise<SubagentManagerShape> | undefined;
354
511
  let restoredIdCounters: SubagentIdCounters = {
@@ -364,19 +521,26 @@ export default function (pi: ExtensionAPI) {
364
521
  */
365
522
  let settledAcknowledgedAt = 0;
366
523
  const stripState = new BelowEditorStripState();
524
+ const statusWriter = createStatusWriter("subagents");
367
525
  const widgetKey = "subagent-navigation";
368
526
  let navigationManager: SubagentManagerShape | undefined;
527
+ let unregisterWebCapability: (() => void) | undefined;
369
528
  let widgetVisible = false;
529
+ let widgetEntryKey: string | undefined;
370
530
  let requestWidgetRender: (() => void) | undefined;
371
531
  let navigationLayerRegistered = false;
372
532
  let dashboardOpen = false;
373
533
  const dispatchResults = createSubagentResultDispatcher(
374
534
  pi,
375
- truncatedOutput,
535
+ projectSubagentOutput,
376
536
  () => sessionContext?.getContextUsage(),
377
537
  );
378
538
  const resultDelivery = createSubagentResultDelivery<SubagentSnapshot>({
379
539
  isIdle: () => sessionContext?.isIdle() === true,
540
+ owner: () =>
541
+ sessionContext
542
+ ? completionOwnerFor(sessionContext.sessionManager)
543
+ : undefined,
380
544
  // Every unconsumed fire-and-forget result must reach the parent. The
381
545
  // delivery coordinator batches results that settled while it was busy.
382
546
  deliver: dispatchResults,
@@ -398,10 +562,20 @@ export default function (pi: ExtensionAPI) {
398
562
 
399
563
  /** Resolve the manager service once per runtime and wire the extension hooks. */
400
564
  const getManager = () => {
565
+ const scope = sessionContext?.sessionManager;
401
566
  managerPromise ??= getRuntime()
402
567
  .runPromise(SubagentManager)
403
568
  .then((manager) => {
404
569
  navigationManager = manager;
570
+ unregisterWebCapability?.();
571
+ unregisterWebCapability =
572
+ scope && sessionContext?.sessionManager === scope
573
+ ? registerWebCapability(scope, {
574
+ kind: "subagents",
575
+ snapshot: () => projectSubagentCapability(manager.view.list()),
576
+ subscribe: (listener) => manager.view.subscribe(listener),
577
+ })
578
+ : undefined;
405
579
  manager.view.setOnSettled(onSettled);
406
580
  unsubStatus?.();
407
581
  unsubStatus = manager.view.subscribe(() => updateStatus(manager));
@@ -422,10 +596,19 @@ export default function (pi: ExtensionAPI) {
422
596
  const updateSubagentWidget = () => {
423
597
  const ctx = sessionContext;
424
598
  if (!ctx || ctx.mode !== "tui") return;
425
- const visible = Boolean(stripEntry());
426
- if (visible === widgetVisible) return;
599
+ const entry = stripEntry();
600
+ const visible = Boolean(entry);
601
+ const entryKey = subagentStripEntryKey(entry);
602
+ if (visible === widgetVisible) {
603
+ if (visible && entryKey !== widgetEntryKey) {
604
+ widgetEntryKey = entryKey;
605
+ requestWidgetRender?.();
606
+ }
607
+ return;
608
+ }
427
609
  if (!visible) {
428
610
  stripState.focused = false;
611
+ widgetEntryKey = undefined;
429
612
  requestWidgetRender = undefined;
430
613
  ctx.ui.setWidget(widgetKey, undefined);
431
614
  widgetVisible = false;
@@ -440,6 +623,7 @@ export default function (pi: ExtensionAPI) {
440
623
  { placement: "belowEditor" },
441
624
  );
442
625
  widgetVisible = true;
626
+ widgetEntryKey = entryKey;
443
627
  };
444
628
 
445
629
  const updateStatus = (manager: SubagentManagerShape) => {
@@ -451,8 +635,8 @@ export default function (pi: ExtensionAPI) {
451
635
  // In the TUI the below-editor strip already reports the same activity and
452
636
  // carries the manage affordance, so a footer status line would repeat it.
453
637
  const tui = sessionContext?.mode === "tui";
454
- ui.setStatus(
455
- "subagents",
638
+ statusWriter.write(
639
+ ui,
456
640
  !tui && hasActivity(counts)
457
641
  ? formatActivityStatus(ui.theme, "subagents", counts)
458
642
  : undefined,
@@ -586,16 +770,20 @@ export default function (pi: ExtensionAPI) {
586
770
  resultDelivery.clear();
587
771
  unsubStatus?.();
588
772
  unsubStatus = undefined;
773
+ unregisterWebCapability?.();
774
+ unregisterWebCapability = undefined;
589
775
  try {
590
776
  ui?.setStatus("subagents", undefined);
591
777
  sessionContext?.ui.setWidget(widgetKey, undefined);
592
778
  } catch {
593
779
  // UI may already be disposed.
594
780
  }
781
+ statusWriter.reset();
595
782
  sessionContext = undefined;
596
783
  ui = undefined;
597
784
  navigationManager = undefined;
598
785
  widgetVisible = false;
786
+ widgetEntryKey = undefined;
599
787
  requestWidgetRender = undefined;
600
788
  stripState.focused = false;
601
789
  dashboardOpen = false;
@@ -770,6 +958,9 @@ export default function (pi: ExtensionAPI) {
770
958
  ...(agentType?.body ? { appendSystemPrompt: [agentType.body] } : {}),
771
959
  ...(childTools ? { tools: childTools } : {}),
772
960
  ...(agentType ? { agentTypeName: agentType.name } : {}),
961
+ ...(params.output_schema !== undefined
962
+ ? { outputSchema: params.output_schema }
963
+ : {}),
773
964
  ...(worktree ? { worktree: { ...worktree, repoCwd: cwd } } : {}),
774
965
  parent: {
775
966
  parentCwd: ctx.cwd,
@@ -791,7 +982,37 @@ export default function (pi: ExtensionAPI) {
791
982
  } catch (error) {
792
983
  // The session scope owns reclamation, but it never opened, so this
793
984
  // worktree would otherwise be orphaned on disk.
794
- if (worktree) await reclaimWorktree(cwd, worktree).catch(() => {});
985
+ if (worktree) {
986
+ const spawnError =
987
+ error instanceof Error ? error.message : String(error);
988
+ let cleanupWarning: string | undefined;
989
+ let cleanupError: unknown;
990
+ try {
991
+ const cleanup = await reclaimWorktree(cwd, worktree);
992
+ cleanupWarning = formatWorktreeCleanupWarning(
993
+ cleanup,
994
+ worktree.path,
995
+ );
996
+ } catch (failure) {
997
+ cleanupError = failure;
998
+ }
999
+ if (cleanupError) {
1000
+ const reason =
1001
+ cleanupError instanceof Error
1002
+ ? cleanupError.message
1003
+ : String(cleanupError);
1004
+ throw new Error(
1005
+ `${spawnError}; worktree cleanup failed: ${reason}; checkout preserved at ${worktree.path}`,
1006
+ { cause: error },
1007
+ );
1008
+ }
1009
+ if (cleanupWarning) {
1010
+ throw new Error(
1011
+ `${spawnError}; worktree cleanup warning: ${cleanupWarning}`,
1012
+ { cause: error },
1013
+ );
1014
+ }
1015
+ }
795
1016
  throw error;
796
1017
  }
797
1018
  persistId(snap.id);
@@ -809,6 +1030,9 @@ export default function (pi: ExtensionAPI) {
809
1030
  ...(worktree ? { worktreeBranch: worktree.branch } : {}),
810
1031
  ...(agentType ? { agentTypeName: agentType.name } : {}),
811
1032
  ...(childTools ? { tools: childTools } : {}),
1033
+ ...(params.output_schema !== undefined
1034
+ ? { structured: true }
1035
+ : {}),
812
1036
  }),
813
1037
  },
814
1038
  ],
@@ -819,6 +1043,7 @@ export default function (pi: ExtensionAPI) {
819
1043
  harness,
820
1044
  model: snap.meta.modelLabel,
821
1045
  ...(agentType ? { agentType: agentType.name } : {}),
1046
+ ...(params.output_schema !== undefined ? { structured: true } : {}),
822
1047
  },
823
1048
  };
824
1049
  },
@@ -941,7 +1166,7 @@ export default function (pi: ExtensionAPI) {
941
1166
  );
942
1167
  const allocation = allocateResultBudgets(
943
1168
  resultEntries.map(({ snap }) =>
944
- Buffer.byteLength(snap.finalText || "(no output)", "utf8"),
1169
+ Buffer.byteLength(subagentResultContent(snap), "utf8"),
945
1170
  ),
946
1171
  ctx.getContextUsage(),
947
1172
  {
@@ -954,10 +1179,15 @@ export default function (pi: ExtensionAPI) {
954
1179
  },
955
1180
  );
956
1181
  let resultIndex = 0;
1182
+ const artifactSaveFailures = new Set<string>();
1183
+ const fullResultsSaved = new Set<string>();
957
1184
  const sections = entries.map((entry) => {
958
1185
  if ("section" in entry) return entry.section;
959
1186
  const outputBudget = allocation.budgets[resultIndex++]!;
960
- return `${entry.header}\n\n${truncatedOutput(entry.snap, outputBudget)}`;
1187
+ const projection = projectSubagentOutput(entry.snap, outputBudget);
1188
+ if (projection.artifactSaveFailed) artifactSaveFailures.add(entry.id);
1189
+ if (projection.artifactPath) fullResultsSaved.add(entry.id);
1190
+ return `${entry.header}\n\n${projection.text}`;
961
1191
  });
962
1192
 
963
1193
  const combined = sections.join("\n\n---\n\n");
@@ -973,7 +1203,26 @@ export default function (pi: ExtensionAPI) {
973
1203
  details: {
974
1204
  results: ids.map((id) => {
975
1205
  const snap = manager.view.get(id);
976
- return { id, title: snap?.title, status: snap?.status };
1206
+ return {
1207
+ id,
1208
+ title: snap?.title,
1209
+ status: snap?.status,
1210
+ ...(snap?.outcome ? { outcome: snap.outcome } : {}),
1211
+ ...(snap?.worktreeBranch
1212
+ ? { worktreeBranch: snap.worktreeBranch }
1213
+ : {}),
1214
+ ...(snap ? { elapsed: formatElapsed(snap) } : {}),
1215
+ ...(fullResultsSaved.has(id) ? { fullResultSaved: true } : {}),
1216
+ ...(artifactSaveFailures.has(id)
1217
+ ? { artifactSaveFailed: true }
1218
+ : {}),
1219
+ ...(snap?.structuredResult
1220
+ ? {
1221
+ structured: snap.structuredResult.value,
1222
+ structuredArtifactPath: snap.structuredResult.artifactPath,
1223
+ }
1224
+ : {}),
1225
+ };
977
1226
  }),
978
1227
  },
979
1228
  };
@@ -998,7 +1247,7 @@ export default function (pi: ExtensionAPI) {
998
1247
  return renderWaitResult(
999
1248
  content,
1000
1249
  result.details as WaitResultDetails | undefined,
1001
- expanded || loadSetupConfig().ui.subagentResultDisplay === "full",
1250
+ expanded || getResultDisplay() === "full",
1002
1251
  theme,
1003
1252
  );
1004
1253
  },
@@ -1193,6 +1442,7 @@ export default function (pi: ExtensionAPI) {
1193
1442
  content,
1194
1443
  (message.details ?? {}) as SubagentResultDetails,
1195
1444
  expanded,
1445
+ getResultDisplay(),
1196
1446
  theme,
1197
1447
  );
1198
1448
  },
@@ -1205,6 +1455,7 @@ export default function (pi: ExtensionAPI) {
1205
1455
  entry.data?.content ?? "",
1206
1456
  entry.data?.details ?? {},
1207
1457
  expanded,
1458
+ getResultDisplay(),
1208
1459
  theme,
1209
1460
  ),
1210
1461
  );
@@ -1247,7 +1498,7 @@ export default function (pi: ExtensionAPI) {
1247
1498
  .filter(Boolean)
1248
1499
  .join("\n\n");
1249
1500
 
1250
- if (expanded || loadSetupConfig().ui.subagentResultDisplay === "full") {
1501
+ if (expanded || getResultDisplay() === "full") {
1251
1502
  const md = new Markdown(body, 0, 0, getMarkdownTheme());
1252
1503
  const container = new Text(header, 0, 0);
1253
1504
  return {