@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
@@ -1,12 +1,28 @@
1
- import { prompt } from "@gajae-code/utils";
2
1
  import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
2
+ import type { BtwConversationScope, BtwTurnCapture } from "../../session/agent-session";
3
+ import {
4
+ BTW_MAX_QUESTION_UTF8_BYTES,
5
+ type BtwTextExchange,
6
+ boundBtwExchanges,
7
+ sanitizeBtwError,
8
+ utf8ByteLength,
9
+ } from "../../session/btw-contract";
3
10
  import { BtwPanelComponent } from "../components/btw-panel";
4
11
  import type { InteractiveModeContext } from "../types";
5
12
 
13
+ type BtwFollowUpResult = "accepted" | "busy" | "closed" | "rejected";
14
+
15
+ interface ActiveBtwTurn extends BtwTurnCapture {
16
+ abortController: AbortController | undefined;
17
+ active: boolean;
18
+ }
19
+
6
20
  interface BtwRequest {
7
21
  component: BtwPanelComponent;
8
- abortController: AbortController;
9
- question: string;
22
+ conversationScope: BtwConversationScope | undefined;
23
+ activeTurn: ActiveBtwTurn | undefined;
24
+ inFlight: boolean;
25
+ contextExchanges: BtwTextExchange[];
10
26
  }
11
27
 
