@gajae-code/coding-agent 0.11.6 → 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 (259) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/cli/args.d.ts +0 -2
  5. package/dist/types/commands/daemon.d.ts +2 -2
  6. package/dist/types/commands/sdk.d.ts +20 -2
  7. package/dist/types/config/keybindings.d.ts +34 -4
  8. package/dist/types/config/model-profile-activation.d.ts +4 -3
  9. package/dist/types/config/model-profile-contract.d.ts +38 -0
  10. package/dist/types/config/settings-schema.d.ts +70 -7
  11. package/dist/types/edit/modes/patch.d.ts +13 -0
  12. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  13. package/dist/types/eval/py/executor.d.ts +1 -1
  14. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  15. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  16. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  17. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +6 -5
  18. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
  19. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +43 -5
  20. package/dist/types/main.d.ts +32 -1
  21. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  22. package/dist/types/modes/components/model-selector.d.ts +2 -0
  23. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  24. package/dist/types/modes/components/tool-status-header.d.ts +4 -2
  25. package/dist/types/modes/components/welcome.d.ts +2 -0
  26. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  27. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  28. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  29. package/dist/types/modes/interactive-mode.d.ts +7 -2
  30. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  31. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  32. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  33. package/dist/types/modes/types.d.ts +4 -0
  34. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  35. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  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/sdk/broker/broker.d.ts +2 -0
  40. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  41. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -1
  42. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  43. package/dist/types/sdk/bus/config.d.ts +2 -0
  44. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  45. package/dist/types/sdk/bus/index.d.ts +26 -1
  46. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  47. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  48. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  49. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  50. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +18 -3
  51. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  52. package/dist/types/sdk/bus/telegram-daemon.d.ts +189 -4
  53. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  54. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  55. package/dist/types/sdk/client/discovery.d.ts +2 -0
  56. package/dist/types/sdk/client/liveness.d.ts +7 -0
  57. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  58. package/dist/types/sdk/host/host.d.ts +1 -1
  59. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  60. package/dist/types/sdk/index.d.ts +2 -0
  61. package/dist/types/sdk/prompt-status.d.ts +88 -0
  62. package/dist/types/sdk/session-directory.d.ts +1 -1
  63. package/dist/types/sdk/startup-capability.d.ts +3 -0
  64. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  65. package/dist/types/sdk/transport/index.d.ts +12 -0
  66. package/dist/types/sdk/transport/relay.d.ts +43 -0
  67. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  68. package/dist/types/sdk/transport/socket.d.ts +7 -0
  69. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  70. package/dist/types/session/agent-session.d.ts +30 -5
  71. package/dist/types/session/btw-contract.d.ts +16 -0
  72. package/dist/types/session/internal/managed-session-scope.d.ts +32 -9
  73. package/dist/types/session/internal/managed-session-storage.d.ts +27 -10
  74. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  75. package/dist/types/session/session-manager.d.ts +25 -4
  76. package/dist/types/session/session-storage.d.ts +18 -0
  77. package/dist/types/setup/credential-import.d.ts +5 -1
  78. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  79. package/dist/types/ssh/utils.d.ts +1 -0
  80. package/dist/types/task/discovery.d.ts +3 -1
  81. package/dist/types/task/index.d.ts +3 -1
  82. package/dist/types/task/receipt.d.ts +2 -0
  83. package/dist/types/task/types.d.ts +94 -0
  84. package/dist/types/tools/ask.d.ts +454 -174
  85. package/dist/types/tools/fetch.d.ts +5 -8
  86. package/dist/types/tools/image-gen.d.ts +63 -1
  87. package/dist/types/tools/index.d.ts +4 -0
  88. package/dist/types/tools/skill-discovery.d.ts +7 -0
  89. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  90. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  91. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  92. package/dist/types/web/insane/bridge.d.ts +1 -0
  93. package/dist/types/web/insane/url-guard.d.ts +17 -0
  94. package/package.json +8 -8
  95. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  96. package/scripts/dogfood-repository-binding.ts +209 -0
  97. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  98. package/scripts/run-test-manifest.ts +4 -4
  99. package/src/capability/index.ts +50 -42
  100. package/src/capability/types.ts +3 -0
  101. package/src/cli/args.ts +2 -92
  102. package/src/cli/notify-cli.ts +5 -38
  103. package/src/cli/setup-cli.ts +10 -4
  104. package/src/cli.ts +52 -9
  105. package/src/commands/daemon.ts +22 -5
  106. package/src/commands/sdk.ts +44 -11
  107. package/src/config/keybindings.ts +158 -25
  108. package/src/config/model-profile-activation.ts +7 -18
  109. package/src/config/model-profile-contract.ts +161 -0
  110. package/src/config/model-profiles.ts +18 -0
  111. package/src/config/model-registry.ts +11 -1
  112. package/src/config/settings-schema.ts +73 -7
  113. package/src/coordinator-mcp/model-preset.ts +21 -74
  114. package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
  115. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  116. package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
  117. package/src/discovery/ssh.ts +12 -1
  118. package/src/edit/index.ts +3 -6
  119. package/src/edit/modes/patch.ts +38 -1
  120. package/src/edit/modes/replace.ts +42 -0
  121. package/src/edit/path-mutation-lock.ts +112 -0
  122. package/src/eval/py/executor.ts +30 -11
  123. package/src/eval/py/kernel.ts +1 -21
  124. package/src/eval/py/prelude.py +2 -2
  125. package/src/eval/py/runner-artifact.ts +84 -0
  126. package/src/eval/py/tool-bridge.ts +34 -18
  127. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  128. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  129. package/src/gjc-runtime/launch-tmux.ts +1 -1
  130. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  131. package/src/gjc-runtime/ralplan-runtime.ts +75 -3
  132. package/src/gjc-runtime/repository-binding.ts +267 -0
  133. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  134. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  135. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  136. package/src/gjc-runtime/ultragoal-guard.ts +93 -15
  137. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
  138. package/src/gjc-runtime/ultragoal-runtime.ts +456 -31
  139. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  140. package/src/gjc-runtime/workflow-manifest.ts +23 -0
  141. package/src/internal-urls/docs-index.generated.ts +12 -10
  142. package/src/internal-urls/local-protocol.ts +64 -21
  143. package/src/internal-urls/mcp-protocol.ts +73 -11
  144. package/src/main.ts +111 -37
  145. package/src/modes/DESIGN.md +5 -0
  146. package/src/modes/acp/acp-agent.ts +50 -16
  147. package/src/modes/action-registry.ts +1 -1
  148. package/src/modes/components/btw-panel.ts +82 -16
  149. package/src/modes/components/model-selector.ts +73 -11
  150. package/src/modes/components/plan-preview-overlay.ts +1 -1
  151. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  152. package/src/modes/components/tool-status-header.ts +25 -4
  153. package/src/modes/components/welcome.ts +22 -18
  154. package/src/modes/controllers/btw-controller.ts +119 -36
  155. package/src/modes/controllers/command-controller.ts +97 -54
  156. package/src/modes/controllers/event-controller.ts +10 -0
  157. package/src/modes/controllers/input-controller.ts +38 -2
  158. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  159. package/src/modes/controllers/selector-controller.ts +149 -63
  160. package/src/modes/interactive-mode.ts +57 -34
  161. package/src/modes/prompt-action-autocomplete.ts +32 -13
  162. package/src/modes/prompt-suggestion-controller.ts +96 -0
  163. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  164. package/src/modes/types.ts +4 -0
  165. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  166. package/src/modes/utils/ui-helpers.ts +5 -3
  167. package/src/prompts/agents/architect.md +0 -1
  168. package/src/prompts/agents/critic.md +0 -1
  169. package/src/prompts/system/btw-user.md +3 -8
  170. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  171. package/src/prompts/system/system-prompt.md +6 -4
  172. package/src/prompts/tools/cron.md +3 -1
  173. package/src/prompts/tools/skill-discovery.md +1 -0
  174. package/src/runtime-mcp/client.ts +41 -55
  175. package/src/runtime-mcp/content-limits.ts +71 -0
  176. package/src/runtime-mcp/json-rpc.ts +23 -9
  177. package/src/runtime-mcp/manager.ts +51 -53
  178. package/src/runtime-mcp/oauth-flow.ts +0 -72
  179. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  180. package/src/runtime-mcp/transports/http.ts +63 -22
  181. package/src/sdk/broker/broker.ts +34 -20
  182. package/src/sdk/broker/lifecycle.ts +185 -36
  183. package/src/sdk/bus/chat-daemon-control.ts +19 -5
  184. package/src/sdk/bus/config-commands.ts +2 -2
  185. package/src/sdk/bus/config.ts +9 -1
  186. package/src/sdk/bus/conversation-store.ts +40 -6
  187. package/src/sdk/bus/index.ts +244 -70
  188. package/src/sdk/bus/lifecycle-commands.ts +63 -10
  189. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  190. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  191. package/src/sdk/bus/notification-orchestration.ts +12 -0
  192. package/src/sdk/bus/notification-service.ts +68 -20
  193. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  194. package/src/sdk/bus/recent-activity.ts +90 -26
  195. package/src/sdk/bus/telegram-daemon-contract.ts +18 -3
  196. package/src/sdk/bus/telegram-daemon-control.ts +147 -35
  197. package/src/sdk/bus/telegram-daemon.ts +2463 -450
  198. package/src/sdk/bus/telegram-reference.ts +20 -2
  199. package/src/sdk/bus/topic-registry.ts +451 -12
  200. package/src/sdk/cli/session-cli.ts +15 -2
  201. package/src/sdk/client/discovery.ts +19 -3
  202. package/src/sdk/client/liveness.ts +49 -0
  203. package/src/sdk/host/control/dispatch.ts +1 -1
  204. package/src/sdk/host/control/operations.ts +1 -1
  205. package/src/sdk/host/host.ts +2 -6
  206. package/src/sdk/host/query/handlers.ts +70 -5
  207. package/src/sdk/index.ts +12 -0
  208. package/src/sdk/prompt-status.ts +97 -0
  209. package/src/sdk/protocol/operation-inventory.generated.json +58 -0
  210. package/src/sdk/protocol/operation-registry.ts +18 -4
  211. package/src/sdk/session-directory.ts +5 -1
  212. package/src/sdk/session.ts +31 -5
  213. package/src/sdk/startup-capability.ts +9 -1
  214. package/src/sdk/transport/auth-preface.ts +82 -0
  215. package/src/sdk/transport/index.ts +18 -0
  216. package/src/sdk/transport/relay.ts +252 -0
  217. package/src/sdk/transport/serve-cli.ts +105 -0
  218. package/src/sdk/transport/socket.ts +197 -0
  219. package/src/sdk/transport/stdio.ts +17 -0
  220. package/src/session/agent-session.ts +292 -154
  221. package/src/session/blob-store.ts +25 -7
  222. package/src/session/btw-contract.ts +59 -0
  223. package/src/session/internal/managed-session-scope.ts +1163 -137
  224. package/src/session/internal/managed-session-storage.ts +397 -234
  225. package/src/session/internal/native-publish-outcome.ts +266 -0
  226. package/src/session/session-manager.ts +620 -403
  227. package/src/session/session-storage.ts +128 -11
  228. package/src/setup/credential-import.ts +28 -3
  229. package/src/setup/provider-onboarding.ts +30 -1
  230. package/src/setup/provider-presets.json +2 -1
  231. package/src/slash-commands/builtin-registry.ts +1 -1
  232. package/src/ssh/utils.ts +14 -0
  233. package/src/task/discovery.ts +8 -2
  234. package/src/task/gjc-command.ts +2 -2
  235. package/src/task/index.ts +121 -15
  236. package/src/task/receipt.ts +3 -0
  237. package/src/task/types.ts +34 -3
  238. package/src/tools/ask.ts +199 -89
  239. package/src/tools/ast-edit.ts +2 -2
  240. package/src/tools/bash-pty-selection.ts +2 -2
  241. package/src/tools/browser/screenshot-format.ts +6 -1
  242. package/src/tools/cron.ts +1 -1
  243. package/src/tools/fetch.ts +13 -131
  244. package/src/tools/image-gen.ts +476 -23
  245. package/src/tools/index.ts +4 -0
  246. package/src/tools/read.ts +2 -0
  247. package/src/tools/render-utils.ts +7 -1
  248. package/src/tools/skill-discovery.ts +39 -3
  249. package/src/tools/sqlite-reader.ts +63 -5
  250. package/src/utils/edit-mode.ts +2 -2
  251. package/src/utils/pasted-image-path.ts +15 -6
  252. package/src/utils/prompt-suggestion.ts +265 -0
  253. package/src/utils/shell-snapshot.ts +173 -63
  254. package/src/utils/sixel.ts +3 -3
  255. package/src/web/insane/bridge.ts +5 -1
  256. package/src/web/insane/url-guard.ts +140 -27
  257. package/src/web/scrapers/docs-rs.ts +30 -5
  258. package/src/web/scrapers/types.ts +19 -49
  259. package/src/web/scrapers/utils.ts +12 -26
