@tt-a1i/openpi 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +102 -40
  2. package/SETUP.md +22 -6
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/background-terminals/index.ts +30 -2
  6. package/extensions/background-terminals/src/domain.ts +2 -0
  7. package/extensions/background-terminals/src/manager.ts +486 -106
  8. package/extensions/background-terminals/src/output.ts +33 -0
  9. package/extensions/background-terminals/src/prompt.ts +13 -5
  10. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  11. package/extensions/clear-context/index.ts +83 -0
  12. package/extensions/context-pivot/index.ts +16 -6
  13. package/extensions/cron/schedule.ts +7 -1
  14. package/extensions/file-mutation-display/render.ts +17 -257
  15. package/extensions/file-search/src/binaries.ts +57 -41
  16. package/extensions/git-read/index.ts +1 -3
  17. package/extensions/model-info/index.ts +21 -33
  18. package/extensions/model-info/session-metrics.ts +96 -0
  19. package/extensions/plan-mode/bash-policy.ts +54 -9
  20. package/extensions/plan-mode/index.ts +7 -2
  21. package/extensions/post-edit/index.ts +16 -6
  22. package/extensions/sessions/git-stats.ts +258 -72
  23. package/extensions/sessions/index.ts +153 -86
  24. package/extensions/sessions/preview-cache.ts +104 -0
  25. package/extensions/sessions/preview-loader.ts +856 -0
  26. package/extensions/sessions/sessions.ts +43 -4
  27. package/extensions/setup/index.ts +123 -127
  28. package/extensions/shared/activity-status.ts +30 -0
  29. package/extensions/shared/agent-session-page.ts +319 -0
  30. package/extensions/shared/agent-tool-renderer.ts +218 -0
  31. package/extensions/shared/agent-transcript.ts +524 -0
  32. package/extensions/shared/capability-intent.ts +1 -1
  33. package/extensions/shared/child-session.ts +437 -21
  34. package/extensions/shared/result-delivery.ts +34 -0
  35. package/extensions/shared/setup-config.ts +73 -33
  36. package/extensions/shared/setup-episode-state.ts +1 -1
  37. package/extensions/shared/terminal-text.ts +110 -23
  38. package/extensions/shared/text-projection.ts +72 -15
  39. package/extensions/shared/tool-activity.ts +382 -0
  40. package/extensions/shared/tool-surface.ts +29 -2
  41. package/extensions/shared/transcript-viewport.ts +46 -0
  42. package/extensions/shared/web-observer-registry.ts +390 -0
  43. package/extensions/shared/worktree.ts +11 -0
  44. package/extensions/subagents/index.ts +270 -59
  45. package/extensions/subagents/navigation.ts +34 -5
  46. package/extensions/subagents/src/backend.ts +12 -1
  47. package/extensions/subagents/src/backends/pi.ts +375 -66
  48. package/extensions/subagents/src/domain.ts +5 -0
  49. package/extensions/subagents/src/manager.ts +34 -2
  50. package/extensions/subagents/src/prompt.ts +32 -4
  51. package/extensions/subagents/src/result-artifact.ts +4 -0
  52. package/extensions/subagents/src/result-delivery.ts +7 -1
  53. package/extensions/subagents/src/runtime.ts +15 -1
  54. package/extensions/subagents/src/ui/takeover.ts +73 -257
  55. package/extensions/subagents/src/ui/transcript.ts +38 -535
  56. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  57. package/extensions/suggestions/src/ui.ts +10 -4
  58. package/extensions/tasks/index.ts +0 -3
  59. package/extensions/ui-customization/footer.ts +0 -40
  60. package/extensions/ui-customization/index.ts +0 -4
  61. package/extensions/user-input-fold/index.ts +1 -1
  62. package/extensions/web/index.ts +234 -0
  63. package/extensions/workflows/artifacts.ts +137 -47
  64. package/extensions/workflows/completion-projection.ts +457 -0
  65. package/extensions/workflows/coordinator.ts +8 -10
  66. package/extensions/workflows/dashboard.ts +167 -228
  67. package/extensions/workflows/handoff.ts +70 -16
  68. package/extensions/workflows/index.ts +488 -198
  69. package/extensions/workflows/journal.ts +148 -13
  70. package/extensions/workflows/model.ts +74 -4
  71. package/extensions/workflows/navigation.ts +32 -8
  72. package/extensions/workflows/progress-projection.ts +306 -0
  73. package/extensions/workflows/prompt.ts +66 -6
  74. package/extensions/workflows/replay-safety.ts +42 -21
  75. package/extensions/workflows/result-delivery.ts +128 -64
  76. package/extensions/workflows/retention.ts +593 -0
  77. package/extensions/workflows/runner.ts +388 -279
  78. package/extensions/workflows/sandbox-child.cjs +25 -3
  79. package/extensions/workflows/sandbox.ts +62 -8
  80. package/extensions/workflows/serialization.ts +325 -17
  81. package/extensions/workflows/tool-renderer.ts +22 -0
  82. package/extensions/workflows/transcript.ts +149 -0
  83. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  84. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  85. package/package.json +28 -8
  86. package/skills/subagents/REFERENCE.md +189 -0
  87. package/skills/subagents/SKILL.md +1 -1
  88. package/skills/workflows/REFERENCE.md +4 -2
  89. package/web/adapter/pi-adapter.ts +661 -0
  90. package/web/host/browser-launcher.ts +20 -0
  91. package/web/host/static-assets.ts +4 -0
  92. package/web/host/terminal-status.ts +38 -0
  93. package/web/host/web-host.ts +789 -0
  94. package/web/http-dispatcher.ts +125 -0
  95. package/web/protocol/types.ts +462 -0
  96. package/web/runtime/pi-runtime.ts +991 -0
  97. package/web/runtime/types.ts +71 -0
  98. package/web/runtime/web-host-lease.ts +497 -0
  99. package/web/trace.ts +18 -0
  100. package/web/ui/app.js +1398 -0
  101. package/web/ui/index.html +139 -0
  102. package/web/ui/styles.css +598 -0
  103. package/web/vite.config.mjs +34 -0
  104. package/extensions/execution-convergence/active-evidence.ts +0 -129
  105. package/extensions/execution-convergence/index.ts +0 -442
  106. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  107. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  108. package/extensions/setup/intercom.ts +0 -603
  109. 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,
