one-code-extension 0.3.1 → 0.4.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 (113) hide show
  1. package/README.md +88 -10
  2. package/docs/guide/README.md +1 -0
  3. package/docs/guide/configuration.md +2 -2
  4. package/docs/guide/differences-from-claude-code.md +13 -4
  5. package/docs/guide/hooks.md +14 -2
  6. package/docs/guide/installation.md +54 -6
  7. package/docs/guide/permissions-modes-and-auto-mode.md +23 -7
  8. package/docs/guide/reference.md +5 -2
  9. package/docs/guide/sessions-and-context.md +18 -0
  10. package/docs/guide/terminal-interface.md +1 -1
  11. package/docs/guide/tools.md +24 -2
  12. package/docs/guide/troubleshooting.md +14 -4
  13. package/docs/guide/windows.md +184 -0
  14. package/extensions/auto-mode/actions.ts +3 -1
  15. package/extensions/auto-mode/paths.ts +21 -23
  16. package/extensions/auto-mode/safety-floor.ts +44 -6
  17. package/extensions/auto-mode/shell-analysis.ts +25 -10
  18. package/extensions/auto-mode/transcript.ts +15 -5
  19. package/extensions/background/index.ts +83 -34
  20. package/extensions/background/monitor-batch.ts +7 -5
  21. package/extensions/background/wakeup.ts +18 -24
  22. package/extensions/bash/background.ts +19 -10
  23. package/extensions/bash/guards.ts +36 -16
  24. package/extensions/bash/index.ts +36 -166
  25. package/extensions/branding/index.ts +35 -4
  26. package/extensions/btw/index.ts +207 -0
  27. package/extensions/btw/panel.ts +179 -0
  28. package/extensions/btw/prompt.ts +45 -0
  29. package/extensions/clear/index.ts +15 -0
  30. package/extensions/compaction/fit.ts +9 -6
  31. package/extensions/doctor/build.ts +22 -2
  32. package/extensions/doctor/dependencies.ts +63 -31
  33. package/extensions/doctor/fix-prompt.ts +12 -4
  34. package/extensions/doctor/index.ts +1 -0
  35. package/extensions/doctor/models.ts +2 -1
  36. package/extensions/doctor/report.ts +2 -3
  37. package/extensions/doctor/update-lookup.ts +19 -11
  38. package/extensions/effort/index.ts +2 -1
  39. package/extensions/footer/footer-line.ts +19 -0
  40. package/extensions/footer/index.ts +25 -5
  41. package/extensions/hooks/executor.ts +84 -18
  42. package/extensions/hooks/index.ts +1 -0
  43. package/extensions/hooks/matcher.ts +23 -9
  44. package/extensions/hooks/settings.ts +15 -0
  45. package/extensions/lib/branch-restore.ts +12 -0
  46. package/extensions/lib/claude-context.ts +2 -3
  47. package/extensions/lib/claude-settings.ts +19 -0
  48. package/extensions/lib/command-launch.ts +71 -0
  49. package/extensions/lib/deferred.ts +173 -33
  50. package/extensions/lib/git.ts +16 -0
  51. package/extensions/lib/keys.ts +38 -0
  52. package/extensions/lib/local-command.ts +112 -0
  53. package/extensions/lib/model-facts.generated.json +263 -68
  54. package/extensions/lib/model-policy.ts +13 -0
  55. package/extensions/lib/model-tier.ts +16 -5
  56. package/extensions/lib/notifications.ts +547 -53
  57. package/extensions/lib/paths.ts +130 -2
  58. package/extensions/lib/pi-ai-estimate.ts +57 -26
  59. package/extensions/lib/pi-version.ts +1 -1
  60. package/extensions/lib/process-tree.ts +131 -5
  61. package/extensions/lib/reminders.ts +80 -27
  62. package/extensions/lib/scratchpad.ts +11 -10
  63. package/extensions/lib/shell-spawn.ts +298 -0
  64. package/extensions/lib/shell-tool.ts +252 -0
  65. package/extensions/lib/shell-tools.ts +13 -0
  66. package/extensions/lib/side-call.ts +76 -0
  67. package/extensions/lib/tui-render.ts +62 -11
  68. package/extensions/lib/update-check.mjs +155 -0
  69. package/extensions/lib/usage-bus.ts +1 -1
  70. package/extensions/lib/which.ts +62 -0
  71. package/extensions/lib/worktree-channel.ts +18 -0
  72. package/extensions/lib/worktree-isolation.ts +2 -1
  73. package/extensions/lsp/client.ts +14 -3
  74. package/extensions/lsp/index.ts +12 -5
  75. package/extensions/lsp/install-hints.ts +33 -3
  76. package/extensions/lsp/plugin-servers.ts +2 -2
  77. package/extensions/lsp/servers.ts +29 -7
  78. package/extensions/mcp/index.ts +3 -2
  79. package/extensions/memory/entries.ts +9 -6
  80. package/extensions/memory/index.ts +3 -2
  81. package/extensions/memory/open-external.ts +4 -1
  82. package/extensions/model-default/index.ts +21 -2
  83. package/extensions/permissions/index.ts +57 -28
  84. package/extensions/permissions/matcher.ts +120 -33
  85. package/extensions/permissions/modes.ts +14 -6
  86. package/extensions/permissions/powershell-rules.ts +549 -0
  87. package/extensions/permissions/session-grant.ts +9 -9
  88. package/extensions/plugins/index.ts +3 -2
  89. package/extensions/plugins/install/install.ts +1 -1
  90. package/extensions/powershell/description.ts +139 -0
  91. package/extensions/powershell/guards.ts +91 -0
  92. package/extensions/powershell/index.ts +89 -0
  93. package/extensions/powershell/policy.ts +91 -0
  94. package/extensions/recap/index.ts +10 -24
  95. package/extensions/recap/prompt.ts +3 -3
  96. package/extensions/session-title/index.ts +120 -0
  97. package/extensions/session-title/title.ts +111 -0
  98. package/extensions/skill/index.ts +40 -18
  99. package/extensions/skill/invoke.ts +1 -0
  100. package/extensions/subagents/hand-back-review.ts +10 -12
  101. package/extensions/subagents/index.ts +221 -78
  102. package/extensions/subagents/model-select.ts +75 -7
  103. package/extensions/system-prompt/environment.ts +14 -1
  104. package/extensions/system-reminder/index.ts +9 -0
  105. package/extensions/tasks/index.ts +2 -1
  106. package/extensions/tool-search/index.ts +41 -20
  107. package/extensions/workflow/agent-session.ts +6 -4
  108. package/extensions/workflow/index.ts +28 -5
  109. package/extensions/worktree/guards.ts +5 -4
  110. package/extensions/worktree/index.ts +15 -2
  111. package/extensions/worktree/rewrite.ts +17 -0
  112. package/package.json +7 -4
  113. package/types/app.d.ts +18 -2
