@oh-my-pi/pi-coding-agent 17.1.6 → 17.1.7

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 (56) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/{CHANGELOG-x9zt79k8.md → CHANGELOG-5k4dq4g6.md} +23 -0
  3. package/dist/cli.js +3149 -3092
  4. package/dist/types/config/settings-schema.d.ts +16 -1
  5. package/dist/types/cursor.d.ts +2 -1
  6. package/dist/types/extensibility/extensions/runner.d.ts +4 -0
  7. package/dist/types/extensibility/shared-events.d.ts +18 -1
  8. package/dist/types/modes/components/custom-editor.d.ts +5 -0
  9. package/dist/types/session/agent-session-types.d.ts +5 -5
  10. package/dist/types/session/agent-session.d.ts +26 -1
  11. package/dist/types/session/model-controls.d.ts +1 -1
  12. package/dist/types/session/session-tools.d.ts +44 -6
  13. package/dist/types/session/streaming-output.d.ts +7 -2
  14. package/dist/types/session/turn-recovery.d.ts +1 -1
  15. package/dist/types/tools/index.d.ts +8 -3
  16. package/dist/types/tools/output-meta.d.ts +5 -0
  17. package/dist/types/tools/read.d.ts +9 -1
  18. package/dist/types/tools/xdev.d.ts +53 -67
  19. package/dist/types/utils/cpuprofile.d.ts +51 -0
  20. package/dist/types/utils/inspect-image-mode.d.ts +29 -0
  21. package/dist/types/utils/profile-tree.d.ts +47 -0
  22. package/dist/types/utils/sample-profile.d.ts +67 -0
  23. package/package.json +12 -12
  24. package/src/config/settings-schema.ts +15 -1
  25. package/src/config/settings.ts +35 -0
  26. package/src/cursor.ts +4 -3
  27. package/src/discovery/builtin-rules/index.ts +2 -0
  28. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  29. package/src/extensibility/extensions/runner.ts +26 -0
  30. package/src/extensibility/extensions/wrapper.ts +74 -42
  31. package/src/extensibility/hooks/tool-wrapper.ts +11 -4
  32. package/src/extensibility/shared-events.ts +18 -1
  33. package/src/modes/components/custom-editor.ts +39 -16
  34. package/src/modes/components/tips.txt +2 -1
  35. package/src/modes/components/tool-execution.ts +8 -7
  36. package/src/modes/controllers/extension-ui-controller.ts +4 -4
  37. package/src/modes/controllers/selector-controller.ts +7 -2
  38. package/src/sdk.ts +47 -50
  39. package/src/session/agent-session-types.ts +5 -5
  40. package/src/session/agent-session.ts +123 -7
  41. package/src/session/model-controls.ts +5 -5
  42. package/src/session/session-listing.ts +66 -4
  43. package/src/session/session-tools.ts +158 -52
  44. package/src/session/streaming-output.ts +18 -6
  45. package/src/session/turn-recovery.ts +4 -4
  46. package/src/slash-commands/builtin-registry.ts +69 -0
  47. package/src/tools/bash.ts +16 -9
  48. package/src/tools/index.ts +36 -16
  49. package/src/tools/output-meta.ts +20 -0
  50. package/src/tools/read.ts +87 -13
  51. package/src/tools/write.ts +16 -7
  52. package/src/tools/xdev.ts +198 -210
  53. package/src/utils/cpuprofile.ts +235 -0
  54. package/src/utils/inspect-image-mode.ts +39 -0
  55. package/src/utils/profile-tree.ts +111 -0
  56. package/src/utils/sample-profile.ts +437 -0
@@ -71,6 +71,14 @@ function buildMatchKeys(keys: readonly KeyId[]): Set<string> {
71
71
  return matchKeys;
72
72
  }
73
73
 