@@ -67,13 +70,22 @@ import {
67
70
  planModeAllowsDeclaredTools,
68
71
  planModeChildTools,
69
72
  } from "../shared/plan-mode-state.ts";
70
- import { loadSetupConfig } from "../shared/setup-config.ts";
73
+ import {
74
+ allocateResultBudgets,
75
+ type ParentContextUsage,
76
+ } from "../shared/result-budget.ts";
77
+ import { type DetailDisplay, loadSetupConfig } from "../shared/setup-config.ts";
71
78
  import {
72
79
  OPENPI_TOOL_SURFACE,
73
80
  patchOwnedTools,
74
81
  } from "../shared/tool-surface.ts";
82
+ import {
83
+ projectSubagentCapability,
84
+ registerWebCapability,
85
+ } from "../shared/web-observer-registry.ts";
75
86
  import {
76
87
  createWorktree,
88
+ formatWorktreeCleanupWarning,
77
89
  reclaimWorktree,
78
90
  type Worktree,
79
91
  } from "../shared/worktree.ts";
@@ -81,9 +93,9 @@ import {
81
93
  normalizeSubagentTitle,
82
94
  SubagentStripWidget,
83
95
  selectSubagentStripEntry,
96
+ subagentStripEntryKey,
84
97
  } from "./navigation.ts";