12
28
  export class BtwController {
@@ -18,84 +34,151 @@ export class BtwController {
18
34
  return this.#activeRequest !== undefined;
19
35
  }
20
36
 
37
+ hasOpenPanel(): boolean {
38
+ return this.hasActiveRequest();
39
+ }
40
+
41
+ isTurnInFlight(): boolean {
42
+ return this.#activeRequest?.inFlight ?? false;
43
+ }
44
+
21
45
  handleEscape(): boolean {
22
46
  if (!this.#activeRequest) return false;
23
- this.#closeActiveRequest({ abort: this.#activeRequest.abortController.signal.aborted === false });
47
+ this.#closeActiveRequest();
24
48
  return true;
25
49
  }
26
50
 
27
51
  dispose(): void {
28
- this.#closeActiveRequest({ abort: true });
52
+ this.#closeActiveRequest();
29
53
  }
30
54
 
31
55
  async start(question: string): Promise<void> {
56
+ if (this.hasOpenPanel()) {
57
+ this.ctx.showStatus("A /btw chat is already open. Type a follow-up or press Esc to return to the main chat.");
58
+ return;
59
+ }
32
60
  const trimmedQuestion = question.trim();
33
61
  if (!trimmedQuestion) {
34
62
  this.ctx.showStatus("Usage: /btw <question>");
35
63
  return;
36
64
  }
65
+ if (!this.#acceptQuestion(trimmedQuestion) || !this.#hasModel()) return;
66
+ const scope = this.ctx.session.createBtwConversationScope(btwUserPrompt);
67
+ const request = this.#openRequest(trimmedQuestion, scope);
68
+ void this.#runRequest(request);
69
+ }
37
70
 
38
- const model = this.ctx.session.model;
39
- if (!model) {
40
- this.ctx.showError("No active model available for /btw.");
41
- return;
71
+ async submitFollowUp(question: string): Promise<BtwFollowUpResult> {
72
+ const request = this.#activeRequest;
73
+ if (!request) return "closed";
74
+ if (request.inFlight) {
75
+ this.ctx.showStatus("The /btw chat is still answering. Wait for it to finish.");
76
+ return "busy";
42
77
  }
78
+ const trimmedQuestion = question.trim();
79
+ if (!trimmedQuestion) return "closed";
80
+ if (!this.#acceptQuestion(trimmedQuestion)) return "rejected";
81
+ if (!request.conversationScope) return "closed";
82
+ request.activeTurn = this.#createTurn(trimmedQuestion, request.conversationScope);
83
+ request.inFlight = true;
84
+ request.component.beginTurn(trimmedQuestion);
85
+ void this.#runRequest(request);
86
+ return "accepted";
87
+ }
88
+
89
+ #acceptQuestion(question: string): boolean {
90
+ if (utf8ByteLength(question) <= BTW_MAX_QUESTION_UTF8_BYTES) return true;
91
+ this.ctx.showError(`/btw questions are limited to ${BTW_MAX_QUESTION_UTF8_BYTES} UTF-8 bytes.`);
92
+ return false;
93
+ }
94
+
95
+ #hasModel(): boolean {
96
+ if (this.ctx.session.model) return true;
97
+ this.ctx.showError("No active model available for /btw.");
98
+ return false;
99
+ }
43
100
 
44
- this.#closeActiveRequest({ abort: true });
101
+ #createTurn(question: string, scope: BtwConversationScope): ActiveBtwTurn {
102
+ return { question, scope, abortController: new AbortController(), active: true };
103
+ }
45
104
 
105
+ #openRequest(question: string, scope: BtwConversationScope): BtwRequest {
46
106
  const request: BtwRequest = {
47
- component: new BtwPanelComponent({ question: trimmedQuestion, tui: this.ctx.ui }),
48
- abortController: new AbortController(),
49
- question: trimmedQuestion,
107
+ component: new BtwPanelComponent({ question, tui: this.ctx.ui }),
108
+ conversationScope: scope,
109
+ activeTurn: this.#createTurn(question, scope),
110
+ inFlight: true,
111
+ contextExchanges: [],
50
112
  };
51
113
  this.ctx.btwContainer.clear();
52
114
  this.ctx.btwContainer.addChild(request.component);
53
115
  this.ctx.ui.requestRender();
54
116
  this.#activeRequest = request;
55
- void this.#runRequest(request);
117
+ return request;
56
118
  }
57
119
 
58
120
  async #runRequest(request: BtwRequest): Promise<void> {
121
+ const turn = request.activeTurn;
122
+ const abortController = turn?.abortController;
123
+ if (!turn?.scope || !abortController) return;
59
124
  try {
60
- const promptText = prompt.render(btwUserPrompt, { question: request.question });
61
125
  const { replyText } = await this.ctx.session.runEphemeralTurn({
62
126
  purpose: "btw",
63
- promptText,
127
+ turn,
128
+ contextExchanges: request.contextExchanges.map(exchange => ({ ...exchange })),
64
129
  onTextDelta: delta => {
65
- if (this.#isActiveRequest(request)) {
66
- request.component.appendText(delta);
67
- }
130
+ if (turn.active && this.#isActiveRequest(request)) request.component.appendText(delta);
68
131
  },
69
- signal: request.abortController.signal,
132
+ signal: abortController.signal,
70
133
  });
71
-
72
- if (!this.#isActiveRequest(request)) {
73
- return;
74
- }
75
- if (replyText) {
76
- request.component.setAnswer(replyText);
77
- }
134
+ if (!turn.active || !this.#isActiveRequest(request)) return;
135
+ request.inFlight = false;
136
+ request.contextExchanges = boundBtwExchanges([
137
+ ...request.contextExchanges,
138
+ { question: turn.question, answer: replyText },
139
+ ]);
140
+ if (replyText) request.component.setAnswer(replyText);
78
141
  request.component.markComplete();
79
- } catch (error) {
80
- if (!this.#isActiveRequest(request)) {
81
- return;
82
- }
83
- if (request.abortController.signal.aborted) {
142
+ } catch {
143
+ if (!turn.active || !this.#isActiveRequest(request)) return;
144
+ request.inFlight = false;
145
+ if (abortController.signal.aborted) {
84
146
  request.component.markAborted();
85
147
  return;
86
148
  }
87
- request.component.markError(error instanceof Error ? error.message : String(error));
149
+ request.component.markError(sanitizeBtwError("Side-chat request failed."));
150
+ } finally {
151
+ if (request.activeTurn === turn) request.activeTurn = undefined;
152
+ this.#scrubTurn(turn);
88
153
  }
89
154
  }
90
155
 
91
- #closeActiveRequest(options: { abort: boolean }): void {
156
+ #scrubTurn(turn: ActiveBtwTurn): void {
157
+ turn.active = false;
158
+ turn.question = "";
159
+ turn.abortController = undefined;
160
+ turn.scope = undefined;
161
+ }
162
+
163
+ #closeActiveRequest(): void {
92
164
  const request = this.#activeRequest;
93
165
  if (!request) return;
94
166
  this.#activeRequest = undefined;
95
- if (options.abort) {
96
- request.abortController.abort();
167
+ const turn = request.activeTurn;
168
+ const abortController = turn?.abortController;
169
+ if (turn) this.#scrubTurn(turn);
170
+ request.activeTurn = undefined;
171
+ request.contextExchanges.splice(0);
172
+ request.inFlight = false;
173
+ if (request.conversationScope) {
174
+ request.conversationScope.messages.splice(0);
175
+ request.conversationScope.systemPrompt.splice(0);
176
+ request.conversationScope = undefined;
97
177
  }
178
+ abortController?.abort();
98
179
  request.component.close();
180
+ this.ctx.editor?.setText("");
181
+ this.ctx.pendingImages = [];
99
182
  this.ctx.btwContainer.clear();
100
183
  this.ctx.ui.requestRender();
101
184
  }
@@ -10,12 +10,12 @@ import {
10
10
  type UsageLimit,
11
11
  type UsageReport,
12
12
  } from "@gajae-code/ai";
13
- import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@gajae-code/tui";
13
+ import { type Keybinding, Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@gajae-code/tui";
14
14
  import { formatDuration, Snowflake, setProjectDir } from "@gajae-code/utils";
15
- import { $ } from "bun";
16
15
  import { resolveAppendOnlyMode } from "../../append-only-mode";
17
16
  import { jobElapsedMs } from "../../async";
18
17
  import { reset as resetCapabilities } from "../../capability";
18
+ import type { KeybindingsManager } from "../../config/keybindings";
19
19
  import { clearClaudePluginRootsCache } from "../../discovery/helpers";
20
20
  import { loadCustomShare } from "../../export/custom-share";
21
21
  import type { CompactOptions } from "../../extensibility/extensions/types";
@@ -36,7 +36,7 @@ import { EvalExecutionComponent } from "../../modes/components/eval-execution";
36
36
  import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme";
37
37
  import type { InteractiveModeContext } from "../../modes/types";
38
38
  import { computeContextBreakdown, renderContextUsage } from "../../modes/utils/context-usage";
39
- import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
39
+ import { buildHotkeysMarkdown, formatHotkeyMarkdownCode } from "../../modes/utils/hotkeys-markdown";
40
40
  import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
41
41
  import type { AsyncJobSnapshotItem } from "../../session/agent-session";
42
42
  import type { AuthStorage } from "../../session/auth-storage";
@@ -61,6 +61,39 @@ function showMarkdownPanel(ctx: InteractiveModeContext, title: string, markdown:
61
61
  ctx.ui.requestRender();
62
62
  }
63
63
 
64
+ export function buildHelpMarkdown(keybindings: Pick<KeybindingsManager, "getAccessibleDisplayString">): string {
65
+ const displayKey = (action: Keybinding): string => keybindings.getAccessibleDisplayString(action) || "Disabled";
66
+ const sessionNewKey = formatHotkeyMarkdownCode(displayKey("app.session.new"));
67
+ const selectModelKey = formatHotkeyMarkdownCode(displayKey("app.model.select"));
68
+ const autocompleteNavigationKeys = formatHotkeyMarkdownCode(
69
+ `${displayKey("tui.select.up")}/${displayKey("tui.select.down")}`,
70
+ false,
71
+ );
72
+ const autocompleteConfirmKeys = formatHotkeyMarkdownCode(
73
+ `${displayKey("tui.input.submit")}/${displayKey("tui.input.tab")}`,
74
+ false,
75
+ );
76
+
77
+ return [
78
+ "**Beginner actions**",
79
+ "",
80
+ "| Action | How |",
81
+ "|---|---|",
82
+ `| Start a fresh session | ${sessionNewKey} or \`/new\` |`,
83
+ "| Resume another session | `/resume` |",
84
+ "| Show session details | `/session info` |",
85
+ "| Delete current session transcript/artifacts | `/session delete` |",
86
+ `| Select a model | \`/model\` or ${selectModelKey} |`,
87
+ "| Show all shortcuts | `?` on an empty prompt or `/hotkeys` |",
88
+ "",
89
+ "**Finding commands**",
90
+ "",
91
+ `- Type \`/\` to browse slash commands, then use ${autocompleteNavigationKeys} and ${autocompleteConfirmKeys}.`,
92
+ "- Type `#` to browse prompt actions like starting a session, pasting an image, or moving the cursor.",
93
+ "- Type `!` for shell commands and `$` for Python snippets.",
94
+ ].join("\n");
95
+ }
96
+
64
97
  export class CommandController {
65
98
  constructor(private readonly ctx: InteractiveModeContext) {}
66
99
 
@@ -120,21 +153,34 @@ export class CommandController {
120
153
  }
121
154
 
122
155
  async handleShareCommand(): Promise<void> {
123
- const tmpFile = path.join(os.tmpdir(), `${Snowflake.next()}.html`);
124
- const cleanupTempFile = async () => {
156
+ let tempDir: string | undefined;
157
+ try {
158
+ let tmpFile: string;
125
159
  try {
126
- await fs.rm(tmpFile, { force: true });
127
- } catch {
128
- // Ignore cleanup errors
160
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-share-"));
161
+ if (process.platform !== "win32") await fs.chmod(tempDir, 0o700);
162
+ tmpFile = path.join(tempDir, "session.html");
163
+ const file = await fs.open(tmpFile, "wx", 0o600);
164
+ await file.close();
165
+ await this.ctx.session.exportToHtml(tmpFile);
166
+ if (process.platform !== "win32") await fs.chmod(tmpFile, 0o600);
167
+ } catch (error: unknown) {
168
+ this.ctx.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
169
+ return;
170
+ }
171
+ await this.#shareExport(tmpFile);
172
+ } finally {
173
+ if (tempDir) {
174
+ try {
175
+ await fs.rm(tempDir, { recursive: true, force: true });
176
+ } catch {
177
+ // Ignore cleanup errors
178
+ }
129
179
  }
130
- };
131
- try {
132
- await this.ctx.session.exportToHtml(tmpFile);
133
- } catch (error: unknown) {
134
- this.ctx.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
135
- return;
136
180
  }
181
+ }
137
182
 
183
+ async #shareExport(tmpFile: string): Promise<void> {
138
184
  try {
139
185
  const customShare = await loadCustomShare();
140
186
  if (customShare) {
@@ -149,7 +195,6 @@ export class CommandController {
149
195
  this.ctx.editorContainer.clear();
150
196
  this.ctx.editorContainer.addChild(this.ctx.editor);
151
197
  this.ctx.ui.setFocus(this.ctx.editor);
152
- await cleanupTempFile();
153
198
  };
154
199
 
155
200
  try {
@@ -176,20 +221,22 @@ export class CommandController {
176
221
  }
177
222
  }
178
223
  } catch (err) {
179
- await cleanupTempFile();
180
224
  this.ctx.showError(err instanceof Error ? err.message : String(err));
181
225
  return;
182
226
  }
183
227
 
184
228
  try {
185
- const authResult = await $`gh auth status`.quiet().nothrow();
186
- if (authResult.exitCode !== 0) {
187
- await cleanupTempFile();
229
+ const authProcess = Bun.spawn(["gh", "auth", "status"], {
230
+ env: process.env,
231
+ stdin: "ignore",
232
+ stdout: "ignore",
233
+ stderr: "ignore",
234
+ });
235
+ if ((await authProcess.exited) !== 0) {
188
236
  this.ctx.showError("GitHub CLI is not logged in. Run 'gh auth login' first.");
189
237
  return;
190
238
  }
191
239
  } catch {
192
- await cleanupTempFile();
193
240
  this.ctx.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
194
241
  return;
195
242
  }
@@ -205,27 +252,40 @@ export class CommandController {
205
252
  this.ctx.editorContainer.clear();
206
253
  this.ctx.editorContainer.addChild(this.ctx.editor);
207
254
  this.ctx.ui.setFocus(this.ctx.editor);
208
- await cleanupTempFile();
209
- };
210
-
211
- loader.onAbort = () => {
212
- void restoreEditor();
213
- this.ctx.showStatus("Share cancelled");
214
255
  };
215
256
 
257
+ let cancellationRequested = false;
216
258
  try {
217
- const result = await $`gh gist create --public=false ${tmpFile}`.quiet().nothrow();
218
- if (loader.signal.aborted) return;
259
+ const gistProcess = Bun.spawn(["gh", "gist", "create", "--public=false", tmpFile], {
260
+ env: process.env,
261
+ stdin: "ignore",
262
+ stdout: "pipe",
263
+ stderr: "pipe",
264
+ signal: loader.signal,
265
+ });
266
+ loader.onAbort = () => {
267
+ cancellationRequested = gistProcess.exitCode === null;
268
+ };
269
+ const stdoutPromise = new Response(gistProcess.stdout).text();
270
+ const stderrPromise = new Response(gistProcess.stderr).text();
271
+ const exitCode = await gistProcess.exited;
272
+ const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
273
+
274
+ if (cancellationRequested) {
275
+ await restoreEditor();
276
+ this.ctx.showStatus("Share cancelled");
277
+ return;
278
+ }
219
279
 
220
280
  await restoreEditor();
221
281
 
222
- if (result.exitCode !== 0) {
223
- const errorMsg = result.stderr.toString("utf-8").trim() || "Unknown error";
282
+ if (exitCode !== 0) {
283
+ const errorMsg = stderr.trim() || "Unknown error";
224
284
  this.ctx.showError(`Failed to create gist: ${errorMsg}`);
225
285
  return;
226
286
  }
227
287
 
228
- const gistUrl = result.stdout.toString("utf-8").trim();
288
+ const gistUrl = stdout.trim();
229
289
  const gistId = gistUrl.split("/").pop();
230
290
  if (!gistId) {
231
291
  this.ctx.showError("Failed to parse gist ID from gh output");
@@ -236,10 +296,12 @@ export class CommandController {
236
296
  this.ctx.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
237
297
  this.openInBrowser(previewUrl);
238
298
  } catch (error: unknown) {
239
- if (!loader.signal.aborted) {
240
- await restoreEditor();
241
- this.ctx.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`);
299
+ await restoreEditor();
300
+ if (cancellationRequested) {
301
+ this.ctx.showStatus("Share cancelled");
302
+ return;
242
303
  }
304
+ this.ctx.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`);
243
305
  }
244
306
  }
245
307
 
@@ -577,26 +639,7 @@ export class CommandController {
577
639
  }
578
640
 
579
641
  handleHelpCommand(): void {
580
- const sessionNewKey = this.ctx.keybindings.getDisplayString("app.session.new") || "/new";
581
- const markdown = [
582
- "**Beginner actions**",
583
- "",
584
- "| Action | How |",
585
- "|---|---|",
586
- `| Start a fresh session | \`${sessionNewKey}\` or \`/new\` |`,
587
- "| Resume another session | `/resume` |",
588
- "| Show session details | `/session info` |",
589
- "| Delete current session transcript/artifacts | `/session delete` |",
590
- "| Select a model | `/model` or `Ctrl+L` |",
591
- "| Show all shortcuts | `?` on an empty prompt or `/hotkeys` |",
592
- "",
593
- "**Finding commands**",
594
- "",
595
- "- Type `/` to browse slash commands, then use arrows and Enter/Tab.",
596
- "- Type `#` to browse prompt actions like starting a session, pasting an image, or moving the cursor.",
597
- "- Type `!` for shell commands and `$` for Python snippets.",
598
- ].join("\n");
599
- showMarkdownPanel(this.ctx, "GJC Help", markdown);
642
+ showMarkdownPanel(this.ctx, "GJC Help", buildHelpMarkdown(this.ctx.keybindings));
600
643
  }
601
644
 
602
645
  handleToolsCommand(): void {
@@ -242,6 +242,7 @@ export class EventController {
242
242
  }
243
243
 
244
244
  async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
245
+ this.ctx.promptSuggestion?.onAgentStart();
245
246
  this.#lastIntent = undefined;
246
247
  this.#readToolCallArgs.clear();
247
248
  this.#readToolCallAssistantComponents.clear();
@@ -836,6 +837,7 @@ export class EventController {
836
837
  this.ctx.ui.requestRender();
837
838
  this.#scheduleIdleCompaction();
838
839
  this.sendCompletionNotification();
840
+ this.ctx.promptSuggestion?.onAgentEnd();
839
841
  }
840
842
 
841
843
  async #handleAutoCompactionStart(
@@ -847,6 +849,10 @@ export class EventController {
847
849
  this.ctx.editor.onEscape = () => {
848
850
  this.ctx.session.abortCompaction();
849
851
  };
852
+ if (this.ctx.loadingAnimation) {
853
+ this.ctx.loadingAnimation.stop();
854
+ this.ctx.loadingAnimation = undefined;
855
+ }
850
856
  this.ctx.statusContainer.clear();
851
857
  const reasonText =
852
858
  event.reason === "overflow" ? "Context overflow detected, " : event.reason === "idle" ? "Idle " : "";
@@ -939,6 +945,10 @@ export class EventController {
939
945
  this.ctx.session.abortRetry();
940
946
  }
941
947
  };
948
+ if (this.ctx.loadingAnimation) {
949
+ this.ctx.loadingAnimation.stop();
950
+ this.ctx.loadingAnimation = undefined;
951
+ }
942
952
  this.ctx.statusContainer.clear();
943
953
  // Stop any prior retry loader/timer before installing a new one.
944
954
  this.ctx.retryLoader?.stop();
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
4
4
  import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@gajae-code/tui";
5
- import { $env, logger, sanitizeText } from "@gajae-code/utils";
5
+ import { $pickenv, logger, sanitizeText } from "@gajae-code/utils";
6
6
  import { type AppKeybinding, KEYBINDINGS } from "../../config/keybindings";
7
7
  import { isSettingsInitialized, settings } from "../../config/settings";
8
8
  import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
@@ -725,8 +725,12 @@ export class InputController {
725
725
  });
726
726
  }
727
727
 
728
+ // Tab on an empty composer accepts the pending ghost-text prompt suggestion.
729
+ this.ctx.editor.onTab = (text: string) => this.ctx.promptSuggestion?.tryAcceptOnTab(text) === true;
730
+
728
731
  this.ctx.editor.onChange = (text: string) => {
729
732
  this.#resetEscapeGestures();
733
+ this.ctx.promptSuggestion?.notifyEditorChanged(text);
730
734
  const wasBashMode = this.ctx.isBashMode;
731
735
  const wasBashNoContext = this.ctx.isBashNoContext;
732
736
  const wasPythonMode = this.ctx.isPythonMode;
@@ -752,6 +756,29 @@ export class InputController {
752
756
  async submitText(text: string, composer: ComposerSubmissionOptions): Promise<void> {
753
757
  text = text.trim();
754
758
  if ((!isSettingsInitialized() || settings.get("emojiAutocomplete")) && text) text = expandEmoticons(text);
759
+ if (this.ctx.hasActiveBtw()) {
760
+ if (!text) return;
761
+ if (!text.startsWith("/")) {
762
+ const result = await this.ctx.handleBtwFollowUp(text);
763
+ if (result === "accepted" && this.#canModifyComposer(composer)) {
764
+ this.ctx.editor.setText("");
765
+ this.ctx.pendingImages = [];
766
+ }
767
+ return;
768
+ }
769
+ }
770
+
771
+ if (/^\/btw(?:\s|$)/.test(text)) {
772
+ const slashResult = await executeBuiltinSlashCommand(text, {
773
+ ctx: this.ctx,
774
+ handleBackgroundCommand: () => this.handleBackgroundCommand(),
775
+ composer,
776
+ });
777
+ if (slashResult === true) {
778
+ this.ctx.pendingImages = [];
779
+ return;
780
+ }
781
+ }
755
782
 
756
783
  // Empty submit while streaming with queued messages: flush queues immediately
757
784
  if (!text && this.ctx.session.isStreaming && this.ctx.session.queuedMessageCount > 0) {
@@ -907,7 +934,7 @@ export class InputController {
907
934
 
908
935
  // Generate session title on first message
909
936
  const hasUserMessages = this.ctx.session.messages.some((m: AgentMessage) => m.role === "user");
910
- if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !$env.PI_NO_TITLE) {
937
+ if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !$pickenv("GJC_NO_TITLE", "PI_NO_TITLE")) {
911
938
  const registry = this.ctx.session.modelRegistry;
912
939
  generateSessionTitle(
913
940
  text,
@@ -1357,6 +1384,14 @@ export class InputController {
1357
1384
  async handleFollowUp(): Promise<void> {
1358
1385
  const text = this.ctx.editor.getText().trim();
1359
1386
  if (!text) return;
1387
+ // While /btw is open, plain text stays in the side chat. Slash-origin
1388
+ // input keeps normal dispatch so commands remain available.
1389
+ if (this.ctx.hasActiveBtw() && !text.startsWith("/")) {
1390
+ if ((await this.ctx.handleBtwFollowUp(text)) === "accepted") {
1391
+ this.ctx.editor.setText("");
1392
+ }
1393
+ return;
1394
+ }
1360
1395
 
1361
1396
  // Compaction first: while compacting, queue free text and `/skill:*`
1362
1397
  // commands in the compaction-local queue. `flushCompactionQueue`
@@ -1758,6 +1793,7 @@ export class InputController {
1758
1793
  moveCursorToMessageStart: () => this.ctx.editor.moveToMessageStart(),
1759
1794
  moveCursorToLineStart: () => this.ctx.editor.moveToLineStart(),
1760
1795
  moveCursorToLineEnd: () => this.ctx.editor.moveToLineEnd(),
1796
+ getPromptSuggestion: () => this.ctx.promptSuggestion?.current ?? null,
1761
1797
  });
1762
1798
  }
1763
1799