@gajae-code/coding-agent 0.11.7 → 0.11.9

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 (213) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/dist/types/cli/args.d.ts +0 -2
  3. package/dist/types/cli.d.ts +1 -0
  4. package/dist/types/config/keybindings.d.ts +33 -3
  5. package/dist/types/config/model-profile-activation.d.ts +4 -3
  6. package/dist/types/config/model-profile-contract.d.ts +38 -0
  7. package/dist/types/config/model-profiles.d.ts +2 -3
  8. package/dist/types/config/settings-schema.d.ts +15 -2
  9. package/dist/types/edit/modes/patch.d.ts +13 -0
  10. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  11. package/dist/types/eval/py/executor.d.ts +1 -1
  12. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
  14. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
  15. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
  16. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
  17. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
  18. package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
  19. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  20. package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
  21. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
  22. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
  23. package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
  24. package/dist/types/main.d.ts +8 -0
  25. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  26. package/dist/types/modes/components/tool-status-header.d.ts +3 -2
  27. package/dist/types/modes/components/welcome.d.ts +4 -0
  28. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  30. package/dist/types/modes/interactive-mode.d.ts +5 -2
  31. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  32. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  33. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  34. package/dist/types/modes/types.d.ts +3 -0
  35. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  36. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  37. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  38. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  39. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  40. package/dist/types/sdk/broker/broker.d.ts +2 -0
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
  43. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  44. package/dist/types/sdk/bus/config.d.ts +2 -0
  45. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  46. package/dist/types/sdk/bus/index.d.ts +25 -0
  47. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +15 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
  52. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  53. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  54. package/dist/types/sdk/host/host.d.ts +1 -1
  55. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  56. package/dist/types/sdk/index.d.ts +2 -0
  57. package/dist/types/sdk/prompt-status.d.ts +88 -0
  58. package/dist/types/sdk/session-directory.d.ts +1 -1
  59. package/dist/types/sdk/startup-capability.d.ts +3 -0
  60. package/dist/types/session/agent-session.d.ts +2 -0
  61. package/dist/types/session/internal/managed-session-scope.d.ts +22 -3
  62. package/dist/types/session/internal/managed-session-storage.d.ts +24 -4
  63. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  64. package/dist/types/session/session-manager.d.ts +5 -1
  65. package/dist/types/session/session-storage.d.ts +31 -0
  66. package/dist/types/skill-state/active-state.d.ts +4 -0
  67. package/dist/types/task/index.d.ts +3 -1
  68. package/dist/types/task/receipt.d.ts +2 -0
  69. package/dist/types/task/types.d.ts +94 -0
  70. package/dist/types/tools/ask.d.ts +454 -174
  71. package/dist/types/tools/image-gen.d.ts +47 -1
  72. package/dist/types/tools/index.d.ts +4 -0
  73. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  74. package/package.json +8 -8
  75. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  76. package/scripts/dogfood-repository-binding.ts +209 -0
  77. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  78. package/scripts/run-test-manifest.ts +4 -4
  79. package/src/cli/args.ts +2 -92
  80. package/src/cli/mcp-cli.ts +4 -21
  81. package/src/cli/notify-cli.ts +5 -38
  82. package/src/cli.ts +50 -0
  83. package/src/commands/deep-interview.ts +9 -4
  84. package/src/commands/sdk.ts +2 -2
  85. package/src/commit/agentic/index.ts +3 -3
  86. package/src/commit/map-reduce/index.ts +2 -2
  87. package/src/config/keybindings.ts +157 -24
  88. package/src/config/model-profile-activation.ts +7 -18
  89. package/src/config/model-profile-contract.ts +161 -0
  90. package/src/config/model-profiles.ts +2 -3
  91. package/src/config/model-registry.ts +11 -1
  92. package/src/config/settings-schema.ts +17 -2
  93. package/src/coordinator-mcp/model-preset.ts +21 -74
  94. package/src/defaults/gjc/skills/deep-interview/SKILL.md +80 -165
  95. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  96. package/src/edit/index.ts +3 -6
  97. package/src/edit/modes/patch.ts +38 -1
  98. package/src/edit/modes/replace.ts +42 -0
  99. package/src/edit/path-mutation-lock.ts +112 -0
  100. package/src/eval/py/executor.ts +30 -11
  101. package/src/eval/py/prelude.py +2 -2
  102. package/src/eval/py/tool-bridge.ts +34 -18
  103. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  104. package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
  105. package/src/gjc-runtime/deep-interview-draft.ts +842 -0
  106. package/src/gjc-runtime/deep-interview-payload.ts +262 -0
  107. package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
  108. package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
  109. package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
  110. package/src/gjc-runtime/deep-interview-state.ts +916 -4
  111. package/src/gjc-runtime/launch-tmux.ts +1 -1
  112. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  113. package/src/gjc-runtime/ralplan-runtime.ts +78 -4
  114. package/src/gjc-runtime/repository-binding.ts +267 -0
  115. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  116. package/src/gjc-runtime/state-writer.ts +166 -2
  117. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  118. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  119. package/src/gjc-runtime/ultragoal-guard.ts +5 -5
  120. package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
  121. package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
  122. package/src/gjc-runtime/workflow-manifest.ts +191 -5
  123. package/src/internal-urls/docs-index.generated.ts +8 -6
  124. package/src/internal-urls/local-protocol.ts +33 -16
  125. package/src/internal-urls/mcp-protocol.ts +73 -11
  126. package/src/main.ts +25 -10
  127. package/src/modes/DESIGN.md +5 -0
  128. package/src/modes/acp/acp-agent.ts +50 -16
  129. package/src/modes/components/model-selector.ts +42 -8
  130. package/src/modes/components/plan-preview-overlay.ts +1 -1
  131. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  132. package/src/modes/components/tool-status-header.ts +20 -4
  133. package/src/modes/components/welcome.ts +32 -20
  134. package/src/modes/controllers/command-controller.ts +97 -54
  135. package/src/modes/controllers/event-controller.ts +10 -0
  136. package/src/modes/controllers/input-controller.ts +7 -2
  137. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  138. package/src/modes/controllers/selector-controller.ts +46 -54
  139. package/src/modes/interactive-mode.ts +70 -36
  140. package/src/modes/prompt-action-autocomplete.ts +32 -13
  141. package/src/modes/prompt-suggestion-controller.ts +96 -0
  142. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  143. package/src/modes/types.ts +3 -0
  144. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  145. package/src/modes/utils/ui-helpers.ts +5 -3
  146. package/src/prompts/agents/architect.md +0 -1
  147. package/src/prompts/agents/critic.md +0 -1
  148. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  149. package/src/prompts/system/system-prompt.md +7 -4
  150. package/src/prompts/tools/cron.md +3 -1
  151. package/src/runtime-mcp/json-rpc.ts +7 -4
  152. package/src/runtime-mcp/manager.ts +51 -53
  153. package/src/runtime-mcp/oauth-flow.ts +0 -72
  154. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  155. package/src/runtime-mcp/redaction.ts +30 -0
  156. package/src/runtime-mcp/transports/http.ts +13 -6
  157. package/src/sdk/broker/broker.ts +34 -20
  158. package/src/sdk/broker/lifecycle.ts +185 -36
  159. package/src/sdk/broker/session-index.ts +11 -1
  160. package/src/sdk/bus/chat-daemon-control.ts +20 -5
  161. package/src/sdk/bus/config-commands.ts +2 -2
  162. package/src/sdk/bus/config.ts +9 -1
  163. package/src/sdk/bus/conversation-store.ts +40 -6
  164. package/src/sdk/bus/index.ts +240 -65
  165. package/src/sdk/bus/lifecycle-commands.ts +47 -3
  166. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  167. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  168. package/src/sdk/bus/notification-orchestration.ts +12 -0
  169. package/src/sdk/bus/notification-service.ts +27 -2
  170. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  171. package/src/sdk/bus/recent-activity.ts +90 -26
  172. package/src/sdk/bus/telegram-daemon-contract.ts +15 -3
  173. package/src/sdk/bus/telegram-daemon-control.ts +145 -33
  174. package/src/sdk/bus/telegram-daemon.ts +853 -112
  175. package/src/sdk/bus/telegram-reference.ts +20 -2
  176. package/src/sdk/cli/session-cli.ts +15 -2
  177. package/src/sdk/host/control/dispatch.ts +1 -1
  178. package/src/sdk/host/control/operations.ts +1 -1
  179. package/src/sdk/host/host.ts +2 -6
  180. package/src/sdk/host/query/handlers.ts +70 -5
  181. package/src/sdk/index.ts +12 -0
  182. package/src/sdk/prompt-status.ts +97 -0
  183. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  184. package/src/sdk/protocol/operation-registry.ts +18 -4
  185. package/src/sdk/session-directory.ts +5 -1
  186. package/src/sdk/session.ts +2 -0
  187. package/src/sdk/startup-capability.ts +9 -1
  188. package/src/session/agent-session.ts +138 -5
  189. package/src/session/blob-store.ts +7 -2
  190. package/src/session/internal/managed-session-scope.ts +998 -133
  191. package/src/session/internal/managed-session-storage.ts +268 -203
  192. package/src/session/internal/native-publish-outcome.ts +266 -0
  193. package/src/session/session-manager.ts +406 -360
  194. package/src/session/session-storage.ts +151 -11
  195. package/src/skill-state/active-state.ts +10 -0
  196. package/src/task/executor.ts +13 -5
  197. package/src/task/gjc-command.ts +2 -2
  198. package/src/task/index.ts +121 -15
  199. package/src/task/receipt.ts +3 -0
  200. package/src/task/types.ts +34 -3
  201. package/src/tools/ask.ts +199 -88
  202. package/src/tools/ast-edit.ts +2 -2
  203. package/src/tools/bash-pty-selection.ts +2 -2
  204. package/src/tools/browser/screenshot-format.ts +6 -1
  205. package/src/tools/cron.ts +1 -1
  206. package/src/tools/image-gen.ts +178 -6
  207. package/src/tools/index.ts +4 -0
  208. package/src/tools/render-utils.ts +7 -1
  209. package/src/utils/edit-mode.ts +2 -2
  210. package/src/utils/prompt-suggestion.ts +265 -0
  211. package/src/utils/sixel.ts +3 -3
  212. package/src/web/insane/url-guard.ts +3 -1
  213. package/src/web/scrapers/docs-rs.ts +30 -5
