@gajae-code/coding-agent 0.11.7 → 0.11.8

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 (180) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/types/cli/args.d.ts +0 -2
  3. package/dist/types/config/keybindings.d.ts +33 -3
  4. package/dist/types/config/model-profile-activation.d.ts +4 -3
  5. package/dist/types/config/model-profile-contract.d.ts +38 -0
  6. package/dist/types/config/settings-schema.d.ts +15 -2
  7. package/dist/types/edit/modes/patch.d.ts +13 -0
  8. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  9. package/dist/types/eval/py/executor.d.ts +1 -1
  10. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  11. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  12. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
  13. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
  14. package/dist/types/main.d.ts +8 -0
  15. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  16. package/dist/types/modes/components/tool-status-header.d.ts +3 -2
  17. package/dist/types/modes/components/welcome.d.ts +2 -0
  18. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  19. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  20. package/dist/types/modes/interactive-mode.d.ts +5 -2
  21. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  22. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  23. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  24. package/dist/types/modes/types.d.ts +3 -0
  25. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  26. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  27. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  28. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  29. package/dist/types/sdk/broker/broker.d.ts +2 -0
  30. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  31. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  32. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  33. package/dist/types/sdk/bus/config.d.ts +2 -0
  34. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  35. package/dist/types/sdk/bus/index.d.ts +25 -0
  36. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  37. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  38. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
  39. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
  41. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  42. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  43. package/dist/types/sdk/host/host.d.ts +1 -1
  44. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  45. package/dist/types/sdk/index.d.ts +2 -0
  46. package/dist/types/sdk/prompt-status.d.ts +88 -0
  47. package/dist/types/sdk/session-directory.d.ts +1 -1
  48. package/dist/types/sdk/startup-capability.d.ts +3 -0
  49. package/dist/types/session/agent-session.d.ts +2 -0
  50. package/dist/types/session/internal/managed-session-scope.d.ts +11 -2
  51. package/dist/types/session/internal/managed-session-storage.d.ts +17 -2
  52. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  53. package/dist/types/session/session-storage.d.ts +18 -0
  54. package/dist/types/task/index.d.ts +3 -1
  55. package/dist/types/task/receipt.d.ts +2 -0
  56. package/dist/types/task/types.d.ts +94 -0
  57. package/dist/types/tools/ask.d.ts +454 -174
  58. package/dist/types/tools/image-gen.d.ts +47 -1
  59. package/dist/types/tools/index.d.ts +4 -0
  60. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  61. package/package.json +8 -8
  62. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  63. package/scripts/dogfood-repository-binding.ts +209 -0
  64. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  65. package/scripts/run-test-manifest.ts +4 -4
  66. package/src/cli/args.ts +2 -92
  67. package/src/cli/notify-cli.ts +5 -38
  68. package/src/cli.ts +18 -0
  69. package/src/commands/sdk.ts +2 -2
  70. package/src/config/keybindings.ts +157 -24
  71. package/src/config/model-profile-activation.ts +7 -18
  72. package/src/config/model-profile-contract.ts +161 -0
  73. package/src/config/model-registry.ts +11 -1
  74. package/src/config/settings-schema.ts +17 -2
  75. package/src/coordinator-mcp/model-preset.ts +21 -74
  76. package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
  77. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  78. package/src/edit/index.ts +3 -6
  79. package/src/edit/modes/patch.ts +38 -1
  80. package/src/edit/modes/replace.ts +42 -0
  81. package/src/edit/path-mutation-lock.ts +112 -0
  82. package/src/eval/py/executor.ts +30 -11
  83. package/src/eval/py/prelude.py +2 -2
  84. package/src/eval/py/tool-bridge.ts +34 -18
  85. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  86. package/src/gjc-runtime/launch-tmux.ts +1 -1
  87. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  88. package/src/gjc-runtime/ralplan-runtime.ts +75 -3
  89. package/src/gjc-runtime/repository-binding.ts +267 -0
  90. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  91. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  92. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  93. package/src/gjc-runtime/ultragoal-guard.ts +5 -5
  94. package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
  95. package/src/internal-urls/docs-index.generated.ts +6 -5
  96. package/src/internal-urls/local-protocol.ts +33 -16
  97. package/src/internal-urls/mcp-protocol.ts +73 -11
  98. package/src/main.ts +25 -10
  99. package/src/modes/DESIGN.md +5 -0
  100. package/src/modes/acp/acp-agent.ts +50 -16
  101. package/src/modes/components/model-selector.ts +42 -8
  102. package/src/modes/components/plan-preview-overlay.ts +1 -1
  103. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  104. package/src/modes/components/tool-status-header.ts +20 -4
  105. package/src/modes/components/welcome.ts +22 -18
  106. package/src/modes/controllers/command-controller.ts +97 -54
  107. package/src/modes/controllers/event-controller.ts +10 -0
  108. package/src/modes/controllers/input-controller.ts +7 -2
  109. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  110. package/src/modes/controllers/selector-controller.ts +46 -54
  111. package/src/modes/interactive-mode.ts +41 -27
  112. package/src/modes/prompt-action-autocomplete.ts +32 -13
  113. package/src/modes/prompt-suggestion-controller.ts +96 -0
  114. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  115. package/src/modes/types.ts +3 -0
  116. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  117. package/src/modes/utils/ui-helpers.ts +5 -3
  118. package/src/prompts/agents/architect.md +0 -1
  119. package/src/prompts/agents/critic.md +0 -1
  120. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  121. package/src/prompts/system/system-prompt.md +6 -4
  122. package/src/prompts/tools/cron.md +3 -1
  123. package/src/runtime-mcp/manager.ts +51 -53
  124. package/src/runtime-mcp/oauth-flow.ts +0 -72
  125. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  126. package/src/runtime-mcp/transports/http.ts +13 -6
  127. package/src/sdk/broker/broker.ts +34 -20
  128. package/src/sdk/broker/lifecycle.ts +185 -36
  129. package/src/sdk/bus/chat-daemon-control.ts +18 -5
  130. package/src/sdk/bus/config-commands.ts +2 -2
  131. package/src/sdk/bus/config.ts +9 -1
  132. package/src/sdk/bus/conversation-store.ts +40 -6
  133. package/src/sdk/bus/index.ts +240 -65
  134. package/src/sdk/bus/lifecycle-commands.ts +47 -3
  135. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  136. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  137. package/src/sdk/bus/notification-orchestration.ts +12 -0
  138. package/src/sdk/bus/notification-service.ts +27 -2
  139. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  140. package/src/sdk/bus/recent-activity.ts +90 -26
  141. package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
  142. package/src/sdk/bus/telegram-daemon-control.ts +145 -33
  143. package/src/sdk/bus/telegram-daemon.ts +835 -110
  144. package/src/sdk/bus/telegram-reference.ts +20 -2
  145. package/src/sdk/cli/session-cli.ts +15 -2
  146. package/src/sdk/host/control/dispatch.ts +1 -1
  147. package/src/sdk/host/control/operations.ts +1 -1
  148. package/src/sdk/host/host.ts +2 -6
  149. package/src/sdk/host/query/handlers.ts +70 -5
  150. package/src/sdk/index.ts +12 -0
  151. package/src/sdk/prompt-status.ts +97 -0
  152. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  153. package/src/sdk/protocol/operation-registry.ts +18 -4
  154. package/src/sdk/session-directory.ts +5 -1
  155. package/src/sdk/session.ts +2 -0
  156. package/src/sdk/startup-capability.ts +9 -1
  157. package/src/session/agent-session.ts +52 -5
  158. package/src/session/blob-store.ts +7 -2
  159. package/src/session/internal/managed-session-scope.ts +854 -84
  160. package/src/session/internal/managed-session-storage.ts +251 -199
  161. package/src/session/internal/native-publish-outcome.ts +266 -0
  162. package/src/session/session-manager.ts +310 -327
  163. package/src/session/session-storage.ts +128 -11
  164. package/src/task/gjc-command.ts +2 -2
  165. package/src/task/index.ts +121 -15
  166. package/src/task/receipt.ts +3 -0
  167. package/src/task/types.ts +34 -3
  168. package/src/tools/ask.ts +198 -88
  169. package/src/tools/ast-edit.ts +2 -2
  170. package/src/tools/bash-pty-selection.ts +2 -2
  171. package/src/tools/browser/screenshot-format.ts +6 -1
  172. package/src/tools/cron.ts +1 -1
  173. package/src/tools/image-gen.ts +178 -6
  174. package/src/tools/index.ts +4 -0
  175. package/src/tools/render-utils.ts +7 -1
  176. package/src/utils/edit-mode.ts +2 -2
  177. package/src/utils/prompt-suggestion.ts +265 -0
  178. package/src/utils/sixel.ts +3 -3
  179. package/src/web/insane/url-guard.ts +3 -1
  180. 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 {
@@ -83,6 +89,7 @@ import { TodoCommandController } from "./controllers/todo-command-controller";
83
89
  import { IrcObservationLedger } from "./irc-observation-ledger";
84
90
  import { JobsObserver } from "./jobs-observer";
85
91
  import { OAuthManualInputManager } from "./oauth-manual-input";
92
+ import { PromptSuggestionController } from "./prompt-suggestion-controller";
86
93
  import { SessionObserverRegistry } from "./session-observer-registry";
87
94
  import { interruptHint } from "./shared";
88
95
  import { shouldShowExtensionCommand } from "./slash-command-visibility";
@@ -105,8 +112,15 @@ import {
105
112
  import type { ParsedIrcMessage } from "./utils/irc-message";
106
113
  import { addChatChild, prepareTranscriptRebuild, UiHelpers } from "./utils/ui-helpers";
107
114
 
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`;
115
+ export function getDefaultComposerPlaceholder(context: KeyDisplayContext = { platform: process.platform }): string {
116
+ const newlineKeys = context.platform === "win32" ? ["alt+enter", "ctrl+j"] : ["shift+enter", "ctrl+j"];
117
+ return `Type your message... ${formatKeyHints(newlineKeys, context)}: New line · ${formatKeyHint(
118
+ "ctrl+c",
119
+ context,
120
+ )}: Clear · ${formatKeyHint("ctrl+r", context)}: Search history · ${formatKeyHint("shift+tab", context)}: Reasoning`;
121
+ }
122
+
123
+ export const DEFAULT_COMPOSER_PLACEHOLDER = getDefaultComposerPlaceholder();
110
124
  const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
111
125
 
112
126
  const IRC_SIDEBAR_TOGGLE_SHADOWING_ACTIONS: readonly AppKeybinding[] = [
@@ -128,23 +142,6 @@ export function getWelcomeTranscriptReservedRows(chatContainer: Container, width
128
142
  ? 0
129
143
  : chatContainer.render(width).length;
130
144
  }
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
145
 
149
146
  const HINT_SHIMMER_PALETTE: ShimmerPalette = {
150
147
  low: "dim",
@@ -169,7 +166,7 @@ function configureDefaultComposerChrome(editor: CustomEditor): void {
169
166
  editor.setClosedBorderBox(true);
170
167
  editor.setPromptGutter(undefined);
171
168
  editor.setInputPrefix(getDefaultInputPrefix());
172
- editor.setPlaceholder(DEFAULT_COMPOSER_PLACEHOLDER);
169
+ editor.setPlaceholder(getDefaultComposerPlaceholder());
173
170
  editor.setPaddingX(1);
174
171
  editor.setRightGutterWidth(1);
175
172
  editor.setTopBorder(undefined);
@@ -340,6 +337,7 @@ export class InteractiveMode implements InteractiveModeContext {
340
337
  fileSlashCommands: Set<string> = new Set();
341
338
  skillCommands: Map<string, Skill> = new Map();
342
339
  oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
340
+ promptSuggestion: PromptSuggestionController;
343
341
 
344
342
  #baseSlashCommands: SlashCommand[] = [];
345
343
  #resolvedSlashCommands: SlashCommand[] = [];
@@ -351,6 +349,7 @@ export class InteractiveMode implements InteractiveModeContext {
351
349
  #petUnavailableWarningDisposer?: () => void;
352
350
  readonly #version: string;
353
351
  readonly #changelogMarkdown: string | undefined;
352
+ readonly #keyDisplayContext: KeyDisplayContext;
354
353
 
355
354
  readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
356
355
  mcpManager?: import("../runtime-mcp").MCPManager;
@@ -398,6 +397,7 @@ export class InteractiveMode implements InteractiveModeContext {
398
397
  lspServers: LspStartupServerInfo[] | undefined = undefined,
399
398
  mcpManager?: import("../runtime-mcp").MCPManager,
400
399
  eventBus?: EventBus,
400
+ keyDisplayContext: KeyDisplayContext = { platform: process.platform },
401
401
  ) {
402
402
  this.session = session;
403
403
  this.sessionManager = session.sessionManager;
@@ -436,6 +436,8 @@ export class InteractiveMode implements InteractiveModeContext {
436
436
  this.lspServers = lspServers;
437
437
  this.mcpManager = mcpManager;
438
438
  this.#eventBus = eventBus;
439
+ this.#keyDisplayContext = keyDisplayContext;
440
+ this.keybindings.setDisplayContext(this.#keyDisplayContext);
439
441
  const thisMode = this;
440
442
  this.#goalModeController = new GoalModeController({
441
443
  session: this.session,
@@ -517,6 +519,7 @@ export class InteractiveMode implements InteractiveModeContext {
517
519
  this.btwContainer = new Container();
518
520
  this.editor = new CustomEditor(getEditorTheme());
519
521
  configureDefaultComposerChrome(this.editor);
522
+ this.editor.setPlaceholder(this.#getComposerPlaceholder());
520
523
  this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
521
524
  this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
522
525
  this.editor.onAutocompleteCancel = () => {
@@ -544,7 +547,11 @@ export class InteractiveMode implements InteractiveModeContext {
544
547
  this.hookWidgetContainerBelow = new Container();
545
548
  this.editorContainer = new Container();
546
549
  this.editorContainer.addChild(this.editor);
547
- this.statusLine = new StatusLineComponent(session, { version: this.#version, focusDomain: "composer" });
550
+ this.statusLine = new StatusLineComponent(session, {
551
+ version: this.#version,
552
+ focusDomain: "composer",
553
+ keyDisplayContext: this.#keyDisplayContext,
554
+ });
548
555
  this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
549
556
 
550
557
  this.hideThinkingBlock = settings.get("hideThinkingBlock");
@@ -580,6 +587,7 @@ export class InteractiveMode implements InteractiveModeContext {
580
587
  this.#selectorController = new SelectorController(this);
581
588
  this.#inputController = new InputController(this);
582
589
  this.statusLine.setActionRegistry(this.#inputController.actionRegistry, () => this.keybindings);
590
+ this.promptSuggestion = new PromptSuggestionController(this);
583
591
  this.#observerRegistry = new SessionObserverRegistry();
584
592
  }
585
593
 
@@ -600,6 +608,7 @@ export class InteractiveMode implements InteractiveModeContext {
600
608
  if (this.isInitialized) return;
601
609
 
602
610
  this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
611
+ this.keybindings.setDisplayContext(this.#keyDisplayContext);
603
612
 
604
613
  // Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
605
614
  this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush());
@@ -657,6 +666,7 @@ export class InteractiveMode implements InteractiveModeContext {
657
666
  getReservedBottomRows: getWelcomeReservedBottomRows,
658
667
  changelogMarkdown: this.#changelogMarkdown,
659
668
  collapseChangelog: settings.get("collapseChangelog"),
669
+ keyDisplayContext: this.#keyDisplayContext,
660
670
  },
661
671
  );
662
672
 
@@ -807,6 +817,7 @@ export class InteractiveMode implements InteractiveModeContext {
807
817
  onThemeChange(() => {
808
818
  clearRenderCache();
809
819
  configureDefaultComposerChrome(this.editor);
820
+ this.editor.setPlaceholder(this.#getComposerPlaceholder());
810
821
  this.ui.invalidate();
811
822
  this.updateEditorChrome();
812
823
  this.ui.requestRender();
@@ -1038,12 +1049,14 @@ export class InteractiveMode implements InteractiveModeContext {
1038
1049
  }
1039
1050
 
1040
1051
  #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}`];
1052
+ const defaultPlaceholder = getDefaultComposerPlaceholder(this.#keyDisplayContext);
1053
+ if (!this.#isPromptDeliveryBusy()) return defaultPlaceholder;
1054
+ const submitAction = this.settings.get("busyPromptMode") === "steer" ? "Steer" : "Queue";
1055
+ const submitKey = this.keybindings.getDisplayString("tui.input.submit", this.#keyDisplayContext);
1056
+ const parts = submitKey ? [`${submitKey}: ${submitAction}`] : [];
1044
1057
  const queueKey = this.#getMessageQueueShortcut();
1045
- if (queueKey) parts.push(`${formatShortcutForPlaceholder(queueKey)}: Queue`);
1046
- return `${DEFAULT_COMPOSER_PLACEHOLDER} · ${parts.join(" · ")}`;
1058
+ if (queueKey) parts.push(`${formatKeyHint(queueKey, this.#keyDisplayContext)}: Queue`);
1059
+ return parts.length > 0 ? `${defaultPlaceholder} · ${parts.join(" · ")}` : defaultPlaceholder;
1047
1060
  }
1048
1061
 
1049
1062
  #getWelcomeReservedRows(width: number): number {
@@ -1398,6 +1411,7 @@ export class InteractiveMode implements InteractiveModeContext {
1398
1411
  : new CustomEditor(getEditorTheme());
1399
1412
 
1400
1413
  configureDefaultComposerChrome(nextEditor);
1414
+ nextEditor.setPlaceholder(this.#getComposerPlaceholder());
1401
1415
  nextEditor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
1402
1416
  nextEditor.setAutocompleteMaxVisible(this.settings.get("autocompleteMaxVisible"));
1403
1417
  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
+ }
@@ -95,7 +95,7 @@ export interface AskSelectedAckRecoveryParticipant {
95
95
 
96
96
  /** SDK-native surface for emitting a workflow gate and awaiting its answer. */
97
97
  export interface WorkflowGateEmitter {
98
- isUnattended(): boolean;
98
+ supportsRemoteGateAnswers(): boolean;
99
99
  emitGate(input: OpenGateInput): Promise<unknown>;
100
100
  onGateEmitted?(listener: (gate: WorkflowGate) => void): () => void;
101
101
  resolveGate?(response: WorkflowGateResponse): Promise<WorkflowGateResolution>;
@@ -167,7 +167,7 @@ export class BrokerWorkflowGateEmitter implements WorkflowGateEmitter {
167
167
  });
168
168
  }
169
169
 
170
- isUnattended(): boolean {
170
+ supportsRemoteGateAnswers(): boolean {
171
171
  return true;
172
172
  }
173
173
  setRuntimeTurnProvider(provider: (() => string | undefined) | null): void {
@@ -32,6 +32,7 @@ import type { ToolExecutionHandle } from "./components/tool-execution";
32
32
  import type { StatusLineComponent } from "./components/tool-status-header";
33
33
  import type { IrcObservationLedger } from "./irc-observation-ledger";
34
34
  import type { OAuthManualInputManager } from "./oauth-manual-input";
35
+ import type { PromptSuggestionController } from "./prompt-suggestion-controller";
35
36
  import type { Theme } from "./theme/theme";
36
37
  import type { ParsedIrcMessage } from "./utils/irc-message";
37
38
 
@@ -156,6 +157,8 @@ export interface InteractiveModeContext {
156
157
  skillCommands: Map<string, Skill>;
157
158
  oauthManualInput: OAuthManualInputManager;
158
159
  todoPhases: TodoPhase[];
160
+ /** Ghost-text next-prompt prediction; absent in ACP/lightweight test contexts. */
161
+ promptSuggestion?: PromptSuggestionController;
159
162
 
160
163
  // Lifecycle
161
164
  init(): Promise<void>;