@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.1

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 (101) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/cli.js +3305 -3302
  3. package/dist/types/advisor/advise-tool.d.ts +5 -2
  4. package/dist/types/cli-commands.d.ts +14 -1
  5. package/dist/types/config/model-registry.d.ts +4 -0
  6. package/dist/types/config/models-config-schema.d.ts +10 -0
  7. package/dist/types/config/models-config.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +1 -1
  9. package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
  10. package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
  11. package/dist/types/lib/xai-http.d.ts +11 -0
  12. package/dist/types/modes/acp/acp-agent.d.ts +3 -0
  13. package/dist/types/modes/acp/acp-mode.d.ts +6 -2
  14. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  15. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
  16. package/dist/types/modes/components/custom-editor.d.ts +23 -1
  17. package/dist/types/modes/components/hook-editor.d.ts +9 -2
  18. package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
  19. package/dist/types/modes/components/session-selector.d.ts +2 -0
  20. package/dist/types/modes/components/tool-execution.d.ts +8 -1
  21. package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
  22. package/dist/types/modes/interactive-mode.d.ts +6 -0
  23. package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
  24. package/dist/types/modes/types.d.ts +10 -0
  25. package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
  26. package/dist/types/session/agent-session.d.ts +9 -1
  27. package/dist/types/session/streaming-output.d.ts +2 -0
  28. package/dist/types/slash-commands/types.d.ts +4 -4
  29. package/dist/types/system-prompt.d.ts +1 -1
  30. package/dist/types/tools/browser/registry.d.ts +7 -3
  31. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  32. package/dist/types/tools/index.d.ts +3 -1
  33. package/dist/types/tools/manage-skill.d.ts +2 -0
  34. package/dist/types/web/search/index.d.ts +2 -0
  35. package/dist/types/web/search/providers/base.d.ts +3 -0
  36. package/package.json +12 -12
  37. package/src/advisor/__tests__/advisor.test.ts +22 -12
  38. package/src/advisor/advise-tool.ts +7 -3
  39. package/src/cli/bench-cli.ts +4 -1
  40. package/src/cli/grep-cli.ts +2 -1
  41. package/src/cli-commands.ts +57 -23
  42. package/src/config/model-registry.ts +9 -0
  43. package/src/config/model-resolver.ts +43 -5
  44. package/src/config/models-config-schema.ts +1 -0
  45. package/src/config/settings-schema.ts +2 -1
  46. package/src/discovery/codex.ts +14 -4
  47. package/src/discovery/substitute-plugin-root.ts +23 -7
  48. package/src/export/html/template.js +2 -0
  49. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
  50. package/src/internal-urls/agent-protocol.ts +73 -39
  51. package/src/lib/xai-http.ts +28 -2
  52. package/src/modes/acp/acp-agent.ts +28 -9
  53. package/src/modes/acp/acp-event-mapper.ts +8 -0
  54. package/src/modes/acp/acp-mode.ts +18 -4
  55. package/src/modes/components/assistant-message.ts +35 -14
  56. package/src/modes/components/chat-transcript-builder.ts +2 -0
  57. package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
  58. package/src/modes/components/custom-editor.ts +62 -1
  59. package/src/modes/components/hook-editor.ts +17 -2
  60. package/src/modes/components/login-dialog.test.ts +56 -0
  61. package/src/modes/components/login-dialog.ts +7 -3
  62. package/src/modes/components/plan-review-overlay.ts +51 -10
  63. package/src/modes/components/session-selector.ts +6 -0
  64. package/src/modes/components/status-line/component.ts +2 -4
  65. package/src/modes/components/tool-execution.test.ts +101 -0
  66. package/src/modes/components/tool-execution.ts +117 -9
  67. package/src/modes/components/transcript-container.ts +30 -2
  68. package/src/modes/controllers/command-controller-shared.ts +1 -1
  69. package/src/modes/controllers/command-controller.ts +1 -1
  70. package/src/modes/controllers/event-controller.ts +1 -0
  71. package/src/modes/controllers/selector-controller.ts +19 -17
  72. package/src/modes/interactive-mode.ts +66 -11
  73. package/src/modes/noninteractive-dispose.test.ts +60 -0
  74. package/src/modes/print-mode.ts +8 -3
  75. package/src/modes/rpc/rpc-mode.ts +12 -3
  76. package/src/modes/theme/theme.ts +2 -2
  77. package/src/modes/types.ts +10 -0
  78. package/src/modes/utils/interactive-context-helpers.ts +3 -1
  79. package/src/modes/utils/transcript-render-helpers.ts +3 -2
  80. package/src/prompts/system/system-prompt.md +1 -1
  81. package/src/sdk.ts +6 -2
  82. package/src/session/agent-session.ts +79 -34
  83. package/src/session/session-listing.ts +6 -5
  84. package/src/session/session-loader.ts +9 -0
  85. package/src/session/session-persistence.ts +11 -0
  86. package/src/session/streaming-output.ts +3 -1
  87. package/src/slash-commands/builtin-registry.ts +2 -0
  88. package/src/slash-commands/types.ts +4 -4
  89. package/src/system-prompt.ts +2 -2
  90. package/src/tools/browser/registry.ts +9 -2
  91. package/src/tools/browser/tab-supervisor.ts +65 -10
  92. package/src/tools/browser.ts +4 -3
  93. package/src/tools/eval.ts +3 -0
  94. package/src/tools/image-gen.ts +561 -512
  95. package/src/tools/index.ts +3 -1
  96. package/src/tools/manage-skill.ts +5 -3
  97. package/src/tools/output-meta.ts +7 -0
  98. package/src/tools/path-utils.ts +8 -5
  99. package/src/web/search/index.ts +21 -6
  100. package/src/web/search/providers/base.ts +3 -0
  101. package/src/web/search/providers/xai.ts +40 -9
