@gajae-code/coding-agent 0.2.5 → 0.3.1

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 (234) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/types/async/job-manager.d.ts +91 -2
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/commands/harness.d.ts +37 -0
  6. package/dist/types/config/keybindings.d.ts +5 -0
  7. package/dist/types/config/settings-schema.d.ts +10 -4
  8. package/dist/types/config/settings.d.ts +2 -0
  9. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  10. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  11. package/dist/types/deep-interview/render-middleware.d.ts +6 -0
  12. package/dist/types/eval/py/executor.d.ts +2 -0
  13. package/dist/types/eval/py/kernel.d.ts +2 -0
  14. package/dist/types/exec/bash-executor.d.ts +10 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  17. package/dist/types/extensibility/shared-events.d.ts +1 -0
  18. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  19. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  20. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  21. package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
  22. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  23. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  25. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  26. package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  28. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  29. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  30. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  31. package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
  32. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  33. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  34. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  35. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  36. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  37. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  38. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  39. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  40. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  41. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  42. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  43. package/dist/types/harness-control-plane/types.d.ts +162 -0
  44. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  45. package/dist/types/hooks/skill-state.d.ts +23 -29
  46. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  47. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  48. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  49. package/dist/types/internal-urls/types.d.ts +4 -0
  50. package/dist/types/lsp/index.d.ts +10 -10
  51. package/dist/types/modes/bridge/auth.d.ts +12 -0
  52. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  53. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  54. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  55. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  56. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  57. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  58. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  59. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  60. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  61. package/dist/types/modes/components/status-line.d.ts +2 -0
  62. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  63. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  64. package/dist/types/modes/index.d.ts +1 -0
  65. package/dist/types/modes/interactive-mode.d.ts +2 -0
  66. package/dist/types/modes/jobs-observer.d.ts +57 -0
  67. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  68. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  69. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  70. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  71. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  72. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  73. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  74. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  75. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  76. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  77. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  78. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  79. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  80. package/dist/types/modes/types.d.ts +2 -0
  81. package/dist/types/sdk.d.ts +4 -0
  82. package/dist/types/session/agent-session.d.ts +19 -1
  83. package/dist/types/skill-state/active-state.d.ts +2 -0
  84. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  85. package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
  86. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  87. package/dist/types/task/executor.d.ts +3 -0
  88. package/dist/types/task/id.d.ts +7 -0
  89. package/dist/types/task/index.d.ts +5 -0
  90. package/dist/types/task/receipt.d.ts +85 -0
  91. package/dist/types/task/spawn-gate.d.ts +38 -0
  92. package/dist/types/task/types.d.ts +198 -14
  93. package/dist/types/tools/cron.d.ts +6 -0
  94. package/dist/types/tools/index.d.ts +2 -0
  95. package/dist/types/tools/path-utils.d.ts +1 -0
  96. package/dist/types/tools/subagent.d.ts +26 -1
  97. package/package.json +7 -7
  98. package/scripts/build-binary.ts +7 -0
  99. package/src/async/job-manager.ts +334 -6
  100. package/src/cli/args.ts +9 -2
  101. package/src/cli/auth-broker-cli.ts +1 -0
  102. package/src/cli/config-cli.ts +10 -2
  103. package/src/cli.ts +2 -0
  104. package/src/commands/deep-interview.ts +1 -0
  105. package/src/commands/harness.ts +862 -0
  106. package/src/commands/launch.ts +2 -2
  107. package/src/commands/state.ts +2 -1
  108. package/src/commands/team.ts +54 -39
  109. package/src/config/keybindings.ts +6 -0
  110. package/src/config/settings-schema.ts +13 -3
  111. package/src/config/settings.ts +5 -0
  112. package/src/dap/client.ts +17 -3
  113. package/src/debug/crash-diagnostics.ts +223 -0
  114. package/src/debug/runtime-gauges.ts +20 -0
  115. package/src/deep-interview/render-middleware.ts +372 -0
  116. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  117. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  118. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  119. package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
  120. package/src/eval/py/executor.ts +21 -1
  121. package/src/eval/py/kernel.ts +15 -0
  122. package/src/exec/bash-executor.ts +41 -0
  123. package/src/extensibility/custom-tools/types.ts +1 -0
  124. package/src/extensibility/extensions/types.ts +6 -0
  125. package/src/extensibility/shared-events.ts +1 -0
  126. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  127. package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
  128. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  129. package/src/gjc-runtime/ralplan-runtime.ts +235 -43
  130. package/src/gjc-runtime/state-graph.ts +86 -0
  131. package/src/gjc-runtime/state-migrations.ts +179 -0
  132. package/src/gjc-runtime/state-renderer.ts +345 -0
  133. package/src/gjc-runtime/state-runtime.ts +1155 -46
  134. package/src/gjc-runtime/state-schema.ts +192 -0
  135. package/src/gjc-runtime/state-validation.ts +49 -0
  136. package/src/gjc-runtime/state-writer.ts +749 -0
  137. package/src/gjc-runtime/team-runtime.ts +1255 -189
  138. package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
  139. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  140. package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
  141. package/src/gjc-runtime/workflow-manifest.ts +427 -0
  142. package/src/harness-control-plane/classifier.ts +128 -0
  143. package/src/harness-control-plane/control-endpoint.ts +148 -0
  144. package/src/harness-control-plane/finalize.ts +222 -0
  145. package/src/harness-control-plane/frame-mapper.ts +286 -0
  146. package/src/harness-control-plane/operate.ts +225 -0
  147. package/src/harness-control-plane/owner.ts +600 -0
  148. package/src/harness-control-plane/preserve.ts +102 -0
  149. package/src/harness-control-plane/receipts.ts +216 -0
  150. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  151. package/src/harness-control-plane/seams.ts +39 -0
  152. package/src/harness-control-plane/session-lease.ts +388 -0
  153. package/src/harness-control-plane/state-machine.ts +98 -0
  154. package/src/harness-control-plane/storage.ts +257 -0
  155. package/src/harness-control-plane/types.ts +214 -0
  156. package/src/hooks/skill-keywords.ts +4 -2
  157. package/src/hooks/skill-state.ts +197 -64
  158. package/src/internal-urls/agent-protocol.ts +68 -21
  159. package/src/internal-urls/artifact-protocol.ts +12 -17
  160. package/src/internal-urls/docs-index.generated.ts +3 -2
  161. package/src/internal-urls/registry-helpers.ts +19 -16
  162. package/src/internal-urls/types.ts +4 -0
  163. package/src/lsp/client.ts +18 -2
  164. package/src/main.ts +21 -5
  165. package/src/modes/bridge/auth.ts +41 -0
  166. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  167. package/src/modes/bridge/bridge-mode.ts +520 -0
  168. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  169. package/src/modes/bridge/event-stream.ts +70 -0
  170. package/src/modes/components/assistant-message.ts +5 -1
  171. package/src/modes/components/custom-editor.ts +101 -0
  172. package/src/modes/components/hook-selector.ts +133 -20
  173. package/src/modes/components/jobs-overlay-model.ts +109 -0
  174. package/src/modes/components/jobs-overlay.ts +172 -0
  175. package/src/modes/components/status-line/presets.ts +7 -5
  176. package/src/modes/components/status-line/segments.ts +25 -0
  177. package/src/modes/components/status-line/types.ts +2 -0
  178. package/src/modes/components/status-line.ts +9 -1
  179. package/src/modes/controllers/event-controller.ts +71 -6
  180. package/src/modes/controllers/extension-ui-controller.ts +43 -1
  181. package/src/modes/controllers/input-controller.ts +105 -9
  182. package/src/modes/controllers/selector-controller.ts +31 -1
  183. package/src/modes/index.ts +1 -0
  184. package/src/modes/interactive-mode.ts +28 -0
  185. package/src/modes/jobs-observer.ts +204 -0
  186. package/src/modes/rpc/host-tools.ts +1 -186
  187. package/src/modes/rpc/host-uris.ts +1 -235
  188. package/src/modes/rpc/rpc-client.ts +25 -10
  189. package/src/modes/rpc/rpc-mode.ts +12 -381
  190. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  191. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  192. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  193. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  194. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  195. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  196. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  197. package/src/modes/shared/agent-wire/responses.ts +17 -0
  198. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  199. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  200. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  201. package/src/modes/types.ts +2 -0
  202. package/src/prompts/agents/executor.md +13 -0
  203. package/src/prompts/tools/subagent.md +39 -4
  204. package/src/prompts/tools/task-summary.md +3 -9
  205. package/src/prompts/tools/task.md +5 -1
  206. package/src/sdk.ts +8 -0
  207. package/src/session/agent-session.ts +445 -71
  208. package/src/session/session-manager.ts +13 -1
  209. package/src/skill-state/active-state.ts +58 -65
  210. package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
  211. package/src/skill-state/initial-phase.ts +2 -0
  212. package/src/skill-state/workflow-state-contract.ts +33 -4
  213. package/src/skill-state/workflow-state-version.ts +3 -0
  214. package/src/slash-commands/builtin-registry.ts +8 -0
  215. package/src/task/executor.ts +79 -13
  216. package/src/task/id.ts +33 -0
  217. package/src/task/index.ts +376 -74
  218. package/src/task/output-manager.ts +5 -4
  219. package/src/task/receipt.ts +297 -0
  220. package/src/task/render.ts +54 -134
  221. package/src/task/spawn-gate.ts +132 -0
  222. package/src/task/types.ts +104 -10
  223. package/src/tools/ask.ts +88 -27
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash.ts +1 -1
  227. package/src/tools/cron.ts +48 -0
  228. package/src/tools/find.ts +4 -1
  229. package/src/tools/index.ts +2 -0
  230. package/src/tools/path-utils.ts +3 -2
  231. package/src/tools/read.ts +1 -0
  232. package/src/tools/search.ts +1 -0
  233. package/src/tools/skill.ts +6 -1
  234. package/src/tools/subagent.ts +423 -79
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
2
3
  import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
