apex-code 0.5.2 → 0.6.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 (99) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +41 -2
  3. package/dist/core/agent-session.d.ts +37 -0
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +264 -60
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/default-tool-names.d.ts +7 -0
  8. package/dist/core/default-tool-names.d.ts.map +1 -0
  9. package/dist/core/default-tool-names.js +14 -0
  10. package/dist/core/default-tool-names.js.map +1 -0
  11. package/dist/core/extensions/runner.d.ts.map +1 -1
  12. package/dist/core/extensions/runner.js +2 -0
  13. package/dist/core/extensions/runner.js.map +1 -1
  14. package/dist/core/extensions/types.d.ts +3 -0
  15. package/dist/core/extensions/types.d.ts.map +1 -1
  16. package/dist/core/extensions/types.js.map +1 -1
  17. package/dist/core/keybindings.d.ts +16 -1
  18. package/dist/core/keybindings.d.ts.map +1 -1
  19. package/dist/core/keybindings.js +7 -1
  20. package/dist/core/keybindings.js.map +1 -1
  21. package/dist/core/permissions/startup.d.ts +1 -1
  22. package/dist/core/permissions/startup.d.ts.map +1 -1
  23. package/dist/core/permissions/startup.js.map +1 -1
  24. package/dist/core/sdk.d.ts.map +1 -1
  25. package/dist/core/sdk.js +13 -9
  26. package/dist/core/sdk.js.map +1 -1
  27. package/dist/core/settings-manager.d.ts +4 -0
  28. package/dist/core/settings-manager.d.ts.map +1 -1
  29. package/dist/core/settings-manager.js +13 -0
  30. package/dist/core/settings-manager.js.map +1 -1
  31. package/dist/core/slash-commands.d.ts.map +1 -1
  32. package/dist/core/slash-commands.js +1 -0
  33. package/dist/core/slash-commands.js.map +1 -1
  34. package/dist/core/tools/contract.d.ts +1 -0
  35. package/dist/core/tools/contract.d.ts.map +1 -1
  36. package/dist/core/tools/contract.js.map +1 -1
  37. package/dist/core/tools/index.d.ts +1 -1
  38. package/dist/core/tools/index.d.ts.map +1 -1
  39. package/dist/core/tools/index.js.map +1 -1
  40. package/dist/core/tools/plan-present.d.ts +13 -12
  41. package/dist/core/tools/plan-present.d.ts.map +1 -1
  42. package/dist/core/tools/plan-present.js +32 -19
  43. package/dist/core/tools/plan-present.js.map +1 -1
  44. package/dist/core/tools/renderers/index.d.ts +1 -1
  45. package/dist/core/tools/renderers/index.d.ts.map +1 -1
  46. package/dist/core/tools/renderers/index.js +2 -0
  47. package/dist/core/tools/renderers/index.js.map +1 -1
  48. package/dist/core/tools/renderers/todo-write.d.ts +7 -0
  49. package/dist/core/tools/renderers/todo-write.d.ts.map +1 -0
  50. package/dist/core/tools/renderers/todo-write.js +58 -0
  51. package/dist/core/tools/renderers/todo-write.js.map +1 -0
  52. package/dist/index.d.ts +2 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +1 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/modes/interactive/components/bash-execution.d.ts +13 -2
  57. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  58. package/dist/modes/interactive/components/bash-execution.js +56 -1
  59. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  60. package/dist/modes/interactive/components/error-summary.d.ts +2 -0
  61. package/dist/modes/interactive/components/error-summary.d.ts.map +1 -1
  62. package/dist/modes/interactive/components/error-summary.js +10 -0
  63. package/dist/modes/interactive/components/error-summary.js.map +1 -1
  64. package/dist/modes/interactive/components/first-use-hints.d.ts +1 -1
  65. package/dist/modes/interactive/components/first-use-hints.d.ts.map +1 -1
  66. package/dist/modes/interactive/components/first-use-hints.js +1 -0
  67. package/dist/modes/interactive/components/first-use-hints.js.map +1 -1
  68. package/dist/modes/interactive/components/footer.d.ts +6 -1
  69. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  70. package/dist/modes/interactive/components/footer.js +38 -11
  71. package/dist/modes/interactive/components/footer.js.map +1 -1
  72. package/dist/modes/interactive/components/settings-selector.d.ts +5 -0
  73. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  74. package/dist/modes/interactive/components/settings-selector.js +20 -1
  75. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  76. package/dist/modes/interactive/components/task-panel.d.ts +27 -0
  77. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -0
  78. package/dist/modes/interactive/components/task-panel.js +92 -0
  79. package/dist/modes/interactive/components/task-panel.js.map +1 -0
  80. package/dist/modes/interactive/components/tool-execution.d.ts +9 -1
  81. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  82. package/dist/modes/interactive/components/tool-execution.js +81 -8
  83. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  84. package/dist/modes/interactive/components/tool-panel.d.ts +7 -0
  85. package/dist/modes/interactive/components/tool-panel.d.ts.map +1 -1
  86. package/dist/modes/interactive/components/tool-panel.js +35 -0
  87. package/dist/modes/interactive/components/tool-panel.js.map +1 -1
  88. package/dist/modes/interactive/components/turn-summary.d.ts +6 -0
  89. package/dist/modes/interactive/components/turn-summary.d.ts.map +1 -0
  90. package/dist/modes/interactive/components/turn-summary.js +12 -0
  91. package/dist/modes/interactive/components/turn-summary.js.map +1 -0
  92. package/dist/modes/interactive/interactive-mode.d.ts +10 -0
  93. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  94. package/dist/modes/interactive/interactive-mode.js +191 -22
  95. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  96. package/docs/keybindings.md +21 -2
  97. package/docs/settings.md +8 -0
  98. package/npm-shrinkwrap.json +5 -5
  99. package/package.json +2 -2
