@gajae-code/coding-agent 0.11.6 → 0.11.7

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 (131) hide show
  1. package/CHANGELOG.md +37 -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/commands/daemon.d.ts +2 -2
  5. package/dist/types/commands/sdk.d.ts +20 -2
  6. package/dist/types/config/keybindings.d.ts +1 -1
  7. package/dist/types/config/settings-schema.d.ts +56 -6
  8. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  9. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
  10. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
  12. package/dist/types/main.d.ts +24 -1
  13. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  14. package/dist/types/modes/components/model-selector.d.ts +2 -0
  15. package/dist/types/modes/components/tool-status-header.d.ts +1 -0
  16. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  17. package/dist/types/modes/interactive-mode.d.ts +2 -0
  18. package/dist/types/modes/types.d.ts +1 -0
  19. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  20. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  21. package/dist/types/sdk/bus/index.d.ts +1 -1
  22. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  23. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -3
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
  26. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  27. package/dist/types/sdk/client/discovery.d.ts +2 -0
  28. package/dist/types/sdk/client/liveness.d.ts +7 -0
  29. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  30. package/dist/types/sdk/transport/index.d.ts +12 -0
  31. package/dist/types/sdk/transport/relay.d.ts +43 -0
  32. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  33. package/dist/types/sdk/transport/socket.d.ts +7 -0
  34. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  35. package/dist/types/session/agent-session.d.ts +28 -5
  36. package/dist/types/session/btw-contract.d.ts +16 -0
  37. package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
  38. package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
  39. package/dist/types/session/session-manager.d.ts +25 -4
  40. package/dist/types/setup/credential-import.d.ts +5 -1
  41. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  42. package/dist/types/ssh/utils.d.ts +1 -0
  43. package/dist/types/task/discovery.d.ts +3 -1
  44. package/dist/types/tools/fetch.d.ts +5 -8
  45. package/dist/types/tools/image-gen.d.ts +16 -0
  46. package/dist/types/tools/skill-discovery.d.ts +7 -0
  47. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  48. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  49. package/dist/types/web/insane/bridge.d.ts +1 -0
  50. package/dist/types/web/insane/url-guard.d.ts +17 -0
  51. package/package.json +8 -8
  52. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  53. package/src/capability/index.ts +50 -42
  54. package/src/capability/types.ts +3 -0
  55. package/src/cli/setup-cli.ts +10 -4
  56. package/src/cli.ts +34 -9
  57. package/src/commands/daemon.ts +22 -5
  58. package/src/commands/sdk.ts +42 -9
  59. package/src/config/keybindings.ts +1 -1
  60. package/src/config/model-profiles.ts +18 -0
  61. package/src/config/settings-schema.ts +57 -6
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
  63. package/src/discovery/ssh.ts +12 -1
  64. package/src/eval/py/kernel.ts +1 -21
  65. package/src/eval/py/runner-artifact.ts +84 -0
  66. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  67. package/src/gjc-runtime/ultragoal-guard.ts +88 -10
  68. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
  69. package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
  70. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  71. package/src/gjc-runtime/workflow-manifest.ts +23 -0
  72. package/src/internal-urls/docs-index.generated.ts +8 -7
  73. package/src/internal-urls/local-protocol.ts +32 -6
  74. package/src/main.ts +86 -27
  75. package/src/modes/action-registry.ts +1 -1
  76. package/src/modes/components/btw-panel.ts +82 -16
  77. package/src/modes/components/model-selector.ts +31 -3
  78. package/src/modes/components/tool-status-header.ts +6 -1
  79. package/src/modes/controllers/btw-controller.ts +119 -36
  80. package/src/modes/controllers/input-controller.ts +31 -0
  81. package/src/modes/controllers/selector-controller.ts +106 -12
  82. package/src/modes/interactive-mode.ts +16 -7
  83. package/src/modes/types.ts +1 -0
  84. package/src/prompts/system/btw-user.md +3 -8
  85. package/src/prompts/tools/skill-discovery.md +1 -0
  86. package/src/runtime-mcp/client.ts +41 -55
  87. package/src/runtime-mcp/content-limits.ts +71 -0
  88. package/src/runtime-mcp/json-rpc.ts +23 -9
  89. package/src/runtime-mcp/transports/http.ts +50 -16
  90. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  91. package/src/sdk/bus/index.ts +4 -5
  92. package/src/sdk/bus/lifecycle-commands.ts +16 -7
  93. package/src/sdk/bus/notification-service.ts +41 -18
  94. package/src/sdk/bus/telegram-daemon-contract.ts +8 -3
  95. package/src/sdk/bus/telegram-daemon-control.ts +2 -2
  96. package/src/sdk/bus/telegram-daemon.ts +1663 -375
  97. package/src/sdk/bus/topic-registry.ts +451 -12
  98. package/src/sdk/client/discovery.ts +19 -3
  99. package/src/sdk/client/liveness.ts +49 -0
  100. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  101. package/src/sdk/session.ts +29 -5
  102. package/src/sdk/transport/auth-preface.ts +82 -0
  103. package/src/sdk/transport/index.ts +18 -0
  104. package/src/sdk/transport/relay.ts +252 -0
  105. package/src/sdk/transport/serve-cli.ts +105 -0
  106. package/src/sdk/transport/socket.ts +197 -0
  107. package/src/sdk/transport/stdio.ts +17 -0
  108. package/src/session/agent-session.ts +240 -149
  109. package/src/session/blob-store.ts +19 -6
  110. package/src/session/btw-contract.ts +59 -0
  111. package/src/session/internal/managed-session-scope.ts +313 -57
  112. package/src/session/internal/managed-session-storage.ts +146 -35
  113. package/src/session/session-manager.ts +319 -85
  114. package/src/setup/credential-import.ts +28 -3
  115. package/src/setup/provider-onboarding.ts +30 -1
  116. package/src/setup/provider-presets.json +2 -1
  117. package/src/slash-commands/builtin-registry.ts +1 -1
  118. package/src/ssh/utils.ts +14 -0
  119. package/src/task/discovery.ts +8 -2
  120. package/src/tools/ask.ts +1 -1
  121. package/src/tools/fetch.ts +13 -131
  122. package/src/tools/image-gen.ts +301 -20
  123. package/src/tools/read.ts +2 -0
  124. package/src/tools/skill-discovery.ts +39 -3
  125. package/src/tools/sqlite-reader.ts +63 -5
  126. package/src/utils/pasted-image-path.ts +15 -6
  127. package/src/utils/shell-snapshot.ts +173 -63
  128. package/src/web/insane/bridge.ts +5 -1
  129. package/src/web/insane/url-guard.ts +138 -27
  130. package/src/web/scrapers/types.ts +19 -49
  131. 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
  }
