@gajae-code/coding-agent 0.11.7 → 0.11.9

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 (213) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/dist/types/cli/args.d.ts +0 -2
  3. package/dist/types/cli.d.ts +1 -0
  4. package/dist/types/config/keybindings.d.ts +33 -3
  5. package/dist/types/config/model-profile-activation.d.ts +4 -3
  6. package/dist/types/config/model-profile-contract.d.ts +38 -0
  7. package/dist/types/config/model-profiles.d.ts +2 -3
  8. package/dist/types/config/settings-schema.d.ts +15 -2
  9. package/dist/types/edit/modes/patch.d.ts +13 -0
  10. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  11. package/dist/types/eval/py/executor.d.ts +1 -1
  12. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
  14. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
  15. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
  16. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
  17. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
  18. package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
  19. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  20. package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
  21. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
  22. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
  23. package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
  24. package/dist/types/main.d.ts +8 -0
  25. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  26. package/dist/types/modes/components/tool-status-header.d.ts +3 -2
  27. package/dist/types/modes/components/welcome.d.ts +4 -0
  28. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  30. package/dist/types/modes/interactive-mode.d.ts +5 -2
  31. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  32. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  33. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  34. package/dist/types/modes/types.d.ts +3 -0
  35. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  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/runtime-mcp/redaction.d.ts +2 -0
  40. package/dist/types/sdk/broker/broker.d.ts +2 -0
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
  43. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  44. package/dist/types/sdk/bus/config.d.ts +2 -0
  45. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  46. package/dist/types/sdk/bus/index.d.ts +25 -0
  47. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +15 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
  52. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  53. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  54. package/dist/types/sdk/host/host.d.ts +1 -1
  55. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  56. package/dist/types/sdk/index.d.ts +2 -0
  57. package/dist/types/sdk/prompt-status.d.ts +88 -0
  58. package/dist/types/sdk/session-directory.d.ts +1 -1
  59. package/dist/types/sdk/startup-capability.d.ts +3 -0
  60. package/dist/types/session/agent-session.d.ts +2 -0
  61. package/dist/types/session/internal/managed-session-scope.d.ts +22 -3
  62. package/dist/types/session/internal/managed-session-storage.d.ts +24 -4
  63. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  64. package/dist/types/session/session-manager.d.ts +5 -1
  65. package/dist/types/session/session-storage.d.ts +31 -0
  66. package/dist/types/skill-state/active-state.d.ts +4 -0
  67. package/dist/types/task/index.d.ts +3 -1
  68. package/dist/types/task/receipt.d.ts +2 -0
  69. package/dist/types/task/types.d.ts +94 -0
  70. package/dist/types/tools/ask.d.ts +454 -174
  71. package/dist/types/tools/image-gen.d.ts +47 -1
  72. package/dist/types/tools/index.d.ts +4 -0
  73. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  74. package/package.json +8 -8
  75. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  76. package/scripts/dogfood-repository-binding.ts +209 -0
  77. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  78. package/scripts/run-test-manifest.ts +4 -4
  79. package/src/cli/args.ts +2 -92
  80. package/src/cli/mcp-cli.ts +4 -21
  81. package/src/cli/notify-cli.ts +5 -38
  82. package/src/cli.ts +50 -0
  83. package/src/commands/deep-interview.ts +9 -4
  84. package/src/commands/sdk.ts +2 -2
  85. package/src/commit/agentic/index.ts +3 -3
  86. package/src/commit/map-reduce/index.ts +2 -2
  87. package/src/config/keybindings.ts +157 -24
  88. package/src/config/model-profile-activation.ts +7 -18
  89. package/src/config/model-profile-contract.ts +161 -0
  90. package/src/config/model-profiles.ts +2 -3
  91. package/src/config/model-registry.ts +11 -1
  92. package/src/config/settings-schema.ts +17 -2
  93. package/src/coordinator-mcp/model-preset.ts +21 -74
  94. package/src/defaults/gjc/skills/deep-interview/SKILL.md +80 -165
  95. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  96. package/src/edit/index.ts +3 -6
  97. package/src/edit/modes/patch.ts +38 -1
  98. package/src/edit/modes/replace.ts +42 -0
  99. package/src/edit/path-mutation-lock.ts +112 -0
  100. package/src/eval/py/executor.ts +30 -11
  101. package/src/eval/py/prelude.py +2 -2
  102. package/src/eval/py/tool-bridge.ts +34 -18
  103. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  104. package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
  105. package/src/gjc-runtime/deep-interview-draft.ts +842 -0
  106. package/src/gjc-runtime/deep-interview-payload.ts +262 -0
  107. package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
  108. package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
  109. package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
  110. package/src/gjc-runtime/deep-interview-state.ts +916 -4
  111. package/src/gjc-runtime/launch-tmux.ts +1 -1
  112. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  113. package/src/gjc-runtime/ralplan-runtime.ts +78 -4
  114. package/src/gjc-runtime/repository-binding.ts +267 -0
  115. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  116. package/src/gjc-runtime/state-writer.ts +166 -2
  117. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  118. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  119. package/src/gjc-runtime/ultragoal-guard.ts +5 -5
  120. package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
  121. package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
  122. package/src/gjc-runtime/workflow-manifest.ts +191 -5
  123. package/src/internal-urls/docs-index.generated.ts +8 -6
  124. package/src/internal-urls/local-protocol.ts +33 -16
  125. package/src/internal-urls/mcp-protocol.ts +73 -11
  126. package/src/main.ts +25 -10
  127. package/src/modes/DESIGN.md +5 -0
  128. package/src/modes/acp/acp-agent.ts +50 -16
  129. package/src/modes/components/model-selector.ts +42 -8
  130. package/src/modes/components/plan-preview-overlay.ts +1 -1
  131. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  132. package/src/modes/components/tool-status-header.ts +20 -4
  133. package/src/modes/components/welcome.ts +32 -20
  134. package/src/modes/controllers/command-controller.ts +97 -54
  135. package/src/modes/controllers/event-controller.ts +10 -0
  136. package/src/modes/controllers/input-controller.ts +7 -2
  137. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  138. package/src/modes/controllers/selector-controller.ts +46 -54
  139. package/src/modes/interactive-mode.ts +70 -36
  140. package/src/modes/prompt-action-autocomplete.ts +32 -13
  141. package/src/modes/prompt-suggestion-controller.ts +96 -0
  142. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  143. package/src/modes/types.ts +3 -0
  144. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  145. package/src/modes/utils/ui-helpers.ts +5 -3
  146. package/src/prompts/agents/architect.md +0 -1
  147. package/src/prompts/agents/critic.md +0 -1
  148. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  149. package/src/prompts/system/system-prompt.md +7 -4
  150. package/src/prompts/tools/cron.md +3 -1
  151. package/src/runtime-mcp/json-rpc.ts +7 -4
  152. package/src/runtime-mcp/manager.ts +51 -53
  153. package/src/runtime-mcp/oauth-flow.ts +0 -72
  154. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  155. package/src/runtime-mcp/redaction.ts +30 -0
  156. package/src/runtime-mcp/transports/http.ts +13 -6
  157. package/src/sdk/broker/broker.ts +34 -20
  158. package/src/sdk/broker/lifecycle.ts +185 -36
  159. package/src/sdk/broker/session-index.ts +11 -1
  160. package/src/sdk/bus/chat-daemon-control.ts +20 -5
  161. package/src/sdk/bus/config-commands.ts +2 -2
  162. package/src/sdk/bus/config.ts +9 -1
  163. package/src/sdk/bus/conversation-store.ts +40 -6
  164. package/src/sdk/bus/index.ts +240 -65
  165. package/src/sdk/bus/lifecycle-commands.ts +47 -3
  166. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  167. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  168. package/src/sdk/bus/notification-orchestration.ts +12 -0
  169. package/src/sdk/bus/notification-service.ts +27 -2
  170. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  171. package/src/sdk/bus/recent-activity.ts +90 -26
  172. package/src/sdk/bus/telegram-daemon-contract.ts +15 -3
  173. package/src/sdk/bus/telegram-daemon-control.ts +145 -33
  174. package/src/sdk/bus/telegram-daemon.ts +853 -112
  175. package/src/sdk/bus/telegram-reference.ts +20 -2
  176. package/src/sdk/cli/session-cli.ts +15 -2
  177. package/src/sdk/host/control/dispatch.ts +1 -1
  178. package/src/sdk/host/control/operations.ts +1 -1
  179. package/src/sdk/host/host.ts +2 -6
  180. package/src/sdk/host/query/handlers.ts +70 -5
  181. package/src/sdk/index.ts +12 -0
  182. package/src/sdk/prompt-status.ts +97 -0
  183. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  184. package/src/sdk/protocol/operation-registry.ts +18 -4
  185. package/src/sdk/session-directory.ts +5 -1
  186. package/src/sdk/session.ts +2 -0
  187. package/src/sdk/startup-capability.ts +9 -1
  188. package/src/session/agent-session.ts +138 -5
  189. package/src/session/blob-store.ts +7 -2
  190. package/src/session/internal/managed-session-scope.ts +998 -133
  191. package/src/session/internal/managed-session-storage.ts +268 -203
  192. package/src/session/internal/native-publish-outcome.ts +266 -0
  193. package/src/session/session-manager.ts +406 -360
  194. package/src/session/session-storage.ts +151 -11
  195. package/src/skill-state/active-state.ts +10 -0
  196. package/src/task/executor.ts +13 -5
  197. package/src/task/gjc-command.ts +2 -2
  198. package/src/task/index.ts +121 -15
  199. package/src/task/receipt.ts +3 -0
  200. package/src/task/types.ts +34 -3
  201. package/src/tools/ask.ts +199 -88
  202. package/src/tools/ast-edit.ts +2 -2
  203. package/src/tools/bash-pty-selection.ts +2 -2
  204. package/src/tools/browser/screenshot-format.ts +6 -1
  205. package/src/tools/cron.ts +1 -1
  206. package/src/tools/image-gen.ts +178 -6
  207. package/src/tools/index.ts +4 -0
  208. package/src/tools/render-utils.ts +7 -1
  209. package/src/utils/edit-mode.ts +2 -2
  210. package/src/utils/prompt-suggestion.ts +265 -0
  211. package/src/utils/sixel.ts +3 -3
  212. package/src/web/insane/url-guard.ts +3 -1
  213. package/src/web/scrapers/docs-rs.ts +30 -5