74
+ function unionOfMatchKeys(matchKeys: ReadonlyMap<ConfigurableEditorAction, ReadonlySet<string>>): Set<string> {
75
+ const union = new Set<string>();
76
+ for (const keys of matchKeys.values()) {
77
+ for (const key of keys) union.add(key);
78
+ }
79
+ return union;
80
+ }
81
+
74
82
  const BRACKETED_PASTE_START = "\x1b[200~";
75
83
  const BRACKETED_PASTE_END = "\x1b[201~";
76
84
  const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
@@ -419,6 +427,17 @@ export class CustomEditor extends Editor {
419
427
  this.pendingImageLinks = [];
420
428
  }
421
429
 
430
+ /** Replace the composer draft with a restored historical prompt: sets the text and
431
+ * re-attaches the message's images so positional `[Image #N]` markers resolve on
432
+ * resubmit instead of degrading to literal text (esc-esc branch, `/tree`). Source
433
+ * links are unknown for restored drafts, so every link slot is `undefined`. */
434
+ setDraft(text: string, images?: readonly ImageContent[]): void {
435
+ this.setText(text);
436
+ this.imageLinks = undefined;
437
+ this.pendingImages = images ? [...images] : [];
438
+ this.pendingImageLinks = images ? images.map(() => undefined) : [];
439
+ }
440
+
422
441
  /** Treat image/paste markers as indivisible: a stray backspace deletes the whole token
423
442
  * instead of corrupting `[Paste #1, +30 lines]` into plain text. */
424
443
  override atomicTokenPattern = PLACEHOLDER_REGEX;
@@ -600,14 +619,14 @@ export class CustomEditor extends Editor {
600
619
  buildMatchKeys(keys),
601
620
  ]),
602
621
  );
622
+ /** Union of every action's match keys: one probe in `handleInput` decides
623
+ * whether the per-action interception chain can match at all. */
624
+ #actionMatchKeyUnion = unionOfMatchKeys(this.#actionMatchKeys);
603
625
 
604
626
  setActionKeys(action: ConfigurableEditorAction, keys: KeyId[]): void {
605
627
  this.#actionKeys.set(action, [...keys]);
606
- this.#rebuildActionMatchKeys(action);
607
- }
608
-
609
- #rebuildActionMatchKeys(action: ConfigurableEditorAction): void {
610
- this.#actionMatchKeys.set(action, buildMatchKeys(this.#actionKeys.get(action) ?? []));
628
+ this.#actionMatchKeys.set(action, buildMatchKeys(keys));
629
+ this.#actionMatchKeyUnion = unionOfMatchKeys(this.#actionMatchKeys);
611
630
  }
612
631
 
613
632
  #rebuildCustomMatchKeys(): void {
@@ -760,8 +779,10 @@ export class CustomEditor extends Editor {
760
779
  this.#pendingInput.push(data);
761
780
  return;
762
781
  }