@@ -26,7 +26,7 @@ import { getAgentDir, type ExtensionAPI, type ExtensionContext, type ToolDefinit
26
26
  import { Type } from "typebox";
27
27
  import { SUBAGENT_ACTIONS_CHANNEL, type SubagentActionsPayload } from "../auto-mode/actions.ts";
28
28
  import { type AgentDefinition, type AgentSource, agentDirs, discoverAgents } from "./agents.ts";
29
- import { modelIdentity, modelSpec } from "../lib/model-policy.ts";
29
+ import { modelIdentity, modelSpec, supportsImageInput } from "../lib/model-policy.ts";
30
30
  import { MODEL_UNUSABLE_CHANNEL, type ModelUnusableEvent, withoutUnusable } from "../lib/model-unusable.ts";
31
31
  import { applicableSubagentDefault, loadSubagentDefault, persistSubagentModel, type SubagentDefault } from "./default-model.ts";
32
32
  import {
@@ -56,12 +56,25 @@ import { emptyUsage, formatStats, type UsageTotals } from "./usage.ts";
56
56
  import { cleanupWorktree, createWorktree, isGitRepo, type Worktree } from "./worktree.ts";
57
57
  import { findGitRoot } from "../lib/git.ts";
58
58
  import { registerWorktreeIsolation } from "../lib/worktree-isolation.ts";
59
- import { createTaskNotifier, oneShotNote, sessionOutlivesTurn, systemNotification } from "../lib/notifications.ts";
59
+ import {
60
+ AGENT_NOTE,
61
+ agentMessage,
62
+ agentSummary,
63
+ createTaskNotifier,
64
+ type HandBackVerdict,
65
+ handBackPointer,
66
+ handBackWarning,
67
+ oneShotNote,
68
+ sessionOutlivesTurn,
69
+ taskNotification,
70
+ type TaskStatus,
71
+ withReview,
72
+ } from "../lib/notifications.ts";
60
73
  import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
61
74
  import { ccToolRenderers, customMessageText, liveUiCtx, notificationComponent, safeThemeBold, safeThemePaint, truncateLine } from "../lib/tui-render.ts";
62
75
  import { deriveActivity, type FinishOutcome, LiveRunRegistry } from "./live-runs.ts";
63
76
  import { DELEGATION_STEER } from "./delegation-steer.ts";
64
- import { awaitHandBackReview, withReview } from "./hand-back-review.ts";
77
+ import { awaitHandBackReview } from "./hand-back-review.ts";
65
78
  import type { LiveSink } from "./runner.ts";
66
79
  import { recordUsage } from "../lib/usage-bus.ts";
67
80
  import { SUBAGENT_DEFAULT_CHANGED_CHANNEL } from "../lib/settings-channels.ts";
@@ -71,6 +84,7 @@ import { decodeStripKey, type StripKey } from "./panel-keys.ts";
71
84
  import { reduceShellKey } from "./shell-panel.ts";
72
85
  import { trackShellTasks } from "../lib/shell-tasks.ts";
73
86
  import { createMarkdownProse } from "./prose.ts";
87
+ import { registerLocalCommand } from "../lib/local-command.ts";
74
88
 
75
89
  /** The catalog shipped in this package: <package>/agents. */
76
90
  const BUNDLED_AGENTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
@@ -195,13 +209,15 @@ const RESIDENT_IDLE_MS = 15 * 60_000;
195
209
  /** A background agent's process, kept alive after its run so it can be messaged. */
196
210
  interface Resident {
197
211
  handle: RpcChildHandle;
212
+ /** When the resident was started: its hand-backs report tokens and tool uses cumulatively, so the duration is too. */
213
+ startedAt: number;
198
214
  /**
199
215
  * FIFO — the head entry handles the next turn_end (initial task, then one per
200
216
  * idle-time message). `review` is auto mode's rendered hand-back flag for
201
217
  * that turn (undefined when clean or auto mode is off); the handler puts it
202
218
  * ahead of the report in its notification.
203
219
  */
204
- turnHandlers: Array<(outcome: ChildOutcome, review: string | undefined) => void>;
220
+ turnHandlers: Array<(outcome: ChildOutcome, review: HandBackVerdict | undefined, stopped: boolean) => void>;
205
221
  }
206
222
 
207
223
  export default function subagentsExtension(pi: ExtensionAPI) {
@@ -232,7 +248,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
232
248
  /** taskId → kill handle: a resident's kill resolves when it has exited; a blocking run exposes `result`. */
233
249
  const liveHandles = new Map<string, { kill(): void | Promise<void>; result?: Promise<unknown> }>();
234
250
  /**
235
- * Task ids the user asked to stop (x / ctrl+x ctrl+k / session teardown). A
251
+ * Task ids the user asked to stop (task_stop / x / ctrl+x ctrl+k / session teardown). A
236
252
  * kill still surfaces as an exit, but it neither completed nor failed — so the
237
253
  * strip and the notification read this to say "Stopped" instead of the
238
254
  * misleading "Completed"/"failed" (TUI-REVIEW L1). Cleared when a run (re)starts.
@@ -458,7 +474,14 @@ export default function subagentsExtension(pi: ExtensionAPI) {
458
474
  const session = sessionModel ? modelSpec(sessionModel) : "(none)";
459
475
  const signature = `${session}|${configured?.spec ?? ""}|${configured?.setForContainment ?? ""}|${configured?.source ?? ""}|${unusableModels.size}`;
460
476
  if (autoDefaultCache?.signature === signature) return autoDefaultCache.resolution;
461
- const resolution = resolveSubagentModel({ configuredDefault: configured, sessionModel, available });
477
+ // The image-modality gate keys off the session model, which is part of the
478
+ // cache signature above, so the cached resolution stays correct per session.
479
+ const resolution = resolveSubagentModel({
480
+ configuredDefault: configured,
481
+ sessionModel,
482
+ available,
483
+ requireImageInput: supportsImageInput(sessionModel),
484
+ });
462
485
  autoDefaultCache = { signature, resolution };
463
486
  return resolution;
464
487
  };
@@ -481,6 +504,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
481
504
  defaultModel: resolution.model,
482
505
  defaultSource: resolution.source,
483
506
  configured,
507
+ requireImageInput: supportsImageInput(sessionModel),
484
508
  }),
485
509
  scope: "every-turn",
486
510
  key: "subagent-models",
@@ -534,8 +558,12 @@ export default function subagentsExtension(pi: ExtensionAPI) {
534
558
  };
535
559
  pi.on("agent_start", () => {
536
560
  spawnedThisLoop.clear();
561
+ parentBusy = true;
537
562
  return undefined;
538
563
  });
564
+ pi.on("agent_settled", () => {
565
+ parentBusy = false;
566
+ });
539
567
  pi.on("agent_end", (_event, ctx) => {
540
568
  const spawned = [...spawnedThisLoop];
541
569
  spawnedThisLoop.clear();
@@ -600,17 +628,18 @@ export default function subagentsExtension(pi: ExtensionAPI) {
600
628
 
601
629
  // --- The subagent panel: strip soft focus + Enter-to-view transcript swap ---
602
630
 
603
- /** Stop one live agent by task id (blocking-run handle or resident task). */
604
- const stopAgent = (taskId: string) => {
605
- stoppedTaskIds.add(taskId); // the coming exit is a user stop, not a completion (L1)
606
- void liveHandles.get(taskId)?.kill();
631
+ /** Stop an agent, recording its persistent id before kill can settle its turn. */
632
+ const stopAgent = (taskId: string, handle = liveHandles.get(taskId)) => {
633
+ // An old task can still await review after the agent has resumed. Its
634
+ // captured handle must not mark the replacement run as stopped.
635
+ if (handle && liveHandles.get(taskId) === handle) stoppedTaskIds.add(taskId);
636
+ return handle?.kill();
607
637
  };
608
638
  /** Stop every live agent; returns one settle promise per agent (a resident's exit, a blocking run's result). */
609
639
  const stopAllAgents = (): Promise<unknown>[] => {
610
640
  const exits: Promise<unknown>[] = [];
611
641
  for (const [taskId, handle] of liveHandles) {
612
- stoppedTaskIds.add(taskId);
613
- const killed = handle.kill();
642
+ const killed = stopAgent(taskId, handle);
614
643
  const settled = handle.result ?? killed;
615
644
  if (settled) exits.push(settled);
616
645
  }
@@ -823,10 +852,14 @@ export default function subagentsExtension(pi: ExtensionAPI) {
823
852
  return undefined; // typing resumes in the editor, byte included
824
853
  }
825
854
  if (result.effect === "toAgents") {
826
- // No agent rows to land on → the byte passes through to the editor,
827
- // consistent with the initial-entry branch.
828
855
  panel.setShellFocus(undefined);
829
- return focusFirstAgentRow() ? { consume: true } : undefined;
856
+ if (focusFirstAgentRow()) return { consume: true };
857
+ // No agent rows below the chip: stay on it. Dropping focus here handed
858
+ // the NEXT key to the editor, where ↑ recalls the last prompt — the
859
+ // "sometimes ↓ recalls history" seen on the Windows VM (2026-09-19):
860
+ // ↓ chip, ↓ (nothing below, focus silently gone), ↑ → history.
861
+ panel.setShellFocus({ stage: "chip" });
862
+ return { consume: true };
830
863
  }
831
864
  if (result.effect === "stopSelected") {
832
865
  try {
@@ -972,7 +1005,10 @@ export default function subagentsExtension(pi: ExtensionAPI) {
972
1005
  );
973
1006
  }
974
1007
 
975
- const notifier = createTaskNotifier(pi);
1008
+ // A hand-back the model already read through task_output (block: true on the
1009
+ // agent's task) is withdrawn — One Code's task_output returns the full report,
1010
+ // so the message and its pointer would only repeat it (see decisions).
1011
+ const notifier = createTaskNotifier(pi, { withdrawOnDelivery: true });
976
1012
  /** Every subagent notification; silent during shutdown (a teardown kill is not news). */
977
1013
  const notify: typeof notifier = (customType, text, details) => {
978
1014
  if (shuttingDown) return;
@@ -998,10 +1034,10 @@ export default function subagentsExtension(pi: ExtensionAPI) {
998
1034
  * the spawning child never saw the message and the main model was told about
999
1035
  * work it did not delegate (SUBAGENT-REVIEW L4).
1000
1036
  */
1001
- const relayToParent = (parentTaskId: string, name: string, message: string, summary?: string) => {
1037
+ const relayToParent = (parentTaskId: string, from: string, name: string, message: string, summary?: string) => {
1002
1038
  // Bounded once: the fallback reuses the same body (and persisted file).
1003
1039
  const body = boundedMessage(name, message);
1004
- const toMain = () => announceAgentMessage(name, body, summary);
1040
+ const toMain = () => announceAgentMessage(from, name, body);
1005
1041
  const parent = residents.get(parentTaskId);
1006
1042
  if (!parent || parent.handle.exited()) {
1007
1043
  toMain();
@@ -1011,10 +1047,78 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1011
1047
  };
1012
1048
 
1013
1049
  const boundedMessage = (name: string, message: string) => bounded(message, `message-${name}-${Date.now()}`, MESSAGE_TO_MAIN_CAP);
1014
- const announceAgentMessage = (name: string, body: string, summary?: string) =>
1015
- notify("subagent-message", systemNotification(`Message from agent ${name}${summary ? ` (${summary})` : ""}:\n\n${body}`), { name, summary });
1050
+ /** True between agent_start and agent_settled: a child's mid-run message then lands mid-turn (CC's other opener). */
1051
+ let parentBusy = false;
1052
+ /**
1053
+ * A child's mid-run message as CC's `<agent-message>` (no hand-back preamble:
1054
+ * it is not a report), addressed from the child's task id — the address
1055
+ * SendMessage reaches it by. CC's frame has no slot for the child's own
1056
+ * one-line summary, so it is dropped here (it still labels the relay to a
1057
+ * parent child).
1058
+ */
1059
+ const announceAgentMessage = (from: string, name: string, body: string) =>
1060
+ notify("subagent-message", agentMessage({ from, body, midTurn: parentBusy }), { taskId: from, name });
1016
1061
  /** Relay a child's send_message {to: "main"} into this conversation. */
1017
- const notifyAgentMessage = (name: string, message: string, summary?: string) => announceAgentMessage(name, boundedMessage(name, message), summary);
1062
+ const notifyAgentMessage = (from: string, name: string, message: string) => announceAgentMessage(from, name, boundedMessage(name, message));
1063
+
1064
+ /** The spool a resident's turns are written to (`task_output` and the notification's `<output-file>`). */
1065
+ const outputLogPath = (record: AgentRunRecord): string | undefined =>
1066
+ record.sessionSearchDir ? join(record.sessionSearchDir, "output.log") : undefined;
1067
+
1068
+ /**
1069
+ * Claude Code's two-message hand-back for a resident agent's finished turn
1070
+ * (its first report, an update, a reply): the report goes out as an
1071
+ * `<agent-message>` from the agent — the preamble, every line indented, auto
1072
+ * mode's review flag above it — and the agent's own `<task-notification>`
1073
+ * follows, its `<result>` a pointer at that message, never the report again.
1074
+ * A turn that failed or was stopped delivered no report, so it is one
1075
+ * notification with the output (and any flag) inline in `<result>`. Both go
1076
+ * through the same notifier, in this order, so a coalesced round keeps the
1077
+ * report ahead of the pointer.
1078
+ */
1079
+ const notifyHandBack = (opts: {
1080
+ /** The agent's persistent id: `from=` on the message, the SendMessage address. */
1081
+ from: string;
1082
+ /** The registered task this turn is (the agent's own id, or a SendMessage task's). */
1083
+ taskId: string;
1084
+ name: string;
1085
+ toolUseId?: string;
1086
+ outputFile?: string;
1087
+ /** The turn's outcome: its status, tool count and tokens come from here. */
1088
+ outcome: ChildOutcome;
1089
+ /** The turn ended because the user or the model stopped the agent (CC's `killed`). */
1090
+ stopped?: boolean;
1091
+ /** Start of the span `outcome`'s tokens and tool uses cover, for CC's `<duration_ms>`. */
1092
+ startedAt: number;
1093
+ /** The bounded report text (a failed turn's output rides `<result>` instead). */
1094
+ report: string;
1095
+ review: HandBackVerdict | undefined;
1096
+ details?: Record<string, unknown>;
1097
+ }) => {
1098
+ const status: TaskStatus = opts.stopped ? "killed" : opts.outcome.failed ? "failed" : "completed";
1099
+ const warning = opts.review ? handBackWarning(opts.review) : undefined;
1100
+ const details = { ...opts.details, taskId: opts.taskId, name: opts.name, failed: status === "failed", reviewed: opts.review !== undefined };
1101
+ const envelope = (result: string) =>
1102
+ taskNotification({
1103
+ kind: "agent",
1104
+ taskId: opts.taskId,
1105
+ toolUseId: opts.toolUseId,
1106
+ outputFile: opts.outputFile,
1107
+ status,
1108
+ summary: agentSummary(opts.name, status),
1109
+ note: AGENT_NOTE,
1110
+ result,
1111
+ usage: { subagentTokens: opts.outcome.usage.total, toolUses: opts.outcome.toolCalls, durationMs: Date.now() - opts.startedAt },
1112
+ });
1113
+ if (status !== "completed") {
1114
+ notify("subagent-result", envelope(withReview(opts.report, warning)), details);
1115
+ return;
1116
+ }
1117
+ // CC's mid-turn form applies to hand-backs too (observed live: a report
1118
+ // landing while the parent works takes the other opener and the hint).
1119
+ notify("subagent-message", agentMessage({ from: opts.from, body: opts.report, handBack: true, warning, midTurn: parentBusy }), details);
1120
+ notify("subagent-result", envelope(handBackPointer(opts.from, opts.review !== undefined)), details);
1121
+ };
1018
1122
 
1019
1123
  /** A fork's system prompt is persisted beside its session so a later resume can restore it (review S6). */
1020
1124
  const FORK_PROMPT_FILE = "system-prompt.md";
@@ -1135,6 +1239,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1135
1239
  configuredDefault: applicableSubagentDefault(loadSubagentDefault(os.homedir()), ctx.model),
1136
1240
  sessionModel: ctx.model,
1137
1241
  available,
1242
+ requireImageInput: supportsImageInput(ctx.model),
1138
1243
  });
1139
1244
  for (const notice of resolution.notices) notifyModelOnce(ctx, notice);
1140
1245
  const modelNotes = subagentModelNotes(resolution);
@@ -1297,7 +1402,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1297
1402
  },
1298
1403
  sink: live.sink,
1299
1404
  onMessageToMain: (message, summary) =>
1300
- parent ? relayToParent(parent.taskId, request.name, message, summary) : notifyAgentMessage(request.name, message, summary),
1405
+ parent ? relayToParent(parent.taskId, record.taskId, request.name, message, summary) : notifyAgentMessage(record.taskId, request.name, message),
1301
1406
  extraTools: spawnToolsFor(record),
1302
1407
  });
1303
1408
  liveHandles.set(record.taskId, handle);
@@ -1354,7 +1459,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1354
1459
  "For a single-fact lookup you already know how to run, search directly instead. Once you've delegated work, don't also run it yourself — wait for the result.\n" +
1355
1460
  "\n" +
1356
1461
  "## How agents run\n" +
1357
- "Agents run in the background: the call returns immediately with a task id, and you'll be notified when one completes — the agent's report arrives as a system notification while you keep working, or on its own if you are idle. Never fabricate or predict a pending agent's results — the notification is never something you write yourself; if the user asks before it arrives, say it's still running. Call task_output only if your next step cannot proceed without the result (block=true waits); stop a run with task_stop. Both are deferred — load them with tool_search first. (Exception: in a one-shot print session the call blocks and returns the report directly — no notification follows.)\n" +
1462
+ "Agents run in the background: the call returns immediately with a task id, and you'll be notified when one completes — the agent's report arrives as a task notification while you keep working, or on its own if you are idle. Never fabricate or predict a pending agent's results — the notification is never something you write yourself; if the user asks before it arrives, say it's still running. Call task_output only if your next step cannot proceed without the result (block=true waits); stop a run with task_stop. Both are deferred — load them with tool_search first. (Exception: in a one-shot print session the call blocks and returns the report directly — no notification follows.)\n" +
1358
1463
  "\n" +
1359
1464
  "## Usage notes\n" +
1360
1465
  "- Give a complete, self-contained task: the agent cannot ask follow-up questions.\n" +
@@ -1553,6 +1658,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1553
1658
  configuredDefault: configuredDefault,
1554
1659
  sessionModel: ctx.model,
1555
1660
  available,
1661
+ requireImageInput: supportsImageInput(ctx.model),
1556
1662
  })
1557
1663
  : resolveAutoDefault(ctx.model, available, configuredDefault);
1558
1664
  if (resolution.unresolved) {
@@ -1561,18 +1667,20 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1561
1667
  configuredDefault: configuredDefault,
1562
1668
  sessionModel: ctx.model,
1563
1669
  available,
1670
+ requireImageInput: supportsImageInput(ctx.model),
1564
1671
  });
1565
1672
  return {
1566
1673
  content: [
1567
1674
  {
1568
1675
  type: "text",
1569
1676
  text:
1570
- `Unknown model "${resolution.unresolved}" — no available model matches it.\n\n` +
1677
+ `${resolution.unresolvedReason ?? `Unknown model "${resolution.unresolved}" — no available model matches it.`}\n\n` +
1571
1678
  subagentModelsReminder({
1572
1679
  available,
1573
1680
  sessionModel: ctx.model,
1574
1681
  defaultModel: fallback.model,
1575
1682
  defaultSource: fallback.source,
1683
+ requireImageInput: supportsImageInput(ctx.model),
1576
1684
  }),
1577
1685
  },
1578
1686
  ],
@@ -1586,6 +1694,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1586
1694
  configuredDefault: configuredDefault,
1587
1695
  sessionModel: ctx.model,
1588
1696
  available,
1697
+ requireImageInput: supportsImageInput(ctx.model),
1589
1698
  });
1590
1699
  return {
1591
1700
  content: [
@@ -1600,6 +1709,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1600
1709
  sessionModel: ctx.model,
1601
1710
  defaultModel: fallback.model,
1602
1711
  defaultSource: fallback.source,
1712
+ requireImageInput: supportsImageInput(ctx.model),
1603
1713
  }),
1604
1714
  },