85
98
  import {
86
- type AgentType,
87
99
  formatAgentTypeDiagnostics,
88
100
  loadAgentTypes,
89
101
  roleModelForAgentType,
@@ -104,6 +116,7 @@ import {
104
116
  } from "./src/id-sequence.ts";
105
117
  import { SubagentManager, type SubagentManagerShape } from "./src/manager.ts";
106
118
  import {
119
+ buildSubagentResultDisplayMessage,
107
120
  buildSubagentResultMessage,
108
121
  buildSubagentSendResult,
109
122
  buildSubagentSpawnResult,
@@ -119,12 +132,13 @@ import {
119
132
  SUBAGENT_SPAWN_PROMPT_SNIPPET,
120
133
  SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS,
121
134
  SUBAGENT_WAIT_TOOL_DESCRIPTION,
135
+ stripSubagentResultTransportInstruction,
122
136
  } from "./src/prompt.ts";
123
- import { persistResultArtifact, projectResult } from "./src/result-artifact.ts";
124
137
  import {
125
- allocateResultBudgets,
126
- type ParentContextUsage,
127
- } from "../shared/result-budget.ts";
138
+ persistResultArtifact,
139
+ projectResult,
140
+ type ResultProjection,
141
+ } from "./src/result-artifact.ts";
128
142
  import { createSubagentResultDelivery } from "./src/result-delivery.ts";
129
143
  import {
130
144
  createSubagentRuntime,
@@ -133,8 +147,8 @@ import {
133
147
  } from "./src/runtime.ts";
134
148
  import { openSubagentPicker, openSubagentTakeover } from "./src/ui/takeover.ts";
135
149
  import {
136
- buildWaitResultPreview,
137
150
  renderWaitResult,
151
+ renderWaitResultPreview,
138
152
  type WaitResultDetails,
139
153
  } from "./src/ui/wait-result.ts";
140
154
 
@@ -146,6 +160,8 @@ const WAIT_PER_AGENT_MAX_BYTES = 16 * 1024;
146
160
  const WAIT_MIN_RESULT_BYTES = 512;
147
161
  const RESULT_HEADROOM_SHARE = 0.5;
148
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.]";
149
165
 
150
166
  interface SpawnResultDetails {
151
167
  readonly id?: string;
@@ -166,12 +182,24 @@ interface SubagentResultDetails {
166
182
  readonly id?: string;
167
183
  readonly title?: string;
168
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;
169
190
  readonly count?: number;
170
191
  readonly results?: ReadonlyArray<{
171
192
  readonly id: string;
172
193
  readonly title: string;
173
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;
174
200
  }>;
201
+ /** Display-only projection for the custom message renderer. */
202
+ readonly displayContent?: string;
175
203
  }
176
204
 
177
205
  interface SubagentResultEntryData {
@@ -213,12 +241,53 @@ export function truncatedOutput(
213
241
  }).text;
214
242
  }
215
243
 
244
+ function projectSubagentOutput(
245
+ snap: SubagentSnapshot,
246
+ maxBytes: number,
247
+ ): ResultProjection {
248
+ const output = snap.finalText || "(no output)";
249
+ return projectResult(output, {
250
+ maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
251
+ maxLines: Math.min(600, DEFAULT_MAX_LINES),
252
+ writeArtifact: (content) => persistResultArtifact(getAgentDir(), content),
253
+ });
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}`;
283
+ }
284
+
216
285
  export function createSubagentResultDispatcher(
217
286
  pi: ExtensionAPI,
218
287
  outputFor: (
219
288
  snap: SubagentSnapshot,
220
289
  maxBytes: number,
221
- ) => string = truncatedOutput,
290
+ ) => string | OutputProjection = projectSubagentOutput,
222
291
  getContextUsage: () => ParentContextUsage | undefined = () => undefined,
223
292
  ) {
224
293
  return (snaps: readonly SubagentSnapshot[]) => {
@@ -239,7 +308,7 @@ export function createSubagentResultDispatcher(
239
308
  ) +
240
309
  Math.max(0, snaps.length - 1) * 2;
241
310
  const projectionBatchBytes = Math.max(
242
- AUTOMATIC_MIN_RESULT_BYTES * snaps.length,
311
+ 0,
243
312
  AUTOMATIC_OUTPUT_MAX_BYTES - wrapperBytes,
244
313
  );
245
314
  const allocation = allocateResultBudgets(
@@ -256,34 +325,73 @@ export function createSubagentResultDispatcher(
256
325
  fixedBytes: wrapperBytes,
257
326
  },
258
327
  );
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");
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
+ );
270
358
  const details: SubagentResultDetails =
271
359
  snaps.length === 1
272
360
  ? {
273
361
  id: snaps[0]!.id,
274
362
  title: snaps[0]!.title,
275
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
+ : {}),
276
373
  }
277
374
  : {
278
375
  count: snaps.length,
279
- results: snaps.map((snap) => ({
376
+ results: snaps.map((snap, index) => ({
280
377
  id: snap.id,
281
378
  title: snap.title,
282
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
+ : {}),
283
391
  })),
284
392
  };
285
393
  pi.appendEntry<SubagentResultEntryData>("subagent-result", {
286
- content,
394
+ content: displayContent,
287
395
  details,
288
396
  });
289
397
  pi.sendMessage(
@@ -291,7 +399,7 @@ export function createSubagentResultDispatcher(
291
399
  customType: "subagent-result",
292
400
  content,
293
401
  display: false,
294
- details,
402
+ details: { ...details, displayContent },
295
403
  },
296
404
  { deliverAs: "followUp", triggerTurn: true },
297
405
  );
@@ -304,36 +412,52 @@ function renderSubagentResult(
304
412
  content: string,
305
413
  details: SubagentResultDetails,
306
414
  expanded: boolean,
415
+ resultDisplay: DetailDisplay,
307
416
  theme: SubagentResultTheme,
308
417
  ) {
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);
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);
322
439
  }
323
440
 
324
- const failed = details.status === "error";
441
+ const failed = results.some((result) => result.status === "error");
442
+ const batched = results.length > 1;
325
443
  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
- );
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
+ );
333
455
 
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();
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();
337
461
  const md = new Markdown(body, 0, 0, getMarkdownTheme());
338
462
  const container = new Text(header, 0, 0);
339
463
  return {
@@ -348,7 +472,17 @@ function renderSubagentResult(
348
472
  };
349
473
  }
350
474
 
351
- 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);
352
486
  let runtime: SubagentRuntime | undefined;
353
487
  let managerPromise: Promise<SubagentManagerShape> | undefined;
354
488
  let restoredIdCounters: SubagentIdCounters = {
@@ -364,15 +498,18 @@ export default function (pi: ExtensionAPI) {
364
498
  */
365
499
  let settledAcknowledgedAt = 0;
366
500
  const stripState = new BelowEditorStripState();
501
+ const statusWriter = createStatusWriter("subagents");
367
502
  const widgetKey = "subagent-navigation";
368
503
  let navigationManager: SubagentManagerShape | undefined;
504
+ let unregisterWebCapability: (() => void) | undefined;
369
505
  let widgetVisible = false;
506
+ let widgetEntryKey: string | undefined;
370
507
  let requestWidgetRender: (() => void) | undefined;
371
508
  let navigationLayerRegistered = false;
372
509
  let dashboardOpen = false;
373
510
  const dispatchResults = createSubagentResultDispatcher(
374
511
  pi,
375
- truncatedOutput,
512
+ projectSubagentOutput,
376
513
  () => sessionContext?.getContextUsage(),
377
514
  );
378
515
  const resultDelivery = createSubagentResultDelivery<SubagentSnapshot>({
@@ -398,10 +535,20 @@ export default function (pi: ExtensionAPI) {
398
535
 
399
536
  /** Resolve the manager service once per runtime and wire the extension hooks. */
400
537
  const getManager = () => {
538
+ const scope = sessionContext?.sessionManager;
401
539
  managerPromise ??= getRuntime()
402
540
  .runPromise(SubagentManager)
403
541
  .then((manager) => {
404
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;
405
552
  manager.view.setOnSettled(onSettled);
406
553
  unsubStatus?.();
407
554
  unsubStatus = manager.view.subscribe(() => updateStatus(manager));
@@ -422,10 +569,19 @@ export default function (pi: ExtensionAPI) {
422
569
  const updateSubagentWidget = () => {
423
570
  const ctx = sessionContext;
424
571
  if (!ctx || ctx.mode !== "tui") return;
425
- const visible = Boolean(stripEntry());
426
- 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
+ }
427
582
  if (!visible) {
428
583
  stripState.focused = false;
584
+ widgetEntryKey = undefined;
429
585
  requestWidgetRender = undefined;
430
586
  ctx.ui.setWidget(widgetKey, undefined);
431
587
  widgetVisible = false;
@@ -440,6 +596,7 @@ export default function (pi: ExtensionAPI) {
440
596
  { placement: "belowEditor" },
441
597
  );
442
598
  widgetVisible = true;
599
+ widgetEntryKey = entryKey;
443
600
  };
444
601
 
445
602
  const updateStatus = (manager: SubagentManagerShape) => {
@@ -451,8 +608,8 @@ export default function (pi: ExtensionAPI) {
451
608
  // In the TUI the below-editor strip already reports the same activity and
452
609
  // carries the manage affordance, so a footer status line would repeat it.
453
610
  const tui = sessionContext?.mode === "tui";
454
- ui.setStatus(
455
- "subagents",
611
+ statusWriter.write(
612
+ ui,
456
613
  !tui && hasActivity(counts)
457
614
  ? formatActivityStatus(ui.theme, "subagents", counts)
458
615
  : undefined,
@@ -586,16 +743,20 @@ export default function (pi: ExtensionAPI) {
586
743
  resultDelivery.clear();
587
744
  unsubStatus?.();
588
745
  unsubStatus = undefined;
746
+ unregisterWebCapability?.();
747
+ unregisterWebCapability = undefined;
589
748
  try {
590
749
  ui?.setStatus("subagents", undefined);
591
750
  sessionContext?.ui.setWidget(widgetKey, undefined);
592
751
  } catch {
593
752
  // UI may already be disposed.
594
753
  }
754
+ statusWriter.reset();
595
755
  sessionContext = undefined;
596
756
  ui = undefined;
597
757
  navigationManager = undefined;
598
758
  widgetVisible = false;
759
+ widgetEntryKey = undefined;
599
760
  requestWidgetRender = undefined;
600
761
  stripState.focused = false;
601
762
  dashboardOpen = false;
@@ -791,7 +952,37 @@ export default function (pi: ExtensionAPI) {
791
952
  } catch (error) {
792
953
  // The session scope owns reclamation, but it never opened, so this
793
954
  // worktree would otherwise be orphaned on disk.
794
- 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
+ }
795
986
  throw error;
796
987
  }
797
988
  persistId(snap.id);
@@ -954,10 +1145,15 @@ export default function (pi: ExtensionAPI) {
954
1145
  },
955
1146
  );
956
1147
  let resultIndex = 0;
1148
+ const artifactSaveFailures = new Set<string>();
1149
+ const fullResultsSaved = new Set<string>();
957
1150
  const sections = entries.map((entry) => {
958
1151
  if ("section" in entry) return entry.section;
959
1152
  const outputBudget = allocation.budgets[resultIndex++]!;
960
- return `${entry.header}\n\n${truncatedOutput(entry.snap, outputBudget)}`;
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}`;
961
1157
  });
