@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
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Pure model helpers for the jobs overlay.
3
+ *
4
+ * Kept free of UI/Component dependencies so the grouping/ordering and
5
+ * detail-formatting logic is unit-testable. The selector controller wires these
6
+ * SelectItem lists into nested SelectLists (list -> detail -> confirm).
7
+ */
8
+ import type { SelectItem } from "@gajae-code/tui";
9
+ import type { JobsSnapshot } from "../jobs-observer";
10
+
11
+ export type JobRefKind = "monitor" | "cron";
12
+
13
+ export interface JobRef {
14
+ kind: JobRefKind;
15
+ id: string;
16
+ }
17
+
18
+ const PROMPT_PREVIEW_MAX = 60;
19
+
20
+ function preview(text: string, max = PROMPT_PREVIEW_MAX): string {
21
+ const oneLine = text.replace(/\s+/g, " ").trim();
22
+ return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
23
+ }
24
+
25
+ /** Compact relative time, e.g. "in 5m", "2m ago", "now". */
26
+ export function formatRelative(targetMs: number | undefined, nowMs = Date.now()): string {
27
+ if (targetMs === undefined) return "—";
28
+ const deltaMs = targetMs - nowMs;
29
+ const abs = Math.abs(deltaMs);
30
+ const mins = Math.round(abs / 60_000);
31
+ if (mins < 1) return "now";
32
+ const unit = mins >= 60 ? `${Math.round(mins / 60)}h` : `${mins}m`;
33
+ return deltaMs >= 0 ? `in ${unit}` : `${unit} ago`;
34
+ }
35
+
36
+ /** Parse a list item value back into a job reference. */
37
+ export function parseJobRef(value: string): JobRef | null {
38
+ const sep = value.indexOf(":");
39
+ if (sep === -1) return null;
40
+ const kind = value.slice(0, sep);
41
+ const id = value.slice(sep + 1);
42
+ if ((kind === "monitor" || kind === "cron") && id.length > 0) {
43
+ return { kind, id };
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * Build the grouped jobs list: monitors first (newest-first), then crons
50
+ * (newest-first). The snapshot arrays are already sorted newest-first.
51
+ */
52
+ export function buildJobsListItems(snapshot: JobsSnapshot): SelectItem[] {
53
+ const items: SelectItem[] = [];
54
+ for (const monitor of snapshot.monitors) {
55
+ items.push({
56
+ value: `monitor:${monitor.id}`,
57
+ label: `monitor · ${preview(monitor.label, 40)}`,
58
+ description: monitor.status,
59
+ hint: monitor.status === "failed" ? "failed" : undefined,
60
+ });
61
+ }
62
+ for (const cron of snapshot.crons) {
63
+ items.push({
64
+ value: `cron:${cron.id}`,
65
+ label: `cron · ${cron.humanSchedule}`,
66
+ description: preview(cron.prompt),
67
+ });
68
+ }
69
+ return items;
70
+ }
71
+
72
+ /**
73
+ * Build the detail-level items for a job: read-only info rows (value "noop"),
74
+ * then the destructive action, then a back row. `output` is the bounded monitor
75
+ * output tail (ignored for cron jobs).
76
+ */
77
+ export function buildJobDetailItems(snapshot: JobsSnapshot, ref: JobRef, output = ""): SelectItem[] {
78
+ if (ref.kind === "monitor") {
79
+ const monitor = snapshot.monitors.find(m => m.id === ref.id);
80
+ if (!monitor) return [{ value: "back", label: "Back (job no longer present)" }];
81
+ const lastOutput = output.trim().split("\n").filter(Boolean).slice(-1)[0] ?? "(no output captured)";
82
+ return [
83
+ { value: "noop", label: "Status", description: monitor.status },
84
+ { value: "noop", label: "Label", description: preview(monitor.label) },
85
+ { value: "noop", label: "Started", description: formatRelative(monitor.startTime) },
86
+ { value: "noop", label: "Output", description: preview(lastOutput, 80) },
87
+ { value: "action:cancel", label: "Cancel this monitor", hint: "stops the running job" },
88
+ { value: "back", label: "Back" },
89
+ ];
90
+ }
91
+ const cron = snapshot.crons.find(c => c.id === ref.id);
92
+ if (!cron) return [{ value: "back", label: "Back (job no longer present)" }];
93
+ return [
94
+ { value: "noop", label: "Schedule", description: `${cron.humanSchedule} (${cron.cronExpression})` },
95
+ { value: "noop", label: "Recurring", description: cron.recurring ? "yes" : "no" },
96
+ { value: "noop", label: "Next fire", description: formatRelative(cron.nextFireAt) },
97
+ { value: "noop", label: "Prompt", description: preview(cron.prompt, 80) },
98
+ { value: "action:delete", label: "Delete this cron", hint: "removes the schedule" },
99
+ { value: "back", label: "Back" },
100
+ ];
101
+ }
102
+
103
+ /** Yes/No confirm items for a destructive action. */
104
+ export function buildConfirmItems(actionLabel: string): SelectItem[] {
105
+ return [
106
+ { value: "no", label: `No, keep it` },
107
+ { value: "yes", label: `Yes, ${actionLabel}` },
108
+ ];
109
+ }
@@ -0,0 +1,172 @@
1
+ import { Container, type SelectItem, SelectList } from "@gajae-code/tui";
2
+ import type { JobsSnapshot } from "../jobs-observer";
3
+ import { getSelectListTheme } from "../theme/theme";
4
+ import { DynamicBorder } from "./dynamic-border";
5
+ import {
6
+ buildConfirmItems,
7
+ buildJobDetailItems,
8
+ buildJobsListItems,
9
+ type JobRef,
10
+ parseJobRef,
11
+ } from "./jobs-overlay-model";
12
+
13
+ /**
14
+ * Generic single-level selector used by the jobs overlay. The selector
15
+ * controller mounts a fresh instance per navigation level (list -> detail ->
16
+ * confirm); focus is placed on the inner SelectList, matching the existing
17
+ * selector components (e.g. ThemeSelectorComponent).
18
+ */
19
+ export class JobsSelectorComponent extends Container {
20
+ #selectList: SelectList;
21
+
22
+ constructor(items: SelectItem[], onSelect: (item: SelectItem) => void, onCancel: () => void, maxVisible = 12) {
23
+ super();
24
+ this.addChild(new DynamicBorder());
25
+ this.#selectList = new SelectList(items, maxVisible, getSelectListTheme());
26
+ this.#selectList.onSelect = onSelect;
27
+ this.#selectList.onCancel = onCancel;
28
+ this.addChild(this.#selectList);
29
+ this.addChild(new DynamicBorder());
30
+ }
31
+
32
+ getSelectList(): SelectList {
33
+ return this.#selectList;
34
+ }
35
+ }
36
+
37
+ export interface JobsOverlayController {
38
+ acknowledgeFailures(): void;
39
+ getSnapshot(): JobsSnapshot;
40
+ getMonitorOutput(id: string): string;
41
+ cancelMonitor(id: string): boolean;
42
+ deleteCron(id: string): boolean;
43
+ }
44
+
45
+ export interface JobsOverlayCallbacks {
46
+ close(): void;
47
+ requestRender(): void;
48
+ }
49
+
50
+ type JobsOverlayView = "list" | "detail" | "confirm";
51
+ type JobsOverlayAction = "cancel" | "delete";
52
+
53
+ export class JobsOverlayComponent extends Container {
54
+ readonly #controller: JobsOverlayController;
55
+ readonly #callbacks: JobsOverlayCallbacks;
56
+ #view: JobsOverlayView = "list";
57
+ #ref: JobRef | undefined;
58
+ #action: JobsOverlayAction | undefined;
59
+ #selectList: SelectList | undefined;
60
+
61
+ constructor(controller: JobsOverlayController, callbacks: JobsOverlayCallbacks) {
62
+ super();
63
+ this.#controller = controller;
64
+ this.#callbacks = callbacks;
65
+ this.#controller.acknowledgeFailures();
66
+ this.#renderList();
67
+ }
68
+
69
+ getFocus(): SelectList {
70
+ if (!this.#selectList) throw new Error("Jobs overlay has no focusable list");
71
+ return this.#selectList;
72
+ }
73
+
74
+ handleInput(data: string): void {
75
+ if (this.#view === "confirm") {
76
+ const key = data.toLowerCase();
77
+ if (key === "y") {
78
+ this.#confirmYes();
79
+ return;
80
+ }
81
+ if (key === "n") {
82
+ this.#renderDetail();
83
+ return;
84
+ }
85
+ }
86
+ this.#selectList?.handleInput(data);
87
+ }
88
+
89
+ #replaceList(
90
+ items: SelectItem[],
91
+ onSelect: (item: SelectItem) => void,
92
+ onCancel: () => void,
93
+ maxVisible = 12,
94
+ ): void {
95
+ this.clear();
96
+ this.addChild(new DynamicBorder());
97
+ this.#selectList = new SelectList(items, maxVisible, getSelectListTheme());
98
+ this.#selectList.onSelect = onSelect;
99
+ this.#selectList.onCancel = onCancel;
100
+ this.addChild(this.#selectList);
101
+ this.addChild(new DynamicBorder());
102
+ this.#callbacks.requestRender();
103
+ }
104
+
105
+ #renderList(): void {
106
+ this.#view = "list";
107
+ this.#ref = undefined;
108
+ this.#action = undefined;
109
+ const snapshot = this.#controller.getSnapshot();
110
+ const built = buildJobsListItems(snapshot);
111
+ const items = built.length > 0 ? built : [{ value: "close", label: "No active monitor or cron jobs" }];
112
+ this.#replaceList(
113
+ items,
114
+ item => {
115
+ const ref = parseJobRef(item.value);
116
+ if (ref) this.#renderDetail(ref);
117
+ else this.#callbacks.close();
118
+ },
119
+ () => this.#callbacks.close(),
120
+ );
121
+ }
122
+
123
+ #renderDetail(ref = this.#ref): void {
124
+ if (!ref) {
125
+ this.#renderList();
126
+ return;
127
+ }
128
+ this.#view = "detail";
129
+ this.#ref = ref;
130
+ this.#action = undefined;
131
+ const output = ref.kind === "monitor" ? this.#controller.getMonitorOutput(ref.id) : "";
132
+ const items = buildJobDetailItems(this.#controller.getSnapshot(), ref, output);
133
+ this.#replaceList(
134
+ items,
135
+ item => {
136
+ if (item.value === "action:cancel") this.#renderConfirm("cancel");
137
+ else if (item.value === "action:delete") this.#renderConfirm("delete");
138
+ else if (item.value === "back") this.#renderList();
139
+ },
140
+ () => this.#callbacks.close(),
141
+ );
142
+ }
143
+
144
+ #renderConfirm(action: JobsOverlayAction): void {
145
+ if (!this.#ref) {
146
+ this.#renderList();
147
+ return;
148
+ }
149
+ this.#view = "confirm";
150
+ this.#action = action;
151
+ const label = action === "cancel" ? "cancel this monitor" : "delete this cron";
152
+ this.#replaceList(
153
+ buildConfirmItems(label),
154
+ item => {
155
+ if (item.value === "yes") this.#confirmYes();
156
+ else this.#renderDetail();
157
+ },
158
+ () => this.#renderDetail(),
159
+ 4,
160
+ );
161
+ }
162
+
163
+ #confirmYes(): void {
164
+ if (!this.#ref || !this.#action) {
165
+ this.#renderList();
166
+ return;
167
+ }
168
+ if (this.#action === "cancel") this.#controller.cancelMonitor(this.#ref.id);
169
+ else this.#controller.deleteCron(this.#ref.id);
170
+ this.#renderList();
171
+ }
172
+ }
@@ -3,7 +3,7 @@ import type { PresetDef, StatusLinePreset } from "./types";
3
3
  export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
