@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,200 @@
1
+ import type {
2
+ ExtensionUIContext,
3
+ ExtensionUIDialogOptions,
4
+ ExtensionWidgetContent,
5
+ ExtensionWidgetOptions,
6
+ TerminalInputHandler,
7
+ } from "../../extensibility/extensions";
8
+ import type { UiRequestBroker, UiRequestCancelled, UiRequestResolution } from "../shared/agent-wire/ui-request-broker";
9
+ import type { BridgeUiResult } from "../shared/agent-wire/ui-result";
10
+ import { isUiUnsupported, isUiValue } from "../shared/agent-wire/ui-result";
11
+ import { type Theme, theme } from "../theme/theme";
12
+
13
+ export type BridgeUiRequestPayload =
14
+ | { kind: "select"; title: string; options: string[]; timeout?: number }
15
+ | { kind: "confirm"; title: string; message: string; timeout?: number }
16
+ | { kind: "input"; title: string; placeholder?: string; timeout?: number }
17
+ | { kind: "editor"; title: string; prefill?: string; promptStyle?: boolean }
18
+ | { kind: "notify"; message: string; type?: "info" | "warning" | "error" }
19
+ | { kind: "status"; key: string; text: string | undefined }
20
+ | { kind: "widget"; key: string; lines: string[] | undefined; placement?: "aboveEditor" | "belowEditor" }
21
+ | { kind: "title"; title: string }
22
+ | { kind: "set_editor_text"; text: string }
23
+ | { kind: "unsupported"; capability: string; reason: string };
24
+
25
+ export type BridgeUiBroker = UiRequestBroker<BridgeUiRequestPayload, BridgeUiResult<unknown>>;
26
+ export type BridgeUiEmitter = (payload: BridgeUiRequestPayload) => void;
27
+
28
+ function isBrokerCancelled(resolution: UiRequestResolution<BridgeUiResult<unknown>>): resolution is UiRequestCancelled {
29
+ return resolution.status === "cancelled";
30
+ }
31
+
32
+ function timeoutFor(dialogOptions: ExtensionUIDialogOptions | undefined): number | undefined {
33
+ return dialogOptions?.timeout;
34
+ }
35
+
36
+ export class BridgeExtensionUIContext implements ExtensionUIContext {
37
+ readonly #broker: BridgeUiBroker;
38
+ readonly #emit: BridgeUiEmitter;
39
+
40
+ constructor(options: { broker: BridgeUiBroker; emit: BridgeUiEmitter }) {
41
+ this.#broker = options.broker;
42
+ this.#emit = options.emit;
43
+ }
44
+
45
+ #emitUnsupported(capability: string, reason: string): void {
46
+ this.#emit({ kind: "unsupported", capability, reason });
47
+ }
48
+
49
+ async select(
50
+ title: string,
51
+ options: string[],
52
+ dialogOptions?: ExtensionUIDialogOptions,
53
+ ): Promise<string | undefined> {
54
+ const result = await this.#broker.request(
55
+ { kind: "select", title, options, timeout: timeoutFor(dialogOptions) },
56
+ { timeoutMs: timeoutFor(dialogOptions), signal: dialogOptions?.signal },
57
+ );
58
+ if (isBrokerCancelled(result)) {
59
+ if (result.reason === "timeout") dialogOptions?.onTimeout?.();
60
+ return undefined;
61
+ }
62
+ if (isUiValue(result) && typeof result.value === "string") return result.value;
63
+ return undefined;
64
+ }
65
+
66
+ async confirm(title: string, message: string, dialogOptions?: ExtensionUIDialogOptions): Promise<boolean> {
67
+ const result = await this.#broker.request(
68
+ { kind: "confirm", title, message, timeout: timeoutFor(dialogOptions) },
69
+ { timeoutMs: timeoutFor(dialogOptions), signal: dialogOptions?.signal },
70
+ );
71
+ if (isBrokerCancelled(result)) {
72
+ if (result.reason === "timeout") dialogOptions?.onTimeout?.();
73
+ return false;
74
+ }
75
+ if (isUiValue(result) && typeof result.value === "boolean") return result.value;
76
+ return false;
77
+ }
78
+
79
+ async input(
80
+ title: string,
81
+ placeholder?: string,
82
+ dialogOptions?: ExtensionUIDialogOptions,
83
+ ): Promise<string | undefined> {
84
+ const result = await this.#broker.request(
85
+ { kind: "input", title, placeholder, timeout: timeoutFor(dialogOptions) },
86
+ { timeoutMs: timeoutFor(dialogOptions), signal: dialogOptions?.signal },
87
+ );
88
+ if (isBrokerCancelled(result)) {
89
+ if (result.reason === "timeout") dialogOptions?.onTimeout?.();
90
+ return undefined;
91
+ }
92
+ if (isUiValue(result) && typeof result.value === "string") return result.value;
93
+ return undefined;
94
+ }
95
+
96
+ notify(message: string, type?: "info" | "warning" | "error"): void {
97
+ this.#emit({ kind: "notify", message, type });
98
+ }
99
+
100
+ onTerminalInput(_handler: TerminalInputHandler): () => void {
101
+ this.#emitUnsupported("ui.terminal_input", "Raw terminal input is not supported by the bridge protocol yet");
102
+ return () => {};
103
+ }
104
+
105
+ setStatus(key: string, text: string | undefined): void {
106
+ this.#emit({ kind: "status", key, text });
107
+ }
108
+
109
+ setWorkingMessage(message?: string): void {
110
+ this.#emit({ kind: "status", key: "working", text: message });
111
+ }
112
+
113
+ setWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void {
114
+ if (content === undefined || Array.isArray(content)) {
115
+ this.#emit({ kind: "widget", key, lines: content, placement: options?.placement });
116
+ return;
117
+ }
118
+ this.#emitUnsupported("ui.widget.component", "Component factory widgets are local-only and not serializable");
119
+ }
120
+
121
+ setFooter(_factory?: unknown): void {
122
+ this.#emitUnsupported("ui.footer.component", "Footer component factories are local-only and not serializable");
123
+ }
124
+
125
+ setHeader(_factory?: unknown): void {
126
+ this.#emitUnsupported("ui.header.component", "Header component factories are local-only and not serializable");
127
+ }
128
+
129
+ setTitle(title: string): void {
130
+ this.#emit({ kind: "title", title });
131
+ }
132
+
133
+ async custom<T>(): Promise<T> {
134
+ this.#emitUnsupported("ui.custom.component", "Custom focused components are local-only and not serializable");
135
+ throw new Error("Custom focused components are local-only and not serializable through bridge UI context");
136
+ }
137
+
138
+ setEditorText(text: string): void {
139
+ this.#emit({ kind: "set_editor_text", text });
140
+ }
141
+
142
+ pasteToEditor(text: string): void {
143
+ this.setEditorText(text);
144
+ }
145
+
146
+ getEditorText(): string {
147
+ this.#emitUnsupported(
148
+ "ui.editor.get_text",
149
+ "Synchronous editor reads are local-only and not supported by bridge UI context",
150
+ );
151
+ throw new Error("Synchronous editor reads are local-only and not supported by bridge UI context");
152
+ }
153
+
154
+ async editor(
155
+ title: string,
156
+ prefill?: string,
157
+ dialogOptions?: ExtensionUIDialogOptions,
158
+ editorOptions?: { promptStyle?: boolean },
159
+ ): Promise<string | undefined> {
160
+ const result = await this.#broker.request(
161
+ { kind: "editor", title, prefill, promptStyle: editorOptions?.promptStyle },
162
+ { timeoutMs: timeoutFor(dialogOptions), signal: dialogOptions?.signal },
163
+ );
164
+ if (isBrokerCancelled(result)) {
165
+ if (result.reason === "timeout") dialogOptions?.onTimeout?.();
166
+ return undefined;
167
+ }
168
+ if (isUiUnsupported(result)) return undefined;
169
+ if (isUiValue(result) && typeof result.value === "string") return result.value;
170
+ return undefined;
171
+ }
172
+
173
+ setEditorComponent(_factory?: unknown): void {
174
+ this.#emitUnsupported("ui.editor.component", "Custom editor components are local-only and not serializable");
175
+ }
176
+
177
+ get theme(): Theme {
178
+ return theme;
179
+ }
180
+
181
+ getAllThemes(): Promise<{ name: string; path: string | undefined }[]> {
182
+ return Promise.resolve([]);
183
+ }
184
+
185
+ getTheme(_name: string): Promise<Theme | undefined> {
186
+ return Promise.resolve(undefined);
187
+ }
188
+
189
+ setTheme(_theme: string | Theme): Promise<{ success: boolean; error?: string }> {
190
+ return Promise.resolve({ success: false, error: "Theme switching is not supported by bridge UI context yet" });
191
+ }
192
+
193
+ getToolsExpanded(): boolean {
194
+ return false;
195
+ }
196
+
197
+ setToolsExpanded(_expanded: boolean): void {
198
+ this.#emitUnsupported("ui.tools_expanded", "Tool expansion state is not supported by bridge UI context yet");
199
+ }
200
+ }
@@ -0,0 +1,70 @@
1
+ import type { BridgeFrameEnvelope } from "../shared/agent-wire/protocol";
2
+
3
+ const encoder = new TextEncoder();
4
+ const DEFAULT_REPLAY_LIMIT = 1_000;
5
+
6
+ function encodeSseFrame(frame: BridgeFrameEnvelope): Uint8Array {
7
+ return encoder.encode(`data: ${JSON.stringify(frame)}\n\n`);
8
+ }
9
+
10
+ export class BridgeEventStream {
11
+ #frames: BridgeFrameEnvelope[] = [];
12
+ #subscribers = new Set<ReadableStreamDefaultController<Uint8Array>>();
13
+ #replayLimit: number;
14
+
15
+ constructor(replayLimit = DEFAULT_REPLAY_LIMIT) {
16
+ this.#replayLimit = replayLimit;
17
+ }
18
+ get frameCount(): number {
19
+ return this.#frames.length;
20
+ }
21
+
22
+ publish(frame: BridgeFrameEnvelope): void {
23
+ this.#frames.push(frame);
24
+ if (this.#frames.length > this.#replayLimit) this.#frames.splice(0, this.#frames.length - this.#replayLimit);
25
+ const encoded = encodeSseFrame(frame);
26
+ for (const controller of this.#subscribers) {
27
+ try {
28
+ controller.enqueue(encoded);
29
+ } catch {
30
+ this.#subscribers.delete(controller);
31
+ }
32
+ }
33
+ }
34
+
35
+ response(lastSeq = 0): Response {
36
+ let streamController: ReadableStreamDefaultController<Uint8Array> | undefined;
37
+ const stream = new ReadableStream<Uint8Array>({
38
+ start: controller => {
39
+ streamController = controller;
40
+ const first = this.#frames[0];
41
+ if (first && lastSeq > 0 && first.seq > lastSeq + 1) {
42
+ controller.enqueue(
43
+ encodeSseFrame({
44
+ protocol_version: first.protocol_version,
45
+ session_id: first.session_id,
46
+ seq: first.seq - 1,
47
+ frame_id: `reset-${first.seq}`,
48
+ type: "reset",
49
+ payload: { reason: "replay_window_exceeded", first_seq: first.seq },
50
+ }),
51
+ );
52
+ }
53
+ for (const frame of this.#frames) {
54
+ if (frame.seq > lastSeq) controller.enqueue(encodeSseFrame(frame));
55
+ }
56
+ this.#subscribers.add(controller);
57
+ },
58
+ cancel: () => {
59
+ if (streamController) this.#subscribers.delete(streamController);
60
+ },
61
+ });
62
+ return new Response(stream, {
63
+ headers: {
64
+ "Content-Type": "text/event-stream",
65
+ "Cache-Control": "no-cache",
66
+ Connection: "keep-alive",
67
+ },
68
+ });
69
+ }
70
+ }
@@ -2,6 +2,7 @@ import type { AssistantMessage, ImageContent, Usage } from "@gajae-code/ai";
2
2
  import { Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@gajae-code/tui";
3
3
  import { formatNumber } from "@gajae-code/utils";
4
4
  import { settings } from "../../config/settings";
5
+ import { renderDeepInterviewAssistantText } from "../../deep-interview/render-middleware";
5
6
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
6
7
  import { isSilentAbort } from "../../session/messages";
7
8
  import { resolveImageOptions } from "../../tools/render-utils";
@@ -153,7 +154,10 @@ export class AssistantMessageComponent extends Container {
153
154
  if (content.type === "text" && content.text.trim()) {
154
155
  // Assistant text messages with no background - trim the text
155
156
  // Set paddingY=0 to avoid extra spacing before tool executions
156
- this.#contentContainer.addChild(new Markdown(content.text.trim(), 1, 0, getMarkdownTheme()));
157
+ const text = content.text.trim();
158
+ this.#contentContainer.addChild(
159
+ renderDeepInterviewAssistantText(text, theme) ?? new Markdown(text, 1, 0, getMarkdownTheme()),
160
+ );
157
161
  } else if (content.type === "thinking" && content.thinking.trim()) {
158
162
  // Add spacing only when another visible assistant content block follows.
159
163
  // This avoids a superfluous blank line before separately-rendered tool execution blocks.
@@ -1,4 +1,5 @@
1
1
  import { Editor, type KeyId, matchesKey, parseKittySequence } from "@gajae-code/tui";
2
+ import { BracketedPasteHandler } from "@gajae-code/tui/bracketed-paste";
2
3
  import type { AppKeybinding } from "../../config/keybindings";
3
4
 
4
5
  type ConfigurableEditorAction = Extract<
@@ -40,6 +41,11 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
40
41
  "app.clipboard.copyPrompt": ["alt+shift+c"],
41
42
  };
42
43
 
44
+ const PASTE_DECISION_TIMEOUT_MS = 5_000;
45
+ const PENDING_PASTE_INPUT_MAX = 64;
46
+
47
+ type PastePendingClearReason = "timeout" | "queue-limit";
48
+
43
49
  /**
44
50
  * Custom editor that handles configurable app-level shortcuts for coding-agent.
45
51
  */
@@ -63,6 +69,10 @@ export class CustomEditor extends Editor {
63
69
  onCopyPrompt?: () => void;
64
70
  /** Called when the configured image-paste shortcut is pressed. */
65
71
  onPasteImage?: () => Promise<boolean>;
72
+ /** Called before bracketed paste content is inserted. Return true to consume it. */
73
+ onPasteText?: (text: string) => boolean | Promise<boolean>;
74
+ /** Called when async paste handling drops queued input instead of replaying it. */
75
+ onPastePendingInputCleared?: (reason: PastePendingClearReason, droppedInputCount: number) => void;
66
76
  /** Called when the configured dequeue shortcut is pressed. */
67
77
  onDequeue?: () => void;
68
78
  /** Called when Caps Lock is pressed. */
@@ -73,6 +83,11 @@ export class CustomEditor extends Editor {
73
83
  #actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
74
84
  Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
75
85
  );
86
+ #pasteHandler = new BracketedPasteHandler();
87
+ #pasteDecisionPending = false;
88
+ #pasteDecisionToken = 0;
89
+ #pasteDecisionTimeout: NodeJS.Timeout | undefined;
90
+ #pendingPasteInput: string[] = [];
76
91
 
77
92
  setActionKeys(action: ConfigurableEditorAction, keys: KeyId[]): void {
78
93
  this.#actionKeys.set(action, [...keys]);
@@ -108,7 +123,84 @@ export class CustomEditor extends Editor {
108
123
  this.#customKeyHandlers.clear();
109
124
  }
110
125
 
126
+ #clearPasteDecisionTimeout(): void {
127
+ if (this.#pasteDecisionTimeout) {
128
+ clearTimeout(this.#pasteDecisionTimeout);
129
+ this.#pasteDecisionTimeout = undefined;
130
+ }
131
+ }
132
+
133
+ #clearPendingPasteState(): number {
134
+ this.#clearPasteDecisionTimeout();
135
+ this.#pasteDecisionPending = false;
136
+ this.#pasteDecisionToken += 1;
137
+ const droppedInputCount = this.#pendingPasteInput.length;
138
+ this.#pendingPasteInput = [];
139
+ return droppedInputCount;
140
+ }
141
+
142
+ #startPasteDecisionTimeout(token: number): void {
143
+ this.#clearPasteDecisionTimeout();
144
+ this.#pasteDecisionTimeout = setTimeout(() => {
145
+ if (token !== this.#pasteDecisionToken) return;
146
+ const droppedInputCount = this.#clearPendingPasteState();
147
+ this.onPastePendingInputCleared?.("timeout", droppedInputCount);
148
+ }, PASTE_DECISION_TIMEOUT_MS);
149
+ this.#pasteDecisionTimeout.unref?.();
150
+ }
151
+
152
+ dispose(): void {
153
+ this.#clearPendingPasteState();
154
+ this.#pasteHandler = new BracketedPasteHandler();
155
+ }
156
+
157
+ #drainPendingPasteInput(initialInput?: string): void {
158
+ if (initialInput && initialInput.length > 0) {
159
+ this.handleInput(initialInput);
160
+ }
161
+ while (!this.#pasteDecisionPending) {
162
+ const nextInput = this.#pendingPasteInput.shift();
163
+ if (nextInput === undefined) break;
164
+ this.handleInput(nextInput);
165
+ }
166
+ }
167
+
168
+ #handleBracketedPaste(pasteContent: string, remaining: string): void {
169
+ const applyPasteResult = (token: number, handled: boolean | undefined) => {
170
+ if (token !== this.#pasteDecisionToken) return;
171
+ this.#clearPasteDecisionTimeout();
172
+ if (!handled) {
173
+ super.handleInput(`\x1b[200~${pasteContent}\x1b[201~`);
174
+ }
175
+ this.#pasteDecisionPending = false;
176
+ this.#drainPendingPasteInput(remaining);
177
+ };
178
+ const pasteResult = this.onPasteText?.(pasteContent);
179
+
180
+ if (pasteResult instanceof Promise) {
181
+ const token = this.#pasteDecisionToken + 1;
182
+ this.#pasteDecisionToken = token;
183
+ this.#pasteDecisionPending = true;
184
+ this.#startPasteDecisionTimeout(token);
185
+ void pasteResult.then(
186
+ handled => applyPasteResult(token, handled),
187
+ () => applyPasteResult(token, false),
188
+ );
189
+ } else {
190
+ applyPasteResult(this.#pasteDecisionToken, pasteResult);
191
+ }
192
+ }
193
+
111
194
  handleInput(data: string): void {
195
+ if (this.#pasteDecisionPending) {
196
+ this.#pendingPasteInput.push(data);
197
+ if (this.#pendingPasteInput.length > PENDING_PASTE_INPUT_MAX) {
198
+ const droppedInputCount = this.#clearPendingPasteState();
199
+ this.onPastePendingInputCleared?.("queue-limit", droppedInputCount);
200
+ }
201
+ return;
202
+ }
203
+
112
204
  const parsed = parseKittySequence(data);
113
205
  if (parsed && (parsed.modifier & 64) !== 0 && this.onCapsLock) {
114
206
  // Caps Lock is modifier bit 64
@@ -116,6 +208,15 @@ export class CustomEditor extends Editor {
116
208
  return;
117
209
  }
118
210
 
211
+ if (this.onPasteText) {
212
+ const paste = this.#pasteHandler.process(data);
213
+ if (paste.handled) {
214
+ if (paste.pasteContent !== undefined) {
215
+ this.#handleBracketedPaste(paste.pasteContent, paste.remaining);
216
+ }
217
+ return;
218
+ }
219
+ }
119
220
  // Intercept configured image paste (async - fires and handles result)
120
221
  if (this.#matchesAction(data, "app.clipboard.pasteImage") && this.onPasteImage) {
121
222
  void this.onPasteImage();
@@ -21,6 +21,22 @@ import { matchesAppExternalEditor, matchesSelectCancel } from "../../modes/utils
21
21
  import { CountdownTimer } from "./countdown-timer";
22
22
  import { DynamicBorder } from "./dynamic-border";
23
23
 
24
+ const SGR_MOUSE_PRESS_PATTERN = /^\x1b\[<(\d+);\d+;\d+M$/;
25
+ const MOUSE_WHEEL_TITLE_SCROLL_ROWS = 3;
26
+
27
+ function getMouseWheelTitleScrollRows(keyData: string): number {
28
+ const match = SGR_MOUSE_PRESS_PATTERN.exec(keyData);
29
+ if (!match) return 0;
30
+
31
+ const button = Number.parseInt(match[1] ?? "", 10);
32
+ if (!Number.isFinite(button) || (button & 64) === 0) return 0;
33
+
34
+ const wheelDirection = button & 3;
35
+ if (wheelDirection === 0) return -MOUSE_WHEEL_TITLE_SCROLL_ROWS;
36
+ if (wheelDirection === 1) return MOUSE_WHEEL_TITLE_SCROLL_ROWS;
37
+ return 0;
38
+ }
39
+
24
40
  export interface HookSelectorOptions {
25
41
  tui?: TUI;
26
42
  timeout?: number;
@@ -39,6 +55,7 @@ export interface HookSelectorOptions {
39
55
  * byte-identical to the previous implementation for all consumers.
40
56
  */
41
57
  wrapFocused?: boolean;
58
+ scrollTitleRows?: number;
42
59
  }
43
60
 
44
61
  class OutlinedList extends Container {
@@ -63,6 +80,57 @@ class OutlinedList extends Container {
63
80
  }
64
81
  }
65
82
 
83
+ class ScrollableTitle extends Container {
84
+ #markdown: Markdown;
85
+ #maxRows: number;
86
+ #scrollOffset = 0;
87
+ #lastMaxScrollOffset = 0;
88
+
89
+ constructor(title: string, maxRows: number) {
90
+ super();
91
+ this.#maxRows = Math.max(1, Math.floor(maxRows));
92
+ this.#markdown = new Markdown(title, 1, 0, getMarkdownTheme(), { color: t => theme.fg("accent", t) });
93
+ }
94
+
95
+ setText(text: string): void {
96
+ this.#markdown.setText(text);
97
+ this.#scrollOffset = 0;
98
+ this.invalidate();
99
+ }
100
+
101
+ scrollBy(rows: number): void {
102
+ if (rows === 0) return;
103
+ const nextOffset = Math.max(0, Math.min(this.#lastMaxScrollOffset, this.#scrollOffset + rows));
104
+ if (nextOffset === this.#scrollOffset) return;
105
+ this.#scrollOffset = nextOffset;
106
+ this.invalidate();
107
+ }
108
+
109
+ render(width: number): string[] {
110
+ const lines = this.#markdown.render(width);
111
+ const maxScrollOffset = Math.max(0, lines.length - this.#maxRows);
112
+ this.#lastMaxScrollOffset = maxScrollOffset;
113
+ this.#scrollOffset = Math.max(0, Math.min(this.#scrollOffset, maxScrollOffset));
114
+
115
+ const visibleLines = lines.slice(this.#scrollOffset, this.#scrollOffset + this.#maxRows);
116
+ if (maxScrollOffset === 0 || visibleLines.length === 0) {
117
+ return visibleLines;
118
+ }
119
+
120
+ const indicator =
121
+ this.#scrollOffset === 0
122
+ ? theme.fg("dim", " PgDn↓")
123
+ : this.#scrollOffset >= maxScrollOffset
124
+ ? theme.fg("dim", " PgUp↑")
125
+ : theme.fg("dim", " PgUp/PgDn↕");
126
+ const lastIndex = visibleLines.length - 1;
127
+ const availableWidth = Math.max(1, width - visibleWidth(indicator));
128
+ const fittedLine = truncateToWidth(visibleLines[lastIndex] ?? "", availableWidth);
129
+ visibleLines[lastIndex] = `${fittedLine}${indicator}`;
130
+ return visibleLines;
131
+ }
132
+ }
133
+
66
134
  /**
67
135
  * Width-aware list child that owns wrapped focused-option layout.
68
136
  *
@@ -73,11 +141,10 @@ class OutlinedList extends Container {
73
141
  * `maxVisibleRows`; everything that depends on terminal width is recomputed
74
142
  * on each render so resize Just Works.
75
143
  *
76
- * `maxVisibleRows` is a sibling budget before it is a hard cap: surrounding
77
- * options shrink first so the focused option is never clipped. The single
78
- * allowed overflow exception is when the focused option's wrapped block
79
- * alone exceeds the budget in that case the focused option is rendered
80
- * fully with zero siblings.
144
+ * `maxVisibleRows` is a hard viewport budget for every rendered option-list
145
+ * row. Surrounding options shrink first; if the focused option alone would
146
+ * exceed the remaining budget, it is compacted to contextual rows plus an
147
+ * omitted-rows marker so controls stay reachable for untrusted long labels.
81
148
  */
82
149
  class FocusAwareList extends Container {
83
150
  #options: string[] = [];
@@ -116,19 +183,20 @@ class FocusAwareList extends Container {
116
183
  theme.fg("accent", t),
117
184
  );
118
185
  const focusedWrappedSegments = wrapTextWithAnsi(focusedLabel, availableLabelWidth);
119
- const focusedRows = Math.max(1, focusedWrappedSegments.length);
120
186
 
121
- // Decide whether the position marker is going to be shown. We make a
122
- // pessimistic first pass assuming the marker is needed; if the window
123
- // ends up covering every option we drop it.
187
+ // Reserve one row for the option position marker only when the focused
188
+ // block itself must be compacted. Moderate focused labels keep the legacy
189
+ // wrap-focused behavior and spend the full viewport on label context.
124
190
  const totalOptions = this.#options.length;
125
- const willHaveSiblings = totalOptions > 1;
126
- const wouldNeedMarker = willHaveSiblings; // tentative; refined below
127
- const markerSlot = wouldNeedMarker ? 1 : 0;
191
+ const mustCompactFocused = focusedWrappedSegments.length > this.#maxVisibleRows;
192
+ const positionMarkerSlot = mustCompactFocused && totalOptions > 1 ? 1 : 0;
193
+ const focusedBudget = Math.max(1, this.#maxVisibleRows - positionMarkerSlot);
194
+ const focusedSegments = this.#capFocusedSegments(focusedWrappedSegments, focusedBudget, availableLabelWidth);
195
+ const focusedRows = Math.max(1, focusedSegments.length);
128
196
 
129
- // Sibling budget. If the focused block alone is over budget, render it
130
- // fully with zero siblings (only allowed overflow exception).
131
- const siblingBudget = Math.max(0, this.#maxVisibleRows - focusedRows - markerSlot);
197
+ // Sibling budget. If the focused block consumes the available viewport,
198
+ // render it with zero siblings and the reserved position marker.
199
+ const siblingBudget = Math.max(0, this.#maxVisibleRows - focusedRows - positionMarkerSlot);
132
200
 
133
201
  // Distribute sibling slots around focus, preferring closest options.
134
202
  const availableAbove = this.#selectedIndex;
@@ -151,8 +219,8 @@ class FocusAwareList extends Container {
151
219
  if (i === this.#selectedIndex) {
152
220
  // Emit focused wrapped rows. Cursor only on row 0; continuation
153
221
  // rows are whitespace-aligned under the label start.
154
- for (let r = 0; r < focusedWrappedSegments.length; r++) {
155
- const segment = focusedWrappedSegments[r] ?? "";
222
+ for (let r = 0; r < focusedSegments.length; r++) {
223
+ const segment = focusedSegments[r] ?? "";
156
224
  rows.push(r === 0 ? styledSelectedPrefix + segment : continuationPrefix + segment);
157
225
  }
158
226
  } else {
@@ -165,13 +233,33 @@ class FocusAwareList extends Container {
165
233
  }
166
234
  }
167
235
 
168
- if (showMarker) {
236
+ if (showMarker && rows.length < this.#maxVisibleRows) {
169
237
  rows.push(theme.fg("dim", ` (${this.#selectedIndex + 1}/${totalOptions})`));
170
238
  }
171
239
 
172
240
  return this.#outline ? this.#wrapOutline(rows, width) : rows;
173
241
  }
174
242
 
243
+ #capFocusedSegments(segments: string[], maxRows: number, availableLabelWidth: number): string[] {
244
+ const rows = segments.length > 0 ? segments : [""];
245
+ const budget = Math.max(1, Math.floor(maxRows));
246
+ if (rows.length <= budget) return rows;
247
+
248
+ if (budget === 1) {
249
+ return [truncateToWidth(`… ${rows.length - 1} wrapped rows omitted …`, availableLabelWidth)];
250
+ }
251
+
252
+ if (budget === 2) {
253
+ return [rows[0] ?? "", truncateToWidth(`… ${rows.length - 1} wrapped rows omitted …`, availableLabelWidth)];
254
+ }
255
+
256
+ const tailRows = Math.max(1, Math.floor((budget - 2) / 2));
257
+ const headRows = Math.max(1, budget - 1 - tailRows);
258
+ const omittedRows = Math.max(1, rows.length - headRows - tailRows);
259
+ const marker = truncateToWidth(`… ${omittedRows} wrapped rows omitted …`, availableLabelWidth);
260
+ return [...rows.slice(0, headRows), marker, ...rows.slice(rows.length - tailRows)];
261
+ }
262
+
175
263
  #wrapOutline(rows: string[], width: number): string[] {
176
264
  // Mirror the outline border drawn by `OutlinedList.render(width)`. The
177
265
  // rows passed in are already constrained to `innerWidth` by
@@ -199,7 +287,8 @@ export class HookSelectorComponent extends Container {
199
287
  #focusAwareList: FocusAwareList | undefined;
200
288
  #onSelectCallback: (option: string) => void;
201
289
  #onCancelCallback: () => void;
202
- #titleComponent: Markdown;
290
+ #titleComponent: Markdown | ScrollableTitle;
291
+ #scrollableTitle: ScrollableTitle | undefined;
203
292
  #baseTitle: string;
204
293
  #countdown: CountdownTimer | undefined;
205
294
  #onLeftCallback: (() => void) | undefined;
@@ -207,6 +296,7 @@ export class HookSelectorComponent extends Container {
207
296
  #onExternalEditorCallback: (() => void) | undefined;
208
297
  #wrapFocused: boolean;
209
298
  #outline: boolean;
299
+ #scrollTitleRows: number | undefined;
210
300
  constructor(
211
301
  title: string,
212
302
  options: string[],
@@ -231,7 +321,15 @@ export class HookSelectorComponent extends Container {
231
321
  this.addChild(new DynamicBorder());
232
322
  this.addChild(new Spacer(1));
233
323
 
234
- this.#titleComponent = new Markdown(title, 1, 0, getMarkdownTheme(), { color: t => theme.fg("accent", t) });
324
+ const scrollTitleRows =
325
+ opts?.scrollTitleRows === undefined ? undefined : Math.max(1, Math.floor(opts.scrollTitleRows));
326
+ this.#scrollTitleRows = scrollTitleRows;
327
+ if (scrollTitleRows === undefined) {
328
+ this.#titleComponent = new Markdown(title, 1, 0, getMarkdownTheme(), { color: t => theme.fg("accent", t) });
329
+ } else {
330
+ this.#scrollableTitle = new ScrollableTitle(title, scrollTitleRows);
331
+ this.#titleComponent = this.#scrollableTitle;
332
+ }
235
333
  this.addChild(this.#titleComponent);
236
334
  this.addChild(new Spacer(1));
237
335
 
@@ -319,6 +417,21 @@ export class HookSelectorComponent extends Container {
319
417
  // Reset countdown on any interaction
320
418
  this.#countdown?.reset();
321
419
 
420
+ if (this.#scrollTitleRows !== undefined) {
421
+ const wheelRows = getMouseWheelTitleScrollRows(keyData);
422
+ if (wheelRows !== 0) {
423
+ this.#scrollableTitle?.scrollBy(wheelRows);
424
+ return;
425
+ }
426
+ }
427
+ if (this.#scrollTitleRows !== undefined && matchesKey(keyData, "pageUp")) {
428
+ this.#scrollableTitle?.scrollBy(-this.#scrollTitleRows);
429
+ return;
430
+ }
431
+ if (this.#scrollTitleRows !== undefined && matchesKey(keyData, "pageDown")) {
432
+ this.#scrollableTitle?.scrollBy(this.#scrollTitleRows);
433
+ return;
434
+ }
322
435
  if (matchesKey(keyData, "up") || keyData === "k") {
323
436
  this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
324
437
  this.#updateList();