@gajae-code/coding-agent 0.8.1 → 0.9.0

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 (253) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/types/append-only-mode.d.ts +31 -0
  3. package/dist/types/capability/rule.d.ts +8 -0
  4. package/dist/types/cli/completion-cli.d.ts +41 -0
  5. package/dist/types/cli/config-cli.d.ts +1 -0
  6. package/dist/types/cli/fixture-report.d.ts +20 -0
  7. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  8. package/dist/types/cli/nofile-limit.d.ts +11 -0
  9. package/dist/types/cli/notify-cli.d.ts +7 -0
  10. package/dist/types/cli/update-cli.d.ts +22 -0
  11. package/dist/types/cli.d.ts +124 -1
  12. package/dist/types/commands/completion.d.ts +29 -0
  13. package/dist/types/commands/config.d.ts +3 -0
  14. package/dist/types/commands/deep-interview.d.ts +3 -0
  15. package/dist/types/config/file-lock.d.ts +7 -6
  16. package/dist/types/config/keybindings.d.ts +2 -2
  17. package/dist/types/config/model-profile-activation.d.ts +6 -0
  18. package/dist/types/config/settings-schema.d.ts +105 -1
  19. package/dist/types/config/settings.d.ts +8 -0
  20. package/dist/types/discovery/index.d.ts +1 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  24. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  25. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
  26. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  27. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/deep-interview-state.d.ts +17 -2
  29. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
  30. package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
  31. package/dist/types/gjc-runtime/team-runtime.d.ts +28 -1
  32. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  33. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +120 -13
  34. package/dist/types/goals/runtime.d.ts +1 -0
  35. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  36. package/dist/types/main.d.ts +4 -1
  37. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  38. package/dist/types/modes/components/model-selector.d.ts +4 -0
  39. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  40. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  41. package/dist/types/modes/components/status-line.d.ts +9 -0
  42. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  43. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  44. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  45. package/dist/types/modes/interactive-mode.d.ts +2 -0
  46. package/dist/types/modes/print-mode.d.ts +13 -1
  47. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  48. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  49. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  50. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  51. package/dist/types/modes/types.d.ts +1 -0
  52. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  53. package/dist/types/notifications/config.d.ts +14 -1
  54. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  55. package/dist/types/notifications/index.d.ts +6 -3
  56. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  57. package/dist/types/notifications/telegram-daemon.d.ts +23 -3
  58. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  59. package/dist/types/notifications/threaded-render.d.ts +6 -0
  60. package/dist/types/runtime-api-key.d.ts +7 -0
  61. package/dist/types/sdk.d.ts +3 -0
  62. package/dist/types/session/agent-session.d.ts +53 -1
  63. package/dist/types/session/agent-storage.d.ts +1 -0
  64. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  65. package/dist/types/session/messages.d.ts +4 -0
  66. package/dist/types/session/session-manager.d.ts +17 -1
  67. package/dist/types/skill-state/active-state.d.ts +18 -1
  68. package/dist/types/system-prompt.d.ts +18 -2
  69. package/dist/types/task/executor.d.ts +3 -0
  70. package/dist/types/task/fork-context-budget.d.ts +7 -0
  71. package/dist/types/task/token-log.d.ts +30 -0
  72. package/dist/types/task/token-log.test.d.ts +1 -0
  73. package/dist/types/tools/bisect.d.ts +81 -0
  74. package/dist/types/tools/computer.d.ts +2 -2
  75. package/dist/types/tools/index.d.ts +1 -0
  76. package/dist/types/tools/search.d.ts +2 -2
  77. package/dist/types/utils/file-mentions.d.ts +14 -0
  78. package/dist/types/utils/git.d.ts +32 -0
  79. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  80. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  81. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  82. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  83. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  84. package/package.json +7 -7
  85. package/src/append-only-mode.ts +58 -0
  86. package/src/autoresearch/prompt.md +1 -1
  87. package/src/autoresearch/tools/log-experiment.ts +1 -1
  88. package/src/capability/rule.ts +9 -0
  89. package/src/cli/completion-cli.ts +234 -0
  90. package/src/cli/config-cli.ts +61 -9
  91. package/src/cli/fast-help.ts +1 -0
  92. package/src/cli/fixture-report.test.ts +230 -0
  93. package/src/cli/fixture-report.ts +171 -0
  94. package/src/cli/nofile-limit.ts +48 -0
  95. package/src/cli/notify-cli.ts +91 -7
  96. package/src/cli/update-cli.ts +85 -9
  97. package/src/cli.ts +29 -1
  98. package/src/commands/completion.ts +84 -0
  99. package/src/commands/config.ts +2 -0
  100. package/src/commands/contribution-prep.ts +1 -1
  101. package/src/commands/deep-interview.ts +2 -1
  102. package/src/config/file-lock.ts +73 -34
  103. package/src/config/keybindings.ts +2 -2
  104. package/src/config/model-profile-activation.ts +65 -0
  105. package/src/config/model-registry.ts +14 -2
  106. package/src/config/settings-schema.ts +68 -1
  107. package/src/config/settings.ts +22 -0
  108. package/src/coordinator-mcp/server.ts +34 -8
  109. package/src/defaults/gjc/skills/deep-interview/SKILL.md +50 -18
  110. package/src/defaults/gjc/skills/ralplan/SKILL.md +40 -99
  111. package/src/defaults/gjc/skills/team/SKILL.md +17 -14
  112. package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
  113. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  114. package/src/defaults/gjc-defaults.ts +9 -0
  115. package/src/discovery/helpers.ts +10 -0
  116. package/src/discovery/index.ts +1 -0
  117. package/src/export/html/template.generated.ts +1 -1
  118. package/src/export/html/template.js +60 -2
  119. package/src/export/ttsr.ts +96 -18
  120. package/src/extensibility/extensions/runner.ts +183 -219
  121. package/src/extensibility/extensions/types.ts +9 -0
  122. package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
  123. package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
  124. package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
  125. package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
  126. package/src/gjc-runtime/deep-interview-state.ts +44 -2
  127. package/src/gjc-runtime/session-state-sidecar.ts +263 -20
  128. package/src/gjc-runtime/state-runtime.ts +12 -2
  129. package/src/gjc-runtime/state-writer.ts +15 -0
  130. package/src/gjc-runtime/team-runtime.ts +187 -15
  131. package/src/gjc-runtime/ultragoal-guard.ts +55 -66
  132. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
  133. package/src/gjc-runtime/ultragoal-runtime.ts +1663 -159
  134. package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
  135. package/src/gjc-runtime/workflow-manifest.ts +74 -1
  136. package/src/goals/runtime.ts +10 -2
  137. package/src/hindsight/backend.ts +1 -3
  138. package/src/hooks/native-skill-hook.ts +124 -4
  139. package/src/internal-urls/docs-index.generated.ts +16 -7
  140. package/src/main.ts +95 -43
  141. package/src/memories/index.ts +12 -1
  142. package/src/modes/components/assistant-message.ts +241 -82
  143. package/src/modes/components/custom-editor.ts +1 -0
  144. package/src/modes/components/model-selector.ts +54 -11
  145. package/src/modes/components/queued-message-selector.ts +92 -0
  146. package/src/modes/components/settings-selector.ts +4 -0
  147. package/src/modes/components/status-line.ts +167 -54
  148. package/src/modes/components/tool-execution.ts +44 -19
  149. package/src/modes/components/welcome.ts +160 -44
  150. package/src/modes/controllers/command-controller.ts +40 -4
  151. package/src/modes/controllers/event-controller.ts +66 -47
  152. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  153. package/src/modes/controllers/input-controller.ts +180 -18
  154. package/src/modes/controllers/selector-controller.ts +122 -27
  155. package/src/modes/interactive-mode.ts +51 -16
  156. package/src/modes/print-mode.ts +43 -4
  157. package/src/modes/prompt-action-autocomplete.ts +21 -20
  158. package/src/modes/rpc/rpc-mode.ts +29 -3
  159. package/src/modes/rpc/rpc-types.ts +9 -0
  160. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  161. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  162. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  163. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  164. package/src/modes/types.ts +1 -0
  165. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  166. package/src/modes/utils/ui-helpers.ts +19 -6
  167. package/src/notifications/config.ts +25 -5
  168. package/src/notifications/daemon-paths.ts +2 -0
  169. package/src/notifications/index.ts +117 -14
  170. package/src/notifications/lifecycle-commands.ts +37 -7
  171. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  172. package/src/notifications/telegram-daemon-cli.ts +2 -2
  173. package/src/notifications/telegram-daemon-control.ts +5 -3
  174. package/src/notifications/telegram-daemon.ts +302 -51
  175. package/src/notifications/telegram-reference.ts +0 -16
  176. package/src/notifications/threaded-render.ts +17 -1
  177. package/src/prompts/agents/architect.md +19 -10
  178. package/src/prompts/agents/critic.md +16 -18
  179. package/src/prompts/agents/executor.md +14 -11
  180. package/src/prompts/agents/planner.md +18 -13
  181. package/src/prompts/ci-green-request.md +3 -8
  182. package/src/prompts/goals/goal-continuation.md +0 -5
  183. package/src/prompts/goals/goal-mode-active.md +0 -5
  184. package/src/prompts/system/agent-creation-architect.md +2 -2
  185. package/src/prompts/system/plan-mode-active.md +1 -1
  186. package/src/prompts/system/plan-mode-subagent.md +3 -3
  187. package/src/prompts/system/project-prompt.md +0 -13
  188. package/src/prompts/system/subagent-system-prompt.md +3 -2
  189. package/src/prompts/system/system-prompt.md +29 -19
  190. package/src/prompts/system/volatile-project-context.md +10 -0
  191. package/src/prompts/system/web-search.md +4 -7
  192. package/src/prompts/tools/apply-patch.md +6 -0
  193. package/src/prompts/tools/ast-edit.md +1 -0
  194. package/src/prompts/tools/bash.md +1 -1
  195. package/src/prompts/tools/bisect.md +22 -0
  196. package/src/prompts/tools/browser.md +1 -1
  197. package/src/prompts/tools/computer.md +2 -2
  198. package/src/prompts/tools/cron.md +0 -2
  199. package/src/prompts/tools/eval.md +2 -2
  200. package/src/prompts/tools/find.md +1 -1
  201. package/src/prompts/tools/github.md +6 -6
  202. package/src/prompts/tools/goal.md +1 -3
  203. package/src/prompts/tools/image-gen.md +12 -5
  204. package/src/prompts/tools/irc.md +5 -4
  205. package/src/prompts/tools/job.md +2 -2
  206. package/src/prompts/tools/lsp.md +1 -1
  207. package/src/prompts/tools/monitor.md +2 -2
  208. package/src/prompts/tools/patch.md +2 -2
  209. package/src/prompts/tools/replace.md +2 -21
  210. package/src/prompts/tools/rewind.md +1 -1
  211. package/src/prompts/tools/search-tool-bm25.md +1 -11
  212. package/src/prompts/tools/search.md +1 -1
  213. package/src/prompts/tools/skill.md +3 -2
  214. package/src/prompts/tools/ssh.md +2 -1
  215. package/src/prompts/tools/subagent.md +2 -2
  216. package/src/prompts/tools/task.md +4 -5
  217. package/src/prompts/tools/todo-write.md +1 -1
  218. package/src/prompts/tools/vim.md +1 -0
  219. package/src/prompts/tools/web-search.md +2 -0
  220. package/src/prompts/tools/write.md +8 -2
  221. package/src/runtime-api-key.ts +16 -0
  222. package/src/sdk.ts +52 -28
  223. package/src/session/agent-session.ts +951 -132
  224. package/src/session/agent-storage.ts +4 -0
  225. package/src/session/file-mention-pruning.ts +64 -0
  226. package/src/session/messages.ts +4 -0
  227. package/src/session/session-manager.ts +103 -4
  228. package/src/skill-state/active-state.ts +141 -5
  229. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  230. package/src/slash-commands/builtin-registry.ts +218 -71
  231. package/src/system-prompt.ts +57 -16
  232. package/src/task/executor.ts +132 -24
  233. package/src/task/fork-context-advisory.ts +2 -7
  234. package/src/task/fork-context-budget.ts +9 -0
  235. package/src/task/index.ts +13 -6
  236. package/src/task/output-manager.ts +20 -8
  237. package/src/task/receipt.ts +26 -7
  238. package/src/task/token-log.test.ts +188 -0
  239. package/src/task/token-log.ts +179 -0
  240. package/src/tools/bisect.ts +367 -0
  241. package/src/tools/computer.ts +246 -43
  242. package/src/tools/index.ts +3 -0
  243. package/src/tools/output-meta.ts +116 -4
  244. package/src/tools/search.ts +8 -2
  245. package/src/tools/skill.ts +15 -0
  246. package/src/tools/telegram-send.ts +6 -0
  247. package/src/utils/file-mentions.ts +39 -7
  248. package/src/utils/git.ts +73 -0
  249. package/src/web/scrapers/youtube.ts +24 -12
  250. package/src/workflow/workflow-intent-diff.ts +29 -6
  251. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  252. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  253. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -11,7 +11,7 @@ import { createPromptActionAutocompleteProvider } from "../../modes/prompt-actio
