@gajae-code/coding-agent 0.15.2 → 0.15.4

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 (107) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/dist/types/config/autorouting-tier-map.d.ts +0 -9
  3. package/dist/types/config/file-lock.d.ts +7 -1
  4. package/dist/types/config/keybindings.d.ts +13 -3
  5. package/dist/types/config/model-registry.d.ts +5 -0
  6. package/dist/types/config/model-resolver.d.ts +1 -0
  7. package/dist/types/config/settings-schema.d.ts +65 -1
  8. package/dist/types/coordinator/contract.d.ts +1 -1
  9. package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
  10. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  11. package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
  12. package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
  13. package/dist/types/main.d.ts +1 -1
  14. package/dist/types/modes/action-registry.d.ts +1 -0
  15. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  16. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
  17. package/dist/types/modes/components/welcome.d.ts +2 -0
  18. package/dist/types/modes/controllers/input-controller.d.ts +16 -1
  19. package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
  20. package/dist/types/modes/interactive-mode.d.ts +7 -0
  21. package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
  22. package/dist/types/modes/types.d.ts +3 -0
  23. package/dist/types/modes/ui-language.d.ts +2 -1
  24. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
  25. package/dist/types/sdk/broker/session-index.d.ts +18 -0
  26. package/dist/types/sdk/bus/index.d.ts +4 -1
  27. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  28. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  29. package/dist/types/sdk/cli/session-cli.d.ts +1 -1
  30. package/dist/types/sdk/lifecycle/service.d.ts +32 -5
  31. package/dist/types/sdk/session.d.ts +5 -0
  32. package/dist/types/session/agent-session.d.ts +4 -0
  33. package/dist/types/session/fallback-chain-controller.d.ts +8 -0
  34. package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
  35. package/dist/types/tools/ask.d.ts +9 -0
  36. package/dist/types/utils/clipboard.d.ts +9 -1
  37. package/package.json +7 -7
  38. package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
  39. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  40. package/src/commands/sdk.ts +2 -2
  41. package/src/config/autorouting-tier-map.ts +0 -9
  42. package/src/config/file-lock.ts +15 -1
  43. package/src/config/keybindings.ts +13 -3
  44. package/src/config/model-registry.ts +132 -21
  45. package/src/config/model-resolver.ts +2 -1
  46. package/src/config/models-config-schema.ts +2 -2
  47. package/src/config/settings-schema.ts +66 -1
  48. package/src/config/settings.ts +1 -1
  49. package/src/coordinator/contract.ts +1 -0
  50. package/src/coordinator-mcp/question-state.ts +235 -4
  51. package/src/coordinator-mcp/server.ts +402 -2
  52. package/src/dap/client.ts +19 -8
  53. package/src/deep-interview/render-middleware.ts +6 -2
  54. package/src/extensibility/extensions/types.ts +2 -0
  55. package/src/gjc-runtime/deep-interview-stage.ts +20 -1
  56. package/src/gjc-runtime/deep-interview-state.ts +3 -1
  57. package/src/gjc-runtime/ralplan-runtime.ts +6 -1
  58. package/src/gjc-runtime/session-state-lock.ts +11 -2
  59. package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
  60. package/src/gjc-runtime/workflow-placeholder.ts +33 -0
  61. package/src/internal-urls/docs-index.generated.ts +11 -9
  62. package/src/lsp/client.ts +39 -4
  63. package/src/main.ts +42 -11
  64. package/src/modes/action-registry.ts +7 -0
  65. package/src/modes/components/hook-selector.ts +2 -1
  66. package/src/modes/components/iterm-pet-transport.ts +4 -2
  67. package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
  68. package/src/modes/components/settings-selector.ts +1 -0
  69. package/src/modes/components/tool-status-header.ts +149 -41
  70. package/src/modes/components/welcome.ts +8 -0
  71. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  72. package/src/modes/controllers/goal-mode-controller.ts +7 -2
  73. package/src/modes/controllers/input-controller.ts +141 -7
  74. package/src/modes/controllers/plan-mode-controller.ts +12 -4
  75. package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
  76. package/src/modes/controllers/selector-controller.ts +11 -2
  77. package/src/modes/interactive-mode.ts +88 -1
  78. package/src/modes/shared/oauth-url-copy.ts +33 -0
  79. package/src/modes/types.ts +3 -0
  80. package/src/modes/ui-language.ts +44 -3
  81. package/src/runtime-mcp/oauth-flow.ts +19 -8
  82. package/src/runtime-mcp/transports/stdio.ts +27 -13
  83. package/src/sdk/acp/adapter.ts +29 -2
  84. package/src/sdk/broker/broker.ts +26 -6
  85. package/src/sdk/broker/lifecycle-ledger.ts +28 -1
  86. package/src/sdk/broker/lifecycle.ts +664 -5
  87. package/src/sdk/broker/session-index.ts +246 -0
  88. package/src/sdk/broker/transport.ts +1 -0
  89. package/src/sdk/bus/index.ts +135 -38
  90. package/src/sdk/bus/natives-augment.d.ts +20 -1
  91. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  92. package/src/sdk/bus/telegram-daemon.ts +3 -0
  93. package/src/sdk/cli/session-cli.ts +34 -7
  94. package/src/sdk/lifecycle/service.ts +166 -11
  95. package/src/sdk/mcp/server.ts +2 -1
  96. package/src/sdk/protocol/operation-inventory.generated.json +40 -0
  97. package/src/sdk/protocol/operation-registry.ts +2 -1
  98. package/src/sdk/session.ts +91 -7
  99. package/src/session/agent-session.ts +473 -66
  100. package/src/session/fallback-chain-controller.ts +17 -0
  101. package/src/setup/frictionless-onboarding.ts +4 -4
  102. package/src/slash-commands/aside-command.test.ts +366 -0
  103. package/src/slash-commands/builtin-registry.ts +19 -0
  104. package/src/slash-commands/helpers/aside.ts +299 -0
  105. package/src/tools/ask-contract.ts +54 -3
  106. package/src/tools/ask.ts +54 -6
  107. package/src/utils/clipboard.ts +37 -28
