@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
@@ -49,7 +49,7 @@ export const APP_ACTION_METADATA: readonly ActionMetadata[] = [
49
49
  action("app.clipboard.copyPrompt", "Copy prompt", "Clipboard", ["composer"]),
50
50
  action("app.session.new", "New session", "Session", ["composer"]),
51
51
  action("app.session.tree", "Session tree", "Session", ["composer"]),
52
- action("app.session.fork", "Fork session", "Session", ["composer"]),
52
+ action("app.session.fork", "Branch from message", "Session", ["composer"]),
53
53
  action("app.session.resume", "Resume session", "Session", ["composer"]),
54
54
  action("app.session.observe", "Observe sessions", "Session", ["composer"]),
55
55
  action("app.session.dashboard", "Show sessions dashboard", "Session", ["composer"]),
@@ -1,4 +1,10 @@
1
1
  import { type Component, Container, Markdown, Spacer, Text, type TUI } from "@gajae-code/tui";
2
+ import {
3
+ BTW_MAX_ANSWER_UTF8_BYTES,
4
+ BTW_MAX_CONTEXT_TURNS,
5
+ sanitizeBtwError,
6
+ truncateUtf8,
7
+ } from "../../session/btw-contract";
2
8
  import { replaceTabs } from "../../tools/render-utils";
3
9
  import { getMarkdownTheme, theme } from "../theme/theme";
4
10
  import { DynamicBorder } from "./dynamic-border";
@@ -10,12 +16,21 @@ interface BtwPanelComponentOptions {
10
16
  tui: TUI;
11
17
  }
12
18
 
19
+ interface BtwTurn {
20
+ question: string;
21
+ answer: string;
22
+ state: BtwPanelState;
23
+ errorMessage?: string;
24
+ }
25
+
13
26
  export class BtwPanelComponent extends Container {
14
27
  #question: string;
15
28
  #tui: TUI;
16
29
  #state: BtwPanelState = "running";
17
30
  #answer = "";
18
31
  #errorMessage: string | undefined;
32
+ #completedTurns: BtwTurn[] = [];
33
+ #streamingContent = new Container();
19
34
  #closed = false;
20
35
 
21
36
  constructor(options: BtwPanelComponentOptions) {
@@ -25,16 +40,29 @@ export class BtwPanelComponent extends Container {
25
40
  this.#rebuild();
26
41
  }
27
42
 
43
+ beginTurn(question: string): void {
44
+ if (this.#closed) return;
45
+ if (this.#state === "complete") {
46
+ this.#completedTurns.push(this.#currentTurn());
47
+ if (this.#completedTurns.length > BTW_MAX_CONTEXT_TURNS) this.#completedTurns.shift();
48
+ }
49
+ this.#question = question;
50
+ this.#answer = "";
51
+ this.#errorMessage = undefined;
52
+ this.#state = "running";
53
+ this.#rebuild();
54
+ }
55
+
28
56
  appendText(delta: string): void {
29
57
  if (!delta || this.#closed) return;
30
- this.#answer += delta;
31
- this.#rebuild();
58
+ this.#answer = truncateUtf8(this.#answer + delta, BTW_MAX_ANSWER_UTF8_BYTES);
59
+ this.#updateStreamingContent();
32
60
  }
33
61
 
34
62
  setAnswer(text: string): void {
35
63
  if (this.#closed) return;
36
- this.#answer = text;
37
- this.#rebuild();
64
+ this.#answer = truncateUtf8(text, BTW_MAX_ANSWER_UTF8_BYTES);
65
+ this.#updateStreamingContent();
38
66
  }
39
67
 
40
68
  markComplete(): void {
@@ -54,21 +82,33 @@ export class BtwPanelComponent extends Container {
54
82
  markError(message: string): void {
55
83
  if (this.#closed) return;
56
84
  this.#state = "error";
57
- this.#errorMessage = message;
85
+ this.#answer = "";
86
+ this.#errorMessage = sanitizeBtwError(message);
58
87
  this.#rebuild();
59
88
  }
60
89
 
61
90
  close(): void {
62
91
  this.#closed = true;
92
+ this.#question = "";
93
+ this.#completedTurns = [];
94
+ this.#answer = "";
95
+ this.#errorMessage = undefined;
96
+ this.#streamingContent.clear();
97
+ this.clear();
63
98
  }
64
99
 
65
100
  #rebuild(): void {
66
101
  this.clear();
67
102
  this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
68
103
  this.addChild(new Spacer(1));
104
+ for (const turn of this.#completedTurns) {
105
+ this.addChild(this.#turnComponent(turn));
106
+ this.addChild(new Spacer(1));
107
+ }
69
108
  this.addChild(new Text(theme.fg("accent", replaceTabs(this.#question)), 1, 0));
70
109
  this.addChild(new Spacer(1));
71
- this.addChild(this.#contentComponent());
110
+ this.#updateStreamingContent(false);
111
+ this.addChild(this.#streamingContent);
72
112
  this.addChild(new Spacer(1));
73
113
  this.addChild(new Text(this.#footerLine(), 1, 0));
74
114
  this.addChild(new Spacer(1));
@@ -76,29 +116,55 @@ export class BtwPanelComponent extends Container {
76
116
  this.#tui.requestRender();
77
117
  }
78
118
 
119
+ #updateStreamingContent(requestRender = true): void {
120
+ this.#streamingContent.clear();
121
+ this.#streamingContent.addChild(this.#contentComponent(this.#state, this.#answer, this.#errorMessage));
122
+ if (requestRender) this.#tui.requestRender();
123
+ }
124
+
125
+ #currentTurn(): BtwTurn {
126
+ return {
127
+ question: this.#question,
128
+ answer: this.#answer,
129
+ state: this.#state,
130
+ errorMessage: this.#errorMessage,
131
+ };
132
+ }
133
+
134
+ #turnComponent(turn: BtwTurn): Component {
135
+ const container = new Container();
136
+ container.addChild(new Text(theme.fg("accent", replaceTabs(turn.question)), 1, 0));
137
+ container.addChild(new Spacer(1));
138
+ container.addChild(this.#contentComponent(turn.state, turn.answer, turn.errorMessage));
139
+ return container;
140
+ }
141
+
79
142
  #footerLine(): string {
80
143
  switch (this.#state) {
81
144
  case "running":
82
- return theme.fg("muted", "Esc cancel /btw");
145
+ return theme.fg("muted", "Esc cancel /btw and return to main chat");
83
146
  case "complete":
84
- return theme.fg("muted", "Esc dismiss");
147
+ return theme.fg("muted", "Type a follow-up · Esc return to main chat");
85
148
  case "aborted":
86
- return theme.fg("warning", `${theme.status.warning} Cancelled · Esc dismiss`);
149
+ return theme.fg(
150
+ "warning",
151
+ `${theme.status.warning} Cancelled · Type a follow-up · Esc return to main chat`,
152
+ );
87
153
  case "error":
88
- return theme.fg("error", `${theme.status.error} Error · Esc dismiss`);
154
+ return theme.fg("error", `${theme.status.error} Error · Type a follow-up · Esc return to main chat`);
89
155
  }
90
156
  }
91
157
 
92
- #contentComponent(): Component {
93
- if (this.#state === "error") {
94
- return new Text(theme.fg("error", replaceTabs(this.#errorMessage ?? "Unknown error")), 1, 0);
158
+ #contentComponent(state: BtwPanelState, answer: string, errorMessage: string | undefined): Component {
159
+ if (state === "error") {
160
+ return new Text(theme.fg("error", replaceTabs(errorMessage ?? "Unknown error")), 1, 0);
95
161
  }
96
- const text = replaceTabs(this.#answer).trim();
162
+ const text = replaceTabs(answer).trim();
97
163
  if (!text) {
98
- const waiting =
99
- this.#state === "running" ? `${theme.status.pending} Waiting for response…` : "No text returned.";
164
+ const waiting = state === "running" ? `${theme.status.pending} Waiting for response…` : "No text returned.";
100
165
  return new Text(theme.fg("dim", waiting), 1, 0);
101
166
  }
167
+ if (state === "running") return new Text(text, 1, 0);
102
168
  return new Markdown(text, 1, 0, getMarkdownTheme());
103
169
  }
104
170
  }
@@ -35,6 +35,7 @@ import type { Settings } from "../../config/settings";
35
35
  import { type ThemeColor, theme } from "../../modes/theme/theme";
36
36
  import { formatModelOnboardingInlineHint } from "../../setup/model-onboarding-guidance";
37
37
  import { formatClampedModelSelector, getThinkingLevelMetadata, parseThinkingLevel } from "../../thinking";
38
+ import { getConfiguredImageModel } from "../../tools/image-gen";
38
39
  import { getTabBarTheme } from "../shared";
39
40
  import { DynamicBorder } from "./dynamic-border";
40
41
 
@@ -118,6 +119,9 @@ export type ModelSelectorSelection =
118
119
  | {
119
120
  kind: "deleteProfile";
120
121
  profileName: string;
122
+ }
123
+ | {
124
+ kind: "imageGeneration";
121
125
  };
122
126
 
123
127
  interface PendingThinkingChoice {
@@ -183,13 +187,18 @@ interface PresetBrowseRow {
183
187
  kind: "browse";
184
188
  }
185
189
 
190
+ interface PresetImageGenerationRow {
191
+ kind: "imageGeneration";
192
+ }
193
+
186
194
  type PresetLandingRow =
187
195
  | PresetGroupRow
188
196
  | PresetProfileRow
189
197
  | PresetCreateRow
190
198
  | PresetCreateUnavailableRow
191
199
  | PresetAlreadySavedRow
192
- | PresetBrowseRow;
200
+ | PresetBrowseRow
201
+ | PresetImageGenerationRow;
193
202
 
194
203
  // Stable logical identity for a preset landing row, independent of its current
195
204
  // list position. Used to relocate the cursor after the expanded group changes so
@@ -208,6 +217,8 @@ function presetRowIdentity(row: PresetLandingRow): string {
208
217
  return "createUnavailable";
209
218
  case "alreadySaved":
210
219
  return `alreadySaved:${row.profile.name}`;
220
+ case "imageGeneration":
221
+ return "imageGeneration";
211
222
  }
212
223
  }
213
224
 
@@ -334,6 +345,8 @@ export class ModelSelectorComponent extends Container {
334
345
  #selectedActionIndex: number = 0;
335
346
  #pendingThinkingChoice?: PendingThinkingChoice;
336
347
  #selectedThinkingIndex: number = 0;
348
+ #assignmentState: "idle" | "assigning" = "idle";
349
+ #closeAfterAssignment = false;
337
350
 
338
351
  // Preset landing state
339
352
  #viewMode: ModelSelectorViewMode = "presets";
@@ -962,6 +975,7 @@ export class ModelSelectorComponent extends Container {
962
975
  } else {
963
976
  rows.push({ kind: "createUnavailable", label: "Select a model before creating a custom preset" });
964
977
  }
978
+ rows.push({ kind: "imageGeneration" });
965
979
  rows.push({ kind: "browse" });
966
980
  return rows;
967
981
  }
@@ -1047,7 +1061,8 @@ export class ModelSelectorComponent extends Container {
1047
1061
  selected.kind === "browse" ||
1048
1062
  selected.kind === "create" ||
1049
1063
  selected.kind === "createUnavailable" ||
1050
- selected.kind === "alreadySaved"
1064
+ selected.kind === "alreadySaved" ||
1065
+ selected.kind === "imageGeneration"
1051
1066
  )
1052
1067
  return;
1053
1068
  if (this.#expandedPresetProviderId === selected.groupId) return;
@@ -1063,7 +1078,8 @@ export class ModelSelectorComponent extends Container {
1063
1078
  selected.kind === "browse" ||
1064
1079
  selected.kind === "create" ||
1065
1080
  selected.kind === "createUnavailable" ||
1066
- selected.kind === "alreadySaved"
1081
+ selected.kind === "alreadySaved" ||
1082
+ selected.kind === "imageGeneration"
1067
1083
  )
1068
1084
  return;
1069
1085
  if (this.#expandedPresetProviderId !== selected.groupId) return;
@@ -1162,6 +1178,16 @@ export class ModelSelectorComponent extends Container {
1162
1178
  this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
1163
1179
  continue;
1164
1180
  }
1181
+ if (row.kind === "imageGeneration") {
1182
+ const config = getConfiguredImageModel();
1183
+ const current =
1184
+ config && config.provider !== "auto"
1185
+ ? `${config.provider}${config.model ? ` (${config.model})` : ""}`
1186
+ : "Auto";
1187
+ const label = `Image Generation: ${current}`;
1188
+ this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
1189
+ continue;
1190
+ }
1165
1191
  if (row.kind === "group") {
1166
1192
  const authenticated = this.#isPresetGroupUsable(row.profiles);
1167
1193
  const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
@@ -1469,6 +1495,12 @@ export class ModelSelectorComponent extends Container {
1469
1495
  }
1470
1496
 
1471
1497
  handleInput(keyData: string): void {
1498
+ if (this.#assignmentState === "assigning") {
1499
+ if (getKeybindings().matches(keyData, "tui.select.cancel")) {
1500
+ this.#closeAfterAssignment = true;
1501
+ }
1502
+ return;
1503
+ }
1472
1504
  if (this.#pendingThinkingChoice) {
1473
1505
  this.#handleThinkingMenuInput(keyData);
1474
1506
  return;
@@ -1646,6 +1678,10 @@ export class ModelSelectorComponent extends Container {
1646
1678
  this.#switchToModelMode();
1647
1679
  return;
1648
1680
  }
1681
+ if (row.kind === "imageGeneration") {
1682
+ this.#onSelectCallback({ kind: "imageGeneration" });
1683
+ return;
1684
+ }
1649
1685
  if (row.kind === "group") {
1650
1686
  // A group is a list of alternative presets; only surface a login hint
1651
1687
  // when none of its members are usable. A partially-usable group stays
@@ -1833,24 +1869,50 @@ export class ModelSelectorComponent extends Container {
1833
1869
  ? item.selector
1834
1870
  : formatModelSelectorValue(item.selector, selectedThinkingLevel);
1835
1871
 
1836
- // Update local state for UI
1837
- for (const targetRole of roles ?? [role]) {
1838
- this.#roles[targetRole] = { model: item.model, thinkingLevel: selectedThinkingLevel };
1839
- }
1840
-
1841
- // Notify caller (for updating agent state if needed)
1842
- this.#onSelectCallback({
1872
+ const selection: Extract<ModelSelectorSelection, { kind: "assignment" }> = {
1843
1873
  kind: "assignment",
1844
1874
  model: item.model,
1845
1875
  role,
1846
1876
  roles,
1847
1877
  thinkingLevel: selectedThinkingLevel,
1848
1878
  selector: selectorValue,
1849
- });
1879
+ };
1880
+ if (this.#isTrackedSingleAssignment(selection)) {
1881
+ void this.#handleTrackedAssignment(selection);
1882
+ return;
1883
+ }
1884
+
1885
+ // Update local state for UI
1886
+ for (const targetRole of roles ?? [role]) {
1887
+ this.#roles[targetRole] = { model: item.model, thinkingLevel: selectedThinkingLevel };
1888
+ }
1889
+
1890
+ // Notify caller (for updating agent state if needed)
1891
+ this.#onSelectCallback(selection);
1850
1892
 
1851
1893
  // Update list to show new badges
1852
1894
  this.#updateList();
1853
1895
  }
1896
+ #isTrackedSingleAssignment(selection: Extract<ModelSelectorSelection, { kind: "assignment" }>): boolean {
1897
+ return selection.role !== null && selection.roles === undefined;
1898
+ }
1899
+
1900
+ async #handleTrackedAssignment(selection: Extract<ModelSelectorSelection, { kind: "assignment" }>): Promise<void> {
1901
+ if (this.#assignmentState !== "idle") return;
1902
+ this.#assignmentState = "assigning";
1903
+ this.#tui.requestRender();
1904
+ try {
1905
+ await Promise.resolve(this.#onSelectCallback(selection));
1906
+ } catch {
1907
+ // The controller reports tracked assignment failures before rethrowing.
1908
+ } finally {
1909
+ this.#assignmentState = "idle";
1910
+ const shouldClose = this.#closeAfterAssignment;
1911
+ this.#closeAfterAssignment = false;
1912
+ if (shouldClose) this.#onCancelCallback();
1913
+ else this.#tui.requestRender();
1914
+ }
1915
+ }
1854
1916
 
1855
1917
  getSearchInput(): Input {
1856
1918
  return this.#searchInput;
@@ -122,7 +122,7 @@ export class PlanPreviewOverlay extends Container {
122
122
  ? `${this.#inputKind === "notes" ? "Notes" : `Comment for source line ${this.#sourceLine}`} (Enter save, Esc cancel): ${this.#input.getValue()}`
123
123
  : this.#focus === "sourceSelect"
124
124
  ? `Source line ${this.#sourceLine}/${this.lines.length || 1} j/k:select c:comment Esc:done`
125
- : `${this.#scroll + 1}-${Math.min(this.#scroll + height, body.length)}/${body.length || 1} s:source line n:notes ${this.options.externalEditorKey ? `${this.options.externalEditorKey.toLowerCase()}:edit ` : ""}Tab:actions PgUp/PgDn:page`;
125
+ : `${this.#scroll + 1}-${Math.min(this.#scroll + height, body.length)}/${body.length || 1} s:source line n:notes ${this.options.externalEditorKey ? `${this.options.externalEditorKey}:edit ` : ""}Tab:actions PgUp/PgDn:page`;
126
126
 
127
127
  const rendered = [
128
128
  new DynamicBorder().render(width)[0] ?? "",
@@ -60,6 +60,7 @@ type WizardStep =
60
60
  */
61
61
  export interface MCPAddWizardOAuthResult {
62
62
  credentialId: string;
63
+ tokenEndpoint: string;
63
64
  clientId?: string;
64
65
  clientSecret?: string;
65
66
  }
@@ -122,6 +123,7 @@ export class MCPAddWizard extends Container {
122
123
  #onCancelCallback: () => void;
123
124
  #onOAuthCallback:
124
125
  | ((
126
+ endpointUrl: string,
125
127
  authUrl: string,
126
128
  tokenUrl: string,
127
129
  clientId: string,
@@ -141,6 +143,7 @@ export class MCPAddWizard extends Container {
141
143
  onComplete: (name: string, config: MCPServerConfig, scope: Scope) => void,
142
144
  onCancel: () => void,
143
145
  onOAuth?: (
146
+ endpointUrl: string,
144
147
  authUrl: string,
145
148
  tokenUrl: string,
146
149
  clientId: string,
@@ -1189,6 +1192,7 @@ export class MCPAddWizard extends Container {
1189
1192
  try {
1190
1193
  // Call OAuth handler
1191
1194
  const oauthResult = await this.#onOAuthCallback(
1195
+ this.#state.url,
1192
1196
  this.#state.oauthAuthUrl,
1193
1197
  this.#state.oauthTokenUrl,
1194
1198
  this.#state.oauthClientId,
@@ -1200,6 +1204,7 @@ export class MCPAddWizard extends Container {
1200
1204
  // Store credential ID + any dynamically-registered client credentials,
1201
1205
  // so the final mcp.json entry persists everything needed for refresh.
1202
1206
  this.#state.oauthCredentialId = oauthResult.credentialId;
1207
+ this.#state.oauthTokenUrl = oauthResult.tokenEndpoint;
1203
1208
  if (oauthResult.clientId) this.#state.oauthClientId = oauthResult.clientId;
1204
1209
  if (oauthResult.clientSecret) this.#state.oauthClientSecret = oauthResult.clientSecret;
1205
1210
 
@@ -3,8 +3,12 @@ import * as fs from "node:fs";
3
3
  import { type Component, truncateToWidth, visibleWidth } from "@gajae-code/tui";
4
4
  import { formatCount, getProjectDir } from "@gajae-code/utils";
5
5
  import { $ } from "bun";
6
- import type { AppKeybinding, KeybindingsManager } from "../../config/keybindings";
7
- import { KEYBINDINGS } from "../../config/keybindings";
6
+ import {
7
+ type AppKeybinding,
8
+ KEYBINDINGS,
9
+ type KeybindingsManager,
10
+ type KeyDisplayContext,
11
+ } from "../../config/keybindings";
8
12
  import { settings } from "../../config/settings";
9
13
  import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../config/settings-schema";
10
14
  import { theme } from "../../modes/theme/theme";
@@ -46,6 +50,7 @@ export interface StatusLineSettings {
46
50
  previewHighlightSegment?: StatusLineSegmentId;
47
51
  showHookStatus?: boolean;
48
52
  showSkillHud?: boolean;
53
+ showActionHints?: boolean;
49
54
  sessionAccent?: boolean;
50
55
  maxRows?: number;
51
56
  }
@@ -55,6 +60,7 @@ export interface StatusLineComponentOptions {
55
60
  actionRegistry?: ActionRegistry<void>;
56
61
  getKeybindings?: () => KeybindingsManager;
57
62
  focusDomain?: FocusDomain;
63
+ keyDisplayContext?: KeyDisplayContext;
58
64
  }
59
65
 
60
66
  export interface StatusLineActionHint {
@@ -92,6 +98,7 @@ export function getAvailableActionHints(
92
98
  getKeybindings: (() => KeybindingsManager) | undefined,
93
99
  width: number,
94
100
  domain: FocusDomain = "composer",
101
+ keyDisplayContext?: KeyDisplayContext,
95
102
  ): StatusLineActionHint[] {
96
103
  if (!actionRegistry || !getKeybindings || width <= 0) return [];
97
104
  const keybindings = getKeybindings();
@@ -108,7 +115,9 @@ export function getAvailableActionHints(
108
115
  if (!(bindingId in KEYBINDINGS)) continue;
109
116
  const keys = keybindings.getKeys(bindingId);
110
117
  if (keys.length === 0) continue;
111
- const content = theme.fg("dim", keybindings.getDisplayString(bindingId)) + theme.fg("muted", ` ${action.title}`);
118
+ const content =
119
+ theme.fg("dim", keybindings.getDisplayString(bindingId, keyDisplayContext)) +
120
+ theme.fg("muted", ` ${action.title}`);
112
121
  const nextWidth = visibleWidth(content) + (selected.length === 0 ? 0 : 3);
113
122
  if (used + nextWidth > width) break;
114
123
  selected.push({ id: action.id, content });
@@ -160,6 +169,7 @@ export class StatusLineComponent implements Component {
160
169
  #actionRegistry: ActionRegistry<void> | undefined;
161
170
  #getKeybindings: (() => KeybindingsManager) | undefined;
162
171
  #focusDomain: FocusDomain;
172
+ #keyDisplayContext: KeyDisplayContext | undefined;
163
173
 
164
174
  #resolvedSettingsCache:
165
175
  | (Required<Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">> &
@@ -199,6 +209,7 @@ export class StatusLineComponent implements Component {
199
209
  separator: settings.get("statusLine.separator"),
200
210
  showHookStatus: settings.get("statusLine.showHookStatus"),
201
211
  showSkillHud: settings.get("statusLine.showSkillHud"),
212
+ showActionHints: settings.get("statusLine.showActionHints"),
202
213
  segmentOptions: settings.getGroup("statusLine").segmentOptions,
203
214
  sessionAccent: settings.get("statusLine.sessionAccent"),
204
215
  maxRows: settings.get("statusLine.maxRows"),
@@ -207,6 +218,7 @@ export class StatusLineComponent implements Component {
207
218
  this.#actionRegistry = options.actionRegistry;
208
219
  this.#getKeybindings = options.getKeybindings;
209
220
  this.#focusDomain = options.focusDomain ?? "composer";
221
+ this.#keyDisplayContext = options.keyDisplayContext;
210
222
  }
211
223
 
212
224
  updateSettings(settings: StatusLineSettings): void {
@@ -791,7 +803,16 @@ export class StatusLineComponent implements Component {
791
803
  }
792
804
 
793
805
  const right: string[] = [];
794
- const actionHints = getAvailableActionHints(this.#actionRegistry, this.#getKeybindings, width, this.#focusDomain);
806
+ const actionHints =
807
+ effectiveSettings.showActionHints === false
808
+ ? []
809
+ : getAvailableActionHints(
810
+ this.#actionRegistry,
811
+ this.#getKeybindings,
812
+ width,
813
+ this.#focusDomain,
814
+ this.#keyDisplayContext,
815
+ );
795
816
  for (const segId of effectiveSettings.rightSegments) {
796
817
  const rendered = renderSegment(segId, ctx);
797
818
  if (rendered.visible && rendered.content) {
@@ -1,6 +1,7 @@
1
1
  import { type Component, padding, TERMINAL, truncateToWidth, visibleWidth } from "@gajae-code/tui";
2
2
  import { APP_NAME } from "@gajae-code/utils";
3
3
  import { formatBuildLabel } from "../../build-metadata";
4
+ import { formatKeyHint, type KeyDisplayContext } from "../../config/keybindings";
4
5
  import { type ThemeColor, theme } from "../../modes/theme/theme";
5
6
 
6
7
  export interface RecentSession {
@@ -21,26 +22,28 @@ export interface WelcomeComponentOptions {
21
22
  changelogMarkdown?: string;
22
23
  collapseChangelog?: boolean;
23
24
  buildLabel?: string;
25
+ keyDisplayContext?: KeyDisplayContext;
24
26
  }
25
27
 
26
28
  const WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS = 9;
27
29
  const DEFAULT_WHATS_NEW_ROWS = 3;
28
30
  const MAX_WHATS_NEW_ROWS = 12;
29
- const NEWLINE_FLOW_KEY =
30
- process.platform === "win32" ? { key: "alt+enter", label: "newline" } : { key: "ctrl+j", label: "newline" };
31
-
32
- const FLOW_KEY_ITEMS: ReadonlyArray<{ key: string; label: string }> = [
33
- { key: "/", label: "commands" },
34
- { key: "#", label: "actions" },
35
- { key: "!", label: "shell" },
36
- { key: "$", label: "python" },
37
- { key: "?", label: "keymap" },
38
- { key: "ctrl+l", label: "model" },
39
- { key: "shift+tab", label: "reasoning" },
40
- { key: "tab", label: "complete" },
41
- NEWLINE_FLOW_KEY,
42
- { key: "ctrl+c", label: "clear" },
43
- ];
31
+
32
+ function flowKeyItems(context: KeyDisplayContext): ReadonlyArray<{ key: string; label: string }> {
33
+ const newlineKey = context.platform === "win32" ? "alt+enter" : "ctrl+j";
34
+ return [
35
+ { key: "/", label: "commands" },
36
+ { key: "#", label: "actions" },
37
+ { key: "!", label: "shell" },
38
+ { key: "$", label: "python" },
39
+ { key: "?", label: "keymap" },
40
+ { key: "ctrl+l", label: "model" },
41
+ { key: "shift+tab", label: "reasoning" },
42
+ { key: "tab", label: "complete" },
43
+ { key: newlineKey, label: "newline" },
44
+ { key: "ctrl+c", label: "clear" },
45
+ ];
46
+ }
44
47
 
45
48
  /**
46
49
  * GJC-native launch surface with compact command affordances, project
@@ -325,7 +328,8 @@ export class WelcomeComponent implements Component {
325
328
  }
326
329
 
327
330
  #flowKeyItemText(item: { key: string; label: string }): string {
328
- return `${theme.fg("dim", item.key)}${theme.fg("muted", ` ${item.label}`)}`;
331
+ const context = this.options.keyDisplayContext ?? { platform: process.platform };
332
+ return `${theme.fg("dim", formatKeyHint(item.key, context))}${theme.fg("muted", ` ${item.label}`)}`;
329
333
  }
330
334
 
331
335
  #flowKeyRows(width: number): string[] {
@@ -333,7 +337,7 @@ export class WelcomeComponent implements Component {
333
337
  const separator = ` ${theme.fg("dim", "·")} `;
334
338
  const rows: string[] = [];
335
339
  let current = "";
336
- for (const item of FLOW_KEY_ITEMS) {
340
+ for (const item of flowKeyItems(this.options.keyDisplayContext ?? { platform: process.platform })) {
337
341
  const segment = this.#flowKeyItemText(item);
338
342
  const next = current ? `${current}${separator}${segment}` : segment;
339
343
  if (current && visibleWidth(next) > contentWidth) {
@@ -352,7 +356,7 @@ export class WelcomeComponent implements Component {
352
356
  const rowLimit = Math.max(1, Math.floor(maxRows));
353
357
  if (rows.length <= rowLimit) return rows;
354
358
  if (rowLimit === 1) {
355
- const firstItem = FLOW_KEY_ITEMS[0];
359
+ const firstItem = flowKeyItems(this.options.keyDisplayContext ?? { platform: process.platform })[0];
356
360
  const firstSegment = firstItem ? this.#flowKeyItemText(firstItem) : theme.fg("dim", "keys");
357
361
  return [this.#fitToWidth(` ${firstSegment} ${theme.fg("dim", "· … ")}${theme.bold("/help")}`, width)];
358
362
  }