@@ -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);
@@ -264,6 +261,10 @@ export interface InteractiveModeOptions {
264
261
  initialMessages?: string[];
265
262
  }
266
263
 
264
+ export function selectShutdownDraft(editorText: string, hasActiveBtw: boolean): string {
265
+ return hasActiveBtw ? "" : editorText;
266
+ }
267
+
267
268
  export class InteractiveMode implements InteractiveModeContext {
268
269
  session: AgentSession;
269
270
  sessionManager: SessionManager;
@@ -336,6 +337,7 @@ export class InteractiveMode implements InteractiveModeContext {
336
337
  fileSlashCommands: Set<string> = new Set();
337
338
  skillCommands: Map<string, Skill> = new Map();
338
339
  oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
340
+ promptSuggestion: PromptSuggestionController;
339
341
 
340
342
  #baseSlashCommands: SlashCommand[] = [];
341
343
  #resolvedSlashCommands: SlashCommand[] = [];
@@ -347,6 +349,7 @@ export class InteractiveMode implements InteractiveModeContext {
347
349
  #petUnavailableWarningDisposer?: () => void;
348
350
  readonly #version: string;
349
351
  readonly #changelogMarkdown: string | undefined;
352
+ readonly #keyDisplayContext: KeyDisplayContext;
350
353
 
351
354
  readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
352
355
  mcpManager?: import("../runtime-mcp").MCPManager;
@@ -394,6 +397,7 @@ export class InteractiveMode implements InteractiveModeContext {
394
397
  lspServers: LspStartupServerInfo[] | undefined = undefined,
395
398
  mcpManager?: import("../runtime-mcp").MCPManager,
396
399
  eventBus?: EventBus,
400
+ keyDisplayContext: KeyDisplayContext = { platform: process.platform },
397
401
  ) {
398
402
  this.session = session;
399
403
  this.sessionManager = session.sessionManager;
@@ -432,6 +436,8 @@ export class InteractiveMode implements InteractiveModeContext {
432
436
  this.lspServers = lspServers;
433
437
  this.mcpManager = mcpManager;
434
438
  this.#eventBus = eventBus;
439
+ this.#keyDisplayContext = keyDisplayContext;
440
+ this.keybindings.setDisplayContext(this.#keyDisplayContext);
435
441
  const thisMode = this;
436
442
  this.#goalModeController = new GoalModeController({
437
443
  session: this.session,
@@ -513,6 +519,7 @@ export class InteractiveMode implements InteractiveModeContext {
513
519
  this.btwContainer = new Container();
514
520
  this.editor = new CustomEditor(getEditorTheme());
515
521
  configureDefaultComposerChrome(this.editor);
522
+ this.editor.setPlaceholder(this.#getComposerPlaceholder());
516
523
  this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
517
524
  this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
518
525
  this.editor.onAutocompleteCancel = () => {
@@ -540,7 +547,11 @@ export class InteractiveMode implements InteractiveModeContext {
540
547
  this.hookWidgetContainerBelow = new Container();
541
548
  this.editorContainer = new Container();
542
549
  this.editorContainer.addChild(this.editor);
543
- 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
+ });
544
555
  this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
545
556
 
546
557
  this.hideThinkingBlock = settings.get("hideThinkingBlock");
@@ -576,6 +587,7 @@ export class InteractiveMode implements InteractiveModeContext {
576
587
  this.#selectorController = new SelectorController(this);
577
588
  this.#inputController = new InputController(this);
578
589
  this.statusLine.setActionRegistry(this.#inputController.actionRegistry, () => this.keybindings);
590
+ this.promptSuggestion = new PromptSuggestionController(this);
579
591
  this.#observerRegistry = new SessionObserverRegistry();
580
592
  }
581
593
 
@@ -596,6 +608,7 @@ export class InteractiveMode implements InteractiveModeContext {
596
608
  if (this.isInitialized) return;
597
609
 
598
610
  this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
611
+ this.keybindings.setDisplayContext(this.#keyDisplayContext);
599
612
 
600
613
  // Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
601
614
  this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush());
@@ -653,6 +666,7 @@ export class InteractiveMode implements InteractiveModeContext {
653
666
  getReservedBottomRows: getWelcomeReservedBottomRows,
654
667
  changelogMarkdown: this.#changelogMarkdown,
655
668
  collapseChangelog: settings.get("collapseChangelog"),
669
+ keyDisplayContext: this.#keyDisplayContext,
656
670
  },
657
671
  );
658
672
 
@@ -803,6 +817,7 @@ export class InteractiveMode implements InteractiveModeContext {
803
817
  onThemeChange(() => {
804
818
  clearRenderCache();
805
819
  configureDefaultComposerChrome(this.editor);
820
+ this.editor.setPlaceholder(this.#getComposerPlaceholder());
806
821
  this.ui.invalidate();
807
822
  this.updateEditorChrome();
808
823
  this.ui.requestRender();
@@ -1034,12 +1049,14 @@ export class InteractiveMode implements InteractiveModeContext {
1034
1049
  }
1035
1050
 
1036
1051
  #getComposerPlaceholder(): string {
1037
- if (!this.#isPromptDeliveryBusy()) return DEFAULT_COMPOSER_PLACEHOLDER;
1038
- const enterAction = this.settings.get("busyPromptMode") === "steer" ? "Steer" : "Queue";
1039
- 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}`] : [];
1040
1057
  const queueKey = this.#getMessageQueueShortcut();
1041
- if (queueKey) parts.push(`${formatShortcutForPlaceholder(queueKey)}: Queue`);
1042
- 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;
1043
1060
  }
1044
1061
 
1045
1062
  #getWelcomeReservedRows(width: number): number {
@@ -1322,19 +1339,20 @@ export class InteractiveMode implements InteractiveModeContext {
1322
1339
  if (this.#isShuttingDown) return;
1323
1340
  this.#isShuttingDown = true;
1324
1341
 
1325
- // Snapshot the editor before any teardown empties it. Persisting the draft
1326
- // here covers Ctrl+D shutdown with non-empty text; for /exit the editor is
1327
- // already cleared so saveDraft("") just removes any stale sidecar.
1328
- const draftText = this.editor.getText();
1342
+ // `/btw` owns the shared composer while its panel is open. Never persist a
1343
+ // side-chat draft or pending side-chat images into the main-session draft.
1344
+ const hadActiveBtw = this.#btwController.hasOpenPanel();
1345
+ const draftText = selectShutdownDraft(this.editor.getText(), hadActiveBtw);
1346
+ if (hadActiveBtw) this.pendingImages = [];
1347
+ this.#btwController.dispose();
1329
1348
 
1330
- // Flush pending session writes before shutdown
1349
+ // Flush pending session writes before shutdown.
1331
1350
  await this.sessionManager.flush();
1332
1351
  try {
1333
1352
  await this.sessionManager.saveDraft(draftText);
1334
1353
  } catch (err) {
1335
1354
  logger.warn("Failed to save session draft", { error: String(err) });
1336
1355
  }
1337
- this.#btwController.dispose();
1338
1356
 
1339
1357
  // Emit shutdown event to hooks
1340
1358
  this.session.setSdkPlanModeHandler(null);
@@ -1393,6 +1411,7 @@ export class InteractiveMode implements InteractiveModeContext {
1393
1411
  : new CustomEditor(getEditorTheme());
1394
1412
 
1395
1413
  configureDefaultComposerChrome(nextEditor);
1414
+ nextEditor.setPlaceholder(this.#getComposerPlaceholder());
1396
1415
  nextEditor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
1397
1416
  nextEditor.setAutocompleteMaxVisible(this.settings.get("autocompleteMaxVisible"));
1398
1417
  nextEditor.onAutocompleteCancel = () => {
@@ -2025,7 +2044,11 @@ export class InteractiveMode implements InteractiveModeContext {
2025
2044
  }
2026
2045
 
2027
2046
  hasActiveBtw(): boolean {
2028
- return this.#btwController.hasActiveRequest();
2047
+ return this.#btwController.hasOpenPanel();
2048
+ }
2049
+
2050
+ handleBtwFollowUp(question: string): Promise<"accepted" | "busy" | "closed" | "rejected"> {
2051
+ return this.#btwController.submitFollowUp(question);
2029
2052
  }
2030
2053
 
2031
2054
  handleBtwEscape(): boolean {
@@ -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>;
@@ -338,6 +341,7 @@ export interface InteractiveModeContext {
338
341
  handleBackgroundCommand(): void;
339
342
  handleImagePaste(): Promise<boolean>;
340
343
  handleBtwCommand(question: string): Promise<void>;
344
+ handleBtwFollowUp(question: string): Promise<"accepted" | "busy" | "closed" | "rejected">;
341
345
  hasActiveBtw(): boolean;
342
346
  handleBtwEscape(): boolean;
343
347
  cycleThinkingLevel(): void;
@@ -1,60 +1,88 @@
1
+ import type { Keybinding } from "@gajae-code/tui";
1
2
  import type { AppKeybinding, KeybindingsManager } from "../../config/keybindings";
2
3
 
3
4
  export interface HotkeysMarkdownBindings {
4
- keybindings: Pick<KeybindingsManager, "getDisplayString">;
5
+ keybindings: Pick<KeybindingsManager, "formatAccessibleKeyHint" | "getAccessibleDisplayString">;
6
+ }
7
+
8
+ function key(bindings: HotkeysMarkdownBindings, action: Keybinding): string {
9
+ return bindings.keybindings.getAccessibleDisplayString(action) || "Disabled";
5
10
  }
6
11
 
7
12
  function appKey(bindings: HotkeysMarkdownBindings, action: AppKeybinding): string {
8
- return bindings.keybindings.getDisplayString(action) || "Disabled";
13
+ return key(bindings, action);
14
+ }
15
+
16
+ export function formatHotkeyMarkdownCode(label: string, escapeTablePipes = true): string {
17
+ const escaped = escapeTablePipes ? label.replace(/\|/g, "\\|") : label;
18
+ let longestBacktickRun = 0;
19
+ for (const match of escaped.matchAll(/`+/g)) {
20
+ longestBacktickRun = Math.max(longestBacktickRun, match[0].length);
21
+ }
22
+ const fence = "`".repeat(longestBacktickRun + 1);
23
+ const padding = escaped.startsWith("`") || escaped.endsWith("`") ? " " : "";
24
+ return `${fence}${padding}${escaped}${padding}${fence}`;
9
25
  }
10
26
 
11
27
  export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string {
28
+ const displayKey = (action: Keybinding): string => formatHotkeyMarkdownCode(key(bindings, action));
29
+ const displayAppKey = (action: AppKeybinding): string => formatHotkeyMarkdownCode(appKey(bindings, action));
30
+ const displayKeys = (...labels: string[]): string => formatHotkeyMarkdownCode(labels.join("/"));
31
+ const formatFixedKey = (key: string): string => bindings.keybindings.formatAccessibleKeyHint(key);
32
+ const displayKeyWithFixed = (action: Keybinding, fixedKey: string): string => {
33
+ const configured = key(bindings, action);
34
+ const fixed = formatFixedKey(fixedKey);
35
+ const labels = configured.split("/");
36
+ if (!labels.includes(fixed)) labels.push(fixed);
37
+ return displayKeys(...labels);
38
+ };
39
+
12
40
  return [
13
41
  "**Navigation**",
14
42
  "| Key | Action |",
15
43
  "|-----|--------|",
16
- "| `Arrow keys` | Move cursor / browse history (Up when empty) |",
17
- "| `Option+Left/Right` | Move by word |",
18
- "| `Ctrl+A` / `Home` / `Cmd+Left` | Start of line |",
19
- "| `Ctrl+E` / `End` / `Cmd+Right` | End of line |",
44
+ `| ${displayKeys(key(bindings, "tui.editor.cursorUp"), key(bindings, "tui.editor.cursorDown"))} | Move cursor / browse history (Up when empty) |`,
45
+ `| ${displayKeys(key(bindings, "tui.editor.cursorWordLeft"), key(bindings, "tui.editor.cursorWordRight"))} | Move by word |`,
46
+ `| ${displayKeyWithFixed("tui.editor.cursorLineStart", "ctrl+a")} | Start of line |`,
47
+ `| ${displayKeyWithFixed("tui.editor.cursorLineEnd", "ctrl+e")} | End of line |`,
20
48
  "",
21
49
  "**Editing**",
22
50
  "| Key | Action |",
23
51
  "|-----|--------|",
24
- "| `Enter` | Send / queue while busy |",
25
- `| \`${appKey(bindings, "app.message.queue")}\` | Queue message for next turn |`,
26
- `| \`${appKey(bindings, "app.message.dequeue")}\` | Select queued message to edit |`,
27
- "| `Shift+Enter` / `Ctrl+J` | New line |",
28
- "| `Ctrl+W` / `Option+Backspace` | Delete word backwards |",
29
- "| `Ctrl+U` | Delete to start of line |",
30
- "| `Ctrl+K` | Delete to end of line |",
31
- `| \`${appKey(bindings, "app.clipboard.copyLine")}\` | Copy current line |`,
32
- `| \`${appKey(bindings, "app.clipboard.copyPrompt")}\` | Copy whole prompt |`,
52
+ `| ${displayKey("tui.input.submit")} | Send / queue while busy |`,
53
+ `| ${displayAppKey("app.message.queue")} | Queue message for next turn |`,
54
+ `| ${displayAppKey("app.message.dequeue")} | Select queued message to edit |`,
55
+ `| ${displayKeys(key(bindings, "tui.input.newLine"), formatFixedKey("ctrl+j"))} | New line |`,
56
+ `| ${displayKeys(formatFixedKey("ctrl+w"), formatFixedKey("alt+backspace"))} | Delete word backwards |`,
57
+ `| ${displayKeys(formatFixedKey("ctrl+u"))} | Delete to start of line |`,
58
+ `| ${displayKeys(formatFixedKey("ctrl+k"))} | Delete to end of line |`,
59
+ `| ${displayAppKey("app.clipboard.copyLine")} | Copy current line |`,
60
+ `| ${displayAppKey("app.clipboard.copyPrompt")} | Copy whole prompt |`,
33
61
  "",
34
62
  "**Other**",
35
63
  "| Key | Action |",
36
64
  "|-----|--------|",
37
- "| `Tab` | Path completion / accept autocomplete |",
38
- `| \`${appKey(bindings, "app.interrupt")}\` | Cancel autocomplete / interrupt active work |`,
39
- `| \`${appKey(bindings, "app.clear")}\` | Clear editor (first) / exit (second) |`,
40
- `| \`${appKey(bindings, "app.exit")}\` | Exit (when editor is empty) |`,
41
- `| \`${appKey(bindings, "app.suspend")}\` | Suspend to background |`,
42
- `| \`${appKey(bindings, "app.commandPalette.open")}\` | Open command palette |`,
43
- `| \`${appKey(bindings, "app.session.new")}\` | Start a fresh session |`,
44
- `| \`${appKey(bindings, "app.thinking.cycle")}\` | Cycle thinking level |`,
45
- `| \`${appKey(bindings, "app.model.cycleForward")}\` | Cycle configured model roles |`,
46
- `| \`${appKey(bindings, "app.model.cycleBackward")}\` | Cycle configured model roles temporarily |`,
47
- `| \`${appKey(bindings, "app.model.selectTemporary")}\` | Select model (temporary) |`,
48
- `| \`${appKey(bindings, "app.model.select")}\` | Select default model |`,
49
- `| \`${appKey(bindings, "app.plan.toggle")}\` | Toggle plan mode |`,
50
- `| \`${appKey(bindings, "app.history.search")}\` | Search prompt history |`,
51
- `| \`${appKey(bindings, "app.tools.expand")}\` | Toggle tool output expansion |`,
52
- `| \`${appKey(bindings, "app.tool.backgroundFold")}\` twice | Fold supported foreground bash into a background job |`,
53
- `| \`${appKey(bindings, "app.thinking.toggle")}\` | Toggle thinking block visibility |`,
54
- `| \`${appKey(bindings, "app.editor.external")}\` | Edit message in external editor |`,
55
- `| \`${appKey(bindings, "app.clipboard.pasteImage")}\` | Paste image from clipboard |`,
56
- `| \`${appKey(bindings, "app.stt.toggle")}\` | Toggle speech-to-text recording |`,
57
- `| \`${appKey(bindings, "app.irc.sidebar.toggle")}\` | Toggle IRC sidebar |`,
65
+ `| ${displayKey("tui.input.tab")} | Path completion / accept autocomplete |`,
66
+ `| ${displayAppKey("app.interrupt")} | Cancel autocomplete / interrupt active work |`,
67
+ `| ${displayAppKey("app.clear")} | Clear editor (first) / exit (second) |`,
68
+ `| ${displayAppKey("app.exit")} | Exit (when editor is empty) |`,
69
+ `| ${displayAppKey("app.suspend")} | Suspend to background |`,
70
+ `| ${displayAppKey("app.commandPalette.open")} | Open command palette |`,
71
+ `| ${displayAppKey("app.session.new")} | Start a fresh session |`,
72
+ `| ${displayAppKey("app.thinking.cycle")} | Cycle thinking level |`,
73
+ `| ${displayAppKey("app.model.cycleForward")} | Cycle configured model roles |`,
74
+ `| ${displayAppKey("app.model.cycleBackward")} | Cycle configured model roles temporarily |`,
75
+ `| ${displayAppKey("app.model.selectTemporary")} | Select model (temporary) |`,
76
+ `| ${displayAppKey("app.model.select")} | Select default model |`,
77
+ `| ${displayAppKey("app.plan.toggle")} | Toggle plan mode |`,
78
+ `| ${displayAppKey("app.history.search")} | Search prompt history |`,
79
+ `| ${displayAppKey("app.tools.expand")} | Toggle tool output expansion |`,
80
+ `| ${displayAppKey("app.tool.backgroundFold")} twice | Fold supported foreground bash into a background job |`,
81
+ `| ${displayAppKey("app.thinking.toggle")} | Toggle thinking block visibility |`,
82
+ `| ${displayAppKey("app.editor.external")} | Edit message in external editor |`,
83
+ `| ${displayAppKey("app.clipboard.pasteImage")} | Paste image from clipboard |`,
84
+ `| ${displayAppKey("app.stt.toggle")} | Toggle speech-to-text recording |`,
85
+ `| ${displayAppKey("app.irc.sidebar.toggle")} | Toggle IRC sidebar |`,
58
86
  "| `?` | Show help / shortcuts |",
59
87
  "| `#` | Prompt actions (command-palette style actions) |",
60
88
  "| `/` | Slash commands (try `/help` or `/new`) |",
@@ -916,9 +916,11 @@ export class UiHelpers {
916
916
  const queuedText = theme.fg("dim", `${entry.label}: ${entry.message}`);
917
917
  this.ctx.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
918
918
  }
919
- const dequeueKey = this.ctx.keybindings.getDisplayString("app.message.dequeue") || "Alt+Up/Alt+Down";
920
- const hintText = theme.fg("dim", `${theme.tree.hook} ${dequeueKey} to select/edit/reorder`);
921
- this.ctx.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
919
+ const dequeueKey = this.ctx.keybindings.getDisplayString("app.message.dequeue");
920
+ if (dequeueKey) {
921
+ const hintText = theme.fg("dim", `${theme.tree.hook} ${dequeueKey} to select/edit/reorder`);
922
+ this.ctx.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
923
+ }
922
924
  }
923
925
  }
924
926
 
@@ -33,7 +33,6 @@ You may receive a forked parent-conversation snapshot as background. Your read-o
33
33
  - Never approve CRITICAL or HIGH severity issues.
34
34
  - Do not skip spec compliance to jump to style nitpicks.
35
35
  - Be constructive: explain why an issue matters and how to fix it or strengthen the design.
36
- - Practical sufficiency beats endless logical completeness: treat preemptive logic for edge cases with no observed or evidenced trigger as over-engineering, but never downgrade genuine CRITICAL/HIGH architecture, extensibility, or robustness risks that have observability-based evidence.
37
36
  </constraints>
38
37
 
39
38
  <review_stages>