@@ -264,12 +264,15 @@ export class GoalModeController {
264
264
  if (this.#enabled || this.#paused) this.ctx.modeGate.enter("goal");
265
265
  if (restored?.goal) {
266
266
  this.#previousTools = this.ctx.session.getActiveToolNames();
267
- await this.ctx.session.setActiveToolsByName([...new Set([...this.#previousTools, "goal"])]);
267
+ if (!this.ctx.session.isExplicitEmptyToolSelection()) {
268
+ await this.ctx.session.setActiveToolsByName([...new Set([...this.#previousTools, "goal"])]);
269
+ }
268
270
  }
269
271
  this.#updateStatus();
270
272
  return true;
271
273
  }
272
274
  if (recoveryHydration) return false;
275
+ if (this.ctx.session.isExplicitEmptyToolSelection()) return false;
273
276
  const pendingGoal = goalEnabled
274
277
  ? await consumePendingGoalModeRequest(this.ctx.sessionManager.getCwd(), this.ctx.sessionManager.getSessionId())
275
278
  : null;
@@ -321,7 +324,9 @@ export class GoalModeController {
321
324
  objective: options.objective ?? "",
322
325
  provenance: options.provenance,
323
326
  });
324
- await this.ctx.session.setActiveToolsByName([...new Set([...this.#previousTools, "goal"])]);
327
+ if (!this.ctx.session.isExplicitEmptyToolSelection()) {
328
+ await this.ctx.session.setActiveToolsByName([...new Set([...this.#previousTools, "goal"])]);
329
+ }
325
330
  this.ctx.session.setGoalModeState(state);
326
331
  this.#enabled = true;
327
332
  this.#resetContinuationSuppression();
@@ -15,6 +15,7 @@ import {
15
15
  type ComposerSubmissionOptions,
16
16
  canApplyComposerSubmission,
17
17
  type InteractiveModeContext,
18
+ type SubmittedUserInput,
18
19
  stopInteractiveActivityIndicator,
19
20
  } from "../../modes/types";
20
21
  import type { AgentSessionEvent, QueuedMessageEditEntry } from "../../session/agent-session";
@@ -38,6 +39,7 @@ import { CommandPalette, type CommandPaletteAction, type CommandPaletteEntry } f
38
39
  import type { PasteTextContext } from "../components/custom-editor";
39
40
  import { QueuePaneComponent } from "../components/queue-pane";
40
41
  import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
42
+ import { matchesAppInterrupt } from "../utils/keybinding-matchers";
41
43
 
42
44
  const QUEUE_SELECTOR_NAVIGATION_ACTIONS = [
43
45
  "tui.select.up",
@@ -46,6 +48,23 @@ const QUEUE_SELECTOR_NAVIGATION_ACTIONS = [
46
48
  "tui.select.pageDown",
47
49
  ] as const;
48
50
 
51
+ /**
52
+ * Navigation actions that already carry a registry callback and an availability
53
+ * predicate but shipped with no curated command-palette entry and no key
54
+ * dispatch, leaving them unreachable from the palette. They opt into
55
+ * availability gating so an unavailable id is not listed, and each gets a remap
56
+ * dispatch loop while keeping `defaultKeys: []` -- inert at defaults, but a user
57
+ * binding now dispatches instead of only being rendered by the selector.
58
+ */
59
+ export const AVAILABILITY_GATED_NAV_PALETTE_ACTIONS = [
60
+ "app.session.dashboard",
61
+ "app.transcript.browse",
62
+ "app.transcript.prevTurn",
63
+ "app.transcript.nextTurn",
64
+ "app.queue.togglePane",
65
+ "app.message.sendNow",
66
+ ] as const satisfies readonly AppKeybinding[];
67
+
49
68
  interface Expandable {
50
69
  setExpanded(expanded: boolean): void;
51
70
  setManuallyExpanded?(expanded: boolean): void;
@@ -75,6 +94,11 @@ function isExpandable(obj: unknown): obj is Expandable {
75
94
  export class InputController {
76
95
  readonly actionRegistry: ActionRegistry<void>;
77
96
  readonly #loadPastedImageBatch: typeof loadPastedImageBatch;
97
+ #deferredSubmission?: {
98
+ text: string;
99
+ images?: InteractiveModeContext["pendingImages"];
100
+ composer: ComposerSubmissionOptions;
101
+ };
78
102
  #imagePlaceholderDeletionUndo?: {
79
103
  beforeText: string;
80
104
  afterText: string;
@@ -91,6 +115,7 @@ export class InputController {
91
115
  showError: actionId => this.ctx.showError(actionId),
92
116
  });
93
117
  this.#registerActions();
118
+ this.#installOAuthCopyListener();
94
119
  }
95
120
 
96
121
  #registerActions(): void {
@@ -122,6 +147,7 @@ export class InputController {
122
147
  },
123
148
  "app.clipboard.copyLine": () => this.handleCopyCurrentLine(),
124
149
  "app.clipboard.copyPrompt": () => this.handleCopyPrompt(),
150
+ "app.clipboard.copyOAuthUrl": () => this.ctx.copyOAuthUrl(),
125
151
  "app.session.new": async () => {
126
152
  await this.ctx.handleClearCommand();
127
153
  },
@@ -142,6 +168,7 @@ export class InputController {
142
168
  "app.transcript.prevTurn": () => this.#jumpTranscriptTurn(-1),
143
169
  "app.transcript.nextTurn": () => this.#jumpTranscriptTurn(1),
144
170
  "app.tasks.toggle": () => this.ctx.showTasksPane(),
171
+ "app.todo.toggle": () => this.ctx.toggleTodoExpansion(),
145
172
  "app.queue.togglePane": () => this.toggleQueuePane(),
146
173
  "app.message.sendNow": () => this.sendNow(),
147
174
  };
@@ -166,7 +193,7 @@ export class InputController {
166
193
  case "app.thinking.toggle":
167
194
  return Boolean(this.ctx.session.model?.reasoning);
168
195
  case "app.commandPalette.open":
169
- return this.ctx.editor.getText().trim().length === 0;
196
+ return true;
170
197
  case "app.model.cycleForward":
171
198
  case "app.model.cycleBackward":
172
199
  return this.ctx.session.getRoleModelCycleCandidateCount() > 1;
@@ -197,6 +224,8 @@ export class InputController {
197
224
  );
198
225
  case "app.clipboard.copyPrompt":
199
226
  return this.ctx.editor.getText().length > 0;
227
+ case "app.clipboard.copyOAuthUrl":
228
+ return this.ctx.hasOAuthUrlForCopy();
200
229
  case "app.session.tree":
201
230
  case "app.session.fork":
202
231
  return this.ctx.session.messages.length > 0;
@@ -217,6 +246,8 @@ export class InputController {
217
246
  !this.ctx.goalModeController.enabled &&
218
247
  !this.ctx.goalModeController.paused
219
248
  );
249
+ case "app.todo.toggle":
250
+ return this.ctx.todoPhases.some(phase => phase.tasks.length > 0);
220
251
  case "app.queue.togglePane":
221
252
  return true;
222
253
  case "app.message.sendNow":
@@ -234,7 +265,7 @@ export class InputController {
234
265
  }
235
266
 
236
267
  #executeAction(id: (typeof APP_ACTION_METADATA)[number]["id"]): void {
237
- void this.actionRegistry.execute(id);
268
+ void this.actionRegistry.executeFresh(id);
238
269
  }
239
270
 
240
271
  #transcriptTurnAnchorIds: readonly string[] = [];
@@ -281,17 +312,30 @@ export class InputController {
281
312
  * so abort cleanup going idle cannot turn the second Esc into an idle action. */
282
313
  #steerConsumePending = false;
283
314
  #commandPaletteActions = new Map<AppKeybinding, CommandPaletteAction>();
315
+ /** Opt-in subset of `#commandPaletteActions` whose listing is filtered by the
316
+ * action registry's availability predicate. Deliberately a sibling set rather
317
+ * than a blanket filter over every curated entry: filtering all of them would
318
+ * change visible palette contents for pre-existing thinking, model-cycle,
319
+ * editor, copy, queue, and session actions, which is a separate reviewed
320
+ * migration and not this change. */
321
+ readonly #availabilityGatedPaletteActions = new Set<AppKeybinding>();
284
322
  #paletteCommandInFlight = false;
285
323
 
286
- #registerCommandPaletteAction(action: AppKeybinding, handler: () => void | Promise<void>): void {
324
+ #registerCommandPaletteAction(
325
+ action: AppKeybinding,
326
+ handler: () => void | Promise<void>,
327
+ availabilityGated = false,
328
+ ): void {
287
329
  this.#commandPaletteActions.set(action, {
288
330
  id: action,
289
331
  label: KEYBINDINGS[action].description,
290
332
  handler,
291
333
  });
334
+ if (availabilityGated) this.#availabilityGatedPaletteActions.add(action);
292
335
  }
293
336
 
294
337
  #globalInterruptUnsubscribe: (() => void) | undefined;
338
+ #oauthCopyUnsubscribe: (() => void) | undefined;
295
339
  #draftClearEscapeText: string | undefined;
296
340
 
297
341
  #resetEscapeGestures(): void {
@@ -300,6 +344,21 @@ export class InputController {
300
344
  this.#draftClearEscapeText = undefined;
301
345
  }
302
346
 
347
+ #installOAuthCopyListener(): void {
348
+ if (typeof this.ctx.ui?.addInputListener !== "function") return;
349
+ this.#oauthCopyUnsubscribe?.();
350
+ this.#oauthCopyUnsubscribe = this.ctx.ui.addInputListener(data => {
351
+ if (this.ctx.hasOAuthUrlForCopy?.() !== true) return;
352
+ // Focused overlays own cancellation. In particular, a user remap of
353
+ // copyOAuthUrl to Ctrl+C/Escape must not steal the wizard's interrupt.
354
+ if (data === "\x03" || matchesAppInterrupt(data)) return;
355
+ if (this.ctx.keybindings.getKeys("app.clipboard.copyOAuthUrl").some(key => matchesKey(data, key))) {
356
+ this.#executeAction("app.clipboard.copyOAuthUrl");
357
+ return { consume: true };
358
+ }
359
+ });
360
+ }
361
+
303
362
  #armDraftClearEscape(text: string, now: number): void {
304
363
  this.ctx.lastEscapeTime = 0;
305
364
  this.ctx.lastComposerClearEscapeTime = now;
@@ -548,6 +607,7 @@ export class InputController {
548
607
  return bypassAutocomplete;
549
608
  };
550
609
  this.#installGlobalInterruptListener();
610
+ this.#installOAuthCopyListener();
551
611
 
552
612
  // An open btw panel must stay dismissable with Esc even while another
553
613
  // controller (auto-compaction, auto-retry, manual compaction, etc.) has
@@ -695,6 +755,7 @@ export class InputController {
695
755
  );
696
756
  this.ctx.editor.onCopyPrompt = () => this.#executeAction("app.clipboard.copyPrompt");
697
757
  this.#registerCommandPaletteAction("app.clipboard.copyPrompt", copyPrompt);
758
+ this.#registerCommandPaletteAction("app.clipboard.copyOAuthUrl", () => this.ctx.copyOAuthUrl(), true);
698
759
  this.ctx.editor.onPasteText = (text, context) => this.handleTextPaste(text, context);
699
760
  this.ctx.editor.onPastePendingInputCleared = (reason, restoredInputCount) => {
700
761
  const reasonText = reason === "timeout" ? "timed out" : "exceeded the input queue limit";
@@ -710,10 +771,10 @@ export class InputController {
710
771
  this.ctx.editor.setActionKeys("app.message.dequeue", this.ctx.keybindings.getKeys("app.message.dequeue"));
711
772
  this.ctx.editor.onDequeue = () => this.#executeAction("app.message.dequeue");
712
773
  this.#registerCommandPaletteAction("app.message.dequeue", dequeue);
713
- const queue = () => this.handleQueueSubmit();
774
+ const queue = () => this.#executeAction("app.message.queue");
714
775
  this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
715
776
  this.ctx.editor.onQueue = () => this.#executeAction("app.message.queue");
716
- this.#registerCommandPaletteAction("app.message.queue", queue);
777
+ this.#registerCommandPaletteAction("app.message.queue", queue, true);
717
778
 
718
779
  this.ctx.editor.onViewportPageScroll = direction => this.ctx.ui.scrollViewportPages(direction);
719
780
  this.ctx.editor.onViewportFollowLive = () => {
@@ -788,6 +849,21 @@ export class InputController {
788
849
  return true;
789
850
  });
790
851
  }
852
+ // Six navigation actions that shipped with a registry callback and an
853
+ // availability predicate but no curated palette entry and no key dispatch.
854
+ // The palette handler routes through `#executeAction` so the registry's
855
+ // availability check is enforced on selection as well as on listing, and
856
+ // `defaultKeys` stays `[]` per the settled no-new-defaults boundary.
857
+ for (const id of AVAILABILITY_GATED_NAV_PALETTE_ACTIONS) {
858
+ this.#registerCommandPaletteAction(id, () => this.#executeAction(id), true);
859
+ for (const key of this.ctx.keybindings.getKeys(id)) {
860
+ this.ctx.editor.setCustomKeyHandler(key, () => {
861
+ if (!this.actionRegistry.isAvailable(id)) return false;
862
+ this.#executeAction(id);
863
+ return true;
864
+ });
865
+ }
866
+ }
791
867
  for (const key of this.ctx.keybindings.getKeys("app.message.followUp")) {
792
868
  this.ctx.editor.setCustomKeyHandler(key, () => {
793
869
  if (!this.actionRegistry.isAvailable("app.message.followUp")) return false;
@@ -822,6 +898,17 @@ export class InputController {
822
898
  return true;
823
899
  });
824
900
  }
901
+ // The todo HUD renders a `+N more` row but had no way to expand it:
902
+ // `toggleTodoExpansion()` had no caller anywhere in src/. The palette entry
903
+ // opts into availability gating so it is not offered with an empty model.
904
+ this.#registerCommandPaletteAction("app.todo.toggle", () => this.#executeAction("app.todo.toggle"), true);
905
+ for (const key of this.ctx.keybindings.getKeys("app.todo.toggle")) {
906
+ this.ctx.editor.setCustomKeyHandler(key, () => {
907
+ if (!this.actionRegistry.isAvailable("app.todo.toggle")) return false;
908
+ this.#executeAction("app.todo.toggle");
909
+ return true;
910
+ });
911
+ }
825
912
  for (const key of this.ctx.keybindings.getKeys("app.tool.backgroundFold")) {
826
913
  this.ctx.editor.setCustomKeyHandler(key, () => {
827
914
  if (!this.actionRegistry.isAvailable("app.tool.backgroundFold")) return false;
@@ -1104,12 +1191,48 @@ export class InputController {
1104
1191
  const submission = this.ctx.startPendingSubmission({ text, images }, composer);
1105
1192
 
1106
1193
  this.ctx.onInputCallback(submission);
1194
+ } else if (!this.#deferredSubmission) {
1195
+ const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
1196
+ this.#deferredSubmission = { text, images, composer };
1197
+ if (this.#canModifyComposer(composer)) {
1198
+ this.ctx.editor.setText("");
1199
+ this.#clearPendingImagesIfOwnedBy(pendingImages, composer);
1200
+ this.ctx.editor.addToHistory(text);
1201
+ }
1202
+ return;
1203
+ } else {
1204
+ this.ctx.showStatus("Your previous message is waiting to be sent. Keep this draft and send it again shortly.");
1205
+ return;
1107
1206
  }
1108
1207
  if (this.#canModifyComposer(composer)) {
1109
1208
  this.ctx.editor.addToHistory(text);
1110
1209
  }
1111
1210
  }
1112
1211
 
1212
+ takeDeferredSubmission(): SubmittedUserInput | undefined {
1213
+ const deferredSubmission = this.#deferredSubmission;
1214
+ if (!deferredSubmission) return undefined;
1215
+ this.#deferredSubmission = undefined;
1216
+ return this.ctx.startPendingSubmission(
1217
+ { text: deferredSubmission.text, images: deferredSubmission.images },
1218
+ { ownsComposer: false, editor: deferredSubmission.composer.editor },
1219
+ );
1220
+ }
1221
+
1222
+ takeDeferredSubmissionForShutdown(): { text: string; images?: InteractiveModeContext["pendingImages"] } | undefined {
1223
+ const deferredSubmission = this.#deferredSubmission;
1224
+ if (!deferredSubmission) return undefined;
1225
+ this.#deferredSubmission = undefined;
1226
+ return {
1227
+ text: deferredSubmission.text,
1228
+ images: deferredSubmission.images,
1229
+ };
1230
+ }
1231
+
1232
+ discardDeferredSubmission(): void {
1233
+ this.#deferredSubmission = undefined;
1234
+ }
1235
+
1113
1236
  handleCtrlC(): void {
1114
1237
  const now = Date.now();
1115
1238
  if (now - this.ctx.lastSigintTime < 500) {
@@ -2109,7 +2232,18 @@ export class InputController {
2109
2232
  return;
2110
2233
  }
2111
2234
 
2112
- const actions = [...this.#commandPaletteActions.values()];
2235
+ const actions = [...this.#commandPaletteActions.entries()]
2236
+ .filter(([id]) => !this.#availabilityGatedPaletteActions.has(id) || this.actionRegistry.isAvailable(id))
2237
+ .map(([id, action]) =>
2238
+ this.#availabilityGatedPaletteActions.has(id)
2239
+ ? {
2240
+ ...action,
2241
+ handler: async () => {
2242
+ await this.actionRegistry.executeFresh(id);
2243
+ },
2244
+ }
2245
+ : action,
2246
+ );
2113
2247
  const slashCommands = [...(this.ctx.getSlashCommands?.() ?? this.#slashCommands)];
2114
2248
  if (!this.ctx.showCommandPalette) {
2115
2249
  let overlayHandle: ReturnType<typeof this.ctx.ui.showOverlay> | undefined;
@@ -2129,7 +2263,7 @@ export class InputController {
2129
2263
  keybinding: action.bindingId
2130
2264
  ? this.ctx.keybindings.getKeys(action.bindingId).join(", ") || undefined
2131
2265
  : undefined,
2132
- handler: () => this.actionRegistry.execute(action.id),
2266
+ handler: () => this.actionRegistry.executeFresh(action.id),
2133
2267
  })),
2134
2268
  ...slashCommands.map(command => ({
2135
2269
  id: `slash:/${command.name}`,
@@ -98,6 +98,7 @@ export class PlanModeController {
98
98
  }
99
99
 
100
100
  async restoreFromSession(sessionContext: SessionContext): Promise<void> {
101
+ if (this.ctx.session.isExplicitEmptyToolSelection()) return;
101
102
  if (!this.ctx.session.settings.get("plan.enabled")) {
102
103
  if (sessionContext.mode === "plan" || sessionContext.mode === "plan_paused")
103
104
  this.ctx.sessionManager.appendModeChange("none");
@@ -115,6 +116,10 @@ export class PlanModeController {
115
116
 
116
117
  async enter(options?: { planFilePath?: string; workflow?: "parallel" | "iterative" }): Promise<void> {
117
118
  if (this.#enabled) return;
119
+ if (this.ctx.session.isExplicitEmptyToolSelection()) {
120
+ this.ctx.showWarning("Plan mode requires built-in tools and is unavailable in a --no-tools session.");
121
+ return;
122
+ }
118
123
  if (!this.ctx.modeGate.enter("plan")) return this.ctx.showWarning("Exit goal mode first.");
119
124
  this.#paused = false;
120
125
  const planFilePath = options?.planFilePath ?? "local://PLAN.md";
@@ -122,9 +127,12 @@ export class PlanModeController {
122
127
  this.#previousTools = previousTools;
123
128
  this.#planFilePath = planFilePath;
124
129
  this.#enabled = true;
125
- await this.ctx.session.setActiveToolsByName(
126
- this.ctx.session.getToolByName("resolve") ? [...new Set([...previousTools, "resolve"])] : previousTools,
127
- );
130
+ const planTools = this.ctx.session.isExplicitEmptyToolSelection()
131
+ ? previousTools
132
+ : this.ctx.session.getToolByName("resolve")
133
+ ? [...new Set([...previousTools, "resolve"])]
134
+ : previousTools;
135
+ await this.ctx.session.setActiveToolsByName(planTools);
128
136
  this.ctx.session.setPlanModeState({
129
137
  enabled: true,
130
138
  planFilePath,
@@ -143,7 +151,7 @@ export class PlanModeController {
143
151
  async exit(options?: { silent?: boolean; paused?: boolean }): Promise<void> {
144
152
  if (!this.#enabled) return;
145
153
  await this.ctx.session.abort({ timeoutMs: ABORT_TIMEOUT_MS });
146
- if (this.#previousTools?.length) await this.ctx.session.setActiveToolsByName(this.#previousTools);
154
+ if (this.#previousTools !== undefined) await this.ctx.session.setActiveToolsByName(this.#previousTools);
147
155
  if (this.#providerSessionScope && !this.ctx.session.isStreaming) {
148
156
  if (await this.ctx.session.restoreTemporaryProviderSessionScope(this.#providerSessionScope))
149
157
  this.#providerSessionScope = undefined;