@@ -27,6 +27,7 @@ import { generateBugReportSummary } from "./bug-report.js";
27
27
  import { createSessionCheckpoints } from "./checkpoints/session-checkpoints.js";
28
28
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
29
29
  import { evictionBudget, installContextPipeline, isDefaultStreamFunction } from "./context/pipeline.js";
30
+ import { configuredDefaultToolNames } from "./default-tool-names.js";
30
31
  import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
31
32
  import { RESUME_CHILD_PROMPT, runDelegation } from "./delegation/runtime.js";
32
33
  import { SessionEvidenceSink } from "./evidence.js";
@@ -51,6 +52,7 @@ import { buildSystemPrompt, buildSystemPromptSections, diffSystemPromptSections,
51
52
  import { createLocalBashOperations } from "./tools/bash.js";
52
53
  import { buildToolContractSnapshot, isUnclassifiedTool, } from "./tools/contract-snapshot.js";
53
54
  import { createAllToolDefinitions } from "./tools/index.js";
55
+ import { createPlanPresentToolDefinition, defaultPlanPresenter } from "./tools/plan-present.js";
54
56
  import { createSkillSearchToolDefinition } from "./tools/skill-search.js";
55
57
  import { createTodoWriteToolDefinition } from "./tools/todo-write.js";
56
58
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
@@ -165,9 +167,13 @@ export class AgentSession {
165
167
  _initialActiveToolNames;
166
168
  _allowedToolNames;
167
169
  _excludedToolNames;
170
+ _implicitDefaultToolNames;
168
171
  _baseToolsOverride;
169
172
  _sessionStartEvent;
170
173
  _permissionGate;
174
+ _interactivePermissionMode;
175
+ _interactivePermissionModeGeneration = 0;
176
+ _startupPermissionMode;
171
177
  _evidenceSink;
172
178
  _diagnosticsOperations;
173
179
  _lspOperations;
@@ -206,6 +212,13 @@ export class AgentSession {
206
212
  _baseSystemPromptOptions;
207
213
  /** Prompt options after before_agent_start mutations for the active run. */
208
214
  _runSystemPromptOptions;
215
+ _requestedToolNames = [];
216
+ _runRequestedToolNames;
217
+ _planToolMode;
218
+ _planToolSyncGeneration = 0;
219
+ _pendingPlanToolSync;
220
+ _planToolSyncChange = new AbortController();
221
+ _planPresentationGeneration = 0;
209
222
  constructor(config) {
210
223
  this.agent = config.agent;
211
224
  this.sessionManager = config.sessionManager;
@@ -224,8 +237,26 @@ export class AgentSession {
224
237
  this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
225
238
  this._excludedToolNames = config.excludedToolNames ? new Set(config.excludedToolNames) : undefined;
226
239
  this._baseToolsOverride = config.baseToolsOverride;
240
+ this._implicitDefaultToolNames = config.implicitDefaultToolNames
241
+ ? [...config.implicitDefaultToolNames]
242
+ : undefined;
227
243
  this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
228
- this._permissionGate = config.permissionGate;
244
+ if (config.permissionGate) {
245
+ const configuredGate = config.permissionGate;
246
+ try {
247
+ const startupMode = configuredGate.getMode();
248
+ if (typeof startupMode === "string")
249
+ this._planToolMode = startupMode;
250
+ this._startupPermissionMode = Promise.resolve(startupMode).then((mode) => ({ ok: true, mode }), (error) => ({ ok: false, error }));
251
+ }
252
+ catch (error) {
253
+ this._startupPermissionMode = Promise.resolve({ ok: false, error });
254
+ }
255
+ this._permissionGate = {
256
+ ...configuredGate,
257
+ getMode: () => this._interactivePermissionMode ?? configuredGate.getMode(),
258
+ };
259
+ }
229
260
  this._evidenceSink = config.evidenceSink ?? new SessionEvidenceSink(this.sessionManager);
230
261
  this._diagnosticsOperations = config.diagnosticsOperations;
231
262
  this._lspOperations = config.lspOperations;
@@ -280,6 +311,15 @@ export class AgentSession {
280
311
  });
281
312
  if (this._initialActiveToolNames === undefined)
282
313
  this._restoreToolsFromTranscript();
314
+ const generation = this._planToolSyncGeneration;
315
+ void this._startupPermissionMode
316
+ ?.then((startup) => {
317
+ if (startup.ok && generation === this._planToolSyncGeneration && this._lifecycle !== "closed") {
318
+ this._planToolMode = startup.mode;
319
+ this._applyRequestedTools();
320
+ }
321
+ })
322
+ .catch(() => { });
283
323
  }
284
324
  get modelRuntime() {
285
325
  return this._modelRuntime;
@@ -547,6 +587,7 @@ export class AgentSession {
547
587
  ? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
548
588
  : undefined);
549
589
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
590
+ await this._syncPlanToolMode();
550
591
  const context = await this._compactBeforeNextAssistantResponse({
551
592
  ...turn.context,
552
593
  messages: this.sessionManager.buildSessionProjection().messages,
@@ -556,7 +597,7 @@ export class AgentSession {
556
597
  const runOptions = this._runSystemPromptOptions ?? this._baseSystemPromptOptions;
557
598
  const options = normalizeBuildSystemPromptOptions({
558
599
  ...runOptions,
559
- selectedTools: this.getActiveToolNames(),
600
+ selectedTools: this._projectToolNames(this._runRequestedToolNames ?? this._requestedToolNames),
560
601
  toolSnippets: { ...this._baseSystemPromptOptions.toolSnippets, ...runOptions.toolSnippets },
561
602
  toolGuidelines: { ...this._baseSystemPromptOptions.toolGuidelines, ...runOptions.toolGuidelines },
562
603
  });
@@ -1262,6 +1303,10 @@ export class AgentSession {
1262
1303
  if (this._lifecycle === "closed")
1263
1304
  return;
1264
1305
  this._lifecycle = "closed";
1306
+ this._planPresentationGeneration++;
1307
+ this._planToolSyncGeneration++;
1308
+ this._setPendingPlanToolSync();
1309
+ this._interactivePermissionModeGeneration++;
1265
1310
  try {
1266
1311
  this.abortRetry();
1267
1312
  this.abortCompaction();
@@ -1332,6 +1377,14 @@ export class AgentSession {
1332
1377
  get retryAttempt() {
1333
1378
  return this._retryAttempt;
1334
1379
  }
1380
+ getImplicitDefaultToolNames() {
1381
+ return this._implicitDefaultToolNames
1382
+ ? [...this._implicitDefaultToolNames]
1383
+ : configuredDefaultToolNames({
1384
+ baseNames: this._baseToolsOverride ? Object.keys(this._baseToolsOverride) : undefined,
1385
+ lsp: Boolean(this._lspOperations),
1386
+ });
1387
+ }
1335
1388
  /**
1336
1389
  * Get the names of currently active tools.
1337
1390
  * Returns the names of tools currently set on the agent.
@@ -1372,17 +1425,90 @@ export class AgentSession {
1372
1425
  * Changes take effect on the next agent turn.
1373
1426
  */
1374
1427
  setActiveToolsByName(toolNames) {
1375
- const tools = [];
1376
- const validToolNames = [];
1377
- for (const name of toolNames) {
1428
+ this._requestedToolNames = [...new Set(toolNames)].filter((name) => this._toolRegistry.has(name));
1429
+ this._runRequestedToolNames = undefined;
1430
+ this._applyRequestedTools();
1431
+ }
1432
+ _projectToolNames(names) {
1433
+ const projected = [...new Set(names)].filter((name) => this._toolRegistry.has(name));
1434
+ if (this._planToolMode === "plan" &&
1435
+ this._toolRegistry.has("plan_present") &&
1436
+ !projected.includes("plan_present"))
1437
+ projected.push("plan_present");
1438
+ return projected;
1439
+ }
1440
+ _applyRequestedTools() {
1441
+ const names = this._projectToolNames(this._runRequestedToolNames ?? this._requestedToolNames);
1442
+ this.agent.state.tools = names.flatMap((name) => {
1378
1443
  const tool = this._toolRegistry.get(name);
1379
- if (tool) {
1380
- tools.push(tool);
1381
- validToolNames.push(name);
1444
+ return tool ? [tool] : [];
1445
+ });
1446
+ this._rebuildSystemPrompt(this._projectToolNames(this._requestedToolNames));
1447
+ }
1448
+ _setPendingPlanToolSync(task) {
1449
+ this._pendingPlanToolSync = task;
1450
+ const previous = this._planToolSyncChange;
1451
+ this._planToolSyncChange = new AbortController();
1452
+ previous.abort();
1453
+ }
1454
+ async _syncPlanToolMode() {
1455
+ const generation = ++this._planToolSyncGeneration;
1456
+ const task = (async () => {
1457
+ const mode = await this._permissionGate?.getMode();
1458
+ if (generation !== this._planToolSyncGeneration || this._lifecycle === "closed")
1459
+ return;
1460
+ this._planToolMode = mode;
1461
+ this._applyRequestedTools();
1462
+ })();
1463
+ this._setPendingPlanToolSync(task);
1464
+ let pending = task;
1465
+ for (;;) {
1466
+ const signal = this._planToolSyncChange.signal;
1467
+ let wake;
1468
+ const changed = new Promise((resolve) => {
1469
+ wake = resolve;
1470
+ signal.addEventListener("abort", wake, { once: true });
1471
+ });
1472
+ try {
1473
+ await Promise.race([pending, changed]);
1382
1474
  }
1383
- }
1384
- this.agent.state.tools = tools;
1385
- this._rebuildSystemPrompt(validToolNames);
1475
+ catch (error) {
1476
+ if (pending === this._pendingPlanToolSync)
1477
+ throw error;
1478
+ }
1479
+ finally {
1480
+ signal.removeEventListener("abort", wake);
1481
+ }
1482
+ if (!this._pendingPlanToolSync || pending === this._pendingPlanToolSync)
1483
+ return;
1484
+ pending = this._pendingPlanToolSync;
1485
+ }
1486
+ }
1487
+ _schedulePlanToolSync() {
1488
+ void this._syncPlanToolMode().catch(() => { });
1489
+ }
1490
+ async _presentPlan(plan, ctx, signal) {
1491
+ const generation = this._planPresentationGeneration;
1492
+ const modeGeneration = this._interactivePermissionModeGeneration;
1493
+ const obsolete = () => signal?.aborted ||
1494
+ this._lifecycle === "closed" ||
1495
+ generation !== this._planPresentationGeneration ||
1496
+ modeGeneration !== this._interactivePermissionModeGeneration;
1497
+ const decision = await defaultPlanPresenter.present(plan, ctx, signal);
1498
+ if (obsolete())
1499
+ throw new Error("Plan approval was cancelled because its dialog is obsolete.");
1500
+ if (!decision.approved)
1501
+ return decision;
1502
+ if (!this._permissionGate)
1503
+ throw new Error("Cannot apply the approved plan mode without a permission gate.");
1504
+ const resolution = await this._setInteractivePermissionMode(decision.nextMode, () => !signal?.aborted && this._lifecycle !== "closed" && generation === this._planPresentationGeneration);
1505
+ if (signal?.aborted ||
1506
+ generation !== this._planPresentationGeneration ||
1507
+ modeGeneration + 1 !== this._interactivePermissionModeGeneration ||
1508
+ resolution?.mode !== decision.nextMode ||
1509
+ resolution.origin !== "interactive")
1510
+ throw new Error("Plan approval was cancelled because its dialog is obsolete.");
1511
+ return decision;
1386
1512
  }
1387
1513
  /** Whether compaction or branch summarization is currently running */
1388
1514
  get isCompacting() {
@@ -1483,7 +1609,7 @@ export class AgentSession {
1483
1609
  * {@link _installAgentForcedPromptProjection}.
1484
1610
  */
1485
1611
  _preparePromptAndToolLoadout(options, messages = this.agent.state.messages) {
1486
- options.selectedTools = [...new Set(options.selectedTools)].filter((name) => this._toolRegistry.has(name));
1612
+ options.selectedTools = this._projectToolNames(options.selectedTools);
1487
1613
  this.agent.state.tools = options.selectedTools.flatMap((name) => {
1488
1614
  const tool = this._toolRegistry.get(name);
1489
1615
  return tool ? [tool] : [];
@@ -1523,14 +1649,13 @@ export class AgentSession {
1523
1649
  const current = getCurrentSystemMessage(this.sessionManager.buildSessionContext().messages);
1524
1650
  if (!current)
1525
1651
  return;
1526
- const toolNames = (current.toolsAdded ?? [])
1652
+ const explicitPlanTool = this._requestedToolNames.includes("plan_present");
1653
+ this._requestedToolNames = (current.toolsAdded ?? [])
1527
1654
  .map((tool) => tool.name)
1528
- .filter((name) => this._toolRegistry.has(name));
1529
- this.agent.state.tools = toolNames.flatMap((name) => {
1530
- const registered = this._toolRegistry.get(name);
1531
- return registered ? [registered] : [];
1532
- });
1533
- this._rebuildSystemPrompt(toolNames);
1655
+ .filter((name) => this._toolRegistry.has(name) && (name !== "plan_present" || explicitPlanTool));
1656
+ if (explicitPlanTool && !this._requestedToolNames.includes("plan_present"))
1657
+ this._requestedToolNames.push("plan_present");
1658
+ this._applyRequestedTools();
1534
1659
  }
1535
1660
  // =========================================================================
1536
1661
  // Prompting
@@ -1558,6 +1683,8 @@ export class AgentSession {
1558
1683
  if (this._agentRunAbortRequested)
1559
1684
  this._finishCancelledRetry();
1560
1685
  this._runSystemPromptOptions = undefined;
1686
+ this._runRequestedToolNames = undefined;
1687
+ this._applyRequestedTools();
1561
1688
  this._flushPendingBashMessages();
1562
1689
  this._flushPendingCustomMessages();
1563
1690
  await this._emitAgentSettled();
@@ -1768,6 +1895,7 @@ export class AgentSession {
1768
1895
  if (lastAssistant) {
1769
1896
  await this._checkCompaction(lastAssistant, false);
1770
1897
  }
1898
+ await this._syncPlanToolMode();
1771
1899
  // Emit before_agent_start before normalizing images so extension-driven model
1772
1900
  // selection determines the resize profile used for the request and history.
1773
1901
  const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;
@@ -1777,8 +1905,11 @@ export class AgentSession {
1777
1905
  // loadout is authoritative, so a setActiveTools() call is not undone here.
1778
1906
  const handlerEditedTools = result.systemPromptOptions.selectedTools.length !== selectedToolsBefore.length ||
1779
1907
  result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);
1780
- if (!handlerEditedTools)
1781
- result.systemPromptOptions.selectedTools = this.getActiveToolNames();
1908
+ const inheritedImplicitPlanTool = selectedToolsBefore.includes("plan_present") && !this._requestedToolNames.includes("plan_present");
1909
+ this._runRequestedToolNames = handlerEditedTools
1910
+ ? result.systemPromptOptions.selectedTools.filter((name) => name !== "plan_present" || !inheritedImplicitPlanTool)
1911
+ : undefined;
1912
+ result.systemPromptOptions.selectedTools = this._projectToolNames(this._runRequestedToolNames ?? this._requestedToolNames);
1782
1913
  const normalized = await this._normalizePromptImages(currentImages);
1783
1914
  const userText = normalized.hints.length > 0 ? `${expandedText}\n\n${normalized.hints.join("\n")}` : expandedText;
1784
1915
  // Build messages only after hooks and image normalization have completed.
@@ -2108,6 +2239,8 @@ export class AgentSession {
2108
2239
  * Abort current operation and wait for agent to become idle.
2109
2240
  */
2110
2241
  async abort() {
2242
+ this._planPresentationGeneration++;
2243
+ this._interactivePermissionModeGeneration++;
2111
2244
  if (this._activePrompt && this._lifecycle !== "closed")
2112
2245
  this._lifecycle = "interrupted";
2113
2246
  if (this._isAgentRunActive) {
@@ -2152,9 +2285,60 @@ export class AgentSession {
2152
2285
  async getPermissionMode() {
2153
2286
  if (!this._permissionGate)
2154
2287
  return undefined;
2288
+ if (this._interactivePermissionMode !== undefined) {
2289
+ return { mode: this._interactivePermissionMode, origin: "interactive" };
2290
+ }
2155
2291
  const snapshot = await this._permissionGate.store.snapshot();
2292
+ if (this._interactivePermissionMode !== undefined) {
2293
+ return { mode: this._interactivePermissionMode, origin: "interactive" };
2294
+ }
2156
2295
  return resolveEffectiveModeWithOrigin(this._permissionGate.flagMode, snapshot.modesBySource);
2157
2296
  }
2297
+ getInteractivePermissionMode() {
2298
+ return this._interactivePermissionMode;
2299
+ }
2300
+ async getInteractivePermissionModeCycle() {
2301
+ if (!this._permissionGate)
2302
+ return [];
2303
+ const modes = ["default", "acceptEdits", "plan"];
2304
+ const startup = await this._startupPermissionMode;
2305
+ if (startup?.ok === false)
2306
+ throw startup.error;
2307
+ if (startup?.mode === "bypassPermissions")
2308
+ modes.push("bypassPermissions");
2309
+ return modes;
2310
+ }
2311
+ async setInteractivePermissionMode(mode) {
2312
+ return this._setInteractivePermissionMode(mode);
2313
+ }
2314
+ async _setInteractivePermissionMode(mode, canApply) {
2315
+ if (!this._permissionGate)
2316
+ return undefined;
2317
+ const generation = ++this._interactivePermissionModeGeneration;
2318
+ const startup = await this._startupPermissionMode;
2319
+ if (startup?.ok === false)
2320
+ throw startup.error;
2321
+ if (generation !== this._interactivePermissionModeGeneration)
2322
+ return this.getPermissionMode();
2323
+ if (mode === "bypassPermissions" && startup?.mode !== "bypassPermissions") {
2324
+ throw new Error("Interactive bypassPermissions requires a session that started with bypassPermissions.");
2325
+ }
2326
+ if (canApply && !canApply())
2327
+ throw new Error("Plan approval was cancelled because its dialog is obsolete.");
2328
+ this._interactivePermissionMode = mode;
2329
+ this._planToolSyncGeneration++;
2330
+ this._planToolMode = mode;
2331
+ this._applyRequestedTools();
2332
+ this._setPendingPlanToolSync();
2333
+ return { mode, origin: "interactive" };
2334
+ }
2335
+ clearInteractivePermissionMode() {
2336
+ this._interactivePermissionModeGeneration++;
2337
+ this._interactivePermissionMode = undefined;
2338
+ this._planToolMode = undefined;
2339
+ this._applyRequestedTools();
2340
+ this._schedulePlanToolSync();
2341
+ }
2158
2342
  /**
2159
2343
  * Persist a permission mode to user scope (`~/.apex-code/agent/permissions.json`).
2160
2344
  * Returns the resolution that is actually in force afterwards, which is *not*
@@ -2165,6 +2349,7 @@ export class AgentSession {
2165
2349
  if (!this._permissionGate)
2166
2350
  return undefined;
2167
2351
  await this._permissionGate.store.apply({ type: "setMode", destination: "user", mode });
2352
+ await this._syncPlanToolMode();
2168
2353
  return this.getPermissionMode();
2169
2354
  }
2170
2355
  /**
@@ -3095,6 +3280,7 @@ export class AgentSession {
3095
3280
  }
3096
3281
  async bindExtensions(bindings) {
3097
3282
  if (bindings.uiContext !== undefined) {
3283
+ this._planPresentationGeneration++;
3098
3284
  this._extensionUIContext = bindings.uiContext;
3099
3285
  }
3100
3286
  if (bindings.mode !== undefined) {
@@ -3293,7 +3479,7 @@ export class AgentSession {
3293
3479
  }
3294
3480
  _refreshToolRegistry(options) {
3295
3481
  const previousRegistryNames = new Set(this._toolRegistry.keys());
3296
- const previousActiveToolNames = this.getActiveToolNames();
3482
+ const previousActiveToolNames = this._requestedToolNames;
3297
3483
  const allowedToolNames = this._allowedToolNames;
3298
3484
  const excludedToolNames = this._excludedToolNames;
3299
3485
  const isAllowedTool = (name) => (!allowedToolNames || allowedToolNames.has(name)) && !excludedToolNames?.has(name);
@@ -3374,7 +3560,8 @@ export class AgentSession {
3374
3560
  if (hasDeferredActiveTool && this._toolRegistry.has("tool_schema")) {
3375
3561
  nextActiveToolNames.push("tool_schema");
3376
3562
  }
3377
- this.setActiveToolsByName([...new Set(nextActiveToolNames)]);
3563
+ this._requestedToolNames = [...new Set(nextActiveToolNames)].filter((name) => this._toolRegistry.has(name));
3564
+ this._applyRequestedTools();
3378
3565
  }
3379
3566
  _buildRuntime(options) {
3380
3567
  const autoResizeImages = this.settingsManager.getImageAutoResize();
@@ -3409,6 +3596,9 @@ export class AgentSession {
3409
3596
  this._loadedDeferredSchemas.add(name);
3410
3597
  },
3411
3598
  });
3599
+ baseToolDefinitions.plan_present = createPlanPresentToolDefinition({
3600
+ present: (plan, ctx, signal) => this._presentPlan(plan, ctx, signal),
3601
+ });
3412
3602
  baseToolDefinitions.todo_write = createTodoWriteToolDefinition({
3413
3603
  write: (todos) => {
3414
3604
  this.sessionManager.appendCustomEntry(TODO_CUSTOM_ENTRY_TYPE, todos);
@@ -3433,11 +3623,10 @@ export class AgentSession {
3433
3623
  }
3434
3624
  this._bindExtensionCore(this._extensionRunner);
3435
3625
  this._applyExtensionBindings(this._extensionRunner);
3436
- const defaultActiveToolNames = this._baseToolsOverride
3437
- ? Object.keys(this._baseToolsOverride)
3438
- : this._lspOperations
3439
- ? ["read", "bash", "edit", "write", "lsp"]
3440
- : ["read", "bash", "edit", "write"];
3626
+ const defaultActiveToolNames = configuredDefaultToolNames({
3627
+ baseNames: this._baseToolsOverride ? Object.keys(this._baseToolsOverride) : undefined,
3628
+ lsp: Boolean(this._lspOperations),
3629
+ });
3441
3630
  const baseActiveToolNames = options.activeToolNames ?? defaultActiveToolNames;
3442
3631
  this._refreshToolRegistry({
3443
3632
  activeToolNames: baseActiveToolNames,
@@ -3445,6 +3634,8 @@ export class AgentSession {
3445
3634
  });
3446
3635
  }
3447
3636
  async reload(options) {
3637
+ this._planPresentationGeneration++;
3638
+ this.clearInteractivePermissionMode();
3448
3639
  const oldRunner = this._extensionRunner;
3449
3640
  const previousFlagValues = oldRunner.getFlagValues();
3450
3641
  await emitSessionShutdownEvent(oldRunner, { type: "session_shutdown", reason: "reload" });
@@ -3454,7 +3645,7 @@ export class AgentSession {
3454
3645
  resetApiProviders();
3455
3646
  await this._resourceLoader.reload();
3456
3647
  this._buildRuntime({
3457
- activeToolNames: this.getActiveToolNames(),
3648
+ activeToolNames: this._requestedToolNames,
3458
3649
  flagValues: previousFlagValues,
3459
3650
  includeAllExtensionTools: true,
3460
3651
  });
@@ -3519,6 +3710,7 @@ export class AgentSession {
3519
3710
  success: false,
3520
3711
  attempt,
3521
3712
  finalError: "Retry cancelled",
3713
+ aborted: true,
3522
3714
  });
3523
3715
  }
3524
3716
  /**
@@ -3868,51 +4060,63 @@ export class AgentSession {
3868
4060
  this._resolveIdleWaitIfIdle();
3869
4061
  }
3870
4062
  }
3871
- /**
3872
- * WS.6: resolve what navigation may do to the workspace before the
3873
- * conversation moves. `keep` never touches files; `restore` overwrites
3874
- * only through the engine's pinned pre-restore checkpoint; `fail-if-
3875
- * drifted` and `cancel` refuse when the workspace moved; a missing
3876
- * checkpoint leaves the workspace unchanged and says so. Never throws.
3877
- */
3878
- async _resolveTreeWorkspaceStep(targetId, policy) {
3879
- const unchanged = { policy, outcome: "unchanged", warnings: [] };
3880
- if (policy === "keep")
3881
- return { outcome: unchanged };
4063
+ async previewTreeWorkspace(targetId) {
4064
+ const inspection = await this._inspectTreeWorkspace(targetId);
4065
+ return inspection.state === "unavailable"
4066
+ ? { state: inspection.state, reason: inspection.reason }
4067
+ : { state: inspection.state };
4068
+ }
4069
+ async _inspectTreeWorkspace(targetId) {
3882
4070
  try {
3883
4071
  const engine = await this._checkpoints.engine();
3884
4072
  if (!engine) {
3885
4073
  return {
3886
- outcome: {
3887
- policy,
3888
- outcome: "missing-checkpoint",
3889
- warnings: ["checkpoints are unavailable; workspace left unchanged"],
3890
- },
4074
+ state: "unavailable",
4075
+ reason: "checkpoints are unavailable; workspace left unchanged",
4076
+ navigationOutcome: "missing-checkpoint",
3891
4077
  };
3892
4078
  }
3893
4079
  const checkpoint = await engine.lookup(targetId);
3894
4080
  if (!checkpoint)
3895
- return { outcome: { policy, outcome: "missing-checkpoint", warnings: [] } };
4081
+ return { state: "no-checkpoint" };
3896
4082
  const matches = await engine.matchesWorktree(checkpoint);
3897
4083
  if (matches === undefined) {
3898
4084
  return {
3899
- outcome: {
3900
- policy,
3901
- outcome: "failed",
3902
- warnings: ["could not compare the workspace against the checkpoint"],
3903
- },
4085
+ state: "unavailable",
4086
+ reason: "could not compare the workspace against the checkpoint",
4087
+ navigationOutcome: "failed",
3904
4088
  };
3905
4089
  }
3906
- if (matches)
3907
- return { outcome: unchanged };
3908
- if (policy === "fail-if-drifted" || policy === "cancel") {
3909
- return { cancelled: true, outcome: { policy, outcome: "refused-drifted", warnings: [] } };
3910
- }
3911
- const preRestore = await engine.restore(checkpoint);
4090
+ return { state: matches ? "matches" : "differs", engine, checkpoint };
4091
+ }
4092
+ catch (error) {
4093
+ return {
4094
+ state: "unavailable",
4095
+ reason: `workspace policy check failed: ${error instanceof Error ? error.message : String(error)}`,
4096
+ navigationOutcome: "failed",
4097
+ };
4098
+ }
4099
+ }
4100
+ async _resolveTreeWorkspaceStep(targetId, policy) {
4101
+ const unchanged = { policy, outcome: "unchanged", warnings: [] };
4102
+ if (policy === "keep")
4103
+ return { outcome: unchanged };
4104
+ const inspection = await this._inspectTreeWorkspace(targetId);
4105
+ if (inspection.state === "no-checkpoint") {
4106
+ return { outcome: { policy, outcome: "missing-checkpoint", warnings: [] } };
4107
+ }
4108
+ if (inspection.state === "unavailable") {
4109
+ return { outcome: { policy, outcome: inspection.navigationOutcome, warnings: [inspection.reason] } };
4110
+ }
4111
+ if (inspection.state === "matches")
4112
+ return { outcome: unchanged };
4113
+ if (policy === "fail-if-drifted" || policy === "cancel") {
4114
+ return { cancelled: true, outcome: { policy, outcome: "refused-drifted", warnings: [] } };
4115
+ }
4116
+ try {
4117
+ const preRestore = await inspection.engine.restore(inspection.checkpoint);
3912
4118
  if (!preRestore) {
3913
- return {
3914
- outcome: { policy, outcome: "failed", warnings: ["restore failed; workspace left unchanged"] },
3915
- };
4119
+ return { outcome: { policy, outcome: "failed", warnings: ["restore failed; workspace left unchanged"] } };
3916
4120
  }
3917
4121
  return {
3918
4122
  outcome: {