4
4
  default: {
5
5
  leftSegments: ["model", "mode", "git", "pr", "path"],
6
- rightSegments: ["session_name", "token_rate", "context_pct", "cost"],
6
+ rightSegments: ["session_name", "jobs", "token_rate", "context_pct", "cost"],
7
7
  separator: "slash",
8
8
  segmentOptions: {
9
9
  model: { showThinkingLevel: true },
@@ -14,7 +14,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
14
14
 
15
15
  minimal: {
16
16
  leftSegments: ["path", "git"],
17
- rightSegments: ["session_name", "mode", "context_pct"],
17
+ rightSegments: ["session_name", "jobs", "mode", "context_pct"],
18
18
  separator: "slash",
19
19
  segmentOptions: {
20
20
  path: { abbreviate: true, maxLength: 30 },
@@ -24,7 +24,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
24
24
 
25
25
  compact: {
26
26
  leftSegments: ["model", "mode", "git", "pr"],
27
- rightSegments: ["session_name", "cost", "context_pct"],
27
+ rightSegments: ["session_name", "jobs", "cost", "context_pct"],
28
28
  separator: "slash",
29
29
  segmentOptions: {
30
30
  model: { showThinkingLevel: false },
@@ -36,6 +36,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
36
36
  leftSegments: ["gajae", "hostname", "model", "mode", "path", "git", "pr", "subagents"],
37
37
  rightSegments: [
38
38
  "session_name",
39
+ "jobs",
39
40
  "token_in",
40
41
  "token_out",
41
42
  "token_rate",
@@ -59,6 +60,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
59
60
  leftSegments: ["gajae", "hostname", "model", "mode", "path", "git", "pr", "session", "subagents"],
60
61
  rightSegments: [
61
62
  "session_name",
63
+ "jobs",
62
64
  "token_in",
63
65
  "token_out",
64
66
  "cache_read",
@@ -82,7 +84,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
82
84
  ascii: {
83
85
  // No Nerd Font dependencies
84
86
  leftSegments: ["model", "mode", "path", "git", "pr"],
85
- rightSegments: ["session_name", "token_total", "cost", "context_pct"],
87
+ rightSegments: ["session_name", "jobs", "token_total", "cost", "context_pct"],
86
88
  separator: "ascii",
87
89
  segmentOptions: {
88
90
  model: { showThinkingLevel: true },
@@ -94,7 +96,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
94
96
  custom: {
95
97
  // User-defined - these are just defaults that get overridden
96
98
  leftSegments: ["model", "mode", "path", "git", "pr"],
97
- rightSegments: ["session_name", "token_total", "cost", "context_pct"],
99
+ rightSegments: ["session_name", "jobs", "token_total", "cost", "context_pct"],
98
100
  separator: "slash",
99
101
  segmentOptions: {},
100
102
  },
@@ -270,6 +270,30 @@ const subagentsSegment: StatusLineSegment = {
270
270
  },
271
271
  };
272
272
 
273
+ const jobsSegment: StatusLineSegment = {
274
+ id: "jobs",
275
+ render(ctx) {
276
+ const { jobs } = ctx;
277
+ const visible = jobs.activeMonitorCount > 0 || jobs.activeCronCount > 0 || jobs.worstState === "failed";
278
+ if (!visible) {
279
+ return { content: "", visible: false };
280
+ }
281
+ const parts: string[] = [];
282
+ if (jobs.activeMonitorCount > 0) {
283
+ parts.push(withIcon(theme.icon.agents, `${jobs.activeMonitorCount}`));
284
+ }
285
+ if (jobs.activeCronCount > 0) {
286
+ parts.push(withIcon(theme.icon.time, `${jobs.activeCronCount}`));
287
+ }
288
+ if (parts.length === 0) {
289
+ // Nothing active but a failure is unacknowledged — keep a drill-in marker.
290
+ parts.push(withIcon(theme.icon.warning, "jobs"));
291
+ }
292
+ const color: ThemeColor = jobs.worstState === "failed" ? "error" : "statusLineSubagents";
293
+ return { content: theme.fg(color, parts.join(" ")), visible: true };
294
+ },
295
+ };
296
+
273
297
  const tokenInSegment: StatusLineSegment = {
274
298
  id: "token_in",
275
299
  render(ctx) {
@@ -521,6 +545,7 @@ export const SEGMENTS: Record<StatusLineSegmentId, StatusLineSegment> = {
521
545
  git: gitSegment,
522
546
  pr: prSegment,
523
547
  subagents: subagentsSegment,
548
+ jobs: jobsSegment,
524
549
  token_in: tokenInSegment,
525
550
  token_out: tokenOutSegment,
526
551
  token_total: tokenTotalSegment,
@@ -1,5 +1,6 @@
1
1
  import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../../config/settings-schema";
2
2
  import type { AgentSession } from "../../../session/agent-session";
3
+ import type { JobsSnapshot } from "../../jobs-observer";
3
4
  import type { StatusLineSegmentOptions, StatusLineSettings } from "../status-line";
4
5
 
5
6
  export type {
@@ -42,6 +43,7 @@ export interface SegmentContext {
42
43
  contextWindow: number;
43
44
  autoCompactEnabled: boolean;
44
45
  subagentCount: number;
46
+ jobs: JobsSnapshot;
45
47
  sessionStartTime: number;
46
48
  git: {
47
49
  branch: string | null;
@@ -11,6 +11,7 @@ import type { AgentSession } from "../../session/agent-session";
11
11
  import { readVisibleSkillActiveState, type SkillActiveEntry } from "../../skill-state/active-state";
12
12
  import * as git from "../../utils/git";
13
13
  import { getSessionAccentAnsi, getSessionAccentHex } from "../../utils/session-color";
14
+ import { EMPTY_JOBS_SNAPSHOT, type JobsSnapshot } from "../jobs-observer";
14
15
  import { sanitizeStatusText } from "../shared";
15
16
  import { computeNonMessageTokens } from "../utils/context-usage";
16
17
  import { renderSkillHudBar } from "./skill-hud/render";
@@ -153,6 +154,7 @@ export class StatusLineComponent implements Component {
153
154
  #autoCompactEnabled: boolean = true;
154
155
  #hookStatuses: Map<string, string> = new Map();
155
156
  #subagentCount: number = 0;
157
+ #jobs: JobsSnapshot = EMPTY_JOBS_SNAPSHOT;
156
158
  #sessionStartTime: number = Date.now();
157
159
  #planModeStatus: { enabled: boolean; paused: boolean } | null = null;
158
160
  #goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
@@ -220,6 +222,10 @@ export class StatusLineComponent implements Component {
220
222
  this.#subagentCount = count;
221
223
  }
222
224
 
225
+ setJobs(jobs: JobsSnapshot): void {
226
+ this.#jobs = jobs;
227
+ }
228
+
223
229
  setSessionStartTime(time: number): void {
224
230
  this.#sessionStartTime = time;
225
231
  }
@@ -612,6 +618,7 @@ export class StatusLineComponent implements Component {
612
618
  contextWindow,
613
619
  autoCompactEnabled: this.#autoCompactEnabled,
614
620
  subagentCount: this.#subagentCount,
621
+ jobs: this.#jobs,
615
622
  sessionStartTime: this.#sessionStartTime,
616
623
  git: {
617
624
  branch: this.#getCurrentBranch(),
@@ -687,7 +694,8 @@ export class StatusLineComponent implements Component {
687
694
  }
688
695
  }
689
696
 
690
- const runningBackgroundJobs = this.session.getAsyncJobSnapshot()?.running.length ?? 0;
697
+ const runningBackgroundJobs =
698
+ this.session.getAsyncJobSnapshot()?.running.filter(job => job.metadata?.monitor !== true).length ?? 0;
691
699
  if (runningBackgroundJobs > 0) {
692
700
  const icon = theme.icon.agents ? `${theme.icon.agents} ` : "";
693
701
  const label = `${formatCount("job", runningBackgroundJobs)} running`;
@@ -1,6 +1,7 @@
1
1
  import { INTENT_FIELD } from "@gajae-code/agent-core";
2
2
  import { calculatePromptTokens } from "@gajae-code/agent-core/compaction/compaction";
3
3
  import type { AssistantMessage, ImageContent } from "@gajae-code/ai";
4
+ import { parseRateLimitReason } from "@gajae-code/ai";
4
5
  import { type Component, Loader, TERMINAL, Text } from "@gajae-code/tui";
5
6
  import { settings } from "../../config/settings";
6
7
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
@@ -24,6 +25,24 @@ type AgentSessionEventKind = AgentSessionEvent["type"];
24
25
 
25
26
  const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
26
27
 
28
+ function friendlyRetryReason(errorMessage: string | undefined): string {
29
+ if (!errorMessage) return "";
30
+ switch (parseRateLimitReason(errorMessage)) {
31
+ case "RATE_LIMIT_EXCEEDED":
32
+ return "rate limited";
33
+ case "QUOTA_EXHAUSTED":
34
+ return "usage limit";
35
+ case "MODEL_CAPACITY_EXHAUSTED":
36
+ return "overloaded";
37
+ case "SERVER_ERROR":
38
+ return "server error";
39
+ default:
40
+ return /network|connection|socket|fetch failed|terminated|timeout|timed out|stream/i.test(errorMessage)
41
+ ? "connection error"
42
+ : "transient error";
43
+ }
44
+ }
45
+
27
46
  type AgentSessionEventHandlers = {
28
47
  [E in AgentSessionEventKind]: (event: Extract<AgentSessionEvent, { type: E }>) => Promise<void>;
29
48
  };
@@ -71,6 +90,15 @@ export class EventController {
71
90
 
72
91
  dispose(): void {
73
92
  this.#cancelIdleCompaction();
93
+ this.#clearRetryCountdown();
94
+ if (this.ctx.retryEscapeHandler) {
95
+ this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
96
+ this.ctx.retryEscapeHandler = undefined;
97
+ }
98
+ if (this.ctx.retryLoader) {
99
+ this.ctx.retryLoader.stop();
100
+ this.ctx.retryLoader = undefined;
101
+ }
74
102
  for (const timer of this.#ircExpiryTimers.values()) {
75
103
  clearTimeout(timer);
76
104
  }
@@ -166,6 +194,7 @@ export class EventController {
166
194
  }
167
195
  if (this.ctx.retryLoader) {
168
196
  this.ctx.retryLoader.stop();
197
+ this.#clearRetryCountdown();
169
198
  this.ctx.retryLoader = undefined;
170
199
  this.ctx.statusContainer.clear();
171
200
  }
@@ -648,21 +677,56 @@ export class EventController {
648
677
  this.ctx.ui.requestRender();
649
678
  }
650
679
 
680
+ #clearRetryCountdown(): void {
681
+ if (this.ctx.retryCountdownTimer) {
682
+ clearInterval(this.ctx.retryCountdownTimer);
683
+ this.ctx.retryCountdownTimer = undefined;
684
+ }
685
+ }
686
+
651
687
  async #handleAutoRetryStart(event: Extract<AgentSessionEvent, { type: "auto_retry_start" }>): Promise<void> {
652
- this.ctx.retryEscapeHandler = this.ctx.editor.onEscape;
688
+ // Preserve the ORIGINAL editor Escape handler across repeated retry
689
+ // starts: auto_retry_end only fires at final success/failure, so a
690
+ // second auto_retry_start must not snapshot the prior retry handler.
691
+ if (!this.ctx.retryEscapeHandler) {
692
+ this.ctx.retryEscapeHandler = this.ctx.editor.onEscape;
693
+ }
694
+ let escPressed = false;
653
695
  this.ctx.editor.onEscape = () => {
654
- this.ctx.session.abortRetry();
696
+ if (!escPressed) {
697
+ // First Esc: skip the backoff and retry immediately.
698
+ escPressed = true;
699
+ this.ctx.session.retryNow();
700
+ } else {
701
+ // Second Esc: cancel the retry entirely.
702
+ this.ctx.session.abortRetry();
703
+ }
655
704
  };
656
705
  this.ctx.statusContainer.clear();
657
- const delaySeconds = Math.round(event.delayMs / 1000);
658
- this.ctx.retryLoader = new Loader(
706
+ // Stop any prior retry loader/timer before installing a new one.
707
+ this.ctx.retryLoader?.stop();
708
+ this.#clearRetryCountdown();
709
+ const reason = friendlyRetryReason(event.errorMessage);
710
+ const attemptLabel = event.unbounded ? `attempt ${event.attempt}` : `${event.attempt}/${event.maxAttempts}`;
711
+ const reasonSuffix = reason ? ` — ${reason}` : "";
712
+ const deadline = Date.now() + event.delayMs;
713
+ const buildMessage = () => {
714
+ const remainingSeconds = Math.max(0, Math.round((deadline - Date.now()) / 1000));
715
+ // First Esc retries immediately; a second Esc cancels.
716
+ return `Retrying (${attemptLabel})${reasonSuffix}, next in ${remainingSeconds}s… (esc to retry now)`;
717
+ };
718
+ const retryLoader = new Loader(
659
719
  this.ctx.ui,
660
720
  spinner => theme.fg("warning", spinner),
661
721
  text => theme.fg("muted", text),
662
- `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s… (esc to cancel)`,
722
+ buildMessage(),
663
723
  getSymbolTheme().spinnerFrames,
664
724
  );
665
- this.ctx.statusContainer.addChild(this.ctx.retryLoader);
725
+ this.ctx.retryLoader = retryLoader;
726
+ this.ctx.retryCountdownTimer = setInterval(() => {
727
+ retryLoader.setMessage(buildMessage());
728
+ }, 1000);
729
+ this.ctx.statusContainer.addChild(retryLoader);
666
730
  this.ctx.ui.requestRender();
667
731
  }
668
732
 
@@ -673,6 +737,7 @@ export class EventController {
673
737
  }
674
738
  if (this.ctx.retryLoader) {
675
739
  this.ctx.retryLoader.stop();
740
+ this.#clearRetryCountdown();
676
741
  this.ctx.retryLoader = undefined;
677
742
  this.ctx.statusContainer.clear();
678
743
  }
@@ -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,7 +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));
598
+ const requestedTitleRows = dialogOptions?.scrollTitleRows;
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;
606
+ const scrollTitleRows =
607
+ requestedTitleRows === undefined ? undefined : Math.max(1, Math.min(requestedTitleRows, availableTitleRows));
608
+ if (scrollTitleRows !== undefined) {
609
+ this.#enableHookSelectorMouseReporting();
610
+ }
611
+
593
612
  this.ctx.hookSelector = new HookSelectorComponent(
594
613
  title,
595
614
  options,
@@ -624,6 +643,7 @@ export class ExtensionUiController {
624
643
  tui: this.ctx.ui,
625
644
  outline: dialogOptions?.outline,
626
645
  wrapFocused: dialogOptions?.wrapFocused,
646
+ scrollTitleRows,
627
647
  maxVisible,
628
648
  },
629
649
  );
@@ -634,10 +654,32 @@ export class ExtensionUiController {
634
654
  attachAbort();
635
655
  return promise;
636
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
+
637
678
  /**
638
679
  * Hide the hook selector.
639
680
  */
640
681
  hideHookSelector(): void {
682
+ this.#disableHookSelectorMouseReporting();
641
683
  this.ctx.hookSelector?.dispose();
642
684
  this.ctx.editorContainer.clear();
643
685
  this.ctx.editorContainer.addChild(this.ctx.editor);