@gajae-code/coding-agent 0.3.0 → 0.3.2

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 +32 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +7 -0
  4. package/dist/types/cli/args.d.ts +3 -1
  5. package/dist/types/commands/deep-interview.d.ts +3 -0
  6. package/dist/types/commands/launch.d.ts +6 -0
  7. package/dist/types/config/keybindings.d.ts +5 -0
  8. package/dist/types/config/model-profile-activation.d.ts +30 -0
  9. package/dist/types/config/model-profiles.d.ts +19 -0
  10. package/dist/types/config/model-registry.d.ts +8 -0
  11. package/dist/types/config/model-resolver.d.ts +1 -1
  12. package/dist/types/config/models-config-schema.d.ts +47 -0
  13. package/dist/types/config/settings-schema.d.ts +14 -4
  14. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  15. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  16. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  17. package/dist/types/eval/py/executor.d.ts +2 -0
  18. package/dist/types/eval/py/kernel.d.ts +2 -0
  19. package/dist/types/exec/bash-executor.d.ts +10 -0
  20. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  21. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  22. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  23. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  24. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  25. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -1
  26. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  27. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  28. package/dist/types/hooks/skill-state.d.ts +21 -0
  29. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  30. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  31. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  32. package/dist/types/internal-urls/types.d.ts +4 -0
  33. package/dist/types/lsp/index.d.ts +10 -10
  34. package/dist/types/main.d.ts +10 -1
  35. package/dist/types/modes/bridge/auth.d.ts +12 -0
  36. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  37. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  38. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  39. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  41. package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  43. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  44. package/dist/types/modes/components/model-selector.d.ts +6 -1
  45. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  46. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  47. package/dist/types/modes/components/status-line.d.ts +2 -0
  48. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  49. package/dist/types/modes/controllers/selector-controller.d.ts +9 -0
  50. package/dist/types/modes/index.d.ts +1 -0
  51. package/dist/types/modes/interactive-mode.d.ts +1 -0
  52. package/dist/types/modes/jobs-observer.d.ts +57 -0
  53. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  54. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  55. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  56. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  57. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  58. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  59. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  60. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  61. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  62. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  63. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  64. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  65. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  66. package/dist/types/modes/types.d.ts +2 -0
  67. package/dist/types/sdk.d.ts +3 -1
  68. package/dist/types/session/agent-session.d.ts +11 -1
  69. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  70. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  71. package/dist/types/task/executor.d.ts +1 -0
  72. package/dist/types/task/id.d.ts +7 -0
  73. package/dist/types/task/index.d.ts +5 -0
  74. package/dist/types/task/receipt.d.ts +85 -0
  75. package/dist/types/task/spawn-gate.d.ts +38 -0
  76. package/dist/types/task/types.d.ts +143 -11
  77. package/dist/types/tools/cron.d.ts +6 -0
  78. package/dist/types/tools/hindsight-recall.d.ts +0 -2
  79. package/dist/types/tools/hindsight-reflect.d.ts +0 -2
  80. package/dist/types/tools/hindsight-retain.d.ts +0 -2
  81. package/dist/types/tools/index.d.ts +6 -4
  82. package/dist/types/tools/path-utils.d.ts +1 -0
  83. package/dist/types/tools/subagent.d.ts +15 -0
  84. package/package.json +7 -7
  85. package/scripts/build-binary.ts +7 -0
  86. package/src/async/job-manager.ts +36 -0
  87. package/src/cli/args.ts +19 -2
  88. package/src/commands/deep-interview.ts +1 -0
  89. package/src/commands/harness.ts +289 -19
  90. package/src/commands/launch.ts +10 -2
  91. package/src/commands/state.ts +2 -1
  92. package/src/commands/team.ts +22 -4
  93. package/src/config/keybindings.ts +6 -0
  94. package/src/config/model-profile-activation.ts +157 -0
  95. package/src/config/model-profiles.ts +155 -0
  96. package/src/config/model-registry.ts +19 -0
  97. package/src/config/model-resolver.ts +3 -2
  98. package/src/config/models-config-schema.ts +36 -0
  99. package/src/config/settings-schema.ts +16 -3
  100. package/src/dap/client.ts +17 -3
  101. package/src/debug/crash-diagnostics.ts +223 -0
  102. package/src/debug/runtime-gauges.ts +20 -0
  103. package/src/deep-interview/render-middleware.ts +6 -0
  104. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  105. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  106. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -3
  107. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
  108. package/src/defaults/gjc-defaults.ts +7 -0
  109. package/src/eval/py/executor.ts +21 -1
  110. package/src/eval/py/kernel.ts +15 -0
  111. package/src/exec/bash-executor.ts +41 -0
  112. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  113. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  114. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  115. package/src/gjc-runtime/state-migrations.ts +54 -7
  116. package/src/gjc-runtime/state-runtime.ts +461 -64
  117. package/src/gjc-runtime/state-schema.ts +192 -0
  118. package/src/gjc-runtime/state-writer.ts +32 -1
  119. package/src/gjc-runtime/team-runtime.ts +177 -105
  120. package/src/gjc-runtime/ultragoal-runtime.ts +231 -38
  121. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  122. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  123. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  124. package/src/harness-control-plane/control-endpoint.ts +19 -8
  125. package/src/harness-control-plane/owner.ts +57 -10
  126. package/src/harness-control-plane/state-machine.ts +2 -1
  127. package/src/hooks/skill-state.ts +176 -26
  128. package/src/internal-urls/agent-protocol.ts +68 -21
  129. package/src/internal-urls/artifact-protocol.ts +12 -17
  130. package/src/internal-urls/docs-index.generated.ts +8 -10
  131. package/src/internal-urls/registry-helpers.ts +19 -16
  132. package/src/internal-urls/types.ts +4 -0
  133. package/src/lsp/client.ts +18 -2
  134. package/src/main.ts +88 -6
  135. package/src/modes/bridge/auth.ts +41 -0
  136. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  137. package/src/modes/bridge/bridge-mode.ts +520 -0
  138. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  139. package/src/modes/bridge/event-stream.ts +70 -0
  140. package/src/modes/components/custom-editor.ts +101 -0
  141. package/src/modes/components/custom-provider-wizard.ts +318 -0
  142. package/src/modes/components/hook-selector.ts +61 -18
  143. package/src/modes/components/jobs-overlay-model.ts +109 -0
  144. package/src/modes/components/jobs-overlay.ts +172 -0
  145. package/src/modes/components/model-selector.ts +108 -18
  146. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  147. package/src/modes/components/status-line/presets.ts +7 -5
  148. package/src/modes/components/status-line/segments.ts +25 -0
  149. package/src/modes/components/status-line/types.ts +2 -0
  150. package/src/modes/components/status-line.ts +9 -1
  151. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  152. package/src/modes/controllers/input-controller.ts +97 -9
  153. package/src/modes/controllers/selector-controller.ts +86 -1
  154. package/src/modes/index.ts +1 -0
  155. package/src/modes/interactive-mode.ts +27 -0
  156. package/src/modes/jobs-observer.ts +204 -0
  157. package/src/modes/rpc/host-tools.ts +1 -186
  158. package/src/modes/rpc/host-uris.ts +1 -235
  159. package/src/modes/rpc/rpc-client.ts +25 -10
  160. package/src/modes/rpc/rpc-mode.ts +12 -381
  161. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  162. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  163. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  164. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  165. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  166. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  167. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  168. package/src/modes/shared/agent-wire/responses.ts +17 -0
  169. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  170. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  171. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  172. package/src/modes/types.ts +2 -0
  173. package/src/prompts/memories/consolidation.md +1 -1
  174. package/src/prompts/memories/read-path.md +6 -7
  175. package/src/prompts/memories/unavailable.md +2 -2
  176. package/src/prompts/tools/bash.md +1 -1
  177. package/src/prompts/tools/irc.md +1 -1
  178. package/src/prompts/tools/read.md +2 -2
  179. package/src/prompts/tools/recall.md +1 -0
  180. package/src/prompts/tools/reflect.md +1 -0
  181. package/src/prompts/tools/retain.md +1 -0
  182. package/src/prompts/tools/subagent.md +12 -7
  183. package/src/prompts/tools/task-summary.md +3 -9
  184. package/src/prompts/tools/task.md +5 -1
  185. package/src/sdk.ts +5 -1
  186. package/src/session/agent-session.ts +214 -38
  187. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  188. package/src/skill-state/workflow-state-contract.ts +7 -4
  189. package/src/skill-state/workflow-state-version.ts +3 -0
  190. package/src/slash-commands/builtin-registry.ts +9 -1
  191. package/src/task/executor.ts +31 -5
  192. package/src/task/id.ts +33 -0
  193. package/src/task/index.ts +259 -67
  194. package/src/task/output-manager.ts +5 -4
  195. package/src/task/receipt.ts +297 -0
  196. package/src/task/render.ts +48 -131
  197. package/src/task/spawn-gate.ts +132 -0
  198. package/src/task/types.ts +48 -7
  199. package/src/tools/ask.ts +73 -33
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash.ts +1 -1
  203. package/src/tools/cron.ts +48 -0
  204. package/src/tools/find.ts +4 -1
  205. package/src/tools/hindsight-recall.ts +0 -2
  206. package/src/tools/hindsight-reflect.ts +0 -2
  207. package/src/tools/hindsight-retain.ts +0 -2
  208. package/src/tools/index.ts +6 -18
  209. package/src/tools/path-utils.ts +3 -2
  210. package/src/tools/read.ts +4 -3
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/skill.ts +6 -1
  213. package/src/tools/subagent.ts +237 -84