@@ -41,10 +41,12 @@ const OPENAI_IMAGE_OUTPUT_FORMAT = "webp";
41
41
  const OPENAI_IMAGE_MIME_TYPE = "image/webp";
42
42
 
43
43
  const ANTIGRAVITY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
44
+ const ALIBABA_TOKEN_PLAN_HOST = "https://token-plan.ap-southeast-1.maas.aliyuncs.com";
45
+ const ALIBABA_IMAGE_GENERATION_URL = `${ALIBABA_TOKEN_PLAN_HOST}/api/v1/services/aigc/multimodal-generation/generation`;
44
46
  const IMAGE_SYSTEM_INSTRUCTION =
45
47
  "You are an AI image generator. Generate images based on user descriptions. Focus on creating high-quality, visually appealing images that match the user's request.";
46
48
 
47
- type ImageProvider = "antigravity" | "gemini" | "openai" | "openai-codex" | "openrouter";
49
+ type ImageProvider = "alibaba" | "antigravity" | "gemini" | "openai" | "openai-codex" | "openrouter";
48
50
  interface ImageApiKey {
49
51
  provider: ImageProvider;
50
52
  apiKey: string;
@@ -296,6 +298,81 @@ interface AntigravityResponseChunk {
296
298
  };
297
299
  }