3
4
  import type { AutocompleteProvider, SlashCommand } from "@gajae-code/tui";
4
5
  import { $env, sanitizeText } from "@gajae-code/utils";
@@ -13,7 +14,7 @@ import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../sessio
13
14
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
14
15
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
15
16
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
16
- import { ensureSupportedImageInput } from "../../utils/image-loading";
17
+ import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
17
18
  import { resizeImage } from "../../utils/image-resize";
18
19
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
19
20
 
@@ -22,6 +23,8 @@ interface Expandable {
22
23
  }
23
24
 
24
25
  const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
26
+ const CLIPBOARD_TEMP_IMAGE_FILE_PATTERN = /^clipboard-\d{4}-\d{2}-\d{2}-\d{6}-[A-Za-z0-9]+\.(?:png|jpe?g|gif|webp)$/i;
27
+ const MACOS_CLIPBOARD_TEMP_DIR_PATTERN = /^\/var\/folders\/[^/]+\/[^/]+\/T$/;
25
28
 
26
29
  function isExpandable(obj: unknown): obj is Expandable {
27
30
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
@@ -30,8 +33,17 @@ function isExpandable(obj: unknown): obj is Expandable {
30
33
  export class InputController {
31
34
  constructor(private ctx: InteractiveModeContext) {}
32
35
 
33
- #abortInteractive(): Promise<void> {
34
- return this.ctx.session.abort({ timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS });
36
+ /** Set after a first Esc silently consumes a queued steer. Kept until the
37
+ * queued steer is either cancelled by a second Esc or drained by continuation,
38
+ * so abort cleanup going idle cannot turn the second Esc into an idle action. */
39
+ #steerConsumePending = false;
40
+
41
+ #abortInteractive(options?: { silent?: boolean }): Promise<void> {
42
+ return this.ctx.session.abort({
43
+ timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS,
44
+ cause: "user_interrupt",
45
+ silent: options?.silent,
46
+ });
35
47
  }
36
48
 
37
49
  setupKeyHandlers(): void {
@@ -40,6 +52,7 @@ export class InputController {
40
52
  Boolean(
41
53
  this.ctx.loadingAnimation ||
42
54
  this.ctx.hasActiveBtw() ||
55
+ (this.#steerConsumePending && this.ctx.session.hasQueuedSteering) ||
43
56
  this.ctx.session.isStreaming ||
44
57
  this.ctx.session.isCompacting ||
45
58
  this.ctx.session.isGeneratingHandoff ||
@@ -54,6 +67,17 @@ export class InputController {
54
67
  if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
55
68
  return;
56
69
  }
70
+ if (this.#steerConsumePending) {
71
+ if (this.ctx.session.hasQueuedSteering) {
72
+ // Second Esc before the scheduled steer continuation drains the
73
+ // queue: restore/drop the queued steer and perform a real abort,
74
+ // even if abort cleanup already made the session look idle.
75
+ this.#steerConsumePending = false;
76
+ this.restoreQueuedMessagesToEditor({ abort: true });
77
+ return;
78
+ }
79
+ this.#steerConsumePending = false;
80
+ }
57
81
  if (this.ctx.loadingAnimation) {
58
82
  if (this.ctx.cancelPendingSubmission()) {
59
83
  return;
@@ -73,7 +97,15 @@ export class InputController {
73
97
  this.ctx.isPythonMode = false;
74
98
  this.ctx.updateEditorBorderColor();
75
99
  } else if (this.ctx.session.isStreaming) {
76
- void this.#abortInteractive();
100
+ if (this.ctx.session.hasQueuedSteering && !this.#steerConsumePending) {
101
+ // First Esc with a queued steer: silently consume it and
102
+ // auto-continue via steer-on-interrupt instead of stalling on
103
+ // "Operation aborted".
104
+ this.#steerConsumePending = true;
105
+ void this.#abortInteractive({ silent: true });
106
+ } else {
107
+ void this.#abortInteractive();
108
+ }
77
109
  } else if (!this.ctx.editor.getText().trim()) {
78
110
  // Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
79
111
  const action = settings.get("doubleEscapeAction");
@@ -132,6 +164,13 @@ export class InputController {
132
164
  this.ctx.keybindings.getKeys("app.clipboard.copyPrompt"),
133
165
  );
134
166
  this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
167
+ this.ctx.editor.onPasteText = text => this.handleTextPaste(text);
168
+ this.ctx.editor.onPastePendingInputCleared = (reason, droppedInputCount) => {
169
+ const reasonText = reason === "timeout" ? "timed out" : "exceeded the input queue limit";
170
+ this.ctx.showWarning(
171
+ `Paste handling ${reasonText}; discarded ${droppedInputCount} buffered input event${droppedInputCount === 1 ? "" : "s"}.`,
172
+ );
173
+ };
135
174
  this.ctx.editor.setActionKeys("app.tools.expand", this.ctx.keybindings.getKeys("app.tools.expand"));
136
175
  this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
137
176
  this.ctx.editor.setActionKeys("app.message.dequeue", this.ctx.keybindings.getKeys("app.message.dequeue"));
@@ -170,6 +209,9 @@ export class InputController {
170
209
  for (const key of this.ctx.keybindings.getKeys("app.session.observe")) {
171
210
  this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showSessionObserver());
172
211
  }
212
+ for (const key of this.ctx.keybindings.getKeys("app.jobs.open")) {
213
+ this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showJobsOverlay());
214
+ }
173
215
 
174
216
  this.ctx.editor.onChange = (text: string) => {
175
217
  const wasBashMode = this.ctx.isBashMode;
@@ -568,6 +610,14 @@ export class InputController {
568
610
  this.ctx.retryLoader.stop();
569
611
  this.ctx.retryLoader = undefined;
570
612
  }
613
+ if (this.ctx.retryCountdownTimer) {
614
+ clearInterval(this.ctx.retryCountdownTimer);
615
+ this.ctx.retryCountdownTimer = undefined;
616
+ }
617
+ if (this.ctx.retryEscapeHandler) {
618
+ this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
619
+ this.ctx.retryEscapeHandler = undefined;
620
+ }
571
621
  this.ctx.statusContainer.clear();
572
622
  this.ctx.statusLine.dispose();
573
623
 
@@ -594,6 +644,55 @@ export class InputController {
594
644
  process.kill(0, "SIGTSTP");
595
645
  }
596
646
 
647
+ handleTextPaste(text: string): boolean | Promise<boolean> {
648
+ const imagePath = this.#getPastedImagePathCandidate(text);
649
+ return imagePath ? this.#attachPastedImagePath(imagePath) : false;
650
+ }
651
+
652
+ async #attachPastedImagePath(imagePath: string): Promise<boolean> {
653
+ try {
654
+ const image = await loadImageInput({
655
+ path: imagePath,
656
+ cwd: this.ctx.sessionManager.getCwd(),
657
+ autoResize: this.ctx.settings.get("images.autoResize"),
658
+ });
659
+ if (!image) {
660
+ this.ctx.showStatus("Unsupported pasted clipboard image file");
661
+ return true;
662
+ }
663
+
664
+ this.ctx.pendingImages.push({
665
+ type: "image",
666
+ data: image.data,
667
+ mimeType: image.mimeType,
668
+ });
669
+ this.ctx.editor.insertText(`${this.#nextImagePlaceholder()} `);
670
+ this.ctx.showStatus(`Attached image: ${path.basename(image.resolvedPath)}`, { dim: true });
671
+ this.ctx.ui.requestRender();
672
+ return true;
673
+ } catch (error) {
674
+ if (error instanceof ImageInputTooLargeError) {
675
+ this.ctx.showStatus(error.message);
676
+ return true;
677
+ }
678
+ this.ctx.showStatus("Failed to attach pasted clipboard image");
679
+ return true;
680
+ }
681
+ }
682
+
683
+ #getPastedImagePathCandidate(text: string): string | undefined {
684
+ const resolvedPath = path.resolve(text.trim());
685
+ const parentDir = path.dirname(resolvedPath);
686
+ const isClipboardTempPath =
687
+ (parentDir === "/tmp" || MACOS_CLIPBOARD_TEMP_DIR_PATTERN.test(parentDir)) &&
688
+ CLIPBOARD_TEMP_IMAGE_FILE_PATTERN.test(path.basename(resolvedPath));
689
+ return isClipboardTempPath ? resolvedPath : undefined;
690
+ }
691
+
692
+ #nextImagePlaceholder(): string {
693
+ return `[image ${this.ctx.pendingImages.length}]`;
694
+ }
695
+
597
696
  async handleImagePaste(): Promise<boolean> {
598
697
  try {
599
698
  const image = await readImageFromClipboard();
@@ -608,7 +707,7 @@ export class InputController {
608
707
  this.ctx.showStatus(`Unsupported clipboard image format: ${image.mimeType}`);
609
708
  return false;
610
709
  }
611
- if (settings.get("images.autoResize")) {
710
+ if (this.ctx.settings.get("images.autoResize")) {
612
711
  try {
613
712
  const resized = await resizeImage({
614
713
  type: "image",
@@ -626,10 +725,7 @@ export class InputController {
626
725
  data: imageData.data,
627
726
  mimeType: imageData.mimeType,
628
727
  });
629
- // Insert placeholder at cursor like Anthropic model does
630
- const imageNum = this.ctx.pendingImages.length;
631
- const placeholder = `[Image #${imageNum}]`;
632
- this.ctx.editor.insertText(`${placeholder} `);
728
+ this.ctx.editor.insertText(`${this.#nextImagePlaceholder()} `);
633
729
  this.ctx.ui.requestRender();
634
730
  return true;
635
731
  }
@@ -41,6 +41,7 @@ import { AgentDashboard } from "../components/agent-dashboard";
41
41
  import { AssistantMessageComponent } from "../components/assistant-message";
42
42
  import { ExtensionDashboard } from "../components/extensions";
43
43
  import { HistorySearchComponent } from "../components/history-search";
44
+ import { JobsOverlayComponent } from "../components/jobs-overlay";
44
45
  import { ModelSelectorComponent, type ModelSelectorSelection } from "../components/model-selector";
45
46
  import { OAuthSelectorComponent } from "../components/oauth-selector";
46
47
  import { PluginSelectorComponent } from "../components/plugin-selector";
@@ -55,14 +56,16 @@ import { ThemeSelectorComponent } from "../components/theme-selector";
55
56
  import { ToolExecutionComponent } from "../components/tool-execution";
56
57
  import { TreeSelectorComponent } from "../components/tree-selector";
57
58
  import { UserMessageSelectorComponent } from "../components/user-message-selector";
59
+ import type { JobsObserver } from "../jobs-observer";
58
60
  import type { SessionObserverRegistry } from "../session-observer-registry";
59
61
 
60
- const CALLBACK_SERVER_PROVIDERS = new Set<OAuthProvider>([
62
+ const CALLBACK_SERVER_PROVIDERS = new Set<string>([
61
63
  "anthropic",
62
64
  "openai-codex",
63
65
  "gitlab-duo",
64
66
  "google-gemini-cli",
65
67
  "google-antigravity",
68
+ "xai",
66
69
  ]);
67
70
 
68
71
  const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
@@ -1149,4 +1152,31 @@ export class SelectorController {
1149
1152
  this.ctx.ui.setFocus(selector);
1150
1153
  this.ctx.ui.requestRender();
1151
1154
  }
1155
+
1156
+ /**
1157
+ * Jobs overlay: navigate ongoing monitor + cron jobs (Monitors then Crons,
1158
+ * newest-first), drill into per-type detail, and cancel/delete with a y/N
1159
+ * confirm. Built from nested SelectLists (list -> detail -> confirm) so focus
1160
+ * stays on the active SelectList.
1161
+ */
1162
+ showJobsOverlay(observer: JobsObserver): void {
1163
+ let overlay: JobsOverlayComponent | undefined;
1164
+ const close = () => {
1165
+ this.ctx.editorContainer.clear();
1166
+ this.ctx.editorContainer.addChild(this.ctx.editor);
1167
+ this.ctx.ui.setFocus(this.ctx.editor);
1168
+ this.ctx.ui.requestRender();
1169
+ };
1170
+ overlay = new JobsOverlayComponent(observer, {
1171
+ close,
1172
+ requestRender: () => {
1173
+ if (overlay) this.ctx.ui.setFocus(overlay.getFocus());
1174
+ this.ctx.ui.requestRender();
1175
+ },
1176
+ });
1177
+ this.ctx.editorContainer.clear();
1178
+ this.ctx.editorContainer.addChild(overlay);
1179
+ this.ctx.ui.setFocus(overlay.getFocus());
1180
+ this.ctx.ui.requestRender();
1181
+ }
1152
1182
  }
@@ -5,6 +5,7 @@ import { postmortem } from "@gajae-code/utils";
5
5
  * Run modes for the coding agent.
6
6
  */
7
7
  export { runAcpMode } from "./acp";
8
+ export { runBridgeMode } from "./bridge/bridge-mode";
8
9
  export { InteractiveMode, type InteractiveModeOptions } from "./interactive-mode";
9
10
  export { type PrintModeOptions, runPrintMode } from "./print-mode";
10
11
  export {
@@ -28,6 +28,7 @@ import {
28
28
  } from "@gajae-code/tui";
29
29
  import { APP_NAME, adjustHsv, getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@gajae-code/utils";
30
30
  import chalk from "chalk";
31
+ import { AsyncJobManager } from "../async";
31
32
  import { KeybindingsManager } from "../config/keybindings";
32
33
  import { isSettingsInitialized, type Settings, settings } from "../config/settings";
33
34
  import { DEFAULT_GJC_DEFINITION_NAMES } from "../defaults/gjc-defaults";
@@ -88,6 +89,7 @@ import { InputController } from "./controllers/input-controller";
88
89
  import { SelectorController } from "./controllers/selector-controller";
89
90
  import { SSHCommandController } from "./controllers/ssh-command-controller";
90
91
  import { TodoCommandController } from "./controllers/todo-command-controller";
92
+ import { JobsObserver } from "./jobs-observer";
91
93
  import { OAuthManualInputManager } from "./oauth-manual-input";
92
94
  import { SessionObserverRegistry } from "./session-observer-registry";
93
95
  import { interruptHint } from "./shared";
@@ -276,6 +278,7 @@ export class InteractiveMode implements InteractiveModeContext {
276
278
  }
277
279
  autoCompactionEscapeHandler?: () => void;
278
280
  retryEscapeHandler?: () => void;
281
+ retryCountdownTimer?: ReturnType<typeof setInterval>;
279
282
  unsubscribe?: () => void;
280
283
  onInputCallback?: (input: SubmittedUserInput) => void;
281
284
  optimisticUserMessageSignature: string | undefined = undefined;
@@ -329,6 +332,7 @@ export class InteractiveMode implements InteractiveModeContext {
329
332
  #voicePreviousUseTerminalCursor: boolean | null = null;
330
333
  #resizeHandler?: () => void;
331
334
  #observerRegistry: SessionObserverRegistry;
335
+ #jobsObserver?: JobsObserver;
332
336
  #eventBus?: EventBus;
333
337
  #eventBusUnsubscribers: Array<() => void> = [];
334
338
  #welcomeComponent?: WelcomeComponent;
@@ -524,6 +528,19 @@ export class InteractiveMode implements InteractiveModeContext {
524
528
  this.ui.requestRender();
525
529
  });
526
530
 
531
+ // Event-driven monitor/cron jobs widget. Scoped to this session's owner so
532
+ // overlay actions cannot mutate another agent's background work.
533
+ const jobManager = AsyncJobManager.instance();
534
+ if (jobManager) {
535
+ const jobsObserver = new JobsObserver(jobManager, this.session.getAgentId());
536
+ this.#jobsObserver = jobsObserver;
537
+ this.statusLine.setJobs(jobsObserver.getSnapshot());
538
+ jobsObserver.onChange(() => {
539
+ this.statusLine.setJobs(jobsObserver.getSnapshot());
540
+ this.ui.requestRender();
541
+ });
542
+ }
543
+
527
544
  // Load initial todos
528
545
  await this.#loadTodoList();
529
546
 
@@ -1842,6 +1859,8 @@ export class InteractiveMode implements InteractiveModeContext {
1842
1859
  this.#observerRegistry.dispose();
1843
1860
  this.#eventController.dispose();
1844
1861
  this.statusLine.dispose();
1862
+ this.#jobsObserver?.dispose();
1863
+ this.editor.dispose();
1845
1864
  if (this.#resizeHandler) {
1846
1865
  process.stdout.removeListener("resize", this.#resizeHandler);
1847
1866
  this.#resizeHandler = undefined;
@@ -1943,6 +1962,7 @@ export class InteractiveMode implements InteractiveModeContext {
1943
1962
  nextEditor.setHistoryStorage(this.historyStorage);
1944
1963
  }
1945
1964
  nextEditor.setText(previousText);
1965
+ previousEditor.dispose();
1946
1966
 
1947
1967
  this.editorContainer.clear();
1948
1968
  this.editor = nextEditor;
@@ -2316,6 +2336,14 @@ export class InteractiveMode implements InteractiveModeContext {
2316
2336
  this.#selectorController.showSessionObserver(this.#observerRegistry);
2317
2337
  }
2318
2338
 
2339
+ showJobsOverlay(): void {
2340
+ if (!this.#jobsObserver) {
2341
+ this.showStatus("Background jobs are unavailable in this session");
2342
+ return;
2343
+ }
2344
+ this.#selectorController.showJobsOverlay(this.#jobsObserver);
2345
+ }
2346
+
2319
2347
  resetObserverRegistry(): void {
2320
2348
  this.#observerRegistry.resetSessions();
2321
2349
  this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
@@ -0,0 +1,204 @@
1
+ /**
2
+ * JobsObserver
3
+ *
4
+ * Single, event-driven aggregator over the two background-work sources surfaced
5
+ * by the status-line jobs widget and the jobs overlay:
6
+ * - monitor jobs (bash jobs started by the `monitor` tool, tracked in `AsyncJobManager`)
7
+ * - cron jobs (tracked in the cron module's owner-scoped schedule store)
8
+ *
9
+ * It subscribes to change hooks on both sources (no polling), debounces bursts
10
+ * to a microtask, and exposes a precomputed snapshot so the status-line render
11
+ * loop never scans the underlying stores. A failure latch keeps the widget red
12
+ * until `acknowledgeFailures()` is called (when the overlay opens), so a failed
13
+ * job that evicts before the user looks is not silently lost.
14
+ */
15
+ import type { AsyncJob, AsyncJobManager } from "../async";
16
+ import { deleteCronJobById, listCronSnapshots, onCronChange } from "../tools/cron";
17
+
18
+ export type JobsWorstState = "none" | "running" | "failed";
19
+
20
+ export interface MonitorJobView {
21
+ id: string;
22
+ label: string;
23
+ status: AsyncJob["status"];
24
+ startTime: number;
25
+ }
26
+
27
+ export interface CronJobView {
28
+ id: string;
29
+ humanSchedule: string;
30
+ cronExpression: string;
31
+ prompt: string;
32
+ recurring: boolean;
33
+ nextFireAt?: number;
34
+ createdAt: number;
35
+ }
36
+
37
+ export interface JobsSnapshot {
38
+ monitors: MonitorJobView[];
39
+ crons: CronJobView[];
40
+ activeMonitorCount: number;
41
+ activeCronCount: number;
42
+ worstState: JobsWorstState;
43
+ failedUnacknowledged: boolean;
44
+ }
45
+
46
+ export const EMPTY_JOBS_SNAPSHOT: JobsSnapshot = {
47
+ monitors: [],
48
+ crons: [],
49
+ activeMonitorCount: 0,
50
+ activeCronCount: 0,
51
+ worstState: "none",
52
+ failedUnacknowledged: false,
53
+ };
54
+
55
+ export class JobsObserver {
56
+ readonly #manager: AsyncJobManager;
57
+ readonly #ownerId: string | undefined;
58
+ readonly #unsubscribers: Array<() => void> = [];
59
+ readonly #listeners = new Set<() => void>();
60
+ #failedUnacknowledged = false;
61
+ #notifyScheduled = false;
62
+ #disposed = false;
63
+ #snapshot: JobsSnapshot = EMPTY_JOBS_SNAPSHOT;
64
+ readonly #acknowledgedFailedIds = new Set<string>();
65
+
66
+ constructor(manager: AsyncJobManager, ownerId: string | undefined) {
67
+ this.#manager = manager;
68
+ this.#ownerId = ownerId;
69
+ this.#unsubscribers.push(manager.onChange(() => this.#onUpstreamChange()));
70
+ this.#unsubscribers.push(onCronChange(() => this.#onUpstreamChange()));
71
+ this.#recompute();
72
+ }
73
+
74
+ /** Subscribe to debounced change events. Returns an unsubscribe function. */
75
+ onChange(cb: () => void): () => void {
76
+ this.#listeners.add(cb);
77
+ return () => {
78
+ this.#listeners.delete(cb);
79
+ };
80
+ }
81
+
82
+ #onUpstreamChange(): void {
83
+ if (this.#disposed) return;
84
+ this.#recompute();
85
+ if (this.#notifyScheduled) return;
86
+ this.#notifyScheduled = true;
87
+ queueMicrotask(() => {
88
+ this.#notifyScheduled = false;
89
+ if (this.#disposed) return;
90
+ this.#emit();
91
+ });
92
+ }
93
+
94
+ #emit(): void {
95
+ for (const cb of this.#listeners) {
96
+ try {
97
+ cb();
98
+ } catch {
99
+ // Listener errors are isolated; a bad subscriber must not break others.
100
+ }
101
+ }
102
+ }
103
+
104
+ #listMonitorJobs(): AsyncJob[] {
105
+ const filter = this.#ownerId ? { ownerId: this.#ownerId } : undefined;
106
+ return this.#manager.getAllJobs(filter).filter(job => job.type === "bash" && job.metadata?.monitor === true);
107
+ }
108
+
109
+ /**
110
+ * Recompute and store the snapshot. Called on construction and on every
111
+ * upstream change; the status-line render path only reads the stored
112
+ * snapshot (never scans the manager/cron stores).
113
+ */
114
+ #recompute(): void {
115
+ const monitorJobs = this.#listMonitorJobs();
116
+ const presentIds = new Set(monitorJobs.map(job => job.id));
117
+ // Prune acknowledged ids whose jobs have been evicted.
118
+ for (const id of this.#acknowledgedFailedIds) {
119
+ if (!presentIds.has(id)) this.#acknowledgedFailedIds.delete(id);
120
+ }
121
+ // Sticky failure latch: set when an unacknowledged failed monitor is seen
122
+ // (including at construction); stays set even after the failed job evicts,
123
+ // until acknowledgeFailures() clears it.
124
+ const hasUnacknowledgedFailure = monitorJobs.some(
125
+ job => job.status === "failed" && !this.#acknowledgedFailedIds.has(job.id),
126
+ );
127
+ if (hasUnacknowledgedFailure) this.#failedUnacknowledged = true;
128
+
129
+ const activeMonitors = monitorJobs.filter(job => job.status === "running");
130
+ const cronSnapshots = listCronSnapshots(this.#ownerId);
131
+ const monitors: MonitorJobView[] = monitorJobs
132
+ .map(job => ({ id: job.id, label: job.label, status: job.status, startTime: job.startTime }))
133
+ .sort((a, b) => b.startTime - a.startTime);
134
+ const crons: CronJobView[] = cronSnapshots
135
+ .map(snapshot => ({
136
+ id: snapshot.id,
137
+ humanSchedule: snapshot.humanSchedule,
138
+ cronExpression: snapshot.cron_expression,
139
+ prompt: snapshot.prompt,
140
+ recurring: snapshot.recurring,
141
+ nextFireAt: snapshot.nextFireAt,
142
+ createdAt: snapshot.createdAt,
143
+ }))
144
+ .sort((a, b) => b.createdAt - a.createdAt);
145
+ const worstState: JobsWorstState = this.#failedUnacknowledged
146
+ ? "failed"
147
+ : activeMonitors.length > 0 || crons.length > 0
148
+ ? "running"
149
+ : "none";
150
+ this.#snapshot = {
151
+ monitors,
152
+ crons,
153
+ activeMonitorCount: activeMonitors.length,
154
+ activeCronCount: crons.length,
155
+ worstState,
156
+ failedUnacknowledged: this.#failedUnacknowledged,
157
+ };
158
+ }
159
+
160
+ /** Return the precomputed snapshot (recomputed on each upstream change). */
161
+ getSnapshot(): JobsSnapshot {
162
+ return this.#snapshot;
163
+ }
164
+
165
+ /** Clear the failure latch (called when the user opens the jobs overlay). */
166
+ acknowledgeFailures(): void {
167
+ for (const job of this.#listMonitorJobs()) {
168
+ if (job.status === "failed") this.#acknowledgedFailedIds.add(job.id);
169
+ }
170
+ if (!this.#failedUnacknowledged) return;
171
+ this.#failedUnacknowledged = false;
172
+ this.#recompute();
173
+ this.#emit();
174
+ }
175
+
176
+ /** Cancel a running monitor job. Returns true when the job was cancelled. */
177
+ cancelMonitor(id: string): boolean {
178
+ return this.#manager.cancel(id);
179
+ }
180
+
181
+ /** Delete a visible scheduled cron job. Returns true when removed. */
182
+ deleteCron(id: string): boolean {
183
+ return deleteCronJobById(this.#ownerId, id);
184
+ }
185
+
186
+ /** Bounded tail of a monitor job's captured output (for the detail view). */
187
+ getMonitorOutput(id: string): string {
188
+ const slice = this.#manager.readOutputSince(id, 0, this.#ownerId ? { ownerId: this.#ownerId } : undefined);
189
+ return slice?.text ?? "";
190
+ }
191
+
192
+ dispose(): void {
193
+ this.#disposed = true;
194
+ for (const unsubscribe of this.#unsubscribers) {
195
+ try {
196
+ unsubscribe();
197
+ } catch {
198
+ // best-effort teardown
199
+ }
200
+ }
201
+ this.#unsubscribers.length = 0;
202
+ this.#listeners.clear();
203
+ }
204
+ }