@@ -83,7 +83,7 @@ import { UserMessageSelectorComponent } from "../components/user-message-selecto
83
83
  import type { SessionObserverRegistry } from "../session-observer-registry";
84
84
  import { buildCopyTargets } from "../utils/copy-targets";
85
85
 
86
- const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
86
+ const MANUAL_LOGIN_PROMPT = "Paste the authorization code (or full redirect URL), then press Enter:";
87
87
 
88
88
  export class SelectorController {
89
89
  constructor(private ctx: InteractiveModeContext) {}
@@ -188,6 +188,7 @@ export class SelectorController {
188
188
  onPluginsChanged: async () => {
189
189
  const projectPath = await resolveActiveProjectRegistryPath(this.ctx.sessionManager.getCwd());
190
190
  clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
191
+ await this.ctx.refreshSkillState();
191
192
  await this.ctx.refreshSlashCommandState();
192
193
  resetCapabilities();
193
194
  this.ctx.ui.requestRender();
@@ -1109,12 +1110,10 @@ export class SelectorController {
1109
1110
  // every project's history when the cwd has nothing to resume. See #3099.
1110
1111
  const historyStorage = this.ctx.historyStorage;
1111
1112
  const historyMatcher = historyStorage ? (query: string) => historyStorage.matchingSessionIds(query) : undefined;
1112
- // Fullscreen session picker on the alternate screen (the /settings idiom):
1113
- // the overlay borrows the alt buffer and enables mouse tracking (wheel
1114
- // scroll + click-to-resume) for its lifetime, leaving the transcript
1115
- // untouched underneath. Anchored top-left at full size so a mouse row maps
1116
- // directly to a rendered line (the overlay paints from screen row 0), and
1117
- // `fillHeight` pads the body so the footer pins to the screen bottom.
1113
+ // Keep the fullscreen picker on the alternate buffer while a selected
1114
+ // session is loaded and its transcript is rebuilt. Closing it first exposes
1115
+ // the stale normal buffer for the entire async switch on terminals without
1116
+ // effective synchronized output.
1118
1117
  let overlayHandle: OverlayHandle | undefined;
1119
1118
  const done = () => {
1120
1119
  overlayHandle?.hide();
@@ -1124,8 +1123,12 @@ export class SelectorController {
1124
1123
  const selector = new SessionSelectorComponent(
1125
1124
  sessions,
1126
1125
  async (session: SessionInfo) => {
1127
- done();
1128
- await this.handleResumeSession(session.path);
1126
+ selector.lockInput();
1127
+ try {
1128
+ await this.handleResumeSession(session.path);
1129
+ } finally {
1130
+ done();
1131
+ }
1129
1132
  },
1130
1133
  () => {
1131
1134
  done();
@@ -1272,7 +1275,6 @@ export class SelectorController {
1272
1275
  */
1273
1276
  async #handleOAuthLogin(providerId: string): Promise<boolean> {
1274
1277
  this.ctx.showStatus(`Logging in to ${providerId}…`);
1275
- const manualInput = this.ctx.oauthManualInput;
1276
1278
  const useManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(providerId);
1277
1279
  let restored = false;
1278
1280
  const restoreEditor = () => {
@@ -1300,16 +1302,19 @@ export class SelectorController {
1300
1302
  // The dialog renders the full URL (SSH-safe copy target) and
1301
1303
  // opens the browser best-effort.
1302
1304
  dialog.showAuth(info.url, info.instructions, info.launchUrl);
1303
- if (useManualInput) {
1304
- dialog.showProgress(MANUAL_LOGIN_TIP);
1305
- }
1306
1305
  },
1307
1306
  onPrompt: (prompt: { message: string; placeholder?: string }) =>
1308
1307
  dialog.showPrompt(prompt.message, prompt.placeholder),
1309
1308
  onProgress: (message: string) => {
1310
1309
  dialog.showProgress(message);
1311
1310
  },
1312
- onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
1311
+ // Paste-code providers (e.g. Codex) may need the user to paste the
1312
+ // fallback redirect URL when the loopback callback can't complete
1313
+ // (headless/remote/Windows). Mount a focused input in the dialog so
1314
+ // the paste lands somewhere the OAuth flow consumes — the hidden
1315
+ // editor's `/login <url>` path is unreachable while the dialog holds
1316
+ // focus (#5339).
1317
+ onManualCodeInput: useManualInput ? () => dialog.showManualInput(MANUAL_LOGIN_PROMPT) : undefined,
1313
1318
  });
1314
1319
  this.ctx.session.modelRegistry.refreshInBackground();
1315
1320
  const block = new TranscriptBlock();
@@ -1338,9 +1343,6 @@ export class SelectorController {
1338
1343
  this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
1339
1344
  return false;
1340
1345
  } finally {
1341
- if (useManualInput) {
1342
- manualInput.clear(`Manual OAuth input cleared for ${providerId}`);
1343
- }
1344
1346
  restoreEditor();
1345
1347
  }
1346
1348
  }
@@ -519,6 +519,8 @@ export class InteractiveMode implements InteractiveModeContext {
519
519
  collabHost?: CollabHost;
520
520
  collabGuest?: CollabGuestLink;
521
521
 
522
+ #pendingCommandOutput: Component[] = [];
523
+ #pendingCommandOutputSessionId: string | undefined;
522
524
  #pendingSlashCommands: SlashCommand[] = [];
523
525
  /** Built-in editor autocomplete provider, before extension wrapping. */
524
526
  #baseAutocompleteProvider: AutocompleteProvider | undefined;
@@ -733,15 +735,7 @@ export class InteractiveMode implements InteractiveModeContext {
733
735
  description: `${loaded.command.description} (${loaded.source})`,
734
736
  }));
735
737
 
736
- // Build skill commands from session.skills (if enabled)
737
- const skillCommandList: SlashCommand[] = [];
738
- if (settings.get("skills.enableSkillCommands")) {
739
- for (const skill of this.session.skills) {
740
- const commandName = `skill:${skill.name}`;
741
- this.skillCommands.set(commandName, skill);
742
- skillCommandList.push({ name: commandName, description: skill.description });
743
- }
744
- }
738
+ const skillCommandList = this.#rebuildSkillCommandsFromSession();
745
739
 
746
740
  const builtinCommands = buildTuiBuiltinSlashCommands({ ctx: this });
747
741
  // Store pending commands for init() where file commands are loaded async
@@ -1029,6 +1023,13 @@ export class InteractiveMode implements InteractiveModeContext {
1029
1023
  this.#handleSessionAccentInputsChanged();
1030
1024
  }),
1031
1025
  );
1026
+ this.#eventBusUnsubscribers.push(
1027
+ this.session.subscribeCommandMetadataChanged(() => {
1028
+ const retainedCommands = this.#pendingSlashCommands.filter(command => !command.name.startsWith("skill:"));
1029
+ const skillCommands = this.#rebuildSkillCommandsFromSession();
1030
+ this.#pendingSlashCommands = [...retainedCommands, ...skillCommands];
1031
+ }),
1032
+ );
1032
1033
  // Set up theme file watcher
1033
1034
  this.#eventBusUnsubscribers.push(
1034
1035
  onThemeChange(event => {
@@ -1077,6 +1078,27 @@ export class InteractiveMode implements InteractiveModeContext {
1077
1078
  this.session.setTitleSystemPrompt(resolved);
1078
1079
  }
1079
1080
 
1081
+ #rebuildSkillCommandsFromSession(): SlashCommand[] {
1082
+ const commands: SlashCommand[] = [];
1083
+ this.skillCommands.clear();
1084
+ if (this.session.skillsSettings?.enableSkillCommands !== false) {
1085
+ for (const skill of this.session.skills) {
1086
+ const commandName = `skill:${skill.name}`;
1087
+ this.skillCommands.set(commandName, skill);
1088
+ commands.push({ name: commandName, description: skill.description });
1089
+ }
1090
+ }
1091
+ return commands;
1092
+ }
1093
+
1094
+ /** Reload session skills and the `/skill:<name>` command list. */
1095
+ async refreshSkillState(): Promise<void> {
1096
+ await this.session.refreshSkills();
1097
+ const retainedCommands = this.#pendingSlashCommands.filter(command => !command.name.startsWith("skill:"));
1098
+ const skillCommands = this.#rebuildSkillCommandsFromSession();
1099
+ this.#pendingSlashCommands = [...retainedCommands, ...skillCommands];
1100
+ }
1101
+
1080
1102
  /** Reload slash commands and autocomplete for the provided working directory. */
1081
1103
  async refreshSlashCommandState(cwd?: string): Promise<void> {
1082
1104
  const basePath = cwd ?? this.sessionManager.getCwd();
@@ -1175,6 +1197,7 @@ export class InteractiveMode implements InteractiveModeContext {
1175
1197
  clearClaudePluginRootsCache();
1176
1198
  await this.refreshTitleSystemPrompt(newCwd);
1177
1199
  resetCapabilities();
1200
+ await this.refreshSkillState();
1178
1201
  await this.refreshSlashCommandState(newCwd);
1179
1202
  setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
1180
1203
  this.statusLine.invalidate();
@@ -2498,8 +2521,6 @@ export class InteractiveMode implements InteractiveModeContext {
2498
2521
  const finish = (choice: string | undefined): void => {
2499
2522
  if (settled) return;
2500
2523
  settled = true;
2501
- this.#hidePlanReview();
2502
- this.ui.requestRender();
2503
2524
  resolve(choice);
2504
2525
  };
2505
2526
  const overlay = new PlanReviewOverlay(
@@ -3423,6 +3444,10 @@ export class InteractiveMode implements InteractiveModeContext {
3423
3444
  },
3424
3445
  { slider },
3425
3446
  );
3447
+ const closePlanReview = (): void => {
3448
+ this.#hidePlanReview();
3449
+ this.ui.requestRender();
3450
+ };
3426
3451
 
3427
3452
  if (choice === "Approve and execute" || choice === "Approve and compact context" || choice === keepContextLabel) {
3428
3453
  try {
@@ -3435,6 +3460,7 @@ export class InteractiveMode implements InteractiveModeContext {
3435
3460
  }
3436
3461
  if (!latestPlanContent) {
3437
3462
  this.showError(`Plan file not found at ${planFilePath}`);
3463
+ closePlanReview();
3438
3464
  return;
3439
3465
  }
3440
3466
  // Capture the operator's tier choice and hand it to #approvePlan, which
@@ -3477,6 +3503,7 @@ export class InteractiveMode implements InteractiveModeContext {
3477
3503
  `Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
3478
3504
  );
3479
3505
  }
3506
+ closePlanReview();
3480
3507
  return;
3481
3508
  }
3482
3509
 
@@ -3495,8 +3522,10 @@ export class InteractiveMode implements InteractiveModeContext {
3495
3522
  } catch (error) {
3496
3523
  this.showError(`Failed to refine plan: ${error instanceof Error ? error.message : String(error)}`);
3497
3524
  }
3525
+ closePlanReview();
3498
3526
  return;
3499
3527
  }
3528
+ closePlanReview();
3500
3529
  }
3501
3530
 
3502
3531
  /**
@@ -3711,6 +3740,32 @@ export class InteractiveMode implements InteractiveModeContext {
3711
3740
  this.ui.requestRender();
3712
3741
  }
3713
3742
 
3743
+ /** Defer transcript command panels until the active turn can no longer grow above them. */
3744
+ presentCommandOutput(content: Component | readonly Component[]): void {
3745
+ if (!this.session.isStreaming) {
3746
+ this.present(content);
3747
+ return;
3748
+ }
3749
+ const sessionId = this.sessionManager.getSessionId();
3750
+ if (this.#pendingCommandOutput.length > 0 && this.#pendingCommandOutputSessionId !== sessionId) {
3751
+ this.#pendingCommandOutput = [];
3752
+ }
3753
+ this.#pendingCommandOutputSessionId = sessionId;
3754
+ const items = Array.isArray(content) ? content : [content as Component];
3755
+ this.#pendingCommandOutput.push(...items);
3756
+ }
3757
+
3758
+ /** Mount every command panel queued for the current session while the agent was streaming. */
3759
+ flushPendingCommandOutput(): void {
3760
+ if (this.#pendingCommandOutput.length === 0) return;
3761
+ const pending = this.#pendingCommandOutput;
3762
+ const pendingSessionId = this.#pendingCommandOutputSessionId;
3763
+ this.#pendingCommandOutput = [];
3764
+ this.#pendingCommandOutputSessionId = undefined;
3765
+ if (pendingSessionId !== this.sessionManager.getSessionId()) return;
3766
+ this.present(pending);
3767
+ }
3768
+
3714
3769
  #mountChatChild(item: Component): void {
3715
3770
  this.chatContainer.addChild(item);
3716
3771
  if (item instanceof ChatBlock) item.mount(this.#chatHost);
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Contract: the print-mode assistant-error/aborted exit path MUST run the
3
+ * awaited `session.dispose()` (which contains the bounded browser reaper
4
+ * `releaseTabsForOwner`) before terminating the process. It previously called
5
+ * `process.exit(1)` ahead of the `dispose()` at the end of `runPrintMode`, so
6
+ * an OMP-owned Chromium survived the exit (issue #5643).
7
+ */
8
+ import { describe, expect, it, spyOn } from "bun:test";
9
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
10
+ import type { AgentSession } from "../session/agent-session";
11
+ import { runPrintMode } from "./print-mode";
12
+
13
+ /** Stand-in for `process.exit`: it terminates, so nothing after it should run. */
14
+ class ProcessExit extends Error {
15
+ constructor(readonly code: number) {
16
+ super(`process.exit(${code})`);
17
+ }
18
+ }
19
+
20
+ describe("print-mode error exit disposes the session before exit", () => {
21
+ it("disposes on the assistant-error path before process.exit(1)", async () => {
22
+ const order: string[] = [];
23
+ const errorMsg: AssistantMessage = {
24
+ role: "assistant",
25
+ content: [],
26
+ api: "openai-responses",
27
+ provider: "openai",
28
+ model: "gpt-test",
29
+ usage: {} as AssistantMessage["usage"],
30
+ stopReason: "error",
31
+ errorMessage: "boom",
32
+ timestamp: 1,
33
+ };
34
+ const session = {
35
+ extensionRunner: undefined,
36
+ subscribe: () => {},
37
+ state: { messages: [errorMsg] },
38
+ dispose: async () => {
39
+ order.push("dispose");
40
+ },
41
+ } as unknown as AgentSession;
42
+
43
+ const exitSpy = spyOn(process, "exit").mockImplementation(((code: number) => {
44
+ order.push("exit");
45
+ throw new ProcessExit(code);
46
+ }) as never);
47
+ const stderrSpy = spyOn(process.stderr, "write").mockImplementation((() => true) as never);
48
+
49
+ try {
50
+ await runPrintMode(session, { mode: "text" });
51
+ } catch (err) {
52
+ if (!(err instanceof ProcessExit)) throw err;
53
+ } finally {
54
+ exitSpy.mockRestore();
55
+ stderrSpy.mockRestore();
56
+ }
57
+
58
+ expect(order).toEqual(["dispose", "exit"]);
59
+ });
60
+ });
@@ -144,10 +144,15 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
144
144
  !isSilentAbort(assistantMsg)
145
145
  ) {
146
146
  const errorLine = sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
147
- // Flush before this hard exit — it bypasses the awaited postmortem.quit()
148
- // in main(), and the postmortem `exit` handler can't await, so the error
149
- // spans would otherwise stay buffered in the batch processor and drop.
147
+ // This branch hard-exits, bypassing the `await session.dispose()` at
148
+ // the end of runPrintMode. Flush telemetry and dispose the session
149
+ // HERE so error spans reach the exporter (the postmortem `exit`
150
+ // handler can't await) and the browser reaper installed in
151
+ // `dispose()` (releaseTabsForOwner) actually runs — otherwise an
152
+ // OMP-owned Chromium survives this exit (issue #5643). `dispose()`
153
+ // is idempotent, so the unreachable call below is a harmless no-op.
150
154
  await flushTelemetryExport();
155
+ await session.dispose();
151
156
  const flushed = process.stderr.write(`${errorLine}\n`);
152
157
  if (flushed) {
153
158
  process.exit(1);
@@ -914,6 +914,7 @@ export async function runRpcMode(
914
914
  const projectPath = await resolveActiveProjectRegistryPath(cwd);
915
915
  clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
916
916
  resetCapabilities();
917
+ await session.refreshSkills();
917
918
  session.setSlashCommands(await loadSlashCommands({ cwd }));
918
919
  await emitAvailableCommandsUpdate();
919
920
  };
@@ -1346,9 +1347,12 @@ export async function runRpcMode(
1346
1347
  const shutdownCoordinator = new RpcShutdownCoordinator({
1347
1348
  isShutdownRequested: () => shutdownState.requested,
1348
1349
  performShutdown: async () => {
1349
- if (session.extensionRunner?.hasHandlers("session_shutdown")) {
1350
- await session.extensionRunner.emit({ type: "session_shutdown" });
1351
- }
1350
+ // Route through the idempotent session.dispose() so the browser
1351
+ // reaper (releaseTabsForOwner) and other bounded teardown run before
1352
+ // the process exits. dispose() also emits `session_shutdown`, so we
1353
+ // must NOT emit it separately here or the event fires twice. Skipping
1354
+ // dispose left OMP-owned Chromium alive after RPC shutdown (#5643).
1355
+ await session.dispose();
1352
1356
  process.exit(0);
1353
1357
  },
1354
1358
  });
@@ -1384,5 +1388,10 @@ export async function runRpcMode(
1384
1388
  await inputDispatcher.drain();
1385
1389
  await shutdownCoordinator.drain();
1386
1390
  subagentRegistry?.dispose();
1391
+ // Dispose the main session before exiting so the browser reaper and other
1392
+ // bounded teardown run on the stdin-EOF path too (#5643). Idempotent: a
1393
+ // prior pi.shutdown() through the coordinator makes this await settle
1394
+ // immediately.
1395
+ await session.dispose();
1387
1396
  process.exit(0);
1388
1397
  }
@@ -614,8 +614,8 @@ const NERD_SYMBOLS: SymbolMap = {
614
614
  "icon.throughput": "\uf0e4",
615
615
  // pick:  | alt:  
616
616
  "icon.host": "\uf109",
617
- // pick: | alt:  
618
- "icon.session": "\uf550",
617
+ // pick: 󰁑 (nf-md-arrow_left_bold_hexagon_outline) | alt:  
618
+ "icon.session": "\u{f0051}",
619
619
  // pick:  | alt: 
620
620
  "icon.package": "\uf487",
621
621
  // pick:  | alt:  
@@ -237,6 +237,14 @@ export interface InteractiveModeContext {
237
237
  * runs) so their timers/subscriptions start.
238
238
  */
239
239
  present(content: Component | readonly Component[]): void;
240
+ /**
241
+ * Mount command output immediately while idle, or defer it until the active
242
+ * agent turn ends so a growing live block cannot push duplicate rows into
243
+ * native scrollback.
244
+ */
245
+ presentCommandOutput(content: Component | readonly Component[]): void;
246
+ /** Mount command output deferred by {@link presentCommandOutput}. */
247
+ flushPendingCommandOutput(): void;
240
248
  /**
241
249
  * Dispose every live block in the transcript (stopping timers/subscriptions)
242
250
  * and clear it. Used before a full rebuild so animated/streaming blocks do not
@@ -347,6 +355,8 @@ export interface InteractiveModeContext {
347
355
  ): Promise<CompactionOutcome>;
348
356
  openInBrowser(urlOrPath: string): void;
349
357
  refreshSlashCommandState(cwd?: string): Promise<void>;
358
+ /** Reload session skills and derived `/skill:<name>` commands. */
359
+ refreshSkillState(): Promise<void>;
350
360
  applyCwdChange(newCwd: string): Promise<void>;
351
361
 
352
362
  // Selector handling
@@ -16,7 +16,7 @@ export function createAssistantMessageComponent(
16
16
  ctx: InteractiveModeContext,
17
17
  message?: AssistantMessage,
18
18
  ): AssistantMessageComponent {
19
- return new AssistantMessageComponent(
19
+ const component = new AssistantMessageComponent(
20
20
  message,
21
21
  ctx.effectiveHideThinkingBlock,
22
22
  () => ctx.ui.requestRender(),
@@ -24,4 +24,6 @@ export function createAssistantMessageComponent(
24
24
  ctx.ui.imageBudget,
25
25
  ctx.proseOnlyThinking,
26
26
  );
27
+ component.setImagesVisible(ctx.settings.get("terminal.showImages"));
28
+ return component;
27
29
  }
@@ -125,12 +125,13 @@ export function buildFileMentionBlock(files: FileMentionMessage["files"], indent
125
125
  }
126
126
 
127
127
  /**
128
- * Whether an assistant turn has visible text or thinking content (after
129
- * canonicalization) — i.e. content that closes the current read-tool run.
128
+ * Whether an assistant turn has visible text, thinking, or image content — i.e.
129
+ * content that closes the current read-tool run.
130
130
  */
131
131
  export function assistantHasVisibleContent(message: AssistantAgentMessage): boolean {
132
132
  return message.content.some(
133
133
  content =>
134
+ content.type === "image" ||
134
135
  (content.type === "text" && canonicalizeMessage(content.text)) ||
135
136
  (content.type === "thinking" && canonicalizeMessage(content.thinking)),
136
137
  );
@@ -56,7 +56,7 @@ Special URLs for internal resources; with most FS/bash tools they auto-resolve t
56
56
  {{#if hasMemoryRoot}}
57
57
  - `memory://root`: project memory summary
58
58
  {{/if}}
59
- - `agent://<id>`: agent output artifact; `/<path>` extracts a JSON field
59
+ - `agent://<id>`: agent output artifact; `/<child>` reads a nested subagent's output, else `/<path>` extracts a JSON field
60
60
  - `history://<id>`: read-only markdown transcript of an agent (live, parked, or released); bare `history://` lists all agents. Serves any agent whose session file persists on disk, not just registered peers.
61
61
  - `artifact://<id>`: artifact content
62
62
  - `local://<name>.md`: plan artifacts or shared content for subagents
package/src/sdk.ts CHANGED
@@ -1557,7 +1557,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1557
1557
  skipPythonPreflight: options.skipPythonPreflight,
1558
1558
  contextFiles,
1559
1559
  workspaceTree: resolvedWorkspaceTree,
1560
- skills,
1560
+ get skills() {
1561
+ return session?.skills ?? skills;
1562
+ },
1563
+ refreshSkills: () => session.refreshSkills(),
1561
1564
  rules: allRules,
1562
1565
  eventBus,
1563
1566
  outputSchema: options.outputSchema,
@@ -2341,7 +2344,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2341
2344
  xdevDocs: toolSession.xdevRegistry?.docsAll() ?? "",
2342
2345
  autoQaEnabled: isAutoQaEnabled(settings),
2343
2346
  resolvedCustomPrompt: options.customSystemPrompt,
2344
- skills,
2347
+ skills: session?.skills ?? skills,
2345
2348
  contextFiles,
2346
2349
  tools: promptTools,
2347
2350
  toolNames,
@@ -2737,6 +2740,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2737
2740
  customCommands: customCommandsResult.commands,
2738
2741
  skills,
2739
2742
  skillWarnings,
2743
+ skillsReloadable: options.skills === undefined,
2740
2744
  skillsSettings: settings.getGroup("skills"),
2741
2745
  modelRegistry,
2742
2746
  toolRegistry,