1605
1715
  ],
@@ -1685,7 +1795,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1685
1795
  }
1686
1796
  }
1687
1797
 
1688
- const logPath = p.record.sessionSearchDir ? join(p.record.sessionSearchDir, "output.log") : undefined;
1798
+ const logPath = outputLogPath(p.record);
1689
1799
  let lastLogWrite = 0;
1690
1800
  let finish!: () => void;
1691
1801
  const finished = new Promise<void>((resolve) => {
@@ -1703,28 +1813,27 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1703
1813
  // the same way a SendMessage task returns one reply (review S12).
1704
1814
  let firstTurnOutput: string | undefined;
1705
1815
  const live = trackLiveRun(p.record, p.request);
1706
- const resident: Resident = { handle: undefined as never, turnHandlers: [] };
1816
+ const resident: Resident = { handle: undefined as never, startedAt: Date.now(), turnHandlers: [] };
1707
1817
  const worktreeNote = worktree
1708
1818
  ? `\n\n(Running in worktree ${worktree.path} — kept while the agent stays resident.)`
1709
1819
  : "";
1710
- resident.turnHandlers.push((outcome, review) => {
1711
- const stopped = stoppedTaskIds.has(p.record.taskId);
1820
+ resident.turnHandlers.push((outcome, review, stopped) => {
1712
1821
  task.status = stopped ? "stopped" : outcome.failed ? "failed" : "completed";
1713
1822
  task.finishedAt = Date.now();
1714
1823
  firstTurnOutput = outcome.output;
1715
1824
  finish();
1716
- const stats = formatStats(outcome.toolCalls, outcome.usage);
1717
- const verb = stopped ? "was stopped" : outcome.failed ? "failed" : "completed";
1718
- notify(
1719
- "subagent-result",
1720
- systemNotification(
1721
- withReview(
1722
- `Agent ${p.record.name} (task ${p.record.taskId}) ${verb} (${stats}). It stays reachable with SendMessage.\n\n${bounded(outcome.output, `${p.record.taskId}-report`, OUTPUT_CAP)}${worktreeNote}`,
1723
- review,
1724
- ),
1725
- ),
1726
- { taskId: p.record.taskId, name: p.record.name, failed: stopped ? false : (outcome.failed ?? false), reviewed: review !== undefined },
1727
- );
1825
+ notifyHandBack({
1826
+ from: p.record.taskId,
1827
+ taskId: p.record.taskId,
1828
+ name: p.record.name,
1829
+ toolUseId: toolCallId,
1830
+ outputFile: logPath,
1831
+ outcome,
1832
+ stopped,
1833
+ startedAt: task.startedAt,
1834
+ report: `${bounded(outcome.output, `${p.record.taskId}-report`, OUTPUT_CAP)}${worktreeNote}`,
1835
+ review,
1836
+ });
1728
1837
  });
1729
1838
 
1730
1839
  // Idle reaper: every run is a resident now, so without this a long
@@ -1778,7 +1887,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1778
1887
  }
1779
1888
  },
1780
1889
  sink: live.sink,
1781
- onMessageToMain: (message, summary) => notifyAgentMessage(p.record.name, message, summary),
1890
+ onMessageToMain: (message) => notifyAgentMessage(p.record.taskId, p.record.name, message),
1782
1891
  extraTools: spawnToolsFor(p.record),
1783
1892
  onTurnEnd: (outcome) => {
1784
1893
  registry.sessionFileFor(p.record);
@@ -1792,18 +1901,28 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1792
1901
  // trailing it. The wait is bounded (hand-back-review.ts) and
1793
1902
  // answered synchronously when auto mode is off.
1794
1903
  const handler = resident.turnHandlers.shift();
1904
+ // A stop or resume during review must not change this turn's outcome.
1905
+ const stopped = stoppedTaskIds.has(p.record.taskId);
1795
1906
  armReaper();
1796
1907
  void awaitHandBackReview(pi.events, p.record, outcome.actions).then((review) => {
1797
1908
  if (handler) {
1798
- handler(outcome, review);
1909
+ handler(outcome, review, stopped);
1799
1910
  } else {
1800
1911
  // A turn nobody is waiting on (e.g. a steer that raced past its
1801
1912
  // target turn and ran on its own) must still surface.
1802
- notify(
1803
- "subagent-result",
1804
- systemNotification(withReview(`Update from ${p.record.name}:\n\n${bounded(outcome.output, `${p.record.taskId}-update-${Date.now()}`, OUTPUT_CAP)}`, review)),
1805
- { name: p.record.name, failed: outcome.failed ?? false, reviewed: review !== undefined },
1806
- );
1913
+ notifyHandBack({
1914
+ from: p.record.taskId,
1915
+ taskId: p.record.taskId,
1916
+ name: p.record.name,
1917
+ toolUseId: toolCallId,
1918
+ outputFile: logPath,
1919
+ outcome,
1920
+ stopped,
1921
+ startedAt: resident.startedAt,
1922
+ report: bounded(outcome.output, `${p.record.taskId}-update-${Date.now()}`, OUTPUT_CAP),
1923
+ review,
1924
+ details: { update: true },
1925
+ });
1807
1926
  }
1808
1927
  });
1809
1928
  },
@@ -1828,7 +1947,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1828
1947
  startedAt: Date.now(),
1829
1948
  logPath,
1830
1949
  output: () => firstTurnOutput ?? (handle.snapshot().text || lastOutput),
1831
- stop: () => handle.kill(),
1950
+ stop: () => stopAgent(p.record.taskId, handle),
1832
1951
  resident: () => !handle.exited(),
1833
1952
  finished,
1834
1953
  };
@@ -1844,7 +1963,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1844
1963
  content: [
1845
1964
  {
1846
1965
  type: "text",
1847
- text: `${lines.join("\n")}\n\nCompletion (with the agent's report) will arrive as a system notification on its own — you do not need to wait for it or poll; keep working. ${PENDING_RESULT_PROHIBITION} Call task_output only if your next step cannot proceed without the result (block=true waits). Stop with task_stop; SendMessage reaches the agent even while it runs (the message is steered into its current turn).`,
1966
+ text: `${lines.join("\n")}\n\nCompletion (with the agent's report) will arrive as a task notification on its own — you do not need to wait for it or poll; keep working. ${PENDING_RESULT_PROHIBITION} Call task_output only if your next step cannot proceed without the result (block=true waits). Stop with task_stop; SendMessage reaches the agent even while it runs (the message is steered into its current turn).`,
1848
1967
  },
1849
1968
  ],
1850
1969
  details: { agentRuns: records, background: true },
@@ -1859,13 +1978,13 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1859
1978
  title: (a) => (a?.to ? `to ${a.to}${a.summary ? `: ${a.summary}` : ""}` : undefined),
1860
1979
  }),
