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
@@ -12,7 +12,7 @@ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities,
12
12
  import chalk from "chalk";
13
13
  import { spawn, spawnSync } from "child_process";
14
14
  import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
15
- import { parseSkillBlock } from "../../core/agent-session.js";
15
+ import { parseSkillBlock, } from "../../core/agent-session.js";
16
16
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
17
17
  import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, } from "../../core/cache-stats.js";
18
18
  import { formatCacheWarmingStatus, formatCacheWarmingUsage } from "../../core/cache-warmer.js";
@@ -81,10 +81,12 @@ import { PERMISSION_MODE_OVERRIDE_HINTS, SettingsSelectorComponent } from "./com
81
81
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
82
82
  import { ApexSplashHeader } from "./components/splash-header.js";
83
83
  import { BranchSummaryStatusIndicator, CompactionStatusIndicator, IdleStatus, RetryStatusIndicator, WorkingStatusIndicator, } from "./components/status-indicator.js";
84
+ import { TaskPanelComponent } from "./components/task-panel.js";
84
85
  import { ThinkingSelectorComponent } from "./components/thinking-selector.js";
85
86
  import { ToolExecutionComponent } from "./components/tool-execution.js";
86
87
  import { TreeSelectorComponent } from "./components/tree-selector.js";
87
88
  import { TrustSelectorComponent } from "./components/trust-selector.js";
89
+ import { TurnSummaryComponent } from "./components/turn-summary.js";
88
90
  import { UserMessageComponent } from "./components/user-message.js";
89
91
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
90
92
  import { editInExternalEditor } from "./external-editor.js";