763
- const hadBareQueuePrefix = this.getText() === "->" || this.getText() === "=>";
764
- const kittyParsed = parseKittySequence(data);
782
+ // textEquals avoids getText()'s O(buffer) join on every keystroke; kitty
783
+ // sequences always start with ESC, so plain bytes skip the native parse.
784
+ const hadBareQueuePrefix = this.textEquals("->") || this.textEquals("=>");
785
+ const kittyParsed = data.charCodeAt(0) === 0x1b ? parseKittySequence(data) : null;
765
786
  if (kittyParsed && (kittyParsed.modifier & 64) !== 0 && this.onCapsLock) {
766
787
  // Caps Lock is modifier bit 64
767
788
  this.onCapsLock();
@@ -823,7 +844,12 @@ export class CustomEditor extends Editor {
823
844
  // Space-hold push-to-talk: a sustained space bar starts/stops STT instead of typing spaces.
824
845
  if (this.#handleSpaceHold(data, canonical)) return;
825
846
 
826
- if (canonical !== undefined) {
847
+ // One union probe decides whether any per-action interception below can
848
+ // match — plain typing then skips the ~20 per-action set lookups per key.
849
+ if (
850
+ canonical !== undefined &&
851
+ (this.#actionMatchKeyUnion.has(canonical) || this.#customMatchKeys.has(canonical))
852
+ ) {
827
853
  // Intercept configured image paste (async - fires and handles result)
828
854
  if (this.#matchesAction(canonical, "app.clipboard.pasteImage") && this.onPasteImage) {
829
855
  void this.onPasteImage();
@@ -964,14 +990,11 @@ export class CustomEditor extends Editor {
964
990
 
965
991
  // Pass to parent for normal handling
966
992
  super.handleInput(data);
967
- const cursor = this.getCursor();
968
- if (
969
- !hadBareQueuePrefix &&
970
- (this.getText() === "->" || this.getText() === "=>") &&
971
- cursor.line === 0 &&
972
- cursor.col === 2
973
- ) {
974
- this.insertText("\n");
993
+ if (!hadBareQueuePrefix && (this.textEquals("->") || this.textEquals("=>"))) {
994
+ const cursor = this.getCursor();
995
+ if (cursor.line === 0 && cursor.col === 2) {
996
+ this.insertText("\n");
997
+ }
975
998
  }
976
999
  }
977
1000
 
@@ -22,4 +22,5 @@ Press ← ← to drill into a running or finished agent and inspect its tool cal
22
22
  Hit a Codex rate limit? `/usage reset` spends a saved reset credit to immediately restore your quota
23
23
  No native tool_calling? Inference provider botches parsing them? `PI_DIALECT=glm|kimi|anthropic…` rolls it locally for them!
24
24
  Turn on `/advisor` to attach a second model that reviews every turn and quietly injects advice
25
- Try starting your prompt with a ->, and writing a list (1. Do X, 2. Do Y)
25
+ Try starting your prompt with a ->, and writing a list (1. Do X, 2. Do Y)
26
+ Press shift+tab to cycle through reasoning effort levels
@@ -25,6 +25,7 @@ import { isWaitingPollDetails } from "../../tools/hub";
25
25
  import { formatStatusIcon, replaceTabs, resolveImageOptions } from "../../tools/render-utils";
26
26
  import { type FirstResultViewportRepaint, toolRenderers } from "../../tools/renderers";
27
27
  import { TODO_STRIKE_TOTAL_FRAMES, type TodoToolDetails } from "../../tools/todo";
28
+ import type { XdevState } from "../../tools/xdev";
28
29
  import { isFramedBlockComponent, markFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
29
30
  import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
30
31
  import { renderDiff } from "./diff";
@@ -1316,13 +1317,13 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
1316
1317
  }
1317
1318
  context.renderDiff = renderDiff;
1318
1319
  } else if (this.#toolName === "write") {
1319
- // Device-dispatch previews delegate to the mounted tool's own renderer;
1320
- // expose the session's xd:// registry so custom/MCP renderers survive dispatch.
1321
- const writeTool = this.#tool as
1322
- | { session?: { xdevRegistry?: { get(name: string): AgentTool | undefined } } }
1323
- | undefined;
1324
- const registry = writeTool?.session?.xdevRegistry;
1325
- if (registry) context.resolveXdevMounted = (name: string) => registry.get(name);
1320
+ // Device-dispatch previews resolve renderers from the canonical tool map.
1321
+ const writeTool = this.#tool as { session?: { xdev?: XdevState } } | undefined;
1322
+ const xdev = writeTool?.session?.xdev;
1323
+ if (xdev) {
1324
+ context.resolveXdevMounted = (name: string) =>
1325
+ xdev.mountedNames.has(name) ? xdev.tools.get(name) : undefined;
1326
+ }
1326
1327
  }
1327
1328
 
1328
1329
  return context;
@@ -246,7 +246,7 @@ export class ExtensionUiController {
246
246
  // Update UI
247
247
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
248
248
  await this.ctx.reloadTodos();
249
- this.ctx.editor.setText(result.selectedText);
249
+ this.ctx.editor.setDraft(result.selectedText, result.selectedImages);
250
250
  this.ctx.showStatus("Branched to new session");
251
251
 
252
252
  return { cancelled: false };
@@ -261,7 +261,7 @@ export class ExtensionUiController {
261
261
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
262
262
  await this.ctx.reloadTodos();
263
263
  if (result.editorText && !this.ctx.editor.getText().trim()) {
264
- this.ctx.editor.setText(result.editorText);
264
+ this.ctx.editor.setDraft(result.editorText, result.editorImages);
265
265
  }
266
266
  this.ctx.showStatus("Navigated to selected point");
267
267
 
@@ -476,7 +476,7 @@ export class ExtensionUiController {
476
476
  // Update UI
477
477
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
478
478
  await this.ctx.reloadTodos();
479
- this.ctx.editor.setText(result.selectedText);
479
+ this.ctx.editor.setDraft(result.selectedText, result.selectedImages);
480
480
  this.ctx.showStatus("Branched to new session");
481
481
 
482
482
  return { cancelled: false };
@@ -491,7 +491,7 @@ export class ExtensionUiController {
491
491
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
492
492
  await this.ctx.reloadTodos();
493
493
  if (result.editorText && !this.ctx.editor.getText().trim()) {
494
- this.ctx.editor.setText(result.editorText);
494
+ this.ctx.editor.setDraft(result.editorText, result.editorImages);
495
495
  }
496
496
  this.ctx.showStatus("Navigated to selected point");
497
497
 
@@ -455,6 +455,11 @@ export class SelectorController {
455
455
  this.ctx.showError(`Failed to apply memory backend: ${err}`);
456
456
  });
457
457
  break;
458
+ case "inspect_image.mode":
459
+ void this.ctx.session.applyInspectImageModeChange().catch(err => {
460
+ this.ctx.showError(`Failed to apply vision mode: ${err}`);
461
+ });
462
+ break;
458
463
 
459
464
  case "autocompleteMaxVisible":
460
465
  this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value));
@@ -1097,7 +1102,7 @@ export class SelectorController {
1097
1102
  }
1098
1103
 
1099
1104
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
1100
- this.ctx.editor.setText(result.selectedText);
1105
+ this.ctx.editor.setDraft(result.selectedText, result.selectedImages);
1101
1106
  done();
1102
1107
  this.ctx.showStatus("Branched to new session");
1103
1108
  },
@@ -1272,7 +1277,7 @@ export class SelectorController {
1272
1277
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
1273
1278
  await this.ctx.reloadTodos();
1274
1279
  if (result.editorText && !this.ctx.editor.getText().trim()) {
1275
- this.ctx.editor.setText(result.editorText);
1280
+ this.ctx.editor.setDraft(result.editorText, result.editorImages);
1276
1281
  }
1277
1282
  this.ctx.showStatus("Navigated to selected point");
1278
1283
 
package/src/sdk.ts CHANGED
@@ -190,13 +190,17 @@ import {
190
190
  HIDDEN_TOOLS,
191
191
  isMountableUnderXdev,
192
192
  type LspStartupServerInfo,
193
+ listXdevTools,
193
194
  ReadTool,
194
195
  releaseComputerSessionsForOwner,
196
+ resolveMountedXdevExecutable,
195
197
  type Tool,
196
198
  type ToolSession,
197
199
  WebSearchTool,
198
200
  WriteTool,
199
201
  warmupLspServers,
202
+ xdevDocsAll,
203
+ xdevEntries,
200
204
  } from "./tools";
201
205
  import { isMCPToolName, normalizeToolNames } from "./tools/builtin-names";
202
206
  import { ToolContextStore } from "./tools/context";
@@ -1620,6 +1624,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1620
1624
  // mutation (any tool) bumped it in the meantime.
1621
1625
  const fileMutationVersions = new Map<string, number>();
1622
1626
  const activeToolNames = new Set<string>();
1627
+ const toolRegistry = new Map<string, Tool>();
1623
1628
  const setActiveToolNames = (names: Iterable<string>): void => {
1624
1629
  activeToolNames.clear();
1625
1630
  for (const name of names) {
@@ -1632,6 +1637,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1632
1637
  },
1633
1638
  isToolActive: name => activeToolNames.has(name),
1634
1639
  setActiveToolNames,
1640
+ toolRegistry,
1635
1641
  hasUI: options.hasUI ?? false,
1636
1642
  get additionalDirectories() {
1637
1643
  return sessionManager.getAdditionalDirectories();
@@ -1682,6 +1688,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1682
1688
  getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
1683
1689
  getActiveModelString,
1684
1690
  getActiveModel: () => agent?.state.model ?? model,
1691
+ getInspectImageModeOverride: () => session?.getInspectImageModeOverride(),
1685
1692
  getServiceTierByFamily: () => session?.serviceTierByFamily,
1686
1693
  getImageAttachments: () => session?.getImageAttachments() ?? [],
1687
1694
  getPlanModeState: () => session?.getPlanModeState(),
@@ -1776,7 +1783,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1776
1783
  );
1777
1784
 
1778
1785
  // Create built-in tools (already wrapped with meta notice formatting)
1779
- const builtinTools = await logger.time("createAllTools", createTools, toolSession, options.toolNames);
1786
+ await logger.time("createAllTools", createTools, toolSession, options.toolNames);
1780
1787
 
1781
1788
  // Restricted sessions cannot inherit or discover MCP capabilities.
1782
1789
  const enableMCP = !restrictToolNames && (options.enableMCP ?? true);
@@ -1868,7 +1875,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1868
1875
  // to mirror the AsyncJobManager ownership rule.
1869
1876
  if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
1870
1877
 
1871
- const builtInToolNames = builtinTools.map(t => t.name);
1878
+ const builtInToolNames = [...toolRegistry.keys()];
1872
1879
  let customToolPaths: ToolPathWithSource[] = [];
1873
1880
  const inlineExtensions: ExtensionFactory[] = [];
1874
1881
  if (!restrictToolNames) {
@@ -2549,12 +2556,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2549
2556
  );
2550
2557
 
2551
2558
  // All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
2552
- const builtInRegistryToolNames = new Set<string>();
2553
- const toolRegistry = new Map<string, Tool>();
2554
- for (const tool of builtinTools) {
2555
- toolRegistry.set(tool.name, tool);
2556
- builtInRegistryToolNames.add(tool.name);
2557
- }
2559
+ const builtInRegistryToolNames = toolSession.xdev?.builtInNames ?? new Set(toolRegistry.keys());
2558
2560
  if (!restrictToolNames && !toolRegistry.has("goal") && settings.get("goal.enabled")) {
2559
2561
  const goalTool = await logger.time("createTools:goal:session", HIDDEN_TOOLS.goal, toolSession);
2560
2562
  if (goalTool) {
@@ -2606,31 +2608,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2606
2608
  // Existing staged/device paths need write registered before active-set assembly.
2607
2609
  // Deferred MCP also registers it now, but refresh activates it only after a server connects.
2608
2610
  const hasDeferrableTools = Array.from(toolRegistry.values()).some(tool => tool.deferrable === true);
2609
- const hasXdevTools = (toolSession.xdevRegistry?.size ?? 0) > 0;
2611
+ const hasXdevTools = (toolSession.xdev?.mountedNames.size ?? 0) > 0;
2610
2612
  const planModeAvailable = settings.get("plan.enabled");
2611
2613
  if (!restrictToolNames && (hasDeferrableTools || hasXdevTools || planModeAvailable || deferMCPDiscoveryForUI)) {
2612
2614
  await ensureWriteRegistered();
2613
2615
  }
2614
2616
 
2615
2617
  let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
2616
- // Built-in xd:// devices (ast_edit, debug, browser, lsp, web_search) are
2617
- // mounted in createTools BEFORE this loop wraps registry entries in
2618
- // ExtensionToolWrapper, so the registry holds them unwrapped. The normal
2619
- // `write xd://<tool>` path runs approval through the wrapped `write` tool's
2620
- // tier gate, but Cursor invokes advertised devices via `tool.execute()`
2621
- // directly, and the agent loop's fallback resolver executes mounted
2622
- // devices the model called by their top-level name — so wrap unwrapped
2623
- // devices here to keep the approval/deny/prompt gate. Dynamic mounts
2624
- // (custom/MCP) already come from the wrapped registry.
2618
+ // Cursor and the agent loop may call a mounted device by its top-level
2619
+ // name. Resolve that name from the canonical map and apply the same
2620
+ // execution-only ACP decorator used by `write xd://<tool>`; docs and
2621
+ // renderer lookup continue to use the undecorated canonical instance.
2625
2622
  const resolveDeviceTool = (name: string): AgentTool | undefined => {
2626
- const device = toolSession.xdevRegistry?.get(name);
2627
- if (!device) return undefined;
2628
- return device instanceof ExtensionToolWrapper ? device : new ExtensionToolWrapper(device, extensionRunner);
2623
+ const state = toolSession.xdev;
2624
+ if (!state) return undefined;
2625
+ return resolveMountedXdevExecutable(state, name);
2629
2626
  };
2630
2627
  const cursorExecHandlers = new CursorExecHandlers({
2631
2628
  cwd,
2632
2629
  tools: toolRegistry,
2633
- getTool: resolveDeviceTool,
2630
+ getExecutableTool: resolveDeviceTool,
2634
2631
  getToolContext: () => toolContextStore.getContext(),
2635
2632
  emitEvent: event => cursorEventEmitter?.(event),
2636
2633
  getTodoPhases: () => session.getTodoPhases(),
@@ -2680,7 +2677,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2680
2677
  if (memoryInstructions) appendParts.push(memoryInstructions);
2681
2678
  if (autoLearnInstructions) appendParts.push(autoLearnInstructions);
2682
2679
  const projection = projectMountedMCPXdevGuidance(
2683
- collectMountedMCPToolRoutes(toolSession.xdevRegistry?.list() ?? []),
2680
+ collectMountedMCPToolRoutes(toolSession.xdev ? listXdevTools(toolSession.xdev) : []),
2684
2681
  );
2685
2682
  if (projection.mappings.length > 0 || projection.hasOmittedMappings) {
2686
2683
  appendParts.push(
@@ -2723,13 +2720,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2723
2720
  const defaultPrompt = await buildSystemPromptInternal({
2724
2721
  cwd,
2725
2722
  additionalWorkspaceRoots: sessionManager.getAdditionalDirectories(),
2726
- xdevTools: toolSession.xdevRegistry?.entries() ?? [],
2727
- xdevDocs:
2728
- toolSession.xdevRegistry?.docsAll(
2729
- settings.get("tools.xdevDocs"),
2730
- settings.get("tools.xdevInlineDevices"),
2731
- ) ?? "",
2732
- autoQaEnabled: !restrictToolNames && isAutoQaEnabled(settings),
2723
+ xdevTools: toolSession.xdev ? xdevEntries(toolSession.xdev) : [],
2724
+ xdevDocs: toolSession.xdev
2725
+ ? xdevDocsAll(toolSession.xdev, settings.get("tools.xdevDocs"), settings.get("tools.xdevInlineDevices"))
2726
+ : "",
2733
2727
  resolvedCustomPrompt: options.customSystemPrompt,
2734
2728
  skills: session?.skills ?? skills,
2735
2729
  contextFiles,
@@ -2747,6 +2741,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2747
2741
  taskBatch: settings.get("task.batch"),
2748
2742
  taskMaxConcurrency: settings.get("task.maxConcurrency"),
2749
2743
  taskIrcEnabled: !restrictToolNames && isIrcEnabled(settings, options.taskDepth ?? 0),
2744
+ autoQaEnabled: !restrictToolNames && isAutoQaEnabled(settings),
2750
2745
  secretsEnabled,
2751
2746
  workspaceTree: workspaceTreePromise,
2752
2747
  includeWorkspaceTree,
@@ -2853,31 +2848,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2853
2848
  // constructed and attached. Startup failure therefore leaves it revivable.
2854
2849
  hasRegistered = options.expectedAgentRef === undefined || options.expectedAgentRef === null;
2855
2850
 
2856
- // Partition the initial enabled set for the xd:// transport: ambient
2857
- // discoverable tools become mounted devices, while explicitly requested
2858
- // tools keep their top-level presentation. The registry already holds the
2859
- // default-set built-in devices from createTools; this reconciles dynamic
2860
- // mounts (image-gen, TTS, startup MCP, active extension tools).
2861
- let initialMountedXdevToolNames: string[] = [];
2862
- if (toolSession.xdevRegistry) {
2851
+ // Partition the initial enabled set for the xd:// transport. Tool instances
2852
+ // remain in the canonical map; only presentation names move between layers.
2853
+ if (toolSession.xdev) {
2863
2854
  const topLevelToolNames: string[] = [];
2864
- const mountedTools: Tool[] = [];
2855
+ const mountedNames: string[] = [];
2865
2856
  for (const name of initialToolNames) {
2866
2857
  const tool = toolRegistry.get(name);
2867
2858
  const explicitlyRequested = explicitlyRequestedToolNameSet?.has(name) === true;
2868
2859
  if (tool && xdevReadAvailable && !explicitlyRequested && isMountableUnderXdev(tool))
2869
- mountedTools.push(tool);
2860
+ mountedNames.push(name);
2870
2861
  else topLevelToolNames.push(name);
2871
2862
  }
2872
- const writeTransportAvailable = mountedTools.length === 0 || (await ensureWriteRegistered());
2863
+ const writeTransportAvailable = mountedNames.length === 0 || (await ensureWriteRegistered());
2864
+ toolSession.xdev.mountedNames.clear();
2873
2865
  if (writeTransportAvailable) {
2874
- toolSession.xdevRegistry.reconcile(mountedTools);
2875
- initialMountedXdevToolNames = mountedTools.map(tool => tool.name);
2866
+ for (const name of mountedNames) toolSession.xdev.mountedNames.add(name);
2876
2867
  initialToolNames = topLevelToolNames;
2877
- if (initialMountedXdevToolNames.length > 0 && !initialToolNames.includes("write"))
2878
- initialToolNames.push("write");
2879
- } else {
2880
- toolSession.xdevRegistry.reconcile([]);
2868
+ if (mountedNames.length > 0 && !initialToolNames.includes("write")) initialToolNames.push("write");
2881
2869
  }
2882
2870
  }
2883
2871
 
@@ -3055,7 +3043,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3055
3043
  return settingsAwareStreamFn(streamModel, context, streamOptions);
3056
3044
  },
3057
3045
  cursorExecHandlers,
3058
- getCursorTools: () => [...(toolSession.xdevRegistry?.list() ?? [])],
3046
+ getCursorTools: () => (toolSession.xdev ? listXdevTools(toolSession.xdev) : []),
3059
3047
  transformToolCallArguments,
3060
3048
  resolveFallbackTool: resolveDeviceTool,
3061
3049
  intentTracing: !!intentField,
@@ -3111,6 +3099,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3111
3099
  return id ? `${id}-advisor` : null;
3112
3100
  },
3113
3101
  getAgentId: () => "advisor",
3102
+ // The primary's availability signals are wrong for advisors: their tool
3103
+ // slate is filtered separately at runtime (default read/grep/glob, no
3104
+ // write transport), so xd:// devices are unreachable and read must never
3105
+ // advertise inspect_image — images are inlined, and the provider
3106
+ // boundary handles text-only advisor models.
3107
+ xdev: undefined,
3108
+ isToolActive: name => name !== "inspect_image" && toolSession.isToolActive?.(name) === true,
3114
3109
  };
3115
3110
  const advisorToolBuilds: Array<Tool | null | Promise<Tool | null>> = [];
3116
3111
  for (const name in BUILTIN_TOOLS) {
@@ -3178,6 +3173,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3178
3173
  createComputerTool: restrictToolNames
3179
3174
  ? undefined
3180
3175
  : async () => (await BUILTIN_TOOLS.computer(toolSession)) ?? null,
3176
+ createInspectImageTool: restrictToolNames
3177
+ ? undefined
3178
+ : async () => (await BUILTIN_TOOLS.inspect_image(toolSession)) ?? null,
3181
3179
  createVibeTools:
3182
3180
  (options.taskDepth ?? 0) === 0 && !options.parentTaskPrefix
3183
3181
  ? () => createVibeTools(toolSession)
@@ -3192,9 +3190,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3192
3190
  preferWebsockets: preferOpenAICodexWebsockets,
3193
3191
  convertToLlm: convertToLlmFinal,
3194
3192
  rebuildSystemPrompt,
3195
- getXdevToolEntries: () => toolSession.xdevRegistry?.entries() ?? [],
3196
- xdevRegistry: toolSession.xdevRegistry,
3197
- initialMountedXdevToolNames,
3193
+ getXdevToolEntries: () => (toolSession.xdev ? xdevEntries(toolSession.xdev) : []),
3194
+ xdev: toolSession.xdev,
3198
3195
  presentationPinnedToolNames: explicitlyRequestedToolNameSet,
3199
3196
  setActiveToolNames,
3200
3197
  ensureWriteRegistered,
@@ -26,7 +26,7 @@ import type { Skill, SkillWarning } from "../extensibility/skills";
26
26
  import type { FileSlashCommand } from "../extensibility/slash-commands";
27
27
  import type { SecretObfuscator } from "../secrets/obfuscator";
28
28
  import type { ConfiguredThinkingLevel } from "../thinking";
29
- import type { XdevRegistry } from "../tools/xdev";
29
+ import type { XdevState } from "../tools/xdev";
30
30
  import type { SessionManager } from "./session-manager";
31
31
 
32
32
  /** Maximum time the interactive shutdown path waits for Mnemopi consolidation. */
@@ -139,6 +139,8 @@ export interface AgentSessionConfig {
139
139
  createMemoryTools?: () => Promise<AgentTool[]>;
140
140
  /** Creates the built-in `computer` tool for session-scoped runtime enablement (see {@link AgentSession.setComputerToolEnabled}). */
141
141
  createComputerTool?: () => Promise<AgentTool | null>;
142
+ /** Creates the built-in `inspect_image` tool for session-scoped runtime enablement (see {@link AgentSession.setInspectImageMode}). */
143
+ createInspectImageTool?: () => Promise<AgentTool | null>;
142
144
  /** Model registry for API key resolution and model discovery. */
143
145
  modelRegistry: ModelRegistry;
144
146
  /** Tool registry for LSP and settings. */
@@ -177,10 +179,8 @@ export interface AgentSessionConfig {
177
179
  getLocalCalendarDate?: () => string;
178
180
  /** Tools mounted under `xd://`, for `/tools` display. */
179
181
  getXdevToolEntries?: () => Array<{ name: string; summary: string }>;
180
- /** Session-owned `xd://` registry. */
181
- xdevRegistry?: XdevRegistry;
182
- /** Discoverable tools mounted under `xd://` in the initial enabled set. */
183
- initialMountedXdevToolNames?: string[];
182
+ /** `xd://` presentation state backed by the canonical tool map. */
183
+ xdev?: XdevState;
184
184
  /** Names pinned top-level during runtime repartitioning. */
185
185
  presentationPinnedToolNames?: ReadonlySet<string>;
186
186
  /** Accessor for live MCP server instructions. */