962
1158
 
963
1159
  const combined = sections.join("\n\n---\n\n");
@@ -973,7 +1169,20 @@ export default function (pi: ExtensionAPI) {
973
1169
  details: {
974
1170
  results: ids.map((id) => {
975
1171
  const snap = manager.view.get(id);
976
- 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
+ };
977
1186
  }),
978
1187
  },
979
1188
  };
@@ -998,7 +1207,7 @@ export default function (pi: ExtensionAPI) {
998
1207
  return renderWaitResult(
999
1208
  content,
1000
1209
  result.details as WaitResultDetails | undefined,
1001
- expanded || loadSetupConfig().ui.subagentResultDisplay === "full",
1210
+ expanded || getResultDisplay() === "full",
1002
1211
  theme,
1003
1212
  );
1004
1213
  },
@@ -1193,6 +1402,7 @@ export default function (pi: ExtensionAPI) {
1193
1402
  content,
1194
1403
  (message.details ?? {}) as SubagentResultDetails,
1195
1404
  expanded,
1405
+ getResultDisplay(),
1196
1406
  theme,
1197
1407
  );
1198
1408
  },
@@ -1205,6 +1415,7 @@ export default function (pi: ExtensionAPI) {
1205
1415
  entry.data?.content ?? "",
1206
1416
  entry.data?.details ?? {},
1207
1417
  expanded,
1418
+ getResultDisplay(),
1208
1419
  theme,
1209
1420
  ),