@@ -25,11 +25,17 @@ import type { InteractiveModeContext } from "../../modes/types";
25
25
  import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
26
26
 
27
27
  const MAX_WIDGET_LINES = 10;
28
+ const HOOK_SELECTOR_MOUSE_REPORTING_ENABLE = "\x1b[?1006h\x1b[?1000h";
29
+ const HOOK_SELECTOR_MOUSE_REPORTING_DISABLE = "\x1b[?1000l\x1b[?1006l";
30
+ const HOOK_SELECTOR_CHROME_ROWS = 7;
31
+ const HOOK_SELECTOR_OUTLINE_ROWS = 2;
28
32
 
29
33
  export class ExtensionUiController {
30
34
  #extensionTerminalInputUnsubscribers = new Set<() => void>();
31
35
  #hookWidgetsAbove = new Map<string, ExtensionUiComponent>();
32
36
  #hookWidgetsBelow = new Map<string, ExtensionUiComponent>();
37
+ #hookSelectorMouseReportingEnabled = false;
38
+
33
39
  constructor(private ctx: InteractiveModeContext) {}
34
40
 
35
41
  /**
@@ -589,12 +595,20 @@ export class ExtensionUiController {
589
595
  () => this.hideHookSelector(),
590
596
  dialogOptions?.signal,
591
597
  );
592
- const maxVisible = Math.max(4, Math.min(15, this.ctx.ui.terminal.rows - 12));
593
598
  const requestedTitleRows = dialogOptions?.scrollTitleRows;
594
- const selectorChromeRows = 7;
595
- const availableTitleRows = this.ctx.ui.terminal.rows - maxVisible - selectorChromeRows;
599
+ const baseMaxVisible = Math.max(4, Math.min(15, this.ctx.ui.terminal.rows - 12));
600
+ const scrollOptionRows = Math.max(1, Math.min(baseMaxVisible, options.length));
601
+ const maxVisible =
602
+ requestedTitleRows === undefined ? baseMaxVisible : Math.min(15, Math.max(3, scrollOptionRows + 1));
603
+ const listChromeRows = dialogOptions?.outline === true ? HOOK_SELECTOR_OUTLINE_ROWS : 0;
604
+ const availableTitleRows =
605
+ this.ctx.ui.terminal.rows - scrollOptionRows - listChromeRows - HOOK_SELECTOR_CHROME_ROWS;
596
606
  const scrollTitleRows =
597
607
  requestedTitleRows === undefined ? undefined : Math.max(1, Math.min(requestedTitleRows, availableTitleRows));
608
+ if (scrollTitleRows !== undefined) {
609
+ this.#enableHookSelectorMouseReporting();
610
+ }
611
+
598
612
  this.ctx.hookSelector = new HookSelectorComponent(
599
613
  title,
600
614
  options,
@@ -640,10 +654,32 @@ export class ExtensionUiController {
640
654
  attachAbort();
641
655
  return promise;
642
656
  }
657
+
658
+ #enableHookSelectorMouseReporting(): void {
659
+ if (this.#hookSelectorMouseReportingEnabled) return;
660
+ this.#hookSelectorMouseReportingEnabled = true;
661
+ this.#writeTerminalControl(HOOK_SELECTOR_MOUSE_REPORTING_ENABLE);
662
+ }
663
+
664
+ #disableHookSelectorMouseReporting(): void {
665
+ if (!this.#hookSelectorMouseReportingEnabled) return;
666
+ this.#hookSelectorMouseReportingEnabled = false;
667
+ this.#writeTerminalControl(HOOK_SELECTOR_MOUSE_REPORTING_DISABLE);
668
+ }
669
+
670
+ #writeTerminalControl(sequence: string): void {
671
+ try {
672
+ this.ctx.ui.terminal.write(sequence);
673
+ } catch {
674
+ // Terminal teardown can race selector cleanup; normal shutdown restores modes.
675
+ }
676
+ }
677
+
643
678
  /**
644
679
  * Hide the hook selector.
645
680
  */
646
681
  hideHookSelector(): void {
682
+ this.#disableHookSelectorMouseReporting();
647
683
  this.ctx.hookSelector?.dispose();
648
684
  this.ctx.editorContainer.clear();
649
685
  this.ctx.editorContainer.addChild(this.ctx.editor);
@@ -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, cause: "user_interrupt" });
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;
@@ -602,6 +644,55 @@ export class InputController {
602
644
  process.kill(0, "SIGTSTP");
603
645
  }
