@gajae-code/coding-agent 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +7 -0
  4. package/dist/types/cli/args.d.ts +3 -1
  5. package/dist/types/commands/deep-interview.d.ts +3 -0
  6. package/dist/types/commands/launch.d.ts +6 -0
  7. package/dist/types/config/keybindings.d.ts +5 -0
  8. package/dist/types/config/model-profile-activation.d.ts +30 -0
  9. package/dist/types/config/model-profiles.d.ts +19 -0
  10. package/dist/types/config/model-registry.d.ts +8 -0
  11. package/dist/types/config/model-resolver.d.ts +1 -1
  12. package/dist/types/config/models-config-schema.d.ts +47 -0
  13. package/dist/types/config/settings-schema.d.ts +14 -4
  14. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  15. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  16. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  17. package/dist/types/eval/py/executor.d.ts +2 -0
  18. package/dist/types/eval/py/kernel.d.ts +2 -0
  19. package/dist/types/exec/bash-executor.d.ts +10 -0
  20. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  21. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  22. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  23. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  24. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  25. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -1
  26. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  27. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  28. package/dist/types/hooks/skill-state.d.ts +21 -0
  29. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  30. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  31. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  32. package/dist/types/internal-urls/types.d.ts +4 -0
  33. package/dist/types/lsp/index.d.ts +10 -10
  34. package/dist/types/main.d.ts +10 -1
  35. package/dist/types/modes/bridge/auth.d.ts +12 -0
  36. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  37. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  38. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  39. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  41. package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  43. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  44. package/dist/types/modes/components/model-selector.d.ts +6 -1
  45. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  46. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  47. package/dist/types/modes/components/status-line.d.ts +2 -0
  48. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  49. package/dist/types/modes/controllers/selector-controller.d.ts +9 -0
  50. package/dist/types/modes/index.d.ts +1 -0
  51. package/dist/types/modes/interactive-mode.d.ts +1 -0
  52. package/dist/types/modes/jobs-observer.d.ts +57 -0
  53. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  54. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  55. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  56. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  57. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  58. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  59. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  60. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  61. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  62. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  63. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  64. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  65. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  66. package/dist/types/modes/types.d.ts +2 -0
  67. package/dist/types/sdk.d.ts +3 -1
  68. package/dist/types/session/agent-session.d.ts +11 -1
  69. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  70. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  71. package/dist/types/task/executor.d.ts +1 -0
  72. package/dist/types/task/id.d.ts +7 -0
  73. package/dist/types/task/index.d.ts +5 -0
  74. package/dist/types/task/receipt.d.ts +85 -0
  75. package/dist/types/task/spawn-gate.d.ts +38 -0
  76. package/dist/types/task/types.d.ts +143 -11
  77. package/dist/types/tools/cron.d.ts +6 -0
  78. package/dist/types/tools/hindsight-recall.d.ts +0 -2
  79. package/dist/types/tools/hindsight-reflect.d.ts +0 -2
  80. package/dist/types/tools/hindsight-retain.d.ts +0 -2
  81. package/dist/types/tools/index.d.ts +6 -4
  82. package/dist/types/tools/path-utils.d.ts +1 -0
  83. package/dist/types/tools/subagent.d.ts +15 -0
  84. package/package.json +7 -7
  85. package/scripts/build-binary.ts +7 -0
  86. package/src/async/job-manager.ts +36 -0
  87. package/src/cli/args.ts +19 -2
  88. package/src/commands/deep-interview.ts +1 -0
  89. package/src/commands/harness.ts +289 -19
  90. package/src/commands/launch.ts +10 -2
  91. package/src/commands/state.ts +2 -1
  92. package/src/commands/team.ts +22 -4
  93. package/src/config/keybindings.ts +6 -0
  94. package/src/config/model-profile-activation.ts +157 -0
  95. package/src/config/model-profiles.ts +155 -0
  96. package/src/config/model-registry.ts +19 -0
  97. package/src/config/model-resolver.ts +3 -2
  98. package/src/config/models-config-schema.ts +36 -0
  99. package/src/config/settings-schema.ts +16 -3
  100. package/src/dap/client.ts +17 -3
  101. package/src/debug/crash-diagnostics.ts +223 -0
  102. package/src/debug/runtime-gauges.ts +20 -0
  103. package/src/deep-interview/render-middleware.ts +6 -0
  104. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  105. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  106. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -3
  107. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
  108. package/src/defaults/gjc-defaults.ts +7 -0
  109. package/src/eval/py/executor.ts +21 -1
  110. package/src/eval/py/kernel.ts +15 -0
  111. package/src/exec/bash-executor.ts +41 -0
  112. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  113. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  114. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  115. package/src/gjc-runtime/state-migrations.ts +54 -7
  116. package/src/gjc-runtime/state-runtime.ts +461 -64
  117. package/src/gjc-runtime/state-schema.ts +192 -0
  118. package/src/gjc-runtime/state-writer.ts +32 -1
  119. package/src/gjc-runtime/team-runtime.ts +177 -105
  120. package/src/gjc-runtime/ultragoal-runtime.ts +231 -38
  121. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  122. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  123. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  124. package/src/harness-control-plane/control-endpoint.ts +19 -8
  125. package/src/harness-control-plane/owner.ts +57 -10
  126. package/src/harness-control-plane/state-machine.ts +2 -1
  127. package/src/hooks/skill-state.ts +176 -26
  128. package/src/internal-urls/agent-protocol.ts +68 -21
  129. package/src/internal-urls/artifact-protocol.ts +12 -17
  130. package/src/internal-urls/docs-index.generated.ts +8 -10
  131. package/src/internal-urls/registry-helpers.ts +19 -16
  132. package/src/internal-urls/types.ts +4 -0
  133. package/src/lsp/client.ts +18 -2
  134. package/src/main.ts +88 -6
  135. package/src/modes/bridge/auth.ts +41 -0
  136. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  137. package/src/modes/bridge/bridge-mode.ts +520 -0
  138. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  139. package/src/modes/bridge/event-stream.ts +70 -0
  140. package/src/modes/components/custom-editor.ts +101 -0
  141. package/src/modes/components/custom-provider-wizard.ts +318 -0
  142. package/src/modes/components/hook-selector.ts +61 -18
  143. package/src/modes/components/jobs-overlay-model.ts +109 -0
  144. package/src/modes/components/jobs-overlay.ts +172 -0
  145. package/src/modes/components/model-selector.ts +108 -18
  146. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  147. package/src/modes/components/status-line/presets.ts +7 -5
  148. package/src/modes/components/status-line/segments.ts +25 -0
  149. package/src/modes/components/status-line/types.ts +2 -0
  150. package/src/modes/components/status-line.ts +9 -1
  151. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  152. package/src/modes/controllers/input-controller.ts +97 -9
  153. package/src/modes/controllers/selector-controller.ts +86 -1
  154. package/src/modes/index.ts +1 -0
  155. package/src/modes/interactive-mode.ts +27 -0
  156. package/src/modes/jobs-observer.ts +204 -0
  157. package/src/modes/rpc/host-tools.ts +1 -186
  158. package/src/modes/rpc/host-uris.ts +1 -235
  159. package/src/modes/rpc/rpc-client.ts +25 -10
  160. package/src/modes/rpc/rpc-mode.ts +12 -381
  161. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  162. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  163. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  164. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  165. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  166. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  167. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  168. package/src/modes/shared/agent-wire/responses.ts +17 -0
  169. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  170. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  171. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  172. package/src/modes/types.ts +2 -0
  173. package/src/prompts/memories/consolidation.md +1 -1
  174. package/src/prompts/memories/read-path.md +6 -7
  175. package/src/prompts/memories/unavailable.md +2 -2
  176. package/src/prompts/tools/bash.md +1 -1
  177. package/src/prompts/tools/irc.md +1 -1
  178. package/src/prompts/tools/read.md +2 -2
  179. package/src/prompts/tools/recall.md +1 -0
  180. package/src/prompts/tools/reflect.md +1 -0
  181. package/src/prompts/tools/retain.md +1 -0
  182. package/src/prompts/tools/subagent.md +12 -7
  183. package/src/prompts/tools/task-summary.md +3 -9
  184. package/src/prompts/tools/task.md +5 -1
  185. package/src/sdk.ts +5 -1
  186. package/src/session/agent-session.ts +214 -38
  187. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  188. package/src/skill-state/workflow-state-contract.ts +7 -4
  189. package/src/skill-state/workflow-state-version.ts +3 -0
  190. package/src/slash-commands/builtin-registry.ts +9 -1
  191. package/src/task/executor.ts +31 -5
  192. package/src/task/id.ts +33 -0
  193. package/src/task/index.ts +259 -67
  194. package/src/task/output-manager.ts +5 -4
  195. package/src/task/receipt.ts +297 -0
  196. package/src/task/render.ts +48 -131
  197. package/src/task/spawn-gate.ts +132 -0
  198. package/src/task/types.ts +48 -7
  199. package/src/tools/ask.ts +73 -33
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash.ts +1 -1
  203. package/src/tools/cron.ts +48 -0
  204. package/src/tools/find.ts +4 -1
  205. package/src/tools/hindsight-recall.ts +0 -2
  206. package/src/tools/hindsight-reflect.ts +0 -2
  207. package/src/tools/hindsight-retain.ts +0 -2
  208. package/src/tools/index.ts +6 -18
  209. package/src/tools/path-utils.ts +3 -2
  210. package/src/tools/read.ts +4 -3
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/skill.ts +6 -1
  213. package/src/tools/subagent.ts +237 -84
@@ -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
+ }
@@ -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();