@gajae-code/coding-agent 0.3.0 → 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 (175) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/types/async/job-manager.d.ts +7 -0
  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/config/keybindings.d.ts +5 -0
  6. package/dist/types/config/settings-schema.d.ts +4 -4
  7. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  8. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  9. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  10. package/dist/types/eval/py/executor.d.ts +2 -0
  11. package/dist/types/eval/py/kernel.d.ts +2 -0
  12. package/dist/types/exec/bash-executor.d.ts +10 -0
  13. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  14. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  16. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  17. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  20. package/dist/types/hooks/skill-state.d.ts +21 -0
  21. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  22. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  23. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  24. package/dist/types/internal-urls/types.d.ts +4 -0
  25. package/dist/types/lsp/index.d.ts +10 -10
  26. package/dist/types/modes/bridge/auth.d.ts +12 -0
  27. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  28. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  29. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  30. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  32. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  33. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  34. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  35. package/dist/types/modes/components/status-line.d.ts +2 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  37. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  38. package/dist/types/modes/index.d.ts +1 -0
  39. package/dist/types/modes/interactive-mode.d.ts +1 -0
  40. package/dist/types/modes/jobs-observer.d.ts +57 -0
  41. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  42. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  43. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  44. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  46. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  47. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  48. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  49. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  50. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  51. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  52. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  53. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  54. package/dist/types/modes/types.d.ts +1 -0
  55. package/dist/types/sdk.d.ts +2 -0
  56. package/dist/types/session/agent-session.d.ts +11 -1
  57. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  58. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  59. package/dist/types/task/id.d.ts +7 -0
  60. package/dist/types/task/index.d.ts +5 -0
  61. package/dist/types/task/receipt.d.ts +85 -0
  62. package/dist/types/task/spawn-gate.d.ts +38 -0
  63. package/dist/types/task/types.d.ts +143 -11
  64. package/dist/types/tools/cron.d.ts +6 -0
  65. package/dist/types/tools/index.d.ts +2 -0
  66. package/dist/types/tools/path-utils.d.ts +1 -0
  67. package/dist/types/tools/subagent.d.ts +15 -0
  68. package/package.json +7 -7
  69. package/scripts/build-binary.ts +7 -0
  70. package/src/async/job-manager.ts +36 -0
  71. package/src/cli/args.ts +9 -2
  72. package/src/commands/deep-interview.ts +1 -0
  73. package/src/commands/harness.ts +289 -19
  74. package/src/commands/launch.ts +2 -2
  75. package/src/commands/state.ts +2 -1
  76. package/src/commands/team.ts +22 -4
  77. package/src/config/keybindings.ts +6 -0
  78. package/src/config/settings-schema.ts +6 -3
  79. package/src/dap/client.ts +17 -3
  80. package/src/debug/crash-diagnostics.ts +223 -0
  81. package/src/debug/runtime-gauges.ts +20 -0
  82. package/src/deep-interview/render-middleware.ts +6 -0
  83. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  84. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  85. package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
  86. package/src/eval/py/executor.ts +21 -1
  87. package/src/eval/py/kernel.ts +15 -0
  88. package/src/exec/bash-executor.ts +41 -0
  89. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  90. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  91. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  92. package/src/gjc-runtime/state-migrations.ts +54 -7
  93. package/src/gjc-runtime/state-runtime.ts +461 -64
  94. package/src/gjc-runtime/state-schema.ts +192 -0
  95. package/src/gjc-runtime/state-writer.ts +32 -1
  96. package/src/gjc-runtime/team-runtime.ts +177 -105
  97. package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
  98. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  99. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  100. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  101. package/src/harness-control-plane/control-endpoint.ts +19 -8
  102. package/src/harness-control-plane/owner.ts +57 -10
  103. package/src/harness-control-plane/state-machine.ts +2 -1
  104. package/src/hooks/skill-state.ts +176 -26
  105. package/src/internal-urls/agent-protocol.ts +68 -21
  106. package/src/internal-urls/artifact-protocol.ts +12 -17
  107. package/src/internal-urls/docs-index.generated.ts +3 -2
  108. package/src/internal-urls/registry-helpers.ts +19 -16
  109. package/src/internal-urls/types.ts +4 -0
  110. package/src/lsp/client.ts +18 -2
  111. package/src/main.ts +21 -5
  112. package/src/modes/bridge/auth.ts +41 -0
  113. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  114. package/src/modes/bridge/bridge-mode.ts +520 -0
  115. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  116. package/src/modes/bridge/event-stream.ts +70 -0
  117. package/src/modes/components/custom-editor.ts +101 -0
  118. package/src/modes/components/hook-selector.ts +61 -18
  119. package/src/modes/components/jobs-overlay-model.ts +109 -0
  120. package/src/modes/components/jobs-overlay.ts +172 -0
  121. package/src/modes/components/status-line/presets.ts +7 -5
  122. package/src/modes/components/status-line/segments.ts +25 -0
  123. package/src/modes/components/status-line/types.ts +2 -0
  124. package/src/modes/components/status-line.ts +9 -1
  125. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  126. package/src/modes/controllers/input-controller.ts +97 -9
  127. package/src/modes/controllers/selector-controller.ts +29 -0
  128. package/src/modes/index.ts +1 -0
  129. package/src/modes/interactive-mode.ts +27 -0
  130. package/src/modes/jobs-observer.ts +204 -0
  131. package/src/modes/rpc/host-tools.ts +1 -186
  132. package/src/modes/rpc/host-uris.ts +1 -235
  133. package/src/modes/rpc/rpc-client.ts +25 -10
  134. package/src/modes/rpc/rpc-mode.ts +12 -381
  135. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  136. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  137. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  138. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  139. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  140. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  141. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  142. package/src/modes/shared/agent-wire/responses.ts +17 -0
  143. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  144. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  145. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  146. package/src/modes/types.ts +1 -0
  147. package/src/prompts/tools/subagent.md +12 -7
  148. package/src/prompts/tools/task-summary.md +3 -9
  149. package/src/prompts/tools/task.md +5 -1
  150. package/src/sdk.ts +4 -0
  151. package/src/session/agent-session.ts +214 -38
  152. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  153. package/src/skill-state/workflow-state-contract.ts +7 -4
  154. package/src/skill-state/workflow-state-version.ts +3 -0
  155. package/src/slash-commands/builtin-registry.ts +8 -0
  156. package/src/task/executor.ts +29 -5
  157. package/src/task/id.ts +33 -0
  158. package/src/task/index.ts +257 -67
  159. package/src/task/output-manager.ts +5 -4
  160. package/src/task/receipt.ts +297 -0
  161. package/src/task/render.ts +48 -131
  162. package/src/task/spawn-gate.ts +132 -0
  163. package/src/task/types.ts +48 -7
  164. package/src/tools/ask.ts +73 -33
  165. package/src/tools/ast-edit.ts +1 -0
  166. package/src/tools/ast-grep.ts +1 -0
  167. package/src/tools/bash.ts +1 -1
  168. package/src/tools/cron.ts +48 -0
  169. package/src/tools/find.ts +4 -1
  170. package/src/tools/index.ts +2 -0
  171. package/src/tools/path-utils.ts +3 -2
  172. package/src/tools/read.ts +1 -0
  173. package/src/tools/search.ts +1 -0
  174. package/src/tools/skill.ts +6 -1
  175. package/src/tools/subagent.ts +237 -84
@@ -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`;
@@ -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
  }
@@ -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,6 +56,7 @@ 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
62
  const CALLBACK_SERVER_PROVIDERS = new Set<string>([
@@ -1150,4 +1152,31 @@ export class SelectorController {
1150
1152
  this.ctx.ui.setFocus(selector);
1151
1153
  this.ctx.ui.requestRender();
1152
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
+ }
1153
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 {