1861
1980
  description:
1862
- 'Send a message to a previously spawned agent, addressed by the name from its spawn result (or its task id). To discover the names of running or finished agents, use list_agents (deferred — load it with tool_search select:list_agents). A resident background agent is reached live (mid-turn the message is steered into its current work; when idle it starts a new turn); a finished agent is resumed from its session with full context. Replies arrive as system notifications. (A subagent reporting back to the main conversation uses its own SendMessage with to: "main".)',
1981
+ 'Send a message to a previously spawned agent, addressed by the name from its spawn result (or its task id). To discover the names of running or finished agents, use list_agents (deferred — load it with tool_search select:list_agents). A resident background agent is reached live (mid-turn the message is steered into its current work; when idle it starts a new turn); a finished agent is resumed from its session with full context. Replies arrive as task notifications. (A subagent reporting back to the main conversation uses its own SendMessage with to: "main".)',
1863
1982
  parameters: Type.Object({
1864
1983
  to: Type.String({ description: "Agent name (or task id) from a previous Agent run" }),
1865
1984
  message: Type.String({ description: "Plain text message for the agent" }),
1866
1985
  summary: Type.Optional(Type.String({ description: "5-10 word preview shown in the UI" })),
1867
1986
  }),
1868
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1987
+ async execute(toolCallId, params, _signal, _onUpdate, ctx) {
1869
1988
  if (params.to === "main") {
1870
1989
  // The main conversation's SendMessage only addresses spawned agents; the
1871
1990
  // "main" recipient exists only on a subagent's own injected SendMessage.
@@ -1922,7 +2041,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1922
2041
  content: [
1923
2042
  {
1924
2043
  type: "text",
1925
- text: `${record.name}'s turn had just finished; the message starts its next turn. The reply will arrive as a system notification ("Update from ${record.name}"). ${PENDING_RESULT_PROHIBITION}`,
2044
+ text: `${record.name}'s turn had just finished; the message starts its next turn. The reply will arrive as a task notification from ${record.name}. ${PENDING_RESULT_PROHIBITION}`,
1926
2045
  },
1927
2046
  ],
1928
2047
  details: { agentRuns: [record] },
@@ -1947,21 +2066,31 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1947
2066
  status: "running",
1948
2067
  startedAt: Date.now(),
1949
2068
  output: () => replyOutput || resident.handle.snapshot().text,
1950
- stop: () => resident.handle.kill(),
2069
+ stop: () => stopAgent(record.taskId, resident.handle),
1951
2070
  resident: () => !resident.handle.exited(),
1952
2071
  finished,
1953
2072
  };
1954
- resident.turnHandlers.push((outcome, review) => {
1955
- task.status = outcome.failed ? "failed" : "completed";
2073
+ resident.turnHandlers.push((outcome, review, stopped) => {
2074
+ task.status = stopped ? "stopped" : outcome.failed ? "failed" : "completed";
1956
2075
  task.finishedAt = Date.now();
1957
2076
  replyOutput = outcome.output;
1958
2077
  finish();
1959
- const stats = formatStats(outcome.toolCalls, outcome.usage);
1960
- notify(
1961
- "subagent-result",
1962
- systemNotification(withReview(`Reply from ${record.name} (${stats}):\n\n${bounded(outcome.output, `${taskId}-reply`, OUTPUT_CAP)}`, review)),
1963
- { taskId, name: record.name, failed: outcome.failed ?? false, reviewed: review !== undefined },
1964
- );
2078
+ // `<task-id>` is this message's task (what task_output resolves); `from=`
2079
+ // stays the agent's id, the address a further SendMessage uses. The
2080
+ // tracker's tokens and tool uses span the resident's life, so the
2081
+ // duration does too.
2082
+ notifyHandBack({
2083
+ from: record.taskId,
2084
+ taskId,
2085
+ name: record.name,
2086
+ toolUseId: toolCallId,
2087
+ outputFile: outputLogPath(record),
2088
+ outcome,
2089
+ stopped,
2090
+ startedAt: resident.startedAt,
2091
+ report: bounded(outcome.output, `${taskId}-reply`, OUTPUT_CAP),
2092
+ review,
2093
+ });
1965
2094
  });
1966
2095
  pi.events.emit(TASK_REGISTER_CHANNEL, task);
1967
2096
  void resident.handle.send(params.message);
@@ -1973,7 +2102,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1973
2102
  content: [
1974
2103
  {
1975
2104
  type: "text",
1976
- text: `Message sent to resident agent ${record.name} (task ${taskId}). The reply will arrive as a system notification on its own; inspect with task_output. ${PENDING_RESULT_PROHIBITION}`,
2105
+ text: `Message sent to resident agent ${record.name} (task ${taskId}). The reply will arrive as a task notification on its own; inspect with task_output. ${PENDING_RESULT_PROHIBITION}`,
1977
2106
  },
1978
2107
  ],
1979
2108
  details: { agentRuns: [record], taskId },
@@ -2061,7 +2190,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
2061
2190
  thinking: record.thinking,
2062
2191
  onProgress: (toolCalls, _text, usage) => live.progress(toolCalls, usage),
2063
2192
  sink: live.sink,
2064
- onMessageToMain: (message, summary) => notifyAgentMessage(record.name, message, summary),
2193
+ onMessageToMain: (message) => notifyAgentMessage(record.taskId, record.name, message),
2065
2194
  extraTools: spawnToolsFor(record),
2066
2195
  });
2067
2196
  liveHandles.set(record.taskId, handle);
@@ -2074,23 +2203,29 @@ export default function subagentsExtension(pi: ExtensionAPI) {
2074
2203
  status: "running",
2075
2204
  startedAt: Date.now(),
2076
2205
  output: () => handle.snapshot().text,
2077
- stop: () => handle.kill(),
2206
+ stop: () => stopAgent(record.taskId, handle),
2078
2207
  finished,
2079
2208
  };
2080
2209
  pi.events.emit(TASK_REGISTER_CHANNEL, task);
2081
2210
 
2082
2211
  void handle.result.then((outcome) => {
2083
2212
  runningIds.delete(record.taskId);
2084
- live.finish(Boolean(outcome.failed));
2085
- task.status = outcome.failed ? "failed" : "completed";
2213
+ const stopped = stoppedTaskIds.has(record.taskId);
2214
+ live.finish(stopped ? "stopped" : Boolean(outcome.failed));
2215
+ task.status = stopped ? "stopped" : outcome.failed ? "failed" : "completed";
2086
2216
  task.finishedAt = Date.now();
2087
2217
  finish();
2088
- const stats = formatStats(outcome.toolCalls, outcome.usage);
2089
- notify(
2090
- "subagent-result",
2091
- systemNotification(`Reply from ${record.name} (${stats}):\n\n${relocationNote}${bounded(outcome.output, `${taskId}-reply`, OUTPUT_CAP)}`),
2092
- { taskId, name: record.name, failed: outcome.failed ?? false },
2093
- );
2218
+ notifyHandBack({
2219
+ from: record.taskId,
2220
+ taskId,
2221
+ name: record.name,
2222
+ toolUseId: toolCallId,
2223
+ outcome,
2224
+ stopped,
2225
+ startedAt: task.startedAt,
2226
+ report: `${relocationNote}${bounded(outcome.output, `${taskId}-reply`, OUTPUT_CAP)}`,
2227
+ review: undefined,
2228
+ });
2094
2229
  });
2095
2230
 
2096
2231
  spawnedThisLoop.add(record.taskId); // arm the anti-fabrication backstop (WEAK-MODEL-REVIEW H1)
@@ -2098,7 +2233,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
2098
2233
  content: [
2099
2234
  {
2100
2235
  type: "text",
2101
- text: `Message sent to ${record.name} (task ${taskId}). The reply will arrive as a system notification on its own; inspect with task_output. ${PENDING_RESULT_PROHIBITION}`,
2236
+ text: `Message sent to ${record.name} (task ${taskId}). The reply will arrive as a task notification on its own; inspect with task_output. ${PENDING_RESULT_PROHIBITION}`,
2102
2237
  },
2103
2238
  ],
2104
2239
  details: { agentRuns: [record], taskId },
@@ -2146,9 +2281,9 @@ export default function subagentsExtension(pi: ExtensionAPI) {
2146
2281
  });