@@ -752,6 +752,29 @@ export class InputController {
752
752
  async submitText(text: string, composer: ComposerSubmissionOptions): Promise<void> {
753
753
  text = text.trim();
754
754
  if ((!isSettingsInitialized() || settings.get("emojiAutocomplete")) && text) text = expandEmoticons(text);
755
+ if (this.ctx.hasActiveBtw()) {
756
+ if (!text) return;
757
+ if (!text.startsWith("/")) {
758
+ const result = await this.ctx.handleBtwFollowUp(text);
759
+ if (result === "accepted" && this.#canModifyComposer(composer)) {
760
+ this.ctx.editor.setText("");
761
+ this.ctx.pendingImages = [];
762
+ }
763
+ return;
764
+ }
765
+ }
766
+
767
+ if (/^\/btw(?:\s|$)/.test(text)) {
768
+ const slashResult = await executeBuiltinSlashCommand(text, {
769
+ ctx: this.ctx,
770
+ handleBackgroundCommand: () => this.handleBackgroundCommand(),
771
+ composer,
772
+ });
773
+ if (slashResult === true) {
774
+ this.ctx.pendingImages = [];
775
+ return;
776
+ }
777
+ }
755
778
 
756
779
  // Empty submit while streaming with queued messages: flush queues immediately
757
780
  if (!text && this.ctx.session.isStreaming && this.ctx.session.queuedMessageCount > 0) {
@@ -1357,6 +1380,14 @@ export class InputController {
1357
1380
  async handleFollowUp(): Promise<void> {
1358
1381
  const text = this.ctx.editor.getText().trim();
1359
1382
  if (!text) return;
1383
+ // While /btw is open, plain text stays in the side chat. Slash-origin
1384
+ // input keeps normal dispatch so commands remain available.
1385
+ if (this.ctx.hasActiveBtw() && !text.startsWith("/")) {
1386
+ if ((await this.ctx.handleBtwFollowUp(text)) === "accepted") {
1387
+ this.ctx.editor.setText("");
1388
+ }
1389
+ return;
1390
+ }
1360
1391
 
1361
1392
  // Compaction first: while compacting, queue free text and `/skill:*`
1362
1393
  // commands in the compaction-local queue. `flushCompactionQueue`
@@ -101,8 +101,10 @@ import {
101
101
  } from "../../setup/model-onboarding-guidance";
102
102
  import { addApiCompatibleProvider, formatProviderSetupResult } from "../../setup/provider-onboarding";
103
103
  import {
104
+ IMAGE_PROVIDER_DEFAULTS,
104
105
  isConfigurableSearchProviderId,
105
106
  isSearchProviderPreference,
107
+ setConfiguredImageModel,
106
108
  setPreferredImageProvider,
107
109
  setPreferredSearchProvider,
108
110
  setSearchFallbackProviders,
@@ -190,6 +192,7 @@ export function buildStatusLineSettings(settingsInstance: Settings): StatusLineS
190
192
  rightSegments: settingsInstance.get("statusLine.rightSegments"),
191
193
  separator: settingsInstance.get("statusLine.separator"),
192
194
  showHookStatus: settingsInstance.get("statusLine.showHookStatus"),
195
+ showActionHints: settingsInstance.get("statusLine.showActionHints"),
193
196
  sessionAccent: settingsInstance.get("statusLine.sessionAccent"),
194
197
  maxRows: settingsInstance.get("statusLine.maxRows"),
195
198
  segmentOptions: settingsInstance.get("statusLine.segmentOptions"),
@@ -1103,6 +1106,68 @@ export class SelectorController {
1103
1106
  }
1104
1107
  }
1105
1108
 
1109
+ async #handleImageGenerationConfig(): Promise<void> {
1110
+ const provider = await this.ctx.showHookInput(
1111
+ "Image Generation provider (auto, openai, gemini, openrouter, antigravity, custom)",
1112
+ "auto",
1113
+ );
1114
+ if (provider === undefined) return;
1115
+ const normalized = provider.trim().toLowerCase();
1116
+ const validProviders = ["auto", "openai", "gemini", "openrouter", "antigravity", "custom"];
1117
+ if (!validProviders.includes(normalized)) {
1118
+ this.ctx.showStatus(`Invalid image provider: ${normalized}. Valid: ${validProviders.join(", ")}`);
1119
+ return;
1120
+ }
1121
+ let model: string | undefined;
1122
+ if (normalized !== "auto" && normalized !== "custom") {
1123
+ const defaultModel = IMAGE_PROVIDER_DEFAULTS[normalized];
1124
+ model = await this.ctx.showHookInput(`Image model for ${normalized} (default: ${defaultModel})`, defaultModel);
1125
+ if (model === undefined) return;
1126
+ model = model.trim() || defaultModel;
1127
+ }
1128
+ let customUrl: string | undefined;
1129
+ let customKey: string | undefined;
1130
+ if (normalized === "custom") {
1131
+ customUrl = await this.ctx.showHookInput("Custom image endpoint base URL");
1132
+ if (!customUrl?.trim()) {
1133
+ this.ctx.showStatus("Custom image endpoint requires a base URL");
1134
+ return;
1135
+ }
1136
+ model = await this.ctx.showHookInput("Custom image model", IMAGE_PROVIDER_DEFAULTS.openai);
1137
+ if (model === undefined) return;
1138
+ model = model.trim() || IMAGE_PROVIDER_DEFAULTS.openai;
1139
+ customKey = await this.ctx.showHookInput("Custom image endpoint API key");
1140
+ }
1141
+ const scope = await this.ctx.showHookInput(
1142
+ "Scope: 'session' (this session only) or 'default' (persist)",
1143
+ "session",
1144
+ );
1145
+ if (scope === undefined) return;
1146
+ const persistDefault = scope.trim().toLowerCase() === "default";
1147
+
1148
+ const imageProvider = normalized as "auto" | "openai" | "gemini" | "openrouter" | "antigravity" | "custom";
1149
+ setPreferredImageProvider(imageProvider === "custom" ? "auto" : imageProvider);
1150
+ setConfiguredImageModel({
1151
+ provider: imageProvider,
1152
+ model: model ?? null,
1153
+ customUrl: customUrl?.trim(),
1154
+ customKey: customKey?.trim(),
1155
+ });
1156
+
1157
+ if (persistDefault) {
1158
+ this.ctx.settings.set("providers.image", imageProvider);
1159
+ if (model) this.ctx.settings.set("providers.imageModel", model);
1160
+ if (customUrl?.trim()) this.ctx.settings.set("providers.imageCustomUrl", customUrl.trim());
1161
+ if (customKey?.trim()) this.ctx.settings.set("providers.imageCustomKey", customKey.trim());
1162
+ }
1163
+
1164
+ const displayModel =
1165
+ model ?? (normalized !== "auto" && normalized !== "custom" ? IMAGE_PROVIDER_DEFAULTS[normalized] : undefined);
1166
+ const label = normalized === "auto" ? "Auto" : `${normalized}${displayModel ? ` (${displayModel})` : ""}`;
1167
+ this.ctx.showStatus(`Image Generation: ${label}${persistDefault ? " (default)" : " (session)"}`);
1168
+ this.ctx.ui.requestRender();
1169
+ }
1170
+
1106
1171
  showCustomProviderWizard(): void {
1107
1172
  this.showSelector(done => {
1108
1173
  let wizard: CustomProviderWizardComponent;
@@ -1516,6 +1581,7 @@ export class SelectorController {
1516
1581
  case "statusLine.separator":
1517
1582
  case "statusLineShowHooks":
1518
1583
  case "statusLine.showHookStatus":
1584
+ case "statusLine.showActionHints":
1519
1585
  case "statusLine.sessionAccent":
1520
1586
  case "statusLine.maxRows":
1521
1587
  case "statusLine.leftSegments":
@@ -1563,16 +1629,34 @@ export class SelectorController {
1563
1629
  }
1564
1630
  break;
1565
1631
  case "providers.image":
1632
+ case "providers.imageModel":
1633
+ case "providers.imageCustomUrl":
1634
+ case "providers.imageCustomKey":
1635
+ case "providers.imageCustomKeyEnv": {
1636
+ const imgProvider = this.ctx.settings.get("providers.image");
1637
+ const imgModel = this.ctx.settings.get("providers.imageModel");
1638
+ const imgCustomUrl = this.ctx.settings.get("providers.imageCustomUrl");
1639
+ const imgCustomKey = this.ctx.settings.get("providers.imageCustomKey");
1640
+ const imgCustomKeyEnv = this.ctx.settings.get("providers.imageCustomKeyEnv");
1566
1641
  if (
1567
- value === "auto" ||
1568
- value === "openai" ||
1569
- value === "gemini" ||
1570
- value === "openrouter" ||
1571
- value === "antigravity"
1642
+ imgProvider === "auto" ||
1643
+ imgProvider === "openai" ||
1644
+ imgProvider === "gemini" ||
1645
+ imgProvider === "openrouter" ||
1646
+ imgProvider === "antigravity" ||
1647
+ imgProvider === "custom"
1572
1648
  ) {
1573
- setPreferredImageProvider(value);
1649
+ setPreferredImageProvider(imgProvider === "custom" ? "auto" : imgProvider);
1650
+ setConfiguredImageModel({
1651
+ provider: imgProvider,
1652
+ model: imgModel ?? null,
1653
+ customUrl: imgCustomUrl,
1654
+ customKey: imgCustomKey,
1655
+ customKeyEnv: imgCustomKeyEnv,
1656
+ });
1574
1657
  }
1575
1658
  break;
1659
+ }
1576
1660
 
1577
1661
  // MCP update injection - live subscribe/unsubscribe
1578
1662
  case "mcp.notifications":
@@ -1631,6 +1715,11 @@ export class SelectorController {
1631
1715
  await this.#deleteCustomModelPreset(selection.profileName, modelSelector);
1632
1716
  return;
1633
1717
  }
1718
+ if (selection.kind === "imageGeneration") {
1719
+ done();
1720
+ await this.#handleImageGenerationConfig();
1721
+ return;
1722
+ }
1634
1723
  if (selection.kind === "profile") {
1635
1724
  await this.#applyModelProfile(selection.profileName, selection.setDefault);
1636
1725
  done();
@@ -2057,10 +2146,7 @@ export class SelectorController {
2057
2146
  }
2058
2147
 
2059
2148
  async showSessionSelector(): Promise<void> {
2060
- const sessions = await SessionManager.listForResumePickerReadOnly(
2061
- this.ctx.sessionManager.getCwd(),
2062
- this.ctx.sessionManager.getSessionDir(),
2063
- );
2149
+ const sessions = await this.ctx.sessionManager.listForResumePickerReadOnly();
2064
2150
  this.showSelector(done => {
2065
2151
  const selector = new SessionSelectorComponent(
2066
2152
  sessions,
@@ -2156,8 +2242,16 @@ export class SelectorController {
2156
2242
  this.#clearTransientSessionUi();
2157
2243
  const migrationPolicy =
2158
2244
  this.ctx.settings?.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
2159
- const writableSessionPath = await SessionManager.prepareManagedCandidateForWrite(sessionPath, migrationPolicy);
2160
-
2245
+ let writableSessionPath = sessionPath;
2246
+ if (this.ctx.sessionManager.isManagedDestination()) {
2247
+ const inspection = await SessionManager.inspectSessionTailReadOnly(sessionPath);
2248
+ if (inspection.kind === "error") throw new Error(`Could not inspect selected session: ${inspection.reason}`);
2249
+ writableSessionPath = await this.ctx.sessionManager.prepareManagedCandidateForStrictAdoption(
2250
+ sessionPath,
2251
+ migrationPolicy,
2252
+ inspection.identity,
2253
+ );
2254
+ }
2161
2255
  // Switch session via AgentSession (emits hook and tool session events)
2162
2256
  if (!(await this.ctx.session.switchSession(writableSessionPath))) return;
2163
2257
  const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
@@ -264,6 +264,10 @@ export interface InteractiveModeOptions {
264
264
  initialMessages?: string[];
265
265
  }
266
266
 
267
+ export function selectShutdownDraft(editorText: string, hasActiveBtw: boolean): string {
268
+ return hasActiveBtw ? "" : editorText;
269
+ }
270
+
267
271
  export class InteractiveMode implements InteractiveModeContext {
268
272
  session: AgentSession;
269
273
  sessionManager: SessionManager;
@@ -1322,19 +1326,20 @@ export class InteractiveMode implements InteractiveModeContext {
1322
1326
  if (this.#isShuttingDown) return;
1323
1327
  this.#isShuttingDown = true;
1324
1328
 
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();
1329
+ // `/btw` owns the shared composer while its panel is open. Never persist a
1330
+ // side-chat draft or pending side-chat images into the main-session draft.
1331
+ const hadActiveBtw = this.#btwController.hasOpenPanel();
1332
+ const draftText = selectShutdownDraft(this.editor.getText(), hadActiveBtw);
1333
+ if (hadActiveBtw) this.pendingImages = [];
1334
+ this.#btwController.dispose();
1329
1335
 
1330
- // Flush pending session writes before shutdown
1336
+ // Flush pending session writes before shutdown.
1331
1337
  await this.sessionManager.flush();
1332
1338
  try {
1333
1339
  await this.sessionManager.saveDraft(draftText);
1334
1340
  } catch (err) {
1335
1341
  logger.warn("Failed to save session draft", { error: String(err) });
1336
1342
  }
1337
- this.#btwController.dispose();
1338
1343
 
1339
1344
  // Emit shutdown event to hooks
1340
1345
  this.session.setSdkPlanModeHandler(null);
@@ -2025,7 +2030,11 @@ export class InteractiveMode implements InteractiveModeContext {
2025
2030
  }
2026
2031
 
2027
2032
  hasActiveBtw(): boolean {
2028
- return this.#btwController.hasActiveRequest();
2033
+ return this.#btwController.hasOpenPanel();
2034
+ }
2035
+
2036
+ handleBtwFollowUp(question: string): Promise<"accepted" | "busy" | "closed" | "rejected"> {
2037
+ return this.#btwController.submitFollowUp(question);
2029
2038
  }
2030
2039
 
2031
2040
  handleBtwEscape(): boolean {
@@ -338,6 +338,7 @@ export interface InteractiveModeContext {
338
338
  handleBackgroundCommand(): void;
339
339
  handleImagePaste(): Promise<boolean>;
340
340
  handleBtwCommand(question: string): Promise<void>;
341
+ handleBtwFollowUp(question: string): Promise<"accepted" | "busy" | "closed" | "rejected">;
341
342
  hasActiveBtw(): boolean;
342
343
  handleBtwEscape(): boolean;
343
344
  cycleThinkingLevel(): void;
@@ -1,8 +1,3 @@
1
- <btw>
2
- This is an ephemeral side question for the current interactive session.
3
- Answer briefly and directly using the conversation context already provided.
4
- Do not use tools.
5
- Do not ask follow-up questions.
6
- Question:
7
- {{question}}
8
- </btw>
1
+ You are answering inside an ephemeral multi-turn side chat for the interactive session.
2
+ Use only the sanitized visible-text conversation scope and prior side-chat turns provided as messages.
3
+ Answer directly. Do not use tools or request hidden session state.
@@ -3,6 +3,7 @@ Discover project and user runtime skills without loading full skill content.
3
3
  <instruction>
4
4
  - Searches only custom runtime skill locations: nearest project `.gjc/skills`; then, under the home directory, canonical `<config>/agent/skills`, configured legacy `<config>/skills`, and historical legacy `.gjc/skills`. `<config>` is the home-relative directory name from `GJC_CONFIG_DIR`, then `PI_CONFIG_DIR`, then `.gjc`; even an absolute-looking configured name is joined beneath `<home>`. Duplicate names use that exact precedence. Built-in, bundled, and internal workflow skills are intentionally excluded.
5
5
  - Returns thin metadata only: name, description, source scope, path, and use conditions when present.
6
+ - When zero candidates are returned because discovery config is disabled (`skills.enabled`, `skills.enablePiProject`, `skills.enablePiUser`), the result carries a `notice` explaining which setting blocked the search — an empty result without a `notice` means the searched scopes genuinely contain no matching skills.
6
7
  - To load a selected skill's full `SKILL.md`, invoke it through the existing `skill` tool with the exact `name` returned here.
7
8
  </instruction>
8
9
 
@@ -15,15 +15,12 @@ import type {
15
15
  MCPInitializeParams,
16
16
  MCPInitializeResult,
17
17
  MCPPrompt,
18
- MCPPromptsListResult,
19
18
  MCPRequestOptions,
20
19
  MCPResource,
21
20
  MCPResourceReadParams,
22
21
  MCPResourceReadResult,
23
22
  MCPResourceSubscribeParams,
24
- MCPResourcesListResult,
25
23
  MCPResourceTemplate,
26
- MCPResourceTemplatesListResult,
27
24
  MCPServerCapabilities,
28
25
  MCPServerConfig,
29
26
  MCPServerConnection,
@@ -43,6 +40,9 @@ const PROTOCOL_VERSION = "2025-03-26";
43
40
  /** Default connection timeout in ms */
44
41
  const CONNECTION_TIMEOUT_MS = 30_000;
45
42
 
43
+ const MAX_PAGINATION_PAGES = 100,
44
+ MAX_PAGINATION_ITEMS = 10_000;
45
+
46
46
  /** Client info sent during initialization */
47
47
  const CLIENT_INFO = {
48
48
  name: "gjc-coding-agent",
@@ -109,6 +109,40 @@ function decodeToolsListResult(value: unknown): MCPToolsListResult {
109
109
  return value.nextCursor === undefined ? { tools } : { tools, nextCursor: value.nextCursor };
110
110
  }
111
111
 
112
+ async function collectPaginated<T>(
113
+ connection: MCPServerConnection,
114
+ options: MCPRequestOptions | undefined,
115
+ method: string,
116
+ itemKey: string,
117
+ items: T[],
118
+ decode?: (value: unknown) => unknown,
119
+ ): Promise<void> {
120
+ const seenCursors = new Set<string>();
121
+ const failure = (detail: string) => new MCPExpectedFailure(new Error(`MCP ${method} pagination ${detail}`));
122
+ let cursor: string | undefined;
123
+ for (let page = 1; page <= MAX_PAGINATION_PAGES; page++) {
124
+ const value = await connection.transport.request<unknown>(method, cursor ? { cursor } : {}, options);
125
+ const result = decode ? decode(value) : value;
126
+ if (
127
+ !isRecord(result) ||
128
+ !Array.isArray(result[itemKey]) ||
129
+ (result.nextCursor !== undefined && typeof result.nextCursor !== "string")
130
+ )
131
+ throw new MCPExpectedFailure();
132
+ const nextCursor = result.nextCursor as string | undefined;
133
+ if (nextCursor && seenCursors.has(nextCursor)) throw failure("repeated a cursor");
134
+ const pageItems = result[itemKey] as T[];
135
+ const itemCount = items.length + pageItems.length;
136
+ if (itemCount > MAX_PAGINATION_ITEMS || (itemCount === MAX_PAGINATION_ITEMS && nextCursor))
137
+ throw failure("did not complete within the 10000-item budget");
138
+ items.push(...pageItems);
139
+ if (!nextCursor) return;
140
+ if (page === MAX_PAGINATION_PAGES) throw failure("did not complete within the 100-page budget");
141
+ seenCursors.add(nextCursor);
142
+ cursor = nextCursor;
143
+ }
144
+ }
145
+
112
146
  /**
113
147
  * Default handler for standard MCP server-to-client requests.
114
148
  * Handles `ping` and `roots/list`; rejects unknown methods with -32601.
@@ -292,18 +326,7 @@ export async function listTools(
292
326
  }
293
327
 
294
328
  const allTools: MCPToolDefinition[] = [];
295
- let cursor: string | undefined;
296
-
297
- do {
298
- const params: Record<string, unknown> = {};
299
- if (cursor) {
300
- params.cursor = cursor;
301
- }
302
-
303
- const result = decodeToolsListResult(await connection.transport.request<unknown>("tools/list", params, options));
304
- allTools.push(...result.tools);
305
- cursor = result.nextCursor;
306
- } while (cursor);
329
+ await collectPaginated(connection, options, "tools/list", "tools", allTools, decodeToolsListResult);
307
330
 
308
331
  // Cache tools
309
332
  connection.tools = allTools;
@@ -362,18 +385,7 @@ export async function listResources(
362
385
  }
363
386
 
364
387
  const allResources: MCPResource[] = [];
365
- let cursor: string | undefined;
366
-
367
- do {
368
- const params: Record<string, unknown> = {};
369
- if (cursor) {
370
- params.cursor = cursor;
371
- }
372
-
373
- const result = await connection.transport.request<MCPResourcesListResult>("resources/list", params, options);
374
- allResources.push(...result.resources);
375
- cursor = result.nextCursor;
376
- } while (cursor);
388
+ await collectPaginated(connection, options, "resources/list", "resources", allResources);
377
389
 
378
390
  connection.resources = allResources;
379
391
  return allResources;
@@ -395,22 +407,7 @@ export async function listResourceTemplates(
395
407
  }
396
408
 
397
409
  const allTemplates: MCPResourceTemplate[] = [];
398
- let cursor: string | undefined;
399
-
400
- do {
401
- const params: Record<string, unknown> = {};
402
- if (cursor) {
403
- params.cursor = cursor;
404
- }
405
-
406
- const result = await connection.transport.request<MCPResourceTemplatesListResult>(
407
- "resources/templates/list",
408
- params,
409
- options,
410
- );
411
- allTemplates.push(...result.resourceTemplates);
412
- cursor = result.nextCursor;
413
- } while (cursor);
410
+ await collectPaginated(connection, options, "resources/templates/list", "resourceTemplates", allTemplates);
414
411
 
415
412
  connection.resourceTemplates = allTemplates;
416
413
  return allTemplates;
@@ -514,18 +511,7 @@ export async function listPrompts(
514
511
  }
515
512
 
516
513
  const allPrompts: MCPPrompt[] = [];
517
- let cursor: string | undefined;
518
-
519
- do {
520
- const params: Record<string, unknown> = {};
521
- if (cursor) {
522
- params.cursor = cursor;
523
- }
524
-
525
- const result = await connection.transport.request<MCPPromptsListResult>("prompts/list", params, options);
526
- allPrompts.push(...result.prompts);
527
- cursor = result.nextCursor;
528
- } while (cursor);
514
+ await collectPaginated(connection, options, "prompts/list", "prompts", allPrompts);
529
515
 
530
516
  connection.prompts = allPrompts;
531
517
  return allPrompts;