1210
1421
  );
@@ -1247,7 +1458,7 @@ export default function (pi: ExtensionAPI) {
1247
1458
  .filter(Boolean)
1248
1459
  .join("\n\n");
1249
1460
 
1250
- if (expanded || loadSetupConfig().ui.subagentResultDisplay === "full") {
1461
+ if (expanded || getResultDisplay() === "full") {
1251
1462
  const md = new Markdown(body, 0, 0, getMarkdownTheme());
1252
1463
  const container = new Text(header, 0, 0);
1253
1464
  return {
@@ -19,6 +19,11 @@ export interface SubagentStripEntry {
19
19
  counts: ActivityCounts;
20
20
  }
21
21
 
22
+ /** Changes only when the strip needs an immediate lifecycle repaint. */
23
+ export function subagentStripEntryKey(entry: SubagentStripEntry | undefined) {
24
+ return entry ? `${entry.snapshot.id}:${entry.snapshot.status}` : undefined;
25
+ }
26
+
22
27
  function cleanLine(value: string) {
23
28
  return sanitizeTerminalText(value).replace(/\s+/g, " ").trim();
24
29
  }
@@ -71,9 +76,11 @@ function statusGlyph(snapshot: SubagentSnapshot, theme: Theme, now: number) {
71
76
  return theme.fg("error", "✗");
72
77
  }
73
78
 
79
+ const SUBAGENT_STRIP_INTERVAL_MS = 500;
80
+
74
81
  /** One-line subagent manager entry with the same affordance as Workflow. */
75
82
  export class SubagentStripWidget {
76
- private readonly timer: ReturnType<typeof setInterval>;
83
+ private timer: ReturnType<typeof setInterval> | undefined;
77
84
  private readonly tui: TUI;
78
85
  private readonly theme: Theme;
79
86
  private readonly strip: BelowEditorStripState;
@@ -89,17 +96,39 @@ export class SubagentStripWidget {
89
96
  this.theme = theme;
90
97
  this.strip = strip;
91
98
  this.getEntry = getEntry;
92
- this.timer = setInterval(() => this.tui.requestRender(), 500);
93
- this.timer.unref?.();
99
+ this.syncSpinner();
94
100
  }
95
101
 
96
102
  dispose() {
97
- clearInterval(this.timer);
103
+ if (this.timer) clearInterval(this.timer);
104
+ this.timer = undefined;
98
105
  }
99
106
 
100
- invalidate() {}
107
+ invalidate() {
108
+ this.syncSpinner();
109
+ }
110
+
111
+ private syncSpinner() {
112
+ if (this.getEntry()?.snapshot.status === "running") {
113
+ if (this.timer) return;
114
+ const timer = setInterval(() => {
115
+ if (this.timer !== timer) return;
116
+ if (this.getEntry()?.snapshot.status !== "running") {
117
+ clearInterval(timer);
118
+ this.timer = undefined;
119
+ }
120
+ this.tui.requestRender();
121
+ }, SUBAGENT_STRIP_INTERVAL_MS);
122
+ this.timer = timer;
123
+ timer.unref?.();
124
+ return;
125
+ }
126
+ if (this.timer) clearInterval(this.timer);
127
+ this.timer = undefined;
128
+ }
101
129
 
102
130
  render(width: number) {
131
+ this.syncSpinner();
103
132
  const entry = this.getEntry();
104
133
  if (!entry || width <= 0) return [];
105
134
  const { snapshot, counts } = entry;
@@ -5,11 +5,11 @@
5
5
  * on it rather than on the pi session directly.
6
6
  *
7
7
  * - pi (./backends/pi.ts): in-process `createAgentSession()` via the pi SDK.
8
- * - stub (./backends/stub.ts): scripted sessions, test-only.
9
8
  */
10
9
 
11
10
  import type { Effect, Scope, Stream } from "effect";
12
11
  import { Context } from "effect";
12
+ import type { AgentToolRenderer } from "../../shared/agent-tool-renderer.ts";
13
13
  import type {
14
14
  BackendName,
15
15
  SendError,
@@ -19,6 +19,13 @@ import type {
19
19
  SubagentMeta,
20
20
  } from "./domain.ts";
21
21
 
22
+ export interface SubagentCleanupReceipt {
23
+ /** True when the session/worktree state could not be proven quiescent. */
24
+ readonly uncertain: boolean;
25
+ /** Bounded operator-facing explanation of the cleanup result. */
26
+ readonly message: string;
27
+ }
28
+
22
29
  /**
23
30
  * A live subagent session. The manager is the single consumer of `events`;
24
31
  * it folds them into the `SubagentSnapshot` everything else reads.
@@ -42,6 +49,10 @@ export interface SubagentSession {
42
49
  * this with a timeout and fall back to closing the session scope.
43
50
  */
44
51
  readonly interrupt: Effect.Effect<void>;
52
+ /** Ephemeral Pi-native tool projection for the operator-facing child page. */
53
+ readonly toolRenderer?: AgentToolRenderer;
54
+ /** Cleanup receipt populated when scope teardown cannot prove safety. */
55
+ readonly cleanupReceipt?: () => SubagentCleanupReceipt | undefined;
45
56
  }
46
57
 
47
58
  export interface SubagentBackend {