2147
2282
  pi.events.emit(DEFER_CHANNEL, { name: "list_agents", keywords: ["agents", "list", "running", "spawned", "subagents", "who"] });
2148
2283
 
2149
- pi.registerCommand("agents", {
2284
+ registerLocalCommand(pi, "agents", {
2150
2285
  description: "Open the live subagent panel, or list available agents",
2151
- handler: async (_args, ctx) => {
2286
+ handler: async (args, ctx) => {
2152
2287
  // With live children, focus the strip on the newest and open its view
2153
2288
  // (Claude Code's agent panel); otherwise there is nothing running, so
2154
2289
  // list the catalog the way the tool's action:"list" does.
@@ -2172,6 +2307,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
2172
2307
  configuredDefault: applicable,
2173
2308
  sessionModel: ctx.model,
2174
2309
  available,
2310
+ requireImageInput: supportsImageInput(ctx.model),
2175
2311
  });
2176
2312
  ctx.ui.notify(
2177
2313
  [
@@ -2208,20 +2344,27 @@ export default function subagentsExtension(pi: ExtensionAPI) {
2208
2344
  }
2209
2345
 
2210
2346
  const available = usableModels(ctx);
2211
- const resolution = resolveSubagentModel({ requested: spec, sessionModel: ctx.model, available });
2347
+ const resolution = resolveSubagentModel({
2348
+ requested: spec,
2349
+ sessionModel: ctx.model,
2350
+ available,
2351
+ requireImageInput: supportsImageInput(ctx.model),
2352
+ });
2212
2353
  if (resolution.unresolved) {
2213
2354
  const fallback = resolveSubagentModel({
2214
2355
  configuredDefault: applicableSubagentDefault(loadSubagentDefault(os.homedir()), ctx.model),
2215
2356
  sessionModel: ctx.model,
2216
2357
  available,
2358
+ requireImageInput: supportsImageInput(ctx.model),
2217
2359
  });
2218
2360
  ctx.ui.notify(
2219
- `No available model matches "${spec}".\n\n` +
2361
+ `${resolution.unresolvedReason ?? `No available model matches "${spec}".`}\n\n` +
2220
2362
  subagentModelsReminder({
2221
2363
  available,
2222
2364
  sessionModel: ctx.model,
2223
2365
  defaultModel: fallback.model,
2224
2366
  defaultSource: fallback.source,
2367
+ requireImageInput: supportsImageInput(ctx.model),
2225
2368
  }),
2226
2369
  "error",
2227
2370
  );
@@ -2249,7 +2392,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
2249
2392
  ctx.ui.notify(`Subagent default set to "${spec}" (saved to ~/.onecode/settings.json).`, "info");
2250
2393
  };
2251
2394
 
2252
- pi.registerCommand("subagent", {
2395
+ registerLocalCommand(pi, "subagent", {
2253
2396
  description: "Set the default model for subagent/workflow runs: /subagent [provider/model-id|inherit|status|clear]",
2254
2397
  getArgumentCompletions: (prefix) =>
2255
2398
  ["inherit", "status", "clear"]