604
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
+
605
696
  async handleImagePaste(): Promise<boolean> {
606
697
  try {
607
698
  const image = await readImageFromClipboard();
@@ -616,7 +707,7 @@ export class InputController {
616
707
  this.ctx.showStatus(`Unsupported clipboard image format: ${image.mimeType}`);
617
708
  return false;
618
709
  }
619
- if (settings.get("images.autoResize")) {
710
+ if (this.ctx.settings.get("images.autoResize")) {
620
711
  try {
621
712
  const resized = await resizeImage({
622
713
  type: "image",
@@ -634,10 +725,7 @@ export class InputController {
634
725
  data: imageData.data,
635
726
  mimeType: imageData.mimeType,
636
727
  });
637
- // Insert placeholder at cursor like Anthropic model does
638
- const imageNum = this.ctx.pendingImages.length;
639
- const placeholder = `[Image #${imageNum}]`;
640
- this.ctx.editor.insertText(`${placeholder} `);
728
+ this.ctx.editor.insertText(`${this.#nextImagePlaceholder()} `);
641
729
  this.ctx.ui.requestRender();
642
730
  return true;
643
731
  }
@@ -4,6 +4,7 @@ import type { OAuthProvider } from "@gajae-code/ai/utils/oauth/types";
4
4
  import type { Component, OverlayHandle } from "@gajae-code/tui";
5
5
  import { Input, Loader, Spacer, Text } from "@gajae-code/tui";
6
6
  import { getAgentDbPath, getProjectDir } from "@gajae-code/utils";
7
+ import { activateModelProfile } from "../../config/model-profile-activation";
7
8
  import { settings } from "../../config/settings";
8
9
  import { DebugSelectorComponent } from "../../debug";
9
10
  import { disableProvider, enableProvider } from "../../discovery";
@@ -35,12 +36,15 @@ import {
35
36
  MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
36
37
  MODEL_ONBOARDING_SETUP_COMMAND,
37
38
  } from "../../setup/model-onboarding-guidance";
39
+ import { addApiCompatibleProvider, formatProviderSetupResult } from "../../setup/provider-onboarding";
38
40
  import { isSearchProviderPreference, setPreferredImageProvider, setPreferredSearchProvider } from "../../tools";
39
41
  import { setSessionTerminalTitle } from "../../utils/title-generator";
40
42
  import { AgentDashboard } from "../components/agent-dashboard";
41
43
  import { AssistantMessageComponent } from "../components/assistant-message";
44
+ import { CustomProviderWizardComponent, type CustomProviderWizardSubmit } from "../components/custom-provider-wizard";
42
45
  import { ExtensionDashboard } from "../components/extensions";
43
46
  import { HistorySearchComponent } from "../components/history-search";
47
+ import { JobsOverlayComponent } from "../components/jobs-overlay";
44
48
  import { ModelSelectorComponent, type ModelSelectorSelection } from "../components/model-selector";
45
49
  import { OAuthSelectorComponent } from "../components/oauth-selector";
46
50
  import { PluginSelectorComponent } from "../components/plugin-selector";
@@ -55,6 +59,7 @@ import { ThemeSelectorComponent } from "../components/theme-selector";
55
59
  import { ToolExecutionComponent } from "../components/tool-execution";
56
60
  import { TreeSelectorComponent } from "../components/tree-selector";
57
61
  import { UserMessageSelectorComponent } from "../components/user-message-selector";
62
+ import type { JobsObserver } from "../jobs-observer";
58
63
  import type { SessionObserverRegistry } from "../session-observer-registry";
59
64
 
60
65
  const CALLBACK_SERVER_PROVIDERS = new Set<string>([
@@ -117,7 +122,9 @@ export class SelectorController {
117
122
  const selector = new ProviderOnboardingSelectorComponent(
118
123
  (action: ProviderOnboardingAction) => {
119
124
  done();
120
- if (action === "oauth-login") {
125
+ if (action === "custom-provider-wizard") {
126
+ this.showCustomProviderWizard();
127
+ } else if (action === "oauth-login") {
121
128
  void this.showOAuthSelector("login");
122
129
  } else {
123
130
  this.ctx.showStatus(formatProviderOnboardingCommandGuide());
@@ -132,6 +139,36 @@ export class SelectorController {
132
139
  });
133
140
  }
134
141
 
142
+ showCustomProviderWizard(): void {
143
+ this.showSelector(done => {
144
+ let wizard: CustomProviderWizardComponent;
145
+ const submit = async (input: CustomProviderWizardSubmit): Promise<void> => {
146
+ try {
147
+ const result = await addApiCompatibleProvider(input);
148
+ await this.ctx.session.modelRegistry.refresh("offline");
149
+ await this.ctx.notifyConfigChanged?.();
150
+ this.ctx.showStatus(formatProviderSetupResult(result));
151
+ done();
152
+ this.ctx.ui.requestRender();
153
+ } catch (err) {
154
+ const message = err instanceof Error ? err.message : String(err);
155
+ wizard.setSubmitError(`Provider setup failed: ${message}`);
156
+ }
157
+ };
158
+ wizard = new CustomProviderWizardComponent(
159
+ input => {
160
+ void submit(input);
161
+ },
162
+ () => {
163
+ done();
164
+ this.ctx.ui.requestRender();
165
+ },
166
+ () => this.ctx.ui.requestRender(),
167
+ );
168
+ return { component: wizard, focus: wizard };
169
+ });
170
+ }
171
+
135
172
  showSettingsSelector(): void {
136
173
  getAvailableThemes().then(availableThemes => {
137
174
  this.showSelector(done => {
@@ -500,6 +537,27 @@ export class SelectorController {
500
537
  this.ctx.ui.requestRender();
501
538
  return;
502
539
  }
540
+ if (selection.kind === "profile") {
541
+ await activateModelProfile(
542
+ {
543
+ session: this.ctx.session,
544
+ modelRegistry: this.ctx.session.modelRegistry,
545
+ settings: this.ctx.settings,
546
+ profileName: selection.profileName,
547
+ },
548
+ { persistDefault: selection.setDefault },
549
+ );
550
+ this.ctx.statusLine.invalidate();
551
+ this.ctx.updateEditorBorderColor();
552
+ this.ctx.showStatus(
553
+ selection.setDefault
554
+ ? `Default model profile: ${selection.profileName}`
555
+ : `Model profile: ${selection.profileName}`,
556
+ );
557
+ done();
558
+ this.ctx.ui.requestRender();
559
+ return;
560
+ }
503
561
  const { model, role, thinkingLevel, selector } = selection;
504
562
  if (role === null) {
505
563
  // Temporary: update agent state but don't persist to settings
@@ -1150,4 +1208,31 @@ export class SelectorController {
1150
1208
  this.ctx.ui.setFocus(selector);
1151
1209
  this.ctx.ui.requestRender();
1152
1210
  }
1211
+
1212
+ /**
1213
+ * Jobs overlay: navigate ongoing monitor + cron jobs (Monitors then Crons,
1214
+ * newest-first), drill into per-type detail, and cancel/delete with a y/N
1215
+ * confirm. Built from nested SelectLists (list -> detail -> confirm) so focus
1216
+ * stays on the active SelectList.
1217
+ */
1218
+ showJobsOverlay(observer: JobsObserver): void {
1219
+ let overlay: JobsOverlayComponent | undefined;
1220
+ const close = () => {
1221
+ this.ctx.editorContainer.clear();
1222
+ this.ctx.editorContainer.addChild(this.ctx.editor);
1223
+ this.ctx.ui.setFocus(this.ctx.editor);
1224
+ this.ctx.ui.requestRender();
1225
+ };
1226
+ overlay = new JobsOverlayComponent(observer, {
1227
+ close,
1228
+ requestRender: () => {
1229
+ if (overlay) this.ctx.ui.setFocus(overlay.getFocus());
1230
+ this.ctx.ui.requestRender();
1231
+ },
1232
+ });
1233
+ this.ctx.editorContainer.clear();
1234
+ this.ctx.editorContainer.addChild(overlay);
1235
+ this.ctx.ui.setFocus(overlay.getFocus());
1236
+ this.ctx.ui.requestRender();
1237
+ }
1153
1238
  }
@@ -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";
@@ -330,6 +332,7 @@ export class InteractiveMode implements InteractiveModeContext {
330
332
  #voicePreviousUseTerminalCursor: boolean | null = null;
331
333
  #resizeHandler?: () => void;
332
334
  #observerRegistry: SessionObserverRegistry;
335
+ #jobsObserver?: JobsObserver;
333
336
  #eventBus?: EventBus;
334
337
  #eventBusUnsubscribers: Array<() => void> = [];
335
338
  #welcomeComponent?: WelcomeComponent;
@@ -525,6 +528,19 @@ export class InteractiveMode implements InteractiveModeContext {
525
528
  this.ui.requestRender();
526
529
  });
527
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
+
528
544
  // Load initial todos
529
545
  await this.#loadTodoList();
530
546
 
@@ -1843,6 +1859,8 @@ export class InteractiveMode implements InteractiveModeContext {
1843
1859
  this.#observerRegistry.dispose();
1844
1860
  this.#eventController.dispose();
1845
1861
  this.statusLine.dispose();
1862
+ this.#jobsObserver?.dispose();
1863
+ this.editor.dispose();
1846
1864
  if (this.#resizeHandler) {
1847
1865
  process.stdout.removeListener("resize", this.#resizeHandler);
1848
1866
  this.#resizeHandler = undefined;
@@ -1944,6 +1962,7 @@ export class InteractiveMode implements InteractiveModeContext {
1944
1962
  nextEditor.setHistoryStorage(this.historyStorage);
1945
1963
  }
1946
1964
  nextEditor.setText(previousText);
1965
+ previousEditor.dispose();
1947
1966
 
1948
1967
  this.editorContainer.clear();
1949
1968
  this.editor = nextEditor;
@@ -2317,6 +2336,14 @@ export class InteractiveMode implements InteractiveModeContext {
2317
2336
  this.#selectorController.showSessionObserver(this.#observerRegistry);
2318
2337
  }
2319
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
+
2320
2347
  resetObserverRegistry(): void {
2321
2348
  this.#observerRegistry.resetSessions();
2322
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
+ }