@@ -273,6 +275,8 @@ export class InteractiveMode {
273
275
  runtimeHost;
274
276
  /** Settled once the most recent /settings permission-mode write has hit disk. */
275
277
  pendingPermissionModeWrite;
278
+ pendingPermissionModeCycle;
279
+ permissionModeCycleGeneration = 0;
276
280
  renderer;
277
281
  ui;
278
282
  mainScreenRenderState;
@@ -372,6 +376,7 @@ export class InteractiveMode {
372
376
  retriedAttempt;
373
377
  /** Set when the user presses Escape during a retry wait, which is the only way to cancel one. */
374
378
  retryCancelled = false;
379
+ turnStartedAt;
375
380
  // Messages queued while compaction is running
376
381
  compactionQueuedMessages = [];
377
382
  // Shutdown state
@@ -387,6 +392,8 @@ export class InteractiveMode {
387
392
  extensionWidgetsAbove = new Map();
388
393
  extensionWidgetsBelow = new Map();
389
394
  widgetContainerAbove;
395
+ extensionWidgetContainerAbove;
396
+ taskPanel;
390
397
  widgetContainerBelow;
391
398
  // Custom footer from extension (undefined = use built-in footer)
392
399
  customFooter = undefined;
@@ -415,13 +422,13 @@ export class InteractiveMode {
415
422
  get settingsManager() {
416
423
  return this.session.settingsManager;
417
424
  }
418
- offerFirstUseHint(id) {
425
+ offerFirstUseHint(id, text) {
419
426
  this.firstUseHints ??= new FirstUseHints(this.settingsManager.getFirstUseHints());
420
427
  const hint = this.firstUseHints.offer(id);
421
428
  if (!hint)
422
429
  return;
423
430
  this.settingsManager.setFirstUseHints(this.firstUseHints.getSeen());
424
- this.chatContainer.addChild(new Text(theme.fg("dim", hint), this.outputPad, 0));
431
+ this.chatContainer.addChild(new Text(theme.fg("dim", text ?? hint), this.outputPad, 0));
425
432
  }
426
433
  constructor(runtimeHost, options = {}) {
427
434
  this.runtimeHost = runtimeHost;
@@ -457,6 +464,13 @@ export class InteractiveMode {
457
464
  this.pendingMessagesContainer = new Container();
458
465
  this.statusContainer = new Container();
459
466
  this.widgetContainerAbove = new Container();
467
+ this.extensionWidgetContainerAbove = new Container();
468
+ this.taskPanel = new TaskPanelComponent({
469
+ getSession: () => this.session,
470
+ getSettings: () => this.settingsManager,
471
+ });
472
+ this.widgetContainerAbove.addChild(this.taskPanel);
473
+ this.widgetContainerAbove.addChild(this.extensionWidgetContainerAbove);
460
474
  this.widgetContainerBelow = new Container();
461
475
  this.keybindings = KeybindingsManager.create();
462
476
  setKeybindings(this.keybindings);
@@ -1619,6 +1633,7 @@ export class InteractiveMode {
1619
1633
  customInstructions: options?.customInstructions,
1620
1634
  replaceInstructions: options?.replaceInstructions,
1621
1635
  label: options?.label,
1636
+ workspacePolicy: options?.workspacePolicy,
1622
1637
  });
1623
1638
  if (result.cancelled) {
1624
1639
  return { cancelled: true };
@@ -1688,10 +1703,13 @@ export class InteractiveMode {
1688
1703
  }
1689
1704
  }
1690
1705
  async rebindCurrentSession(options = {}) {
1706
+ this.permissionModeCycleGeneration = (this.permissionModeCycleGeneration ?? 0) + 1;
1707
+ this.pendingPermissionModeCycle = undefined;
1691
1708
  const session = this.session;
1692
1709
  this.unsubscribe?.();
1693
1710
  this.unsubscribe = undefined;
1694
1711
  this.applyRuntimeSettings();
1712
+ this.taskPanel?.rebind();
1695
1713
  if (options.renderBeforeBind) {
1696
1714
  this.renderCurrentSessionState();
1697
1715
  this.subscribeToAgent();
@@ -2027,13 +2045,17 @@ export class InteractiveMode {
2027
2045
  }
2028
2046
  // Maximum total widget lines to prevent viewport overflow
2029
2047
  static MAX_WIDGET_LINES = 10;
2048
+ toggleTaskPanel() {
2049
+ this.taskPanel.toggle();
2050
+ this.ui.requestRender();
2051
+ }
2030
2052
  /**
2031
2053
  * Render all extension widgets to the widget container.
2032
2054
  */
2033
2055
  renderWidgets() {
2034
2056
  if (!this.widgetContainerAbove || !this.widgetContainerBelow)
2035
2057
  return;
2036
- this.renderWidgetContainer(this.widgetContainerAbove, this.extensionWidgetsAbove, true, true);
2058
+ this.renderWidgetContainer(this.extensionWidgetContainerAbove ?? this.widgetContainerAbove, this.extensionWidgetsAbove, true, true);
2037
2059
  this.renderWidgetContainer(this.widgetContainerBelow, this.extensionWidgetsBelow, false, false);
2038
2060
  this.ui.requestRender();
2039
2061
  }
@@ -2599,12 +2621,15 @@ export class InteractiveMode {
2599
2621
  this.defaultEditor.onCtrlD = () => this.handleCtrlD();
2600
2622
  this.defaultEditor.onAction("app.suspend", () => this.handleCtrlZ());
2601
2623
  this.defaultEditor.onAction("app.thinking.cycle", () => this.cycleThinkingLevel());
2624
+ this.defaultEditor.onAction("app.permissionMode.cycle", () => this.cyclePermissionMode());
2602
2625
  this.defaultEditor.onAction("app.model.cycleForward", () => this.cycleModel("forward"));
2603
2626
  this.defaultEditor.onAction("app.model.cycleBackward", () => this.cycleModel("backward"));
2604
2627
  // Global debug handler on TUI (works regardless of focus)
2605
2628
  this.ui.onDebug = () => this.handleDebugCommand();
2606
2629
  this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
2607
2630
  this.defaultEditor.onAction("app.tools.expand", () => this.cycleChatDetail());
2631
+ this.defaultEditor.onAction("app.tools.expandLatest", () => this.toggleLatestOperation());
2632
+ this.defaultEditor.onAction("app.tasks.toggle", () => this.toggleTaskPanel());
2608
2633
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
2609
2634
  this.defaultEditor.onAction("app.editor.external", () => void this.handleOpenExternalEditor());
2610
2635
  this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand({ flashConfirmation: true, preferSelection: true }));
@@ -2690,6 +2715,11 @@ export class InteractiveMode {
2690
2715
  await this.showConfigurationIndex();
2691
2716
  return;
2692
2717
  }
2718
+ if (text === "/tasks") {
2719
+ this.editor.setText("");
2720
+ this.toggleTaskPanel();
2721
+ return;
2722
+ }
2693
2723
  if (text === "/settings") {
2694
2724
  this.editor.setText("");
2695
2725
  await this.showSettingsSelector();
@@ -2907,6 +2937,7 @@ export class InteractiveMode {
2907
2937
  await this.init();
2908
2938
  }
2909
2939
  this.footer.invalidate();
2940
+ this.taskPanel?.handleEvent(event);
2910
2941
  switch (event.type) {
2911
2942
  case "agent_start":
2912
2943
  this.clearPendingTools();
@@ -2921,6 +2952,7 @@ export class InteractiveMode {
2921
2952
  }
2922
2953
  break;
2923
2954
  case "turn_start":
2955
+ this.turnStartedAt ??= Date.now();
2924
2956
  if (this.settingsManager.getShowTerminalProgress()) {
2925
2957
  this.ui.terminal.setProgress(true);
2926
2958
  }
@@ -3108,6 +3140,8 @@ export class InteractiveMode {
3108
3140
  break;
3109
3141
  }
3110
3142
  case "tool_execution_end": {
3143
+ if (event.toolName === "plan_present" && !event.isError)
3144
+ await this.refreshFooterPermissionMode();
3111
3145
  const component = this.pendingTools.get(event.toolCallId);
3112
3146
  if (component) {
3113
3147
  component.updateResult({ ...event.result, isError: event.isError });
@@ -3127,9 +3161,20 @@ export class InteractiveMode {
3127
3161
  this.streamingMessage = undefined;
3128
3162
  }
3129
3163
  this.clearPendingTools();
3164
+ if (!event.willRetry) {
3165
+ const kind = event.stopReason?.kind ??
3166
+ [...event.messages].reverse().find((message) => message.role === "assistant")?.stopReason;
3167
+ this.finishTurnSummary(kind === "aborted"
3168
+ ? "aborted"
3169
+ : kind === "completed" || kind === "stop" || kind === undefined
3170
+ ? "completed"
3171
+ : undefined);
3172
+ }
3130
3173
  this.ui.requestRender();
3131
3174
  break;
3132
3175
  case "agent_settled":
3176
+ this.turnStartedAt = undefined;
3177
+ this.ui.requestRender();
3133
3178
  await this.checkShutdownRequested();
3134
3179
  break;
3135
3180
  case "compaction_start": {
@@ -3229,6 +3274,8 @@ export class InteractiveMode {
3229
3274
  }
3230
3275
  this.retriedAttempt = undefined;
3231
3276
  this.retryCancelled = false;
3277
+ if (event.aborted)
3278
+ this.finishTurnSummary("aborted");
3232
3279
  this.ui.requestRender();
3233
3280
  break;
3234
3281
  }
@@ -3256,6 +3303,16 @@ export class InteractiveMode {
3256
3303
  }
3257
3304
  }
3258
3305
  }
3306
+ finishTurnSummary(outcome) {
3307
+ const startedAt = this.turnStartedAt;
3308
+ this.turnStartedAt = undefined;
3309
+ if (startedAt === undefined || outcome === undefined)
3310
+ return;
3311
+ const elapsedMs = Date.now() - startedAt;
3312
+ if (elapsedMs < 1000)
3313
+ return;
3314
+ this.chatContainer.addChild(new TurnSummaryComponent(elapsedMs, outcome));
3315
+ }
3259
3316
  /** Extract text content from a user message */
3260
3317
  getUserMessageText(message) {
3261
3318
  if (message.role !== "user")
@@ -3323,7 +3380,8 @@ export class InteractiveMode {
3323
3380
  addMessageToChat(message, options) {
3324
3381
  switch (message.role) {
3325
3382
  case "bashExecution": {
3326
- const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext);
3383
+ const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext, this.settingsManager.getSymbolPreset());
3384
+ this.adoptChatDetail(component);
3327
3385
  if (message.output) {
3328
3386
  component.appendOutput(message.output);
3329
3387
  }
@@ -3905,11 +3963,26 @@ export class InteractiveMode {
3905
3963
  * remembering to set every axis.
3906
3964
  */
3907
3965
  adoptChatDetail(component) {
3966
+ if (component instanceof ToolExecutionComponent || component instanceof BashExecutionComponent) {
3967
+ component.setChatDetail(this.chatDetail);
3968
+ return;
3969
+ }
3908
3970
  if (isExpandable(component))
3909
3971
  component.setExpanded(this.toolOutputExpanded);
3910
3972
  if (hasEditDiffsExpansion(component))
3911
3973
  component.setEditDiffsExpanded(this.editDiffsExpanded);
3912
3974
  }
3975
+ toggleLatestOperation() {
3976
+ const pending = this.pendingMessagesContainer?.children ?? [];
3977
+ const operations = [...this.chatContainer.children, ...pending];
3978
+ const latest = operations
3979
+ .reverse()
3980
+ .find((child) => child instanceof ToolExecutionComponent || child instanceof BashExecutionComponent);
3981
+ if (latest instanceof ToolExecutionComponent || latest instanceof BashExecutionComponent) {
3982
+ latest.toggleExpanded();
3983
+ this.ui.requestRender();
3984
+ }
3985
+ }
3913
3986
  /** The expand key. A level the user picks is saved, so the next session opens at it. */
3914
3987
  cycleChatDetail() {
3915
3988
  this.chooseChatDetail(nextChatDetail(this.chatDetail));
@@ -3927,6 +4000,7 @@ export class InteractiveMode {
3927
4000
  this.chatDetail = detail;
3928
4001
  this.toolOutputExpanded = view.toolOutputExpanded;
3929
4002
  this.editDiffsExpanded = view.editDiffsExpanded;
4003
+ this.footer?.setChatDetail(detail);
3930
4004
  }
3931
4005
  /**
3932
4006
  * Presentation only. This never rewrites messages or the session transcript,
@@ -3938,7 +4012,6 @@ export class InteractiveMode {
3938
4012
  return;
3939
4013
  this.assignChatDetail(detail);
3940
4014
  this.applyChatDetail();
3941
- this.showStatus(`Conversation detail: ${detail}`);
3942
4015
  }
3943
4016
  /** True when thinking blocks are hidden right now, setting and detail level combined. */
3944
4017
  isThinkingHidden() {
@@ -3950,17 +4023,14 @@ export class InteractiveMode {
3950
4023
  if (isExpandable(activeHeader)) {
3951
4024
  activeHeader.setExpanded(this.toolOutputExpanded);
3952
4025
  }
3953
- for (const container of [this.loadedResourcesContainer, this.chatContainer]) {
4026
+ for (const container of [this.loadedResourcesContainer, this.chatContainer, this.pendingMessagesContainer]) {
4027
+ if (!container)
4028
+ continue;
3954
4029
  for (const child of container.children) {
3955
4030
  if (child instanceof AssistantMessageComponent) {
3956
4031
  child.setHideThinkingBlock(this.isThinkingHidden());
3957
4032
  }
3958
- if (isExpandable(child)) {
3959
- child.setExpanded(this.toolOutputExpanded);
3960
- }
3961
- if (hasEditDiffsExpansion(child)) {
3962
- child.setEditDiffsExpanded(this.editDiffsExpanded);
3963
- }
4033
+ this.adoptChatDetail(child);
3964
4034
  }
3965
4035
  }
3966
4036
  this.ui.requestRender();
@@ -4278,25 +4348,61 @@ export class InteractiveMode {
4278
4348
  * would under-report bypassPermissions, which is the one direction that matters.
4279
4349
  */
4280
4350
  async refreshFooterPermissionMode() {
4281
- const resolution = await this.session.getPermissionMode();
4282
- this.footer.setPermissionMode(resolution?.mode ?? "default");
4351
+ const session = this.session;
4352
+ const resolution = await session.getPermissionMode();
4353
+ if (this.session !== session)
4354
+ return;
4355
+ this.footer.setPermissionMode(resolution?.mode ?? "default", resolution?.origin);
4283
4356
  if (this.isInitialized) {
4284
4357
  this.ui.requestRender();
4285
4358
  }
4286
4359
  }
4360
+ cyclePermissionMode() {
4361
+ const session = this.session;
4362
+ const generation = this.permissionModeCycleGeneration ?? 0;
4363
+ const isCurrent = () => this.session === session && (this.permissionModeCycleGeneration ?? 0) === generation;
4364
+ const cycle = (this.pendingPermissionModeCycle ?? Promise.resolve())
4365
+ .then(async () => {
4366
+ if (!isCurrent())
4367
+ return;
4368
+ const modes = await session.getInteractivePermissionModeCycle();
4369
+ if (!isCurrent() || modes.length === 0)
4370
+ return;
4371
+ const current = await session.getPermissionMode();
4372
+ if (!isCurrent())
4373
+ return;
4374
+ const index = modes.indexOf(current?.mode ?? "default");
4375
+ const resolution = await session.setInteractivePermissionMode(modes[(index + 1) % modes.length]);
4376
+ if (!isCurrent() || !resolution)
4377
+ return;
4378
+ this.footer.setPermissionMode(resolution.mode, resolution.origin);
4379
+ this.offerFirstUseHint("permission-mode-cycle", `${keyDisplayText("app.permissionMode.cycle")} now cycles permission modes for this session; ${keyDisplayText("app.thinking.cycle")} cycles thinking level.`);
4380
+ this.ui.requestRender();
4381
+ })
4382
+ .catch((error) => {
4383
+ if (!isCurrent())
4384
+ return;
4385
+ this.showError(`Permission mode could not be changed: ${error instanceof Error ? error.message : String(error)}`);
4386
+ });
4387
+ this.pendingPermissionModeCycle = cycle;
4388
+ return cycle;
4389
+ }
4287
4390
  /**
4288
4391
  * Writes the mode to user scope, then reports what is actually in force. The
4289
4392
  * write can be outranked by `--permission-mode` or a project file, and a
4290
4393
  * settings row that silently does nothing is worse than no row at all.
4291
4394
  */
4292
4395
  async applyPermissionMode(mode) {
4396
+ const session = this.session;
4293
4397
  let resolution;
4294
4398
  try {
4295
- const write = this.session.setPermissionMode(mode);
4399
+ const write = session.setPermissionMode(mode);
4296
4400
  this.pendingPermissionModeWrite = write.then(() => { }, () => { });
4297
4401
  resolution = await write;
4298
4402
  }
4299
4403
  catch (error) {
4404
+ if (this.session !== session)
4405
+ return;
4300
4406
  // Never swallow this. A permission mode the user believes they set, and
4301
4407
  // did not, is the one outcome worse than the prompt they were escaping.
4302
4408
  const detail = error instanceof Error ? error.message : String(error);
@@ -4304,17 +4410,27 @@ export class InteractiveMode {
4304
4410
  this.ui.requestRender();
4305
4411
  return;
4306
4412
  }
4307
- if (!resolution)
4413
+ if (this.session !== session || !resolution)
4308
4414
  return;
4309
- const outranked = resolution.mode !== mode;
4415
+ const outranked = resolution.mode !== mode || resolution.origin === "interactive";
4310
4416
  const source = PERMISSION_MODE_OVERRIDE_HINTS[resolution.origin] ?? resolution.origin;
4311
4417
  const line = outranked
4312
4418
  ? `Permission mode saved as ${mode}, but ${source} sets ${resolution.mode} for this session.`
4313
4419
  : `Permission mode set to ${mode}.`;
4314
4420
  this.chatContainer.addChild(new Text(theme.fg(outranked ? "warning" : "muted", line), 1, 0));
4315
- this.footer.setPermissionMode(resolution.mode);
4421
+ this.footer.setPermissionMode(resolution.mode, resolution.origin);
4316
4422
  this.ui.requestRender();
4317
4423
  }
4424
+ setTaskListToolDefault(enabled) {
4425
+ if (this.settingsManager.getProjectSettings().defaultTools !== undefined)
4426
+ return;
4427
+ const names = this.settingsManager.getDefaultTools() ?? this.session.getImplicitDefaultToolNames();
4428
+ this.settingsManager.setDefaultTools(enabled
4429
+ ? names.includes("todo_write")
4430
+ ? names
4431
+ : [...names, "todo_write"]
4432
+ : names.filter((name) => name !== "todo_write"));
4433
+ }
4318
4434
  async showSettingsSelector() {
4319
4435
  const permissionMode = await this.session.getPermissionMode();
4320
4436
  this.showSelector((done) => {
@@ -4324,6 +4440,10 @@ export class InteractiveMode {
4324
4440
  const defaultModel = defaultProvider && defaultModelId ? `${defaultProvider}/${defaultModelId}` : "not set";
4325
4441
  selector = new SettingsSelectorComponent({
4326
4442
  autoCompact: this.session.autoCompactionEnabled,
4443
+ taskListTool: {
4444
+ enabled: (this.settingsManager.getDefaultTools() ?? this.session.getImplicitDefaultToolNames()).includes("todo_write"),
4445
+ projectControlled: this.settingsManager.getProjectSettings().defaultTools !== undefined,
4446
+ },
4327
4447
  defaultModel,
4328
4448
  currentModel: this.session.model,
4329
4449
  availableDefaultModels: this.session.modelRuntime.getAvailableSnapshot(),
@@ -4366,6 +4486,7 @@ export class InteractiveMode {
4366
4486
  fullscreenCopyOnSelect: this.settingsManager.getFullscreenCopyOnSelect(),
4367
4487
  warnings: this.settingsManager.getWarnings(),
4368
4488
  }, {
4489
+ onTaskListToolChange: (enabled) => this.setTaskListToolDefault(enabled),
4369
4490
  onAutoCompactChange: (enabled) => {
4370
4491
  this.session.setAutoCompactionEnabled(enabled);
4371
4492
  this.footer.setAutoCompactEnabled(enabled);
@@ -4982,6 +5103,29 @@ export class InteractiveMode {
4982
5103
  this.showError("Wait for the current compaction or tree navigation to finish before navigating the session tree.");
4983
5104
  return;
4984
5105
  }
5106
+ const workspacePreview = await this.session.previewTreeWorkspace(entryId);
5107
+ if (this.session.isCompacting) {
5108
+ this.showError("Wait for the current compaction or tree navigation to finish before navigating the session tree.");
5109
+ return;
5110
+ }
5111
+ let workspacePolicy = "keep";
5112
+ if (workspacePreview.state === "differs") {
5113
+ const choice = await this.showExtensionSelector("Restore workspace files?", [
5114
+ "Keep current files",
5115
+ "Restore files to this point",
5116
+ "Cancel",
5117
+ ]);
5118
+ if (choice === undefined || choice === "Cancel") {
5119
+ this.showStatus("Navigation cancelled");
5120
+ return;
5121
+ }
5122
+ if (choice === "Restore files to this point")
5123
+ workspacePolicy = "restore";
5124
+ }
5125
+ if (this.session.isCompacting) {
5126
+ this.showError("Wait for the current compaction or tree navigation to finish before navigating the session tree.");
5127
+ return;
5128
+ }
4985
5129
  // Set up escape handler and status indicator if summarizing
4986
5130
  let showingSummaryIndicator = false;
4987
5131
  const originalOnEscape = this.defaultEditor.onEscape;
@@ -4998,6 +5142,7 @@ export class InteractiveMode {
4998
5142
  const result = await this.session.navigateTree(entryId, {
4999
5143
  summarize: wantsSummary,
5000
5144
  customInstructions,
5145
+ workspacePolicy,
5001
5146
  });
5002
5147
  if (result.aborted) {
5003
5148
  // Summarization aborted - re-show tree selector with same selection
@@ -5015,7 +5160,22 @@ export class InteractiveMode {
5015
5160
  if (result.editorText && !this.editor.getText().trim()) {
5016
5161
  this.editor.setText(result.editorText);
5017
5162
  }
5018
- this.showStatus("Navigated to selected point");
5163
+ const workspace = result.workspace;
5164
+ if (workspace?.outcome === "restored") {
5165
+ this.showStatus(workspace.preRestoreCheckpoint
5166
+ ? "Restored files · pre-restore checkpoint saved"
5167
+ : "Restored files");
5168
+ }
5169
+ else if (workspace?.outcome === "failed") {
5170
+ this.showStatus(workspace.warnings.join(" · ") || "Workspace restore failed");
5171
+ }
5172
+ else if (workspace?.outcome === "missing-checkpoint" ||
5173
+ workspacePreview.state === "no-checkpoint") {
5174
+ this.showStatus("Files unchanged · no checkpoint for this point");
5175
+ }
5176
+ else {
5177
+ this.showStatus("Files unchanged");
5178
+ }
5019
5179
  void this.flushCompactionQueue({ willRetry: false });
5020
5180
  }
5021
5181
  catch (error) {
@@ -5600,6 +5760,8 @@ export class InteractiveMode {
5600
5760
  this.showWarning("Wait for compaction to finish before reloading.");
5601
5761
  return;
5602
5762
  }
5763
+ this.permissionModeCycleGeneration = (this.permissionModeCycleGeneration ?? 0) + 1;
5764
+ this.pendingPermissionModeCycle = undefined;
5603
5765
  this.resetExtensionUI();
5604
5766
  const reloadBox = new Container();
5605
5767
  reloadBox.addChild(new DynamicBorder());
@@ -5665,6 +5827,7 @@ export class InteractiveMode {
5665
5827
  dismissReloadBox(previousEditor);
5666
5828
  }
5667
5829
  this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
5830
+ await this.refreshFooterPermissionMode();
5668
5831
  }
5669
5832
  }
5670
5833
  async handleExportCommand(text) {
@@ -6030,9 +6193,11 @@ export class InteractiveMode {
6030
6193
  const exit = this.getAppKeyDisplay("app.exit");
6031
6194
  const suspend = this.getAppKeyDisplay("app.suspend");
6032
6195
  const cycleThinkingLevel = this.getAppKeyDisplay("app.thinking.cycle");
6196
+ const cyclePermissionMode = this.getAppKeyDisplay("app.permissionMode.cycle");
6033
6197
  const cycleModelForward = this.getAppKeyDisplay("app.model.cycleForward");
6034
6198
  const selectModel = this.getAppKeyDisplay("app.model.select");
6035
6199
  const expandTools = this.getAppKeyDisplay("app.tools.expand");
6200
+ const expandLatest = this.getAppKeyDisplay("app.tools.expandLatest");
6036
6201
  const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
6037
6202
  const externalEditor = this.getAppKeyDisplay("app.editor.external");
6038
6203
  const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
@@ -6074,9 +6239,11 @@ export class InteractiveMode {
6074
6239
  | \`${exit}\` | Exit (when editor is empty) |
6075
6240
  | \`${suspend}\` | Suspend to background |
6076
6241
  | \`${cycleThinkingLevel}\` | Cycle thinking level |
6242
+ | \`${cyclePermissionMode}\` | Cycle permission mode for this session |
6077
6243
  | \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models |
6078
6244
  | \`${selectModel}\` | Open model selector |
6079
6245
  | \`${expandTools}\` | Cycle conversation detail (overview / details / all) |
6246
+ | \`${expandLatest}\` | Toggle latest operation details |
6080
6247
  | \`${toggleThinking}\` | Toggle thinking block visibility |
6081
6248
  | \`${externalEditor}\` | Edit message in external editor |
6082
6249
  | \`${copyMessage}\` | Copy selection or last assistant message |
@@ -6227,7 +6394,8 @@ export class InteractiveMode {
6227
6394
  if (eventResult?.result) {
6228
6395
  const result = eventResult.result;
6229
6396
  // Create UI component for display
6230
- this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
6397
+ this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext, this.settingsManager.getSymbolPreset());
6398
+ this.adoptChatDetail(this.bashComponent);
6231
6399
  if (this.session.isStreaming) {
6232
6400
  this.pendingMessagesContainer.addChild(this.bashComponent);
6233
6401
  this.pendingBashComponents.push(this.bashComponent);
@@ -6248,7 +6416,8 @@ export class InteractiveMode {
6248
6416
  }
6249
6417
  // Normal execution path (possibly with custom operations)
6250
6418
  const isDeferred = this.session.isStreaming;
6251
- this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
6419
+ this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext, this.settingsManager.getSymbolPreset());
6420
+ this.adoptChatDetail(this.bashComponent);
6252
6421
  if (isDeferred) {
6253
6422
  // Show in pending area when agent is streaming
6254
6423
  this.pendingMessagesContainer.addChild(this.bashComponent);