11
11
  import { theme } from "../../modes/theme/theme";
12
12
  import { scrollTmuxToPreviousUserInput as scrollTmuxPaneToPreviousUserInput } from "../../modes/tmux-scroll";
13
13
  import type { InteractiveModeContext } from "../../modes/types";
14
- import type { AgentSessionEvent } from "../../session/agent-session";
14
+ import type { AgentSessionEvent, QueuedMessageEditEntry } from "../../session/agent-session";
15
15
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
16
16
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
17
17
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
@@ -19,6 +19,7 @@ import { getEditorCommand, openInEditor } from "../../utils/external-editor";
19
19
  import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
20
20
  import { resizeImage } from "../../utils/image-resize";
21
21
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
22
+ import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
22
23
 
23
24
  interface Expandable {
24
25
  setExpanded(expanded: boolean): void;
@@ -313,14 +314,7 @@ export class InputController {
313
314
  this.ctx.editor.onDequeue = () => this.handleDequeue();
314
315
  this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
315
316
  this.ctx.editor.onQueue = () => void this.handleQueueSubmit();
316
- this.ctx.editor.onTab = () => {
317
- if (!this.ctx.session.isStreaming && !this.ctx.session.isCompacting) return false;
318
- void this.handleQueueSubmit();
319
- return true;
320
- };
321
- this.ctx.editor.onTabDeclined = () => {
322
- if (this.ctx.session.isStreaming || this.ctx.session.isCompacting) void this.handleQueueSubmit();
323
- };
317
+
324
318
  this.ctx.editor.onViewportPageScroll = direction => this.ctx.ui.scrollViewportPages(direction);
325
319
  this.ctx.editor.onViewportFollowLive = () => {
326
320
  this.ctx.ui.followLiveViewport();
@@ -614,12 +608,182 @@ export class InputController {
614
608
  }
615
609
 
616
610
  handleDequeue(): void {
617
- const restored = this.restoreLatestQueuedMessageToEditor();
618
- if (restored === 0) {
611
+ const entries = this.#getEditableQueuedMessages();
612
+ if (entries.length === 0) {
613
+ this.ctx.updatePendingMessagesDisplay();
619
614
  this.ctx.showStatus("No queued messages to restore");
620
- } else {
621
- this.ctx.showStatus(`Restored ${restored} queued message${restored > 1 ? "s" : ""} to editor`);
615
+ return;
616
+ }
617
+ if (entries.length === 1) {
618
+ const restored = this.#restoreQueuedMessageToEditor(entries[0]);
619
+ this.ctx.showStatus(
620
+ restored === 0 ? "Queued message is no longer available" : "Restored queued message to editor",
621
+ );
622
+ return;
623
+ }
624
+ this.#showQueuedMessageSelector(entries, this.#newestQueuedMessageIndex(entries));
625
+ }
626
+
627
+ #compactionQueuedMessageId(index: number): string {
628
+ return `compaction:${index}`;
629
+ }
630
+
631
+ #getEditableQueuedMessages(): QueuedMessageEditEntry[] {
632
+ const compactionEntries = this.ctx.compactionQueuedMessages.map((entry, index): QueuedMessageEditEntry => {
633
+ const label = entry.mode === "steer" ? "Steer" : "Queued";
634
+ return {
635
+ id: this.#compactionQueuedMessageId(index),
636
+ text: entry.text,
637
+ mode: entry.mode,
638
+ label,
639
+ };
640
+ });
641
+ return [...compactionEntries, ...this.ctx.session.getQueuedMessageEntries()];
642
+ }
643
+
644
+ #queuedMessageStableSequence(entry: QueuedMessageEditEntry): number | undefined {
645
+ const [mode, sequenceText] = entry.id.split(":");
646
+ if ((mode !== "steer" && mode !== "followUp") || sequenceText === undefined) return undefined;
647
+ const sequence = Number(sequenceText);
648
+ return Number.isInteger(sequence) ? sequence : undefined;
649
+ }
650
+
651
+ #newestQueuedMessageIndex(entries: QueuedMessageEditEntry[]): number {
652
+ let selectedIndex = entries.length - 1;
653
+ let newestSequence = Number.NEGATIVE_INFINITY;
654
+ for (let index = 0; index < entries.length; index += 1) {
655
+ const sequence = this.#queuedMessageStableSequence(entries[index]);
656
+ if (sequence !== undefined && sequence > newestSequence) {
657
+ newestSequence = sequence;
658
+ selectedIndex = index;
659
+ }
660
+ }
661
+ return Math.max(0, selectedIndex);
662
+ }
663
+
664
+ #restoreEditorFocus(): void {
665
+ this.ctx.editorContainer.clear();
666
+ this.ctx.editorContainer.addChild(this.ctx.editor);
667
+ this.ctx.ui.setFocus(this.ctx.editor);
668
+ }
669
+
670
+ #showQueuedMessageSelector(entries: QueuedMessageEditEntry[], selectedIndex = 0): void {
671
+ const selector = new QueuedMessageSelectorComponent(
672
+ entries,
673
+ entry => {
674
+ const restored = this.#restoreQueuedMessageToEditor(entry);
675
+ this.#restoreEditorFocus();
676
+ this.ctx.showStatus(
677
+ restored === 0 ? "Queued message is no longer available" : "Restored queued message to editor",
678
+ );
679
+ this.ctx.ui.requestRender();
680
+ },
681
+ (entry, index) => {
682
+ const deleted = this.#deleteQueuedMessage(entry);
683
+ const nextEntries = this.#getEditableQueuedMessages();
684
+ if (nextEntries.length === 0) {
685
+ this.#restoreEditorFocus();
686
+ this.ctx.showStatus(deleted ? "Deleted queued message" : "Queued message is no longer available");
687
+ this.ctx.ui.requestRender();
688
+ return;
689
+ }
690
+ this.ctx.showStatus(deleted ? "Deleted queued message" : "Queued message is no longer available");
691
+ this.#showQueuedMessageSelector(nextEntries, Math.min(index, nextEntries.length - 1));
692
+ },
693
+ (entry, index, direction) => {
694
+ const moved = this.#moveQueuedMessage(entry, direction);
695
+ const nextEntries = this.#getEditableQueuedMessages();
696
+ if (nextEntries.length === 0) {
697
+ this.#restoreEditorFocus();
698
+ this.ctx.showStatus("Queued message is no longer available");
699
+ this.ctx.ui.requestRender();
700
+ return;
701
+ }
702
+ const nextIndex = direction === "up" ? index - 1 : index + 1;
703
+ const selectedNextIndex = moved ? nextIndex : index;
704
+ this.ctx.showStatus(moved ? "Moved queued message" : "Queued message cannot move further");
705
+ this.#showQueuedMessageSelector(
706
+ nextEntries,
707
+ Math.max(0, Math.min(selectedNextIndex, nextEntries.length - 1)),
708
+ );
709
+ },
710
+ () => {
711
+ this.#restoreEditorFocus();
712
+ this.ctx.ui.requestRender();
713
+ },
714
+ { selectedIndex },
715
+ );
716
+ this.ctx.editorContainer.clear();
717
+ this.ctx.editorContainer.addChild(selector);
718
+ this.ctx.ui.setFocus(selector);
719
+ this.ctx.ui.requestRender();
720
+ }
721
+
722
+ #removeQueuedMessageForEditing(id: string): string | undefined {
723
+ const compactionPrefix = "compaction:";
724
+ if (id.startsWith(compactionPrefix)) {
725
+ const index = Number(id.slice(compactionPrefix.length));
726
+ if (!Number.isInteger(index)) return undefined;
727
+ const [entry] = this.ctx.compactionQueuedMessages.splice(index, 1);
728
+ return entry?.text;
729
+ }
730
+ return this.ctx.session.removeQueuedMessageForEditing(id);
731
+ }
732
+ #parseCompactionQueuedMessageId(id: string): number | undefined {
733
+ const compactionPrefix = "compaction:";
734
+ if (!id.startsWith(compactionPrefix)) return undefined;
735
+ const index = Number(id.slice(compactionPrefix.length));
736
+ return Number.isInteger(index) ? index : undefined;
737
+ }
738
+
739
+ #moveCompactionQueuedMessage(index: number, direction: QueuedMessageMoveDirection): boolean {
740
+ const targetIndex = direction === "up" ? index - 1 : index + 1;
741
+ if (index < 0 || index >= this.ctx.compactionQueuedMessages.length) return false;
742
+ if (targetIndex < 0 || targetIndex >= this.ctx.compactionQueuedMessages.length) return false;
743
+ const current = this.ctx.compactionQueuedMessages[index];
744
+ const target = this.ctx.compactionQueuedMessages[targetIndex];
745
+ if (!current || !target) return false;
746
+ this.ctx.compactionQueuedMessages[index] = target;
747
+ this.ctx.compactionQueuedMessages[targetIndex] = current;
748
+ this.ctx.updatePendingMessagesDisplay();
749
+ return true;
750
+ }
751
+
752
+ #moveQueuedMessage(entry: QueuedMessageEditEntry, direction: QueuedMessageMoveDirection): boolean {
753
+ const compactionIndex = this.#parseCompactionQueuedMessageId(entry.id);
754
+ if (compactionIndex !== undefined) {
755
+ return this.#moveCompactionQueuedMessage(compactionIndex, direction);
756
+ }
757
+ const moved = this.ctx.session.moveQueuedMessageForEditing(entry.id, direction);
758
+ if (moved) {
759
+ this.ctx.updatePendingMessagesDisplay();
622
760
  }