@@ -5,7 +5,7 @@ import type { OAuthProvider } from "@gajae-code/ai/utils/oauth/types";
5
5
  import type { Component, OverlayHandle, SlashCommand } from "@gajae-code/tui";
6
6
  import { Input, isPetMode, Loader, Spacer, Text } from "@gajae-code/tui";
7
7
  import { getAgentDbPath, getProjectDir, logger, VERSION } from "@gajae-code/utils";
8
- import { type AppKeybinding, formatKeyHints } from "../../config/keybindings";
8
+ import type { AppKeybinding } from "../../config/keybindings";
9
9
  import {
10
10
  activateModelProfile,
11
11
  type MaterializeModelProfileForDeletionResult,
@@ -64,7 +64,7 @@ import {
64
64
  import type { NotificationSessionStatus } from "../../sdk/bus/session-control";
65
65
  import {
66
66
  ensureTelegramDaemonRunningDetailed,
67
- readDaemonState,
67
+ resolveTelegramSetupPreflight,
68
68
  unregisterNotificationRoot,
69
69
  } from "../../sdk/bus/telegram-daemon";
70
70
  import { TelegramDaemonController } from "../../sdk/bus/telegram-daemon-control";
@@ -226,6 +226,7 @@ export interface NotificationsEditorOperationDependencies {
226
226
  sanitizeDiagnostic: typeof sanitizeDiagnostic;
227
227
  ensureTelegramDaemonRunningDetailed: typeof ensureTelegramDaemonRunningDetailed;
228
228
  runTelegramSetup: typeof runTelegramSetup;
229
+ resolveTelegramSetupPreflight: typeof resolveTelegramSetupPreflight;
229
230
  proposedTelegramIdentity: typeof proposedTelegramIdentity;
230
231
  reconcileCommittedTelegramConfiguration: typeof reconcileCommittedTelegramConfiguration;
231
232
  saveTelegramInactive: typeof saveTelegramInactive;
@@ -250,6 +251,7 @@ const notificationEditorOperationDependencies: NotificationsEditorOperationDepen
250
251
  sanitizeDiagnostic,
251
252
  ensureTelegramDaemonRunningDetailed,
252
253
  runTelegramSetup,
254
+ resolveTelegramSetupPreflight,
253
255
  proposedTelegramIdentity,
254
256
  reconcileCommittedTelegramConfiguration,
255
257
  saveTelegramInactive,
@@ -306,33 +308,8 @@ export function createNotificationsEditorOperations(
306
308
  cwd: ctx.sessionManager.getCwd(),
307
309
  sessionId: ctx.sessionManager.getSessionId(),
308
310
  });
309
- const telegramSetupPreflight = async (): Promise<TelegramSetupPreflight> => {
310
- const storedChatId = services.getNotificationConfig(ctx.settings).chatId;
311
- try {
312
- const state = await readDaemonState(ctx.settings);
313
- const validPid = Number.isSafeInteger(state?.pid) && (state?.pid ?? 0) > 0;
314
- if (!state || !validPid) return { storedChatId };
315
- let live = false;
316
- try {
317
- process.kill(state.pid, 0);
318
- live = true;
319
- } catch (error) {
320
- live = (error as NodeJS.ErrnoException).code === "EPERM";
321
- }
322
- return live
323
- ? {
324
- storedChatId,
325
- daemon: {
326
- live,
327
- tokenFingerprint: typeof state.tokenFingerprint === "string" ? state.tokenFingerprint : undefined,
328
- chatId: typeof state.chatId === "string" && state.chatId.trim() ? state.chatId.trim() : undefined,
329
- },
330
- }
331
- : { storedChatId };
332
- } catch {
333
- return { storedChatId };
334
- }
335
- };
311
+ const telegramSetupPreflight = async (): Promise<TelegramSetupPreflight> =>
312
+ await services.resolveTelegramSetupPreflight(ctx.settings);
336
313
 
337
314
  return {
338
315
  loadState: async () => {
@@ -838,7 +815,7 @@ export class SelectorController {
838
815
  id: `action:${action.id}`,
839
816
  label: action.label,
840
817
  description: action.id,
841
- keybinding: formatKeyHints(this.ctx.keybindings.getKeys(action.id as AppKeybinding)) || undefined,
818
+ keybinding: this.ctx.keybindings.getDisplayString(action.id as AppKeybinding) || undefined,
842
819
  searchText: action.id,
843
820
  handler: action.handler,
844
821
  })),
@@ -1108,12 +1085,12 @@ export class SelectorController {
1108
1085
 
1109
1086
  async #handleImageGenerationConfig(): Promise<void> {
1110
1087
  const provider = await this.ctx.showHookInput(
1111
- "Image Generation provider (auto, openai, gemini, openrouter, antigravity, custom)",
1088
+ "Image Generation provider (auto, openai, gemini, openrouter, antigravity, alibaba, custom)",
1112
1089
  "auto",
1113
1090
  );
1114
1091
  if (provider === undefined) return;
1115
1092
  const normalized = provider.trim().toLowerCase();
1116
- const validProviders = ["auto", "openai", "gemini", "openrouter", "antigravity", "custom"];
1093
+ const validProviders = ["auto", "openai", "gemini", "openrouter", "antigravity", "alibaba", "custom"];
1117
1094
  if (!validProviders.includes(normalized)) {
1118
1095
  this.ctx.showStatus(`Invalid image provider: ${normalized}. Valid: ${validProviders.join(", ")}`);
1119
1096
  return;
@@ -1145,7 +1122,14 @@ export class SelectorController {
1145
1122
  if (scope === undefined) return;
1146
1123
  const persistDefault = scope.trim().toLowerCase() === "default";
1147
1124
 
1148
- const imageProvider = normalized as "auto" | "openai" | "gemini" | "openrouter" | "antigravity" | "custom";
1125
+ const imageProvider = normalized as
1126
+ | "auto"
1127
+ | "openai"
1128
+ | "gemini"
1129
+ | "openrouter"
1130
+ | "antigravity"
1131
+ | "alibaba"
1132
+ | "custom";
1149
1133
  setPreferredImageProvider(imageProvider === "custom" ? "auto" : imageProvider);
1150
1134
  setConfiguredImageModel({
1151
1135
  provider: imageProvider,
@@ -1644,6 +1628,7 @@ export class SelectorController {
1644
1628
  imgProvider === "gemini" ||
1645
1629
  imgProvider === "openrouter" ||
1646
1630
  imgProvider === "antigravity" ||
1631
+ imgProvider === "alibaba" ||
1647
1632
  imgProvider === "custom"
1648
1633
  ) {
1649
1634
  setPreferredImageProvider(imgProvider === "custom" ? "auto" : imgProvider);
@@ -1694,6 +1679,14 @@ export class SelectorController {
1694
1679
  showModelSelector(options?: { temporaryOnly?: boolean }): void {
1695
1680
  this.showSelector(done => {
1696
1681
  let modelSelector: ModelSelectorComponent;
1682
+ const refreshRoleAssignments = () => {
1683
+ modelSelector.refreshRoleAssignments({
1684
+ currentModel: this.ctx.session.model,
1685
+ currentThinkingLevel: this.ctx.session.thinkingLevel,
1686
+ activeModelProfile:
1687
+ this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
1688
+ });
1689
+ };
1697
1690
  modelSelector = new ModelSelectorComponent(
1698
1691
  this.ctx.ui,
1699
1692
  this.ctx.session.model,
@@ -1701,6 +1694,8 @@ export class SelectorController {
1701
1694
  this.ctx.session.modelRegistry,
1702
1695
  this.ctx.session.scopedModels,
1703
1696
  async selection => {
1697
+ const isTrackedSingleAssignment =
1698
+ selection.kind === "assignment" && selection.role !== null && selection.roles === undefined;
1704
1699
  try {
1705
1700
  if (selection.kind === "createProfile") {
1706
1701
  done();
@@ -1741,7 +1736,7 @@ export class SelectorController {
1741
1736
  this.ctx.showStatus(`Temporary model: ${selectedSelector ?? model.id}`);
1742
1737
  done();
1743
1738
  this.ctx.ui.requestRender();
1744
- } else if (selection.roles) {
1739
+ } else if (selection.roles !== undefined) {
1745
1740
  const targetRoles: readonly GjcModelAssignmentTargetId[] = selection.roles;
1746
1741
  const includesDefault = targetRoles.includes("default");
1747
1742
  const includesRoleAgent = targetRoles.some(targetRole => targetRole !== "default");
@@ -1819,25 +1814,23 @@ export class SelectorController {
1819
1814
  selectedSelector ?? `${model.provider}/${model.id}`,
1820
1815
  thinkingLevel,
1821
1816
  );
1822
- materializeActiveModelProfileAssignment({
1823
- session: this.ctx.session,
1824
- settings: this.ctx.settings,
1825
- role,
1826
- selector: value,
1827
- });
1817
+ if (
1818
+ !materializeActiveModelProfileAssignment({
1819
+ session: this.ctx.session,
1820
+ settings: this.ctx.settings,
1821
+ role,
1822
+ selector: value,
1823
+ })
1824
+ ) {
1825
+ this.ctx.settings.setModelRole(role, value);
1826
+ }
1828
1827
  if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
1829
1828
  this.ctx.session.setThinkingLevel(thinkingLevel);
1830
1829
  }
1831
- modelSelector.refreshRoleAssignments({
1832
- currentModel: this.ctx.session.model,
1833
- currentThinkingLevel: this.ctx.session.thinkingLevel,
1834
- activeModelProfile:
1835
- this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
1836
- });
1830
+ refreshRoleAssignments();
1837
1831
  this.ctx.statusLine.invalidate();
1838
1832
  this.ctx.updateEditorBorderColor();
1839
1833
  this.ctx.showStatus(`Default model: ${selectedSelector ?? model.id}`);
1840
- done();
1841
1834
  this.ctx.ui.requestRender();
1842
1835
  } else {
1843
1836
  const apiKey = await this.ctx.session.modelRegistry.getApiKey(model, this.ctx.session.sessionId);
@@ -1860,22 +1853,21 @@ export class SelectorController {
1860
1853
  this.ctx.settings.setAgentModelOverride(role, value);
1861
1854
  }
1862
1855
  }
1863
- modelSelector.refreshRoleAssignments({
1864
- currentModel: this.ctx.session.model,
1865
- currentThinkingLevel: this.ctx.session.thinkingLevel,
1866
- activeModelProfile:
1867
- this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
1868
- });
1856
+ refreshRoleAssignments();
1869
1857
  this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
1870
1858
  this.ctx.statusLine.invalidate();
1871
1859
  this.ctx.updateEditorBorderColor();
1872
1860
  await this.ctx.notifyConfigChanged?.();
1873
1861
  this.ctx.showStatus(`${role} agent model: ${value}`);
1874
- done();
1875
1862
  this.ctx.ui.requestRender();
1876
1863
  }
1877
1864
  } catch (error) {
1878
1865
  this.ctx.showError(error instanceof Error ? error.message : String(error));
1866
+ if (isTrackedSingleAssignment) {
1867
+ refreshRoleAssignments();
1868
+ this.ctx.ui.requestRender();
1869
+ throw error;
1870
+ }
1879
1871
  }
1880
1872
  },
1881
1873
  () => {
@@ -16,7 +16,13 @@ import {
16
16
  import { APP_NAME, adjustHsv, getProjectDir, logger, postmortem } from "@gajae-code/utils";
17
17
  import chalk from "chalk";
18
18
  import { AsyncJobManager } from "../async";
19
- import { type AppKeybinding, KeybindingsManager } from "../config/keybindings";
19
+ import {
20
+ type AppKeybinding,
21
+ formatKeyHint,
22
+ formatKeyHints,
23
+ KeybindingsManager,
24
+ type KeyDisplayContext,
25
+ } from "../config/keybindings";
20
26
  import { isSettingsInitialized, type Settings, settings } from "../config/settings";
21
27
  import { DEFAULT_GJC_DEFINITION_NAMES } from "../defaults/gjc-defaults";
22
28
  import type {
@@ -64,6 +70,7 @@ import type { ToolExecutionHandle } from "./components/tool-execution";
64
70
  import { StatusLineComponent } from "./components/tool-status-header";
65
71
  import { composeToolText } from "./components/tool-transcript-format";
66
72
  import {
73
+ type RecentSession,
67
74
  WelcomeComponent,
68
75
  type WelcomeLogoMode,
69
76
  type LspServerInfo as WelcomeLspServerInfo,
@@ -83,6 +90,7 @@ import { TodoCommandController } from "./controllers/todo-command-controller";
83
90
  import { IrcObservationLedger } from "./irc-observation-ledger";
84
91
  import { JobsObserver } from "./jobs-observer";
85
92
  import { OAuthManualInputManager } from "./oauth-manual-input";
93
+ import { PromptSuggestionController } from "./prompt-suggestion-controller";
86
94
  import { SessionObserverRegistry } from "./session-observer-registry";
87
95
  import { interruptHint } from "./shared";
88
96
  import { shouldShowExtensionCommand } from "./slash-command-visibility";
@@ -105,8 +113,15 @@ import {
105
113
  import type { ParsedIrcMessage } from "./utils/irc-message";
106
114
  import { addChatChild, prepareTranscriptRebuild, UiHelpers } from "./utils/ui-helpers";
107
115
 
108
- const COMPOSER_NEWLINE_HINT = process.platform === "win32" ? "Alt+Enter/Ctrl+J" : "Shift+Enter/Ctrl+J";
109
- export const DEFAULT_COMPOSER_PLACEHOLDER = `Type your message... ${COMPOSER_NEWLINE_HINT}: New line · Ctrl+C: Clear · Ctrl+R: Search history · Shift+Tab: Reasoning`;
116
+ export function getDefaultComposerPlaceholder(context: KeyDisplayContext = { platform: process.platform }): string {
117
+ const newlineKeys = context.platform === "win32" ? ["alt+enter", "ctrl+j"] : ["shift+enter", "ctrl+j"];
118
+ return `Type your message... ${formatKeyHints(newlineKeys, context)}: New line · ${formatKeyHint(
119
+ "ctrl+c",
120
+ context,
121
+ )}: Clear · ${formatKeyHint("ctrl+r", context)}: Search history · ${formatKeyHint("shift+tab", context)}: Reasoning`;
122
+ }
123
+
124
+ export const DEFAULT_COMPOSER_PLACEHOLDER = getDefaultComposerPlaceholder();
110
125
  const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
111
126
 
112
127
  const IRC_SIDEBAR_TOGGLE_SHADOWING_ACTIONS: readonly AppKeybinding[] = [
@@ -128,23 +143,6 @@ export function getWelcomeTranscriptReservedRows(chatContainer: Container, width
128
143
  ? 0
129
144
  : chatContainer.render(width).length;
130
145
  }
131
- const FRIENDLY_KEY_PARTS: Record<string, string> = {
132
- alt: "Alt",
133
- cmd: "Cmd",
134
- command: "Cmd",
135
- ctrl: "Ctrl",
136
- enter: "Enter",
137
- meta: process.platform === "darwin" ? "Command" : "Meta",
138
- option: "Option",
139
- shift: "Shift",
140
- };
141
-
142
- function formatShortcutForPlaceholder(key: string): string {
143
- return key
144
- .split("+")
145
- .map(part => FRIENDLY_KEY_PARTS[part.toLowerCase()] ?? (part.length === 1 ? part.toUpperCase() : part))
146
- .join("+");
147
- }
148
146
 
149
147
  const HINT_SHIMMER_PALETTE: ShimmerPalette = {
150
148
  low: "dim",
@@ -169,7 +167,7 @@ function configureDefaultComposerChrome(editor: CustomEditor): void {
169
167
  editor.setClosedBorderBox(true);
170
168
  editor.setPromptGutter(undefined);
171
169
  editor.setInputPrefix(getDefaultInputPrefix());
172
- editor.setPlaceholder(DEFAULT_COMPOSER_PLACEHOLDER);
170
+ editor.setPlaceholder(getDefaultComposerPlaceholder());
173
171
  editor.setPaddingX(1);
174
172
  editor.setRightGutterWidth(1);
175
173
  editor.setTopBorder(undefined);
@@ -340,6 +338,7 @@ export class InteractiveMode implements InteractiveModeContext {
340
338
  fileSlashCommands: Set<string> = new Set();
341
339
  skillCommands: Map<string, Skill> = new Map();
342
340
  oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
341
+ promptSuggestion: PromptSuggestionController;
343
342
 
344
343
  #baseSlashCommands: SlashCommand[] = [];
345
344
  #resolvedSlashCommands: SlashCommand[] = [];
@@ -351,6 +350,7 @@ export class InteractiveMode implements InteractiveModeContext {
351
350
  #petUnavailableWarningDisposer?: () => void;
352
351
  readonly #version: string;
353
352
  readonly #changelogMarkdown: string | undefined;
353
+ readonly #keyDisplayContext: KeyDisplayContext;
354
354
 
355
355
  readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
356
356
  mcpManager?: import("../runtime-mcp").MCPManager;
@@ -398,6 +398,7 @@ export class InteractiveMode implements InteractiveModeContext {
398
398
  lspServers: LspStartupServerInfo[] | undefined = undefined,
399
399
  mcpManager?: import("../runtime-mcp").MCPManager,
400
400
  eventBus?: EventBus,
401
+ keyDisplayContext: KeyDisplayContext = { platform: process.platform },
401
402
  ) {
402
403
  this.session = session;
403
404
  this.sessionManager = session.sessionManager;
@@ -436,6 +437,8 @@ export class InteractiveMode implements InteractiveModeContext {
436
437
  this.lspServers = lspServers;
437
438
  this.mcpManager = mcpManager;
438
439
  this.#eventBus = eventBus;
440
+ this.#keyDisplayContext = keyDisplayContext;
441
+ this.keybindings.setDisplayContext(this.#keyDisplayContext);
439
442
  const thisMode = this;
440
443
  this.#goalModeController = new GoalModeController({
441
444
  session: this.session,
@@ -517,6 +520,7 @@ export class InteractiveMode implements InteractiveModeContext {
517
520
  this.btwContainer = new Container();
518
521
  this.editor = new CustomEditor(getEditorTheme());
519
522
  configureDefaultComposerChrome(this.editor);
523
+ this.editor.setPlaceholder(this.#getComposerPlaceholder());
520
524
  this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
521
525
  this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
522
526
  this.editor.onAutocompleteCancel = () => {
@@ -544,7 +548,11 @@ export class InteractiveMode implements InteractiveModeContext {
544
548
  this.hookWidgetContainerBelow = new Container();
545
549
  this.editorContainer = new Container();
546
550
  this.editorContainer.addChild(this.editor);
547
- this.statusLine = new StatusLineComponent(session, { version: this.#version, focusDomain: "composer" });
551
+ this.statusLine = new StatusLineComponent(session, {
552
+ version: this.#version,
553
+ focusDomain: "composer",
554
+ keyDisplayContext: this.#keyDisplayContext,
555
+ });
548
556
  this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
549
557
 
550
558
  this.hideThinkingBlock = settings.get("hideThinkingBlock");
@@ -580,6 +588,7 @@ export class InteractiveMode implements InteractiveModeContext {
580
588
  this.#selectorController = new SelectorController(this);
581
589
  this.#inputController = new InputController(this);
582
590
  this.statusLine.setActionRegistry(this.#inputController.actionRegistry, () => this.keybindings);
591
+ this.promptSuggestion = new PromptSuggestionController(this);
583
592
  this.#observerRegistry = new SessionObserverRegistry();
584
593
  }
585
594
 
@@ -600,6 +609,7 @@ export class InteractiveMode implements InteractiveModeContext {
600
609
  if (this.isInitialized) return;
601
610
 
602
611
  this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
612
+ this.keybindings.setDisplayContext(this.#keyDisplayContext);
603
613
 
604
614
  // Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
605
615
  this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush());
@@ -622,15 +632,9 @@ export class InteractiveMode implements InteractiveModeContext {
622
632
  const modelName = this.session.model?.name ?? "Unknown";
623
633
  const providerName = this.session.model?.provider ?? "Unknown";
624
634
 
625
- // Get recent sessions
626
- const recentSessions = await logger.time("InteractiveMode.init:recentSessions", () =>
627
- getRecentSessions(this.sessionManager.getSessionDir()).then(sessions =>
628
- sessions.map(s => ({
629
- name: s.name,
630
- timeAgo: s.timeAgo,
631
- })),
632
- ),
633
- );
635
+ // Session history is display-only. Never hold the editor and keyboard behind
636
+ // transcript I/O; populate the welcome trail after the interactive surface exists.
637
+ const recentSessions: RecentSession[] = [];
634
638
 
635
639
  const startupQuiet = settings.get("startup.quiet");
636
640
  const welcomeLogoMode = resolveWelcomeLogoMode(settings.get("startup.welcomeBannerMode"));
@@ -657,6 +661,7 @@ export class InteractiveMode implements InteractiveModeContext {
657
661
  getReservedBottomRows: getWelcomeReservedBottomRows,
658
662
  changelogMarkdown: this.#changelogMarkdown,
659
663
  collapseChangelog: settings.get("collapseChangelog"),
664
+ keyDisplayContext: this.#keyDisplayContext,
660
665
  },
661
666
  );
662
667
 
@@ -753,6 +758,31 @@ export class InteractiveMode implements InteractiveModeContext {
753
758
  if (this.settings.get("tasksPane.defaultVisible")) this.showTasksPane();
754
759
  this.#syncIrcSidebarAvailabilityFromSettings();
755
760
  this.ui.requestRender(true);
761
+ if (this.#welcomeComponent) {
762
+ const welcomeComponent = this.#welcomeComponent;
763
+ const recentSessionsTimer = setTimeout(() => {
764
+ void logger
765
+ .time("InteractiveMode.init:recentSessions", () =>
766
+ getRecentSessions(this.sessionManager.getSessionDir()),
767
+ )
768
+ .then(sessions => {
769
+ if (this.#welcomeComponent !== welcomeComponent) return;
770
+ welcomeComponent.setRecentSessions(
771
+ sessions.map(session => ({
772
+ name: session.name,
773
+ timeAgo: session.timeAgo,
774
+ })),
775
+ );
776
+ this.ui.requestRender();
777
+ })
778
+ .catch(error => {
779
+ logger.debug("Failed to load recent sessions for welcome screen", {
780
+ error: error instanceof Error ? error.message : String(error),
781
+ });
782
+ });
783
+ }, 0);
784
+ recentSessionsTimer.unref?.();
785
+ }
756
786
 
757
787
  // GitHub star reminder (interactive-only). Register the decline-driven
758
788
  // injection contributor and schedule the launch nudge after the first
@@ -807,6 +837,7 @@ export class InteractiveMode implements InteractiveModeContext {
807
837
  onThemeChange(() => {
808
838
  clearRenderCache();
809
839
  configureDefaultComposerChrome(this.editor);
840
+ this.editor.setPlaceholder(this.#getComposerPlaceholder());
810
841
  this.ui.invalidate();
811
842
  this.updateEditorChrome();
812
843
  this.ui.requestRender();
@@ -1038,12 +1069,14 @@ export class InteractiveMode implements InteractiveModeContext {
1038
1069
  }
1039
1070
 
1040
1071
  #getComposerPlaceholder(): string {
1041
- if (!this.#isPromptDeliveryBusy()) return DEFAULT_COMPOSER_PLACEHOLDER;
1042
- const enterAction = this.settings.get("busyPromptMode") === "steer" ? "Steer" : "Queue";
1043
- const parts = [`Enter: ${enterAction}`];
1072
+ const defaultPlaceholder = getDefaultComposerPlaceholder(this.#keyDisplayContext);
1073
+ if (!this.#isPromptDeliveryBusy()) return defaultPlaceholder;
1074
+ const submitAction = this.settings.get("busyPromptMode") === "steer" ? "Steer" : "Queue";
1075
+ const submitKey = this.keybindings.getDisplayString("tui.input.submit", this.#keyDisplayContext);
1076
+ const parts = submitKey ? [`${submitKey}: ${submitAction}`] : [];
1044
1077
  const queueKey = this.#getMessageQueueShortcut();
1045
- if (queueKey) parts.push(`${formatShortcutForPlaceholder(queueKey)}: Queue`);
1046
- return `${DEFAULT_COMPOSER_PLACEHOLDER} · ${parts.join(" · ")}`;
1078
+ if (queueKey) parts.push(`${formatKeyHint(queueKey, this.#keyDisplayContext)}: Queue`);
1079
+ return parts.length > 0 ? `${defaultPlaceholder} · ${parts.join(" · ")}` : defaultPlaceholder;
1047
1080
  }
1048
1081
 
1049
1082
  #getWelcomeReservedRows(width: number): number {
@@ -1398,6 +1431,7 @@ export class InteractiveMode implements InteractiveModeContext {
1398
1431
  : new CustomEditor(getEditorTheme());
1399
1432
 
1400
1433
  configureDefaultComposerChrome(nextEditor);
1434
+ nextEditor.setPlaceholder(this.#getComposerPlaceholder());
1401
1435
  nextEditor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
1402
1436
  nextEditor.setAutocompleteMaxVisible(this.settings.get("autocompleteMaxVisible"));
1403
1437
  nextEditor.onAutocompleteCancel = () => {
@@ -3,12 +3,10 @@ import {
3
3
  type AutocompleteProvider,
4
4
  CombinedAutocompleteProvider,
5
5
  extractSlashCommandTokenPrefix,
6
- getKeybindings,
7
6
  getSlashCommandMatchRank,
8
7
  type SlashCommand,
9
8
  } from "@gajae-code/tui";
10
9
  import type { KeybindingsManager } from "../config/keybindings";
11
- import { formatKeyHints } from "../config/keybindings";
12
10
  import { isSettingsInitialized, settings } from "../config/settings";
13
11
  import { applyEmojiCompletion, getEmojiSuggestions, isEmojiPrefix, tryEmojiInlineReplace } from "./emoji-autocomplete";
14
12
 
@@ -44,6 +42,11 @@ interface PromptActionAutocompleteOptions {
44
42
  moveCursorToMessageStart: () => void;
45
43
  moveCursorToLineStart: () => void;
46
44
  moveCursorToLineEnd: () => void;
45
+ /**
46
+ * Ghost-text next-prompt prediction shown in the empty composer as a dim
47
+ * inline hint (Tab accepts it via the editor's onTab handler).
48
+ */
49
+ getPromptSuggestion?: () => string | null;
47
50
  }
48
51
 
49
52
  function fuzzyMatch(query: string, target: string): boolean {
@@ -186,11 +189,18 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
186
189
  #baseProvider: CombinedAutocompleteProvider;
187
190
  #actions: PromptActionDefinition[];
188
191
  #commands: SlashCommand[];
189
-
190
- constructor(commands: SlashCommand[], basePath: string, actions: PromptActionDefinition[]) {
192
+ #getPromptSuggestion: (() => string | null) | undefined;
193
+
194
+ constructor(
195
+ commands: SlashCommand[],
196
+ basePath: string,
197
+ actions: PromptActionDefinition[],
198
+ getPromptSuggestion?: () => string | null,
199
+ ) {
191
200
  this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath);
192
201
  this.#actions = actions;
193
202
  this.#commands = commands;
203
+ this.#getPromptSuggestion = getPromptSuggestion;
194
204
  }
195
205
 
196
206
  async getSuggestions(
@@ -309,6 +319,11 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
309
319
  }
310
320
 
311
321
  getInlineHint(lines: string[], cursorLine: number, cursorCol: number): string | null {
322
+ // An empty composer renders the pending prompt suggestion as ghost text.
323
+ if (lines.every(line => line === "")) {
324
+ const suggestion = this.#getPromptSuggestion?.();
325
+ if (suggestion) return suggestion;
326
+ }
312
327
  return this.#baseProvider.getInlineHint?.(lines, cursorLine, cursorCol) ?? null;
313
328
  }
314
329
  trySyncSlashCompletion(textBeforeCursor: string): { items: AutocompleteItem[]; prefix: string } | null {
@@ -382,12 +397,11 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
382
397
  export function createPromptActionAutocompleteProvider(
383
398
  options: PromptActionAutocompleteOptions,
384
399
  ): PromptActionAutocompleteProvider {
385
- const editorKeybindings = getKeybindings();
386
400
  const actions: PromptActionDefinition[] = [
387
401
  {
388
402
  id: "new-session",
389
403
  label: "Start new session",
390
- description: formatKeyHints(options.keybindings.getKeys("app.session.new")) || "/new",
404
+ description: options.keybindings.getDisplayString("app.session.new"),
391
405
  keywords: ["new", "session", "fresh", "clear", "start", "conversation"],
392
406
  execute: options.newSession,
393
407
  },
@@ -401,21 +415,21 @@ export function createPromptActionAutocompleteProvider(
401
415
  {
402
416
  id: "copy-line",
403
417
  label: "Copy current line",
404
- description: formatKeyHints(options.keybindings.getKeys("app.clipboard.copyLine")),
418
+ description: options.keybindings.getDisplayString("app.clipboard.copyLine"),
405
419
  keywords: ["copy", "line", "clipboard", "current"],
406
420
  execute: options.copyCurrentLine,
407
421
  },
408
422
  {
409
423
  id: "copy-prompt",
410
424
  label: "Copy whole prompt",
411
- description: formatKeyHints(options.keybindings.getKeys("app.clipboard.copyPrompt")),
425
+ description: options.keybindings.getDisplayString("app.clipboard.copyPrompt"),
412
426
  keywords: ["copy", "prompt", "clipboard", "message"],
413
427
  execute: options.copyPrompt,
414
428
  },
415
429
  {
416
430
  id: "paste-image",
417
431
  label: "Paste image from clipboard",
418
- description: formatKeyHints(options.keybindings.getKeys("app.clipboard.pasteImage")),
432
+ description: options.keybindings.getDisplayString("app.clipboard.pasteImage"),
419
433
  keywords: ["paste", "image", "clipboard", "screenshot", "attach", "vision"],
420
434
  execute: options.pasteImage,
421
435
  },
@@ -429,7 +443,7 @@ export function createPromptActionAutocompleteProvider(
429
443
  {
430
444
  id: "undo",
431
445
  label: "Undo",
432
- description: formatKeyHints(editorKeybindings.getKeys("tui.editor.undo")),
446
+ description: options.keybindings.getDisplayString("tui.editor.undo"),
433
447
  keywords: ["undo", "revert", "edit", "history"],
434
448
  execute: options.undo,
435
449
  },
@@ -450,18 +464,23 @@ export function createPromptActionAutocompleteProvider(
450
464
  {
451
465
  id: "cursor-line-start",
452
466
  label: "Move cursor to beginning of line",
453
- description: formatKeyHints(editorKeybindings.getKeys("tui.editor.cursorLineStart")),
467
+ description: options.keybindings.getDisplayString("tui.editor.cursorLineStart"),
454
468
  keywords: ["move", "cursor", "line", "start", "beginning", "home"],
455
469
  execute: options.moveCursorToLineStart,
456
470
  },
457
471
  {
458
472
  id: "cursor-line-end",
459
473
  label: "Move cursor to end of line",
460
- description: formatKeyHints(editorKeybindings.getKeys("tui.editor.cursorLineEnd")),
474
+ description: options.keybindings.getDisplayString("tui.editor.cursorLineEnd"),
461
475
  keywords: ["move", "cursor", "line", "end"],
462
476
  execute: options.moveCursorToLineEnd,
463
477
  },
464
478
  ];
465
479
 
466
- return new PromptActionAutocompleteProvider(options.commands, options.basePath, actions);
480
+ return new PromptActionAutocompleteProvider(
481
+ options.commands,
482
+ options.basePath,
483
+ actions,
484
+ options.getPromptSuggestion,
485
+ );
467
486
  }
@@ -0,0 +1,96 @@
1
+ import { logger } from "@gajae-code/utils";
2
+ import { generatePromptSuggestion } from "../utils/prompt-suggestion";
3
+ import type { InteractiveModeContext } from "./types";
4
+
5
+ /**
6
+ * Owns the ghost-text next-prompt prediction lifecycle for interactive mode.
7
+ *
8
+ * After each agent turn ends (and only when `promptSuggestions` is enabled
9
+ * and the composer is empty), a smol-model prediction of the user's next
10
+ * prompt is generated in the background. The result renders as dim ghost
11
+ * text in the empty composer via the autocomplete provider's inline hint;
12
+ * Tab accepts it, typing dismisses it, and a new turn clears it.
13
+ */
14
+ export class PromptSuggestionController {
15
+ #ctx: InteractiveModeContext;
16
+ #current: string | null = null;
17
+ /** Monotonic token; bumping it invalidates any in-flight generation. */
18
+ #generation = 0;
19
+
20
+ constructor(ctx: InteractiveModeContext) {
21
+ this.#ctx = ctx;
22
+ }
23
+
24
+ /** Current suggestion to render as ghost text, or null. */
25
+ get current(): string | null {
26
+ return this.#current;
27
+ }
28
+
29
+ /** Drop the current suggestion and invalidate any in-flight generation. */
30
+ clear(): void {
31
+ this.#generation++;
32
+ if (this.#current === null) return;
33
+ this.#current = null;
34
+ this.#ctx.ui.requestRender();
35
+ }
36
+
37
+ /** A new agent turn started; any pending suggestion is stale. */
38
+ onAgentStart(): void {
39
+ this.clear();
40
+ }
41
+
42
+ /**
43
+ * Editor content changed. A non-empty composer means the user is writing
44
+ * their own prompt: dismiss the suggestion and cancel pending generation.
45
+ */
46
+ notifyEditorChanged(text: string): void {
47
+ if (text.length > 0) this.clear();
48
+ }
49
+
50
+ /**
51
+ * Agent turn ended; kick off background generation when the feature is
52
+ * enabled and the composer is idle and empty.
53
+ */
54
+ onAgentEnd(): void {
55
+ if (!this.#ctx.settings.get("promptSuggestions")) return;
56
+ if (this.#ctx.shutdownRequested) return;
57
+ if (this.#ctx.session.isStreaming) return;
58
+ if (this.#ctx.editor.getText().trim() !== "") return;
59
+
60
+ const generation = ++this.#generation;
61
+ void generatePromptSuggestion(
62
+ this.#ctx.session.messages,
63
+ this.#ctx.session.modelRegistry,
64
+ this.#ctx.settings,
65
+ this.#ctx.session.sessionId,
66
+ this.#ctx.session.model,
67
+ provider => this.#ctx.session.agent.metadataForProvider(provider),
68
+ )
69
+ .then(suggestion => {
70
+ if (generation !== this.#generation) return;
71
+ if (!suggestion) return;
72
+ // The world may have moved on while the model was thinking.
73
+ if (this.#ctx.session.isStreaming) return;
74
+ if (this.#ctx.editor.getText().trim() !== "") return;
75
+ this.#current = suggestion;
76
+ this.#ctx.ui.requestRender();
77
+ })
78
+ .catch(error => {
79
+ logger.debug("prompt-suggestion: generation failed", { error: String(error) });
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Tab handler for the composer: accept the suggestion when the composer
85
+ * is empty. Returns true when Tab was consumed.
86
+ */
87
+ tryAcceptOnTab(text: string): boolean {
88
+ const suggestion = this.#current;
89
+ if (!suggestion || text.trim() !== "") return false;
90
+ // setText fires onChange with non-empty text, which clears #current.
91
+ this.#ctx.editor.setText(suggestion);
92
+ this.clear();
93
+ this.#ctx.ui.requestRender();
94
+ return true;
95
+ }
96
+ }