298
300
 
301
+ interface AlibabaImageContentPart {
302
+ type?: string;
303
+ text?: string;
304
+ image?: string;
305
+ }
306
+
307
+ interface AlibabaImageResponse {
308
+ code?: string;
309
+ message?: string;
310
+ output?: {
311
+ choices?: Array<{ message?: { content?: AlibabaImageContentPart[] } }>;
312
+ };
313
+ }
314
+
315
+ /** Map the tool's image_size values onto wan2.7's 1K/2K size classes. */
316
+ export function resolveAlibabaImageSize(imageSize: string | undefined): string | undefined {
317
+ switch (imageSize) {
318
+ case "1024x1024":
319
+ return "1K";
320
+ case "1536x1024":
321
+ case "1024x1536":
322
+ return "2K";
323
+ default:
324
+ return undefined;
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Build the Bailian multimodal-generation request body for wan2.7 image
330
+ * generation/editing. Input images ride along as data URLs; generation-only
331
+ * calls send just the prompt text. The sync endpoint returns OSS URLs inline.
332
+ */
333
+ export function buildAlibabaImageRequest(
334
+ model: string,
335
+ promptText: string,
336
+ inputImages: InlineImageData[],
337
+ imageSize: string | undefined,
338
+ ): {
339
+ model: string;
340
+ input: { messages: Array<{ role: "user"; content: Array<{ text: string } | { image: string }> }> };
341
+ parameters: { n: number; watermark: boolean; size?: string };
342
+ } {
343
+ const content: Array<{ text: string } | { image: string }> = [];
344
+ for (const image of inputImages) {
345
+ content.push({ image: toDataUrl(image) });
346
+ }
347
+ content.push({ text: promptText });
348
+ const size = resolveAlibabaImageSize(imageSize);
349
+ return {
350
+ model,
351
+ input: { messages: [{ role: "user", content }] },
352
+ parameters: { n: 1, watermark: false, ...(size ? { size } : {}) },
353
+ };
354
+ }
355
+
356
+ /** Collect OSS image URLs and any text parts from a Bailian image response. */
357
+ export function collectAlibabaImageResult(response: AlibabaImageResponse): {
358
+ imageUrls: string[];
359
+ responseText?: string;
360
+ } {
361
+ const imageUrls: string[] = [];
362
+ const textParts: string[] = [];
363
+ for (const choice of response.output?.choices ?? []) {
364
+ for (const part of choice.message?.content ?? []) {
365
+ if (part.image) {
366
+ imageUrls.push(part.image);
367
+ } else if (part.text) {
368
+ textParts.push(part.text);
369
+ }
370
+ }
371
+ }
372
+ const responseText = textParts.join("\n").trim();
373
+ return { imageUrls, responseText: responseText.length > 0 ? responseText : undefined };
374
+ }
375
+
299
376
  interface ImageGenToolDetails {
300
377
  provider: ImageProvider;
301
378
  model: string;
@@ -585,6 +662,7 @@ export function setPreferredImageProvider(provider: ImageProvider | "auto"): voi
585
662
  /** Provider → default image model mapping for auto-binding */
586
663
  export const IMAGE_PROVIDER_DEFAULTS: Record<string, string> = {
587
664
  openai: "gpt-image-2",
665
+ alibaba: "wan2.7-image",
588
666
  "openai-codex": "gpt-image-2",
589
667
  antigravity: "gemini-3-pro-image",
590
668
  gemini: "gemini-3-pro-image-preview",
@@ -704,6 +782,18 @@ async function findOpenAIHostedImageCredentials(
704
782
  };
705
783
  }
706
784
 
785
+ async function findAlibabaImageCredentials(
786
+ modelRegistry: ModelRegistry | undefined,
787
+ sessionId?: string,
788
+ ): Promise<ImageApiKey | null> {
789
+ const envKey = getEnvApiKey("alibaba-token-plan");
790
+ if (envKey) return { provider: "alibaba", apiKey: envKey };
791
+ if (!modelRegistry) return null;
792
+ const apiKey = await modelRegistry.getApiKeyForProvider("alibaba-token-plan", sessionId);
793
+ if (!isAuthenticated(apiKey)) return null;
794
+ return { provider: "alibaba", apiKey };
795
+ }
796
+
707
797
  async function findImageApiKey(
708
798
  modelRegistry?: ModelRegistry,
709
799
  activeModel?: Model,
@@ -744,6 +834,9 @@ async function findImageApiKey(
744
834
  const openRouterKey = getEnvApiKey("openrouter");
745
835
  return openRouterKey ? { provider: "openrouter", apiKey: openRouterKey } : null;
746
836
  }
837
+ if (config.provider === "alibaba") {
838
+ return await findAlibabaImageCredentials(modelRegistry, sessionId);
839
+ }
747
840
  }
748
841
 
749
842
  // If a specific provider is preferred (legacy path), try it first.
@@ -762,9 +855,11 @@ async function findImageApiKey(
762
855
  } else if (preferredImageProvider === "openrouter") {
763
856
  const openRouterKey = getEnvApiKey("openrouter");
764
857
  return openRouterKey ? { provider: "openrouter", apiKey: openRouterKey } : null;
858
+ } else if (preferredImageProvider === "alibaba") {
859
+ return await findAlibabaImageCredentials(modelRegistry, sessionId);
765
860
  }
766
861
 
767
- // Auto-detect: GPT hosted image generation, then Antigravity, OpenRouter, Gemini.
862
+ // Auto-detect: GPT hosted image generation, then Antigravity, OpenRouter, Gemini, Alibaba.
768
863
  const openAI = await findOpenAIHostedImageCredentials(modelRegistry, activeModel, sessionId);
769
864
  if (openAI) return openAI;
770
865
 
@@ -782,6 +877,9 @@ async function findImageApiKey(
782
877
  const googleKey = $env.GOOGLE_API_KEY;
783
878
  if (googleKey) return { provider: "gemini", apiKey: googleKey };
784
879
 
880
+ const alibaba = await findAlibabaImageCredentials(modelRegistry, sessionId);
881
+ if (alibaba) return alibaba;
882
+
785
883
  return null;
786
884
  }
787
885
 
@@ -1218,7 +1316,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1218
1316
  const apiKey = await findImageApiKey(ctx.modelRegistry, ctx.model, sessionId);
1219
1317
  if (!apiKey) {
1220
1318
  throw new Error(
1221
- "No image API credentials found. Use a GPT Responses/Codex model with OpenAI credentials, login with google-antigravity, or set OPENROUTER_API_KEY, GEMINI_API_KEY, or GOOGLE_API_KEY.",
1319
+ "No image API credentials found. Use a GPT Responses/Codex model with OpenAI credentials, login with google-antigravity, or set OPENROUTER_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY, or ALIBABA_TOKEN_PLAN_API_KEY.",
1222
1320
  );
1223
1321
  }
1224
1322
 
@@ -1230,9 +1328,11 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1230
1328
  ? (apiKey.model?.id ?? resolveImageModel(provider, null))
1231
1329
  : provider === "antigravity"
1232
1330
  ? resolveImageModel("antigravity", null)
1233
- : provider === "openrouter"
1234
- ? resolveImageModel("openrouter", null)
1235
- : resolveImageModel("gemini", null);
1331
+ : provider === "alibaba"
1332
+ ? resolveImageModel("alibaba", null)
1333
+ : provider === "openrouter"
1334
+ ? resolveImageModel("openrouter", null)
1335
+ : resolveImageModel("gemini", null);
1236
1336
  const resolvedModel = provider === "openrouter" ? resolveOpenRouterModel(model) : model;
1237
1337
  const cwd = ctx.sessionManager.getCwd();
1238
1338
 
@@ -1450,6 +1550,78 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1450
1550
  };
1451
1551
  }
1452
1552
 
1553
+ if (provider === "alibaba") {
1554
+ const requestBody = buildAlibabaImageRequest(
1555
+ model,
1556
+ assemblePrompt(params),
1557
+ resolvedImages,
1558
+ params.image_size,
1559
+ );
1560
+
1561
+ const response = await fetch(ALIBABA_IMAGE_GENERATION_URL, {
1562
+ method: "POST",
1563
+ headers: {
1564
+ "Content-Type": "application/json",
1565
+ Authorization: `Bearer ${apiKey.apiKey}`,
1566
+ },
1567
+ body: JSON.stringify(requestBody),
1568
+ signal: requestSignal,
1569
+ });
1570
+
1571
+ const rawText = await response.text();
1572
+ if (!response.ok) {
1573
+ let message = rawText;
1574
+ try {
1575
+ const parsed = JSON.parse(rawText) as { message?: string; error?: { message?: string } };
1576
+ message = parsed.error?.message ?? parsed.message ?? message;
1577
+ } catch {
1578
+ // Keep raw text.
1579
+ }
1580
+ throw new Error(`Alibaba image request failed (${response.status}): ${message}`);
1581
+ }
1582
+
1583
+ const data = JSON.parse(rawText) as AlibabaImageResponse;
1584
+ if (data.code) {
1585
+ throw new Error(`Alibaba image request failed: ${data.code}: ${data.message ?? ""}`.trim());
1586
+ }
1587
+
1588
+ const { imageUrls, responseText } = collectAlibabaImageResult(data);
1589
+ // Result URLs are short-lived OSS-signed URLs (24h); download immediately.
1590
+ const inlineImages: InlineImageData[] = [];
1591
+ for (const imageUrl of imageUrls) {
1592
+ inlineImages.push(await loadImageFromUrl(imageUrl, requestSignal));
1593
+ }
1594
+
1595
+ if (inlineImages.length === 0) {
1596
+ const messageText = responseText ? `\n\n${responseText}` : "";
1597
+ return {
1598
+ content: [{ type: "text", text: `No image data returned.${messageText}` }],
1599
+ details: {
1600
+ provider,
1601
+ model,
1602
+ imageCount: 0,
1603
+ imagePaths: [],
1604
+ images: [],
1605
+ responseText,
1606
+ },
1607
+ };
1608
+ }
1609
+
1610
+ const imagePaths = await saveImagesToTemp(inlineImages);
1611
+
1612
+ return {
1613
+ content: [{ type: "text", text: buildResponseSummary(provider, model, imagePaths, responseText) }],
1614
+ details: {
1615
+ provider,
1616
+ model,
1617
+ imageCount: inlineImages.length,
1618
+ imagePaths,
1619
+ images: inlineImages,
1620
+ responseText,
1621
+ },
1622
+ };
1623
+ }
1624
+
1453
1625
  const parts = [] as Array<{ text?: string; inlineData?: InlineImageData }>;
1454
1626
  for (const image of resolvedImages) {
1455
1627
  parts.push({ inlineData: image });
@@ -137,6 +137,8 @@ export interface AskAnswerRequest {
137
137
  options: string[];
138
138
  interaction: "selector" | "custom_editor" | "clarification_editor";
139
139
  controls: readonly AskRemoteControl[];
140
+ /** Optional zero-based recommendation into the authoritative raw options. */
141
+ recommendedIndex?: number;
140
142
  }
141
143
 
142
144
  export type AskRemoteInteraction =
@@ -232,6 +234,8 @@ export interface ToolSession {
232
234
  * its terminal-phase chain guard. Returns the raw phase string or undefined
233
235
  * when no active skill (or accessor) is available. */
234
236
  getActiveSkillPhase?: () => string | undefined;
237
+ /** Restrict provider-facing deep-interview ask metadata to the active workflow stage. */
238
+ getDeepInterviewAskStage?: () => "topology" | "post-topology" | undefined;
235
239
  /** Pre-loaded prompt templates */
236
240
  promptTemplates?: PromptTemplate[];
237
241
  /** Whether LSP integrations are enabled */
@@ -571,7 +571,13 @@ export function truncateDiffByHunk(
571
571
  export function shortenPath(filePath: string, homeDir?: string): string {
572
572
  if (typeof filePath !== "string") return "";
573
573
  const home = homeDir ?? os.homedir();
574
- if (home && filePath.startsWith(home)) {
574
+ if (!home) return filePath;
575
+ // Only abbreviate paths that are exactly home or genuinely inside it. A bare
576
+ // startsWith(home) also matches siblings that merely share the prefix
577
+ // (e.g. "/home/woodyx/notes.txt" for home "/home/woody"), which would corrupt
578
+ // them into "~x/notes.txt"; require a path-separator boundary after home.
579
+ const boundary = home.endsWith(path.sep) ? home : home + path.sep;
580
+ if (filePath === home || filePath.startsWith(boundary)) {
575
581
  return `~${filePath.slice(home.length)}`;
576
582
  }
577
583
  return filePath;
@@ -1,4 +1,4 @@
1
- import { $env } from "@gajae-code/utils/env";
1
+ import { $pickenv } from "@gajae-code/utils/env";
2
2
 
3
3
  export type EditMode = "replace" | "patch" | "hashline" | "vim" | "apply_patch";
4
4
 
@@ -34,7 +34,7 @@ export function resolveEditMode(session: EditModeSessionLike): EditMode {
34
34
  const modelVariant = session.settings.getEditVariantForModel?.(activeModel);
35
35
  if (modelVariant) return modelVariant;
36
36
 
37
- const envMode = normalizeEditMode($env.PI_EDIT_VARIANT);
37
+ const envMode = normalizeEditMode($pickenv("GJC_EDIT_VARIANT", "PI_EDIT_VARIANT"));
38
38
  if (envMode) return envMode;
39
39
 
40
40
  const settingsMode = normalizeEditMode(String(session.settings.get("edit.mode") ?? ""));
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Predict the user's likely next prompt after an agent turn using a smol,
3
+ * fast model. Native GJC port of the ghost-text prompt-suggestion behavior
4
+ * from Claude Code: the prediction renders as dim ghost text in the empty
5
+ * composer and Tab accepts it.
6
+ */
7
+ import type { AgentMessage } from "@gajae-code/agent-core";
8
+ import { type Api, type AssistantMessage, completeSimple, type Model } from "@gajae-code/ai";
9
+ import { logger, prompt } from "@gajae-code/utils";
10
+ import type { ModelRegistry } from "../config/model-registry";
11
+ import { resolveRoleSelection } from "../config/model-resolver";
12
+ import type { Settings } from "../config/settings";
13
+ import suggestionSystemPrompt from "../prompts/system/prompt-suggestion-system.md" with { type: "text" };
14
+
15
+ const SUGGESTION_SYSTEM_PROMPT = prompt.render(suggestionSystemPrompt);
16
+
17
+ export const PROMPT_SUGGESTION_MAX_WORDS = 12;
18
+ export const PROMPT_SUGGESTION_MAX_CHARS = 100;
19
+
20
+ const SUGGESTION_MAX_TOKENS = 64;
21
+ const REASONING_SAFE_MAX_TOKENS = 1024;
22
+ const MAX_CONTEXT_MESSAGES = 12;
23
+ const MAX_MESSAGE_CHARS = 1500;
24
+ const MAX_CONTEXT_CHARS = 8000;
25
+
26
+ /** Single words a user plausibly types on their own; anything else needs >= 2 words. */
27
+ const SINGLE_WORD_ALLOWLIST = new Set([
28
+ "yes",
29
+ "yeah",
30
+ "yep",
31
+ "yea",
32
+ "yup",
33
+ "sure",
34
+ "ok",
35
+ "okay",
36
+ "push",
37
+ "commit",
38
+ "deploy",
39
+ "stop",
40
+ "continue",
41
+ "check",
42
+ "exit",
43
+ "quit",
44
+ "no",
45
+ ]);
46
+
47
+ /** Model responses that are meta-statements about staying silent, not suggestions. */
48
+ const META_TEXT_PATTERN = /\bsilence is\b|\bstay(s|ing)? silent\b/;
49
+
50
+ /**
51
+ * Strip wrapper tags and label prefixes a model sometimes adds despite being
52
+ * told to reply with only the suggestion.
53
+ */
54
+ export function sanitizePromptSuggestion(raw: string): string {
55
+ return raw
56
+ .trim()
57
+ .replace(/^<(suggestion|response|output|answer|result)>([\s\S]*)<\/\1>$/i, (match, tag: string, inner: string) =>
58
+ // Keep the original when the inner text itself contains the closing
59
+ // tag (nested/ambiguous markup) instead of producing a mangled strip.
60
+ inner.includes(`</${tag.toLowerCase()}>`) || inner.includes(`</${tag.toUpperCase()}>`) ? match : inner,
61
+ )
62
+ .replace(
63
+ /^\s*(suggested\s+(response|reply|input|prompt)|suggestion|response|reply|answer|output|result)\s*:\s*/i,
64
+ "",
65
+ )
66
+ .trim();
67
+ }
68
+
69
+ /**
70
+ * Heuristic gate rejecting model output that would make a bad ghost-text
71
+ * suggestion. Returns the rejection reason, or null when the text is usable.
72
+ */
73
+ export function suppressPromptSuggestionReason(text: string): string | null {
74
+ if (!text) return "empty";
75
+ const lower = text.toLowerCase();
76
+ const wordCount = text.trim().split(/\s+/).length;
77
+
78
+ if (lower === "done") return "done";
79
+ if (
80
+ lower === "nothing found" ||
81
+ lower === "nothing found." ||
82
+ lower.startsWith("nothing to suggest") ||
83
+ lower.startsWith("no suggestion") ||
84
+ META_TEXT_PATTERN.test(lower) ||
85
+ /^\W*silence\W*$/.test(lower)
86
+ ) {
87
+ return "meta_text";
88
+ }
89
+ if (/^\(.*\)$|^\[.*\]$/.test(text)) return "meta_wrapped";
90
+ if (
91
+ lower.startsWith("api error:") ||
92
+ lower.startsWith("prompt is too long") ||
93
+ lower.startsWith("request timed out") ||
94
+ lower.startsWith("invalid api key")
95
+ ) {
96
+ return "error_message";
97
+ }
98
+ if (/^\w+:\s/.test(text)) return "prefixed_label";
99
+ if (wordCount < 2 && !text.startsWith("/") && !SINGLE_WORD_ALLOWLIST.has(lower)) return "too_few_words";
100
+ if (wordCount > PROMPT_SUGGESTION_MAX_WORDS) return "too_many_words";
101
+ if (text.length >= PROMPT_SUGGESTION_MAX_CHARS) return "too_long";
102
+ if (/[.!?]\s+[A-Z]/.test(text)) return "multiple_sentences";
103
+ if (/[\n*]|\*\*/.test(text)) return "has_formatting";
104
+ if (
105
+ /thanks|thank you|looks good|sounds good|that works|that worked|that's all|nice|great|perfect|makes sense|awesome|excellent/.test(
106
+ lower,
107
+ )
108
+ ) {
109
+ return "evaluative";
110
+ }
111
+ if (
112
+ /^(let me|i'll|i've|i'm|i can|i would|i think|i notice|here's|here is|here are|that's|this is|this will|you can|you should|you could|sure,|of course|certainly)/i.test(
113
+ text,
114
+ )
115
+ ) {
116
+ return "claude_voice";
117
+ }
118
+ return null;
119
+ }
120
+
121
+ function extractTranscriptEntry(message: AgentMessage): { role: "User" | "Assistant"; text: string } | null {
122
+ if (message.role === "user") {
123
+ const content = message.content;
124
+ const text =
125
+ typeof content === "string"
126
+ ? content
127
+ : content
128
+ .filter(block => block.type === "text")
129
+ .map(block => block.text)
130
+ .join("\n");
131
+ const trimmed = text.trim();
132
+ return trimmed ? { role: "User", text: trimmed } : null;
133
+ }
134
+ if (message.role === "assistant") {
135
+ const text = (message as AssistantMessage).content
136
+ .filter(block => block.type === "text")
137
+ .map(block => block.text)
138
+ .join("\n")
139
+ .trim();
140
+ return text ? { role: "Assistant", text } : null;
141
+ }
142
+ return null;
143
+ }
144
+
145
+ /**
146
+ * Build a compact recent-conversation transcript for the prediction request.
147
+ * Returns null when there is no user message to predict from.
148
+ */
149
+ export function buildPromptSuggestionContext(messages: AgentMessage[]): string | null {
150
+ const entries: { role: "User" | "Assistant"; text: string }[] = [];
151
+ let totalChars = 0;
152
+ for (let index = messages.length - 1; index >= 0 && entries.length < MAX_CONTEXT_MESSAGES; index--) {
153
+ const message = messages[index];
154
+ if (!message) continue;
155
+ const entry = extractTranscriptEntry(message);
156
+ if (!entry) continue;
157
+ const text = entry.text.length > MAX_MESSAGE_CHARS ? `${entry.text.slice(0, MAX_MESSAGE_CHARS)}…` : entry.text;
158
+ if (totalChars + text.length > MAX_CONTEXT_CHARS && entries.length > 0) break;
159
+ entries.push({ role: entry.role, text });
160
+ totalChars += text.length;
161
+ }
162
+ if (!entries.some(entry => entry.role === "User")) return null;
163
+ entries.reverse();
164
+ const transcript = entries.map(entry => `${entry.role}: ${entry.text}`).join("\n\n");
165
+ return `<conversation>\n${transcript}\n</conversation>\n\nPredict the user's next message.`;
166
+ }
167
+
168
+ function getSuggestionModel(
169
+ registry: ModelRegistry,
170
+ settings: Settings,
171
+ currentModel?: Model<Api>,
172
+ ): Model<Api> | undefined {
173
+ const availableModels = registry.getAvailable();
174
+ if (availableModels.length === 0) return undefined;
175
+ const model = resolveRoleSelection(["default"], settings, availableModels, registry)?.model;
176
+ if (model) return model;
177
+ return currentModel;
178
+ }
179
+
180
+ function extractSuggestionText(contentBlocks: AssistantMessage["content"]): string {
181
+ let text = "";
182
+ for (const content of contentBlocks) {
183
+ if (content.type === "text") {
184
+ text += content.text;
185
+ }
186
+ }
187
+ return text;
188
+ }
189
+
190
+ /**
191
+ * Generate a predicted next user prompt from the recent conversation.
192
+ * Returns null when no usable suggestion is produced (silence is the
193
+ * expected steady state).
194
+ */
195
+ export async function generatePromptSuggestion(
196
+ messages: AgentMessage[],
197
+ registry: ModelRegistry,
198
+ settings: Settings,
199
+ sessionId?: string,
200
+ currentModel?: Model<Api>,
201
+ metadataResolver?: (provider: string) => Record<string, unknown> | undefined,
202
+ ): Promise<string | null> {
203
+ const context = buildPromptSuggestionContext(messages);
204
+ if (!context) return null;
205
+
206
+ const model = getSuggestionModel(registry, settings, currentModel);
207
+ if (!model) {
208
+ logger.debug("prompt-suggestion: no model available");
209
+ return null;
210
+ }
211
+
212
+ const apiKey = await registry.getApiKey(model, sessionId);
213
+ if (!apiKey) {
214
+ logger.debug("prompt-suggestion: no API key", { provider: model.provider, id: model.id });
215
+ return null;
216
+ }
217
+ // Resolve metadata after getApiKey so the session-sticky credential for
218
+ // this request is already recorded (same contract as title-generator).
219
+ const metadata = metadataResolver?.(model.provider);
220
+
221
+ // A suggestion is a 2-12 word task, but some reasoning backends ignore
222
+ // disableReasoning; reserve output room for the answer after any
223
+ // unavoidable thinking tokens.
224
+ const maxTokens = model.reasoning
225
+ ? Math.max(SUGGESTION_MAX_TOKENS, REASONING_SAFE_MAX_TOKENS)
226
+ : SUGGESTION_MAX_TOKENS;
227
+
228
+ try {
229
+ const response = await completeSimple(
230
+ model,
231
+ {
232
+ systemPrompt: [SUGGESTION_SYSTEM_PROMPT],
233
+ messages: [{ role: "user", content: context, timestamp: Date.now() }],
234
+ },
235
+ {
236
+ apiKey,
237
+ maxTokens,
238
+ disableReasoning: true,
239
+ metadata,
240
+ },
241
+ );
242
+
243
+ if (response.stopReason === "error") {
244
+ logger.debug("prompt-suggestion: response error", {
245
+ model: `${model.provider}/${model.id}`,
246
+ errorMessage: response.errorMessage,
247
+ });
248
+ return null;
249
+ }
250
+
251
+ const suggestion = sanitizePromptSuggestion(extractSuggestionText(response.content));
252
+ const suppressReason = suppressPromptSuggestionReason(suggestion);
253
+ if (suppressReason) {
254
+ logger.debug("prompt-suggestion: suppressed", { reason: suppressReason });
255
+ return null;
256
+ }
257
+ return suggestion;
258
+ } catch (err) {
259
+ logger.debug("prompt-suggestion: error", {
260
+ model: `${model.provider}/${model.id}`,
261
+ error: err instanceof Error ? err.message : String(err),
262
+ });
263
+ return null;
264
+ }
265
+ }
@@ -1,4 +1,4 @@
1
- import { $env, $flag } from "@gajae-code/utils";
1
+ import { $pickenv, $pickflag } from "@gajae-code/utils";
2
2
 
3
3
  const SIXEL_START_REGEX = /\x1bP(?:[0-9;]*)q/u;
4
4
  const SIXEL_END_SEQUENCE = "\x1b\\";
@@ -14,8 +14,8 @@ const SIXEL_PLACEHOLDER_PREFIX = "__GJC_SIXEL_SEQUENCE_";
14
14
  * - PI_ALLOW_SIXEL_PASSTHROUGH=1
15
15
  */
16
16
  export function isSixelPassthroughEnabled(): boolean {
17
- const forcedProtocol = $env.PI_FORCE_IMAGE_PROTOCOL?.trim().toLowerCase();
18
- return forcedProtocol === "sixel" && $flag("PI_ALLOW_SIXEL_PASSTHROUGH");
17
+ const forcedProtocol = $pickenv("GJC_FORCE_IMAGE_PROTOCOL", "PI_FORCE_IMAGE_PROTOCOL")?.trim().toLowerCase();
18
+ return forcedProtocol === "sixel" && $pickflag("GJC_ALLOW_SIXEL_PASSTHROUGH", "PI_ALLOW_SIXEL_PASSTHROUGH");
19
19
  }
20
20
  /** Returns true when the text contains a SIXEL start sequence. */
21
21
  export function containsSixelSequence(text: string): boolean {
@@ -244,8 +244,10 @@ export async function guardedPublicFetch(
244
244
  logicalUrl.protocol === "https:"
245
245
  ? { rejectUnauthorized: true, ...(net.isIP(hostname) === 0 ? { serverName: hostname } : {}) }
246
246
  : undefined;
247
+ const method = (init.method ?? "GET").toUpperCase();
248
+ const addresses = method === "GET" || method === "HEAD" ? guard.addresses : guard.addresses.slice(0, 1);
247
249
  let lastError: unknown;
248
- for (const address of guard.addresses) {
250
+ for (const address of addresses) {
249
251
  if (hasConfiguredProxy(process.env)) {
250
252
  return { ok: false, reason: "proxy routing appeared during resolution", logicalUrl: rawUrl };
251
253
  }
@@ -299,7 +299,21 @@ async function readCachedRustdocCrate(
299
299
  ): Promise<{ crate: RustdocCrate; fetchedAt: string } | null> {
300
300
  const cachePath = getDocsRsCachePath(target);
301
301
  try {
302
- const [jsonStr, stat] = await Promise.all([Bun.file(cachePath).text(), fs.stat(cachePath)]);
302
+ const stat = await fs.stat(cachePath);
303
+ if (stat.size > MAX_BYTES) {
304
+ await fs.rm(cachePath, { force: true });
305
+ return null;
306
+ }
307
+
308
+ const cacheBytes = await Bun.file(cachePath)
309
+ .slice(0, MAX_BYTES + 1)
310
+ .arrayBuffer();
311
+ if (cacheBytes.byteLength > MAX_BYTES) {
312
+ await fs.rm(cachePath, { force: true });
313
+ return null;
314
+ }
315
+
316
+ const jsonStr = Buffer.from(cacheBytes).toString("utf-8");
303
317
  const crate = tryParseJson<RustdocCrate>(jsonStr);
304
318
  if (!crate?.index) return null;
305
319
  return { crate, fetchedAt: stat.mtime.toISOString() };
@@ -378,11 +392,22 @@ export const handleDocsRs: SpecialHandler = async (
378
392
  const requestSignal = ptree.combineSignals(signal, timeout * 1000);
379
393
  const response = await fetch(jsonUrl, {
380
394
  signal: requestSignal,
381
- headers: { "User-Agent": "gjc-web-fetch/1.0", Accept: "application/gzip" },
395
+ headers: {
396
+ "User-Agent": "gjc-web-fetch/1.0",
397
+ Accept: "application/gzip",
398
+ "Accept-Encoding": "identity",
399
+ },
382
400
  redirect: "follow",
401
+ decompress: false,
383
402
  });
384
403
  if (!response.ok) return null;
385
404
 
405
+ const contentEncoding = response.headers.get("Content-Encoding");
406
+ if (contentEncoding && contentEncoding.trim().toLowerCase() !== "identity") {
407
+ await response.body?.cancel();
408
+ return null;
409
+ }
410
+
386
411
  const reader = response.body?.getReader();
387
412
  if (!reader) return null;
388
413
 
@@ -394,13 +419,13 @@ export const handleDocsRs: SpecialHandler = async (
394
419
  chunks.push(value);
395
420
  totalSize += value.length;
396
421
  if (totalSize > MAX_BYTES) {
397
- reader.cancel();
398
- break;
422
+ await reader.cancel();
423
+ return null;
399
424
  }
400
425
  }
401
426
 
402
427
  const compressed = Buffer.concat(chunks);
403
- const jsonStr = gunzipSync(compressed).toString("utf-8");
428
+ const jsonStr = gunzipSync(compressed, { maxOutputLength: MAX_BYTES }).toString("utf-8");
404
429
  crate_ = tryParseJson<RustdocCrate>(jsonStr);
405
430
  if (crate_?.index) {
406
431
  await writeCachedRustdocCrate(target, jsonStr);