761
+ return moved;
762
+ }
763
+
764
+ #deleteQueuedMessage(entry: QueuedMessageEditEntry): boolean {
765
+ const queuedText = this.#removeQueuedMessageForEditing(entry.id);
766
+ this.ctx.updatePendingMessagesDisplay();
767
+ if (!queuedText) return false;
768
+ this.ctx.locallySubmittedUserSignatures.delete(`${queuedText}\u00000`);
769
+ return true;
770
+ }
771
+
772
+ #restoreQueuedMessageToEditor(entry: QueuedMessageEditEntry | undefined): number {
773
+ if (!entry) {
774
+ this.ctx.updatePendingMessagesDisplay();
775
+ return 0;
776
+ }
777
+ const queuedText = this.#removeQueuedMessageForEditing(entry.id);
778
+ if (!queuedText) {
779
+ this.ctx.updatePendingMessagesDisplay();
780
+ return 0;
781
+ }
782
+
783
+ this.ctx.locallySubmittedUserSignatures.delete(`${queuedText}\u00000`);
784
+ this.ctx.editor.setText(queuedText);
785
+ this.ctx.updatePendingMessagesDisplay();
786
+ return 1;
623
787
  }
624
788
 
625
789
  /**
@@ -766,7 +930,7 @@ export class InputController {
766
930
  return this.handleFollowUp();
767
931
  }
768
932
 
769
- restoreLatestQueuedMessageToEditor(options?: { currentText?: string }): number {
933
+ restoreLatestQueuedMessageToEditor(): number {
770
934
  const compactionQueued = this.ctx.compactionQueuedMessages.pop();
771
935
  const queuedText = compactionQueued?.text ?? this.ctx.session.popLastQueuedMessage();
772
936
  if (!queuedText) {
@@ -775,9 +939,7 @@ export class InputController {
775
939
  }
776
940
 
777
941
  this.ctx.locallySubmittedUserSignatures.delete(`${queuedText}\u00000`);
778
- const currentText = options?.currentText ?? this.ctx.editor.getText();
779
- const combinedText = [queuedText, currentText].filter(t => t.trim()).join("\n\n");
780
- this.ctx.editor.setText(combinedText);
942
+ this.ctx.editor.setText(queuedText);
781
943
  this.ctx.updatePendingMessagesDisplay();
782
944
  return 1;
783
945
  }
@@ -1146,7 +1308,7 @@ export class InputController {
1146
1308
  // If streaming, re-add the streaming component with updated visibility and re-render
1147
1309
  if (this.ctx.streamingComponent && this.ctx.streamingMessage) {
1148
1310
  this.ctx.streamingComponent.setHideThinkingBlock(this.ctx.hideThinkingBlock);
1149
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
1311
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage, { streaming: true });
1150
1312
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
1151
1313
  }
1152
1314
 
@@ -8,11 +8,12 @@ import {
8
8
  activateModelProfile,
9
9
  type MaterializeModelProfileForDeletionResult,
10
10
  materializeActiveModelProfileAssignment,
11
+ materializeActiveModelProfileAssignments,
11
12
  materializeModelProfileForDeletion,
12
13
  restoreMaterializedModelProfileForDeletion,
13
14
  } from "../../config/model-profile-activation";
14
15
  import { formatModelProfileDisplayLabel, recommendModelProfileForProvider } from "../../config/model-profiles";
15
- import { GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
16
+ import { GJC_MODEL_ASSIGNMENT_TARGETS, type GjcModelAssignmentTargetId } from "../../config/model-registry";
16
17
  import { formatModelSelectorValue } from "../../config/model-resolver";
17
18
  import type { ModelProfileConfig } from "../../config/models-config-schema";
18
19
  import { type Settings, settings } from "../../config/settings";
@@ -438,6 +439,7 @@ export class SelectorController {
438
439
  availableThinkingLevels: [...this.ctx.session.getAvailableThinkingLevels()],
439
440
  thinkingLevel: this.ctx.session.thinkingLevel,
440
441
  availableThemes,
442
+ availableModelProfiles: [...this.ctx.session.modelRegistry.getModelProfiles().keys()],
441
443
  cwd: getProjectDir(),
442
444
  },
443
445
  {
@@ -634,6 +636,18 @@ export class SelectorController {
634
636
  this.ctx.updateEditorBorderColor();
635
637
  break;
636
638
 
639
+ case "modelProfile.default": {
640
+ // Applying the default profile live mirrors the /model preset flow so the
641
+ // running session switches immediately, not only on next startup.
642
+ const profileName = typeof value === "string" ? value : "";
643
+ if (!profileName) break;
644
+ this.#applyModelProfile(profileName, true)
645
+ .then(() => this.ctx.ui.requestRender())
646
+ .catch(error => {
647
+ this.ctx.showError(error instanceof Error ? error.message : String(error));
648
+ });
649
+ break;
650
+ }
637
651
  case "clearOnShrink":
638
652
  this.ctx.ui.setClearOnShrink(value as boolean);
639
653
  break;
@@ -784,6 +798,29 @@ export class SelectorController {
784
798
  }
785
799
  }
786
800
 
801
+ /**
802
+ * Activate a model profile through the shared /model + /settings path: swap the
803
+ * live session model (and, when persistDefault, persist it as the startup
804
+ * default) then refresh the status surfaces. Rethrows so callers surface errors.
805
+ */
806
+ async #applyModelProfile(profileName: string, persistDefault: boolean): Promise<void> {
807
+ const profileLabel = formatModelProfileDisplayLabel(
808
+ this.ctx.session.modelRegistry.getModelProfile(profileName) ?? { name: profileName },
809
+ );
810
+ await activateModelProfile(
811
+ {
812
+ session: this.ctx.session,
813
+ modelRegistry: this.ctx.session.modelRegistry,
814
+ settings: this.ctx.settings,
815
+ profileName,
816
+ },
817
+ { persistDefault },
818
+ );
819
+ this.ctx.statusLine.invalidate();
820
+ this.ctx.updateEditorBorderColor();
821
+ this.ctx.showStatus(persistDefault ? `Default model profile: ${profileLabel}` : `Model profile: ${profileLabel}`);
822
+ }
823
+
787
824
  showModelSelector(options?: { temporaryOnly?: boolean }): void {
788
825
  this.showSelector(done => {
789
826
  let modelSelector: ModelSelectorComponent;
@@ -809,27 +846,7 @@ export class SelectorController {
809
846
  return;
810
847
  }
811
848
  if (selection.kind === "profile") {
812
- const profileLabel = formatModelProfileDisplayLabel(
813
- this.ctx.session.modelRegistry.getModelProfile(selection.profileName) ?? {
814
- name: selection.profileName,
815
- },
816
- );
817
- await activateModelProfile(
818
- {
819
- session: this.ctx.session,
820
- modelRegistry: this.ctx.session.modelRegistry,
821
- settings: this.ctx.settings,
822
- profileName: selection.profileName,
823
- },
824
- { persistDefault: selection.setDefault },
825
- );
826
- this.ctx.statusLine.invalidate();
827
- this.ctx.updateEditorBorderColor();
828
- this.ctx.showStatus(
829
- selection.setDefault
830
- ? `Default model profile: ${profileLabel}`
831
- : `Model profile: ${profileLabel}`,
832
- );
849
+ await this.#applyModelProfile(selection.profileName, selection.setDefault);
833
850
  done();
834
851
  this.ctx.ui.requestRender();
835
852
  return;
@@ -843,6 +860,79 @@ export class SelectorController {
843
860
  this.ctx.showStatus(`Temporary model: ${selectedSelector ?? model.id}`);
844
861
  done();
845
862
  this.ctx.ui.requestRender();
863
+ } else if (selection.roles) {
864
+ const targetRoles: readonly GjcModelAssignmentTargetId[] = selection.roles;
865
+ const includesDefault = targetRoles.includes("default");
866
+ const includesRoleAgent = targetRoles.some(targetRole => targetRole !== "default");
867
+ if (includesRoleAgent) {
868
+ const apiKey = await this.ctx.session.modelRegistry.getApiKey(
869
+ model,
870
+ this.ctx.session.sessionId,
871
+ );
872
+ if (!apiKey) {
873
+ throw new Error(`No API key for ${model.provider}/${model.id}`);
874
+ }
875
+ }
876
+ const value =
877
+ selectedSelector ?? formatModelSelectorValue(`${model.provider}/${model.id}`, thinkingLevel);
878
+ const assignments = new Map<GjcModelAssignmentTargetId, string>();
879
+ for (const targetRole of targetRoles) assignments.set(targetRole, value);
880
+ const defaultSelector =
881
+ selectedSelector && thinkingLevel && selectedSelector.endsWith(`:${thinkingLevel}`)
882
+ ? selectedSelector.slice(0, -thinkingLevel.length - 1)
883
+ : selectedSelector;
884
+
885
+ if (includesDefault) {
886
+ await this.ctx.session.setModel(model, "default", {
887
+ selector: defaultSelector,
888
+ thinkingLevel,
889
+ });
890
+ if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
891
+ this.ctx.session.setThinkingLevel(thinkingLevel);
892
+ }
893
+ }
894
+ const materializedProfile = materializeActiveModelProfileAssignments({
895
+ session: this.ctx.session,
896
+ settings: this.ctx.settings,
897
+ assignments,
898
+ });
899
+ if (!materializedProfile) {
900
+ const overrides = this.ctx.settings.get("task.agentModelOverrides");
901
+ const nextOverrides = { ...overrides };
902
+ let writesOverrides = false;
903
+ for (const targetRole of targetRoles) {
904
+ const target = GJC_MODEL_ASSIGNMENT_TARGETS[targetRole];
905
+ if (target.settingsPath === "modelRoles") {
906
+ this.ctx.settings.setModelRole(targetRole, value);
907
+ } else {
908
+ nextOverrides[targetRole] = value;
909
+ writesOverrides = true;
910
+ }
911
+ }
912
+ if (writesOverrides) {
913
+ this.ctx.settings.set("task.agentModelOverrides", nextOverrides);
914
+ }
915
+ }
916
+ modelSelector.refreshRoleAssignments({
917
+ currentModel: this.ctx.session.model,
918
+ currentThinkingLevel: this.ctx.session.thinkingLevel,
919
+ activeModelProfile:
920
+ this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
921
+ });
922
+ this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
923
+ this.ctx.statusLine.invalidate();
924
+ this.ctx.updateEditorBorderColor();
925
+ await this.ctx.notifyConfigChanged?.();
926
+ const labels = targetRoles.map(
927
+ targetRole => GJC_MODEL_ASSIGNMENT_TARGETS[targetRole].tag ?? targetRole.toUpperCase(),
928
+ );
929
+ this.ctx.showStatus(
930
+ includesDefault
931
+ ? `All model targets set to ${value} for ${labels.join(", ")}.`
932
+ : `Role-agent models set to ${value} for ${labels.join(", ")}.`,
933
+ );
934
+ done();
935
+ this.ctx.ui.requestRender();
846
936
  } else if (role === "default") {
847
937
  // Default: update agent state and persist as the active default model.
848
938
  await this.ctx.session.setModel(model, role, {
@@ -880,19 +970,21 @@ export class SelectorController {
880
970
  }
881
971
  const value =
882
972
  selectedSelector ?? formatModelSelectorValue(`${model.provider}/${model.id}`, thinkingLevel);
883
- const materializedProfile = materializeActiveModelProfileAssignment({
973
+ const assignments = new Map<GjcModelAssignmentTargetId, string>([[role, value]]);
974
+ const materializedProfile = materializeActiveModelProfileAssignments({
884
975
  session: this.ctx.session,
885
976
  settings: this.ctx.settings,
886
- role,
887
- selector: value,
977
+ assignments,
888
978
  });
889
979
  if (!materializedProfile) {
890
980
  const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
891
981
  if (target.settingsPath === "modelRoles") {
892
982
  this.ctx.settings.setModelRole(role, value);
893
983
  } else {
894
- const overrides = this.ctx.settings.get("task.agentModelOverrides");
895
- this.ctx.settings.set("task.agentModelOverrides", { ...overrides, [role]: value });
984
+ this.ctx.settings.set("task.agentModelOverrides", {
985
+ ...this.ctx.settings.get("task.agentModelOverrides"),
986
+ [role]: value,
987
+ });
896
988
  }
897
989
  }
898
990
  modelSelector.refreshRoleAssignments({
@@ -902,6 +994,9 @@ export class SelectorController {
902
994
  this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
903
995
  });
904
996
  this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
997
+ this.ctx.statusLine.invalidate();
998
+ this.ctx.updateEditorBorderColor();
999
+ await this.ctx.notifyConfigChanged?.();
905
1000
  this.ctx.showStatus(`${role} agent model: ${value}`);
906
1001
  done();
907
1002
  this.ctx.ui.requestRender();
@@ -117,12 +117,14 @@ import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInpu
117
117
  import { UiHelpers } from "./utils/ui-helpers";
118
118
 
119
119
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
120
- const DEFAULT_COMPOSER_PLACEHOLDER = "Type your message...";
120
+ const COMPOSER_NEWLINE_HINT = process.platform === "win32" ? "Alt+Enter/Ctrl+J" : "Shift+Enter/Ctrl+J";
121
+ export const DEFAULT_COMPOSER_PLACEHOLDER = `Type your message... ${COMPOSER_NEWLINE_HINT}: New line · Ctrl+C: Clear · Ctrl+R: Search history · Shift+Tab: Reasoning`;
122
+ const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
121
123
  const FRIENDLY_KEY_PARTS: Record<string, string> = {
122
124
  alt: "Alt",
123
- cmd: "Command",
124
- command: "Command",
125
- ctrl: "Control",
125
+ cmd: "Cmd",
126
+ command: "Cmd",
127
+ ctrl: "Ctrl",
126
128
  enter: "Enter",
127
129
  meta: process.platform === "darwin" ? "Command" : "Meta",
128
130
  option: "Option",
@@ -132,7 +134,7 @@ const FRIENDLY_KEY_PARTS: Record<string, string> = {
132
134
  function formatShortcutForPlaceholder(key: string): string {
133
135
  return key
134
136
  .split("+")
135
- .map(part => FRIENDLY_KEY_PARTS[part.toLowerCase()] ?? part)
137
+ .map(part => FRIENDLY_KEY_PARTS[part.toLowerCase()] ?? (part.length === 1 ? part.toUpperCase() : part))
136
138
  .join("+");
137
139
  }
138
140
 
@@ -161,6 +163,7 @@ function configureDefaultComposerChrome(editor: CustomEditor): void {
161
163
  editor.setInputPrefix(getDefaultInputPrefix());
162
164
  editor.setPlaceholder(DEFAULT_COMPOSER_PLACEHOLDER);
163
165
  editor.setPaddingX(1);
166
+ editor.setRightGutterWidth(1);
164
167
  editor.setTopBorder(undefined);
165
168
  }
166
169
 
@@ -443,7 +446,6 @@ export class InteractiveMode implements InteractiveModeContext {
443
446
  logger.warn("History storage unavailable", { error: String(error) });
444
447
  }
445
448
  this.hookWidgetContainerAbove = new Container();
446
- this.hookWidgetContainerAbove.addChild(new Spacer(1));
447
449
  this.hookWidgetContainerBelow = new Container();
448
450
  this.editorContainer = new Container();
449
451
  this.editorContainer.addChild(this.editor);
@@ -531,13 +533,7 @@ export class InteractiveMode implements InteractiveModeContext {
531
533
  }
532
534
 
533
535
  if (!startupQuiet) {
534
- const getWelcomeReservedBottomRows = (width: number): number =>
535
- [
536
- this.statusLine,
537
- this.hookWidgetContainerAbove,
538
- this.editorContainer,
539
- this.hookWidgetContainerBelow,
540
- ].reduce((rows, component) => rows + component.render(width).length, 0);
536
+ const getWelcomeReservedBottomRows = (width: number): number => this.#getWelcomeReservedRows(width);
541
537
 
542
538
  // Add welcome header
543
539
  this.#welcomeComponent = new WelcomeComponent(
@@ -940,11 +936,37 @@ export class InteractiveMode implements InteractiveModeContext {
940
936
 
941
937
  #getComposerPlaceholder(): string {
942
938
  if (!this.#isPromptDeliveryBusy()) return DEFAULT_COMPOSER_PLACEHOLDER;
943
- const enterAction = this.settings.get("busyPromptMode") === "steer" ? "Steering" : "Message Queueing";
939
+ const enterAction = this.settings.get("busyPromptMode") === "steer" ? "Steer" : "Queue";
944
940
  const parts = [`Enter: ${enterAction}`];
945
941
  const queueKey = this.#getMessageQueueShortcut();
946
- if (queueKey) parts.push(`${formatShortcutForPlaceholder(queueKey)}: Message Queueing`);
947
- return `${DEFAULT_COMPOSER_PLACEHOLDER} ${parts.join(" · ")}`;
942
+ if (queueKey) parts.push(`${formatShortcutForPlaceholder(queueKey)}: Queue`);
943
+ return `${DEFAULT_COMPOSER_PLACEHOLDER} · ${parts.join(" · ")}`;
944
+ }
945
+
946
+ #getWelcomeReservedRows(width: number): number {
947
+ const transientRows = [
948
+ this.chatContainer,
949
+ this.pendingMessagesContainer,
950
+ this.statusContainer,
951
+ this.todoContainer,
952
+ this.btwContainer,
953
+ ].reduce((rows, container) => rows + this.#renderShortContainerRowsForWelcomeReservation(width, container), 0);
954
+
955
+ const pinnedRows = [
956
+ this.statusLine,
957
+ this.hookWidgetContainerAbove,
958
+ this.editorContainer,
959
+ this.hookWidgetContainerBelow,
960
+ ].reduce((rows, component) => rows + component.render(width).length, 0);
961
+
962
+ return transientRows + pinnedRows;
963
+ }
964
+
965
+ #renderShortContainerRowsForWelcomeReservation(width: number, container: Container): number {
966
+ if (container.children.length === 0 || container.children.length > WELCOME_RESERVED_CONTAINER_CHILD_LIMIT) {
967
+ return 0;
968
+ }
969
+ return container.render(width).length;
948
970
  }
949
971
 
950
972
  updateEditorChrome(): void {
@@ -1351,6 +1373,12 @@ export class InteractiveMode implements InteractiveModeContext {
1351
1373
  return;
1352
1374
  }
1353
1375
 
1376
+ // Leaving plan mode invalidates the plan-mode steering and standing
1377
+ // resolve handler that the in-flight turn may still be acting on. Abort
1378
+ // before clearing that state so the model cannot continue from stale
1379
+ // plan-mode context and retry `resolve` against a retired handler.
1380
+ await this.session.abort({ timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS });
1381
+
1354
1382
  const previousTools = this.#planModePreviousTools;
1355
1383
  if (previousTools && previousTools.length > 0) {
1356
1384
  await this.session.setActiveToolsByName(previousTools);
@@ -1688,6 +1716,7 @@ export class InteractiveMode implements InteractiveModeContext {
1688
1716
  planContent,
1689
1717
  finalPlanFilePath: options.finalPlanFilePath,
1690
1718
  contextPreserved: options.preserveContext === true,
1719
+ tools: this.session.getActiveToolNames(),
1691
1720
  });
1692
1721
  await this.session.prompt(planModePrompt, { synthetic: true });
1693
1722
  }
@@ -2179,6 +2208,7 @@ export class InteractiveMode implements InteractiveModeContext {
2179
2208
  message => renderWorkingMessage(message, this.#getWorkingMessageAccent()),
2180
2209
  this.#defaultWorkingMessage,
2181
2210
  getSymbolTheme().spinnerFrames,
2211
+ { timeDependentColor: true },
2182
2212
  );
2183
2213
  this.statusContainer.addChild(this.loadingAnimation);
2184
2214
  }
@@ -2336,6 +2366,11 @@ export class InteractiveMode implements InteractiveModeContext {
2336
2366
  return this.#commandController.handleClearCommand();
2337
2367
  }
2338
2368
 
2369
+ handleContextClearCommand(): Promise<void> {
2370
+ this.#prepareSessionSwitch();
2371
+ return this.#commandController.handleContextClearCommand();
2372
+ }
2373
+
2339
2374
  handleDropCommand(): Promise<void> {
2340
2375
  this.#prepareSessionSwitch();
2341
2376
  return this.#commandController.handleDropCommand();
@@ -5,7 +5,7 @@
5
5
  * - `gjc -p "prompt"` - text output
6
6
  * - `gjc --mode json "prompt"` - JSON event stream
7
7
  */
8
- import type { AssistantMessage, ImageContent } from "@gajae-code/ai";
8
+ import { type AssistantMessage, type ImageContent, isContextOverflow } from "@gajae-code/ai";
9
9
  import { logger, sanitizeText } from "@gajae-code/utils";
10
10
  import type { AgentSession } from "../session/agent-session";
11
11
  import { isSilentAbort } from "../session/messages";
@@ -31,6 +31,33 @@ export interface PrintModeOptions {
31
31
  suppressProcessExit?: boolean;
32
32
  }
33
33
 
34
+ /**
35
+ * Exit code used when a non-interactive **text-mode** run (`gjc -p`) terminates
36
+ * because the model context window is exhausted and automatic compaction could
37
+ * not bring the request under the limit. Distinct from the generic failure code
38
+ * (1) so text-mode callers can detect context exhaustion specifically instead of
39
+ * parsing the raw provider error string.
40
+ *
41
+ * Scope: text-mode final-response path only. JSON mode (`--mode json`) streams
42
+ * events from the subscription and does not run this terminal-error branch, so
43
+ * it is intentionally NOT covered by this exit code.
44
+ */
45
+ export const CONTEXT_OVERFLOW_EXIT_CODE = 2;
46
+
47
+ /**
48
+ * Build an actionable stderr diagnostic for a terminal context-overflow error in
49
+ * text mode. The raw provider message is preserved (appended) for debugging, but
50
+ * the leading guidance explains what happened and what the operator can do —
51
+ * tailored to whether auto-compaction was even enabled.
52
+ */
53
+ function formatContextOverflowError(message: AssistantMessage, autoCompactionEnabled: boolean): string {
54
+ const providerDetail = message.errorMessage ? ` (provider error: ${sanitizeText(message.errorMessage)})` : "";
55
+ const guidance = autoCompactionEnabled
56
+ ? "Context window exhausted: automatic compaction ran but could not reduce the request below the model's context limit. Reduce the input size (smaller file reads / tool output), raise the compaction threshold, or switch to a larger-context model."
57
+ : "Context window exhausted and automatic compaction is disabled. Enable it (compaction.enabled=true with a non-off compaction.strategy) so GJC can compact and continue, reduce the input size, or switch to a larger-context model.";
58
+ return `${guidance}${providerDetail}`;
59
+ }
60
+
34
61
  /**
35
62
  * Run in print (single-shot) mode.
36
63
  * Sends prompts to the agent and outputs the result.
@@ -88,15 +115,27 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
88
115
  (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") &&
89
116
  !isSilentAbort(assistantMsg.errorMessage)
90
117
  ) {
91
- const errorLine = sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
118
+ // Context-overflow is an expected, recoverable-in-principle condition — not
119
+ // an opaque crash. Auto-compaction has already run inside session.prompt();
120
+ // if we still land here the request could not be made to fit. In this
121
+ // text-mode final-response path, surface an actionable diagnostic and a
122
+ // distinct exit code so text-mode (`gjc -p`) callers can detect context
123
+ // exhaustion instead of parsing the raw provider error string. (JSON mode
124
+ // does not reach this branch and is intentionally out of scope.)
125
+ const isOverflow =
126
+ assistantMsg.stopReason === "error" && isContextOverflow(assistantMsg, session.model?.contextWindow);
127
+ const errorLine = isOverflow
128
+ ? formatContextOverflowError(assistantMsg, session.autoCompactionEnabled)
129
+ : sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
130
+ const exitCode = isOverflow ? CONTEXT_OVERFLOW_EXIT_CODE : 1;
92
131
  const flushed = process.stderr.write(`${errorLine}\n`);
93
132
  // When the caller owns finalization (RLM autonomous), return instead of
94
133
  // exiting so its cleanup runs; the caller surfaces a non-zero exit itself.
95
134
  if (!options.suppressProcessExit) {
96
135
  if (flushed) {
97
- process.exit(1);
136
+ process.exit(exitCode);
98
137
  } else {
99
- process.stderr.once("drain", () => process.exit(1));
138
+ process.stderr.once("drain", () => process.exit(exitCode));
100
139
  }
101
140
  }
102
141
  }