@gajae-code/coding-agent 0.11.3 → 0.11.4

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 (82) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/types/cli/worktree-cli.d.ts +8 -25
  3. package/dist/types/commands/gc.d.ts +4 -0
  4. package/dist/types/commands/worktree.d.ts +7 -32
  5. package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
  6. package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
  7. package/dist/types/modes/components/custom-editor.d.ts +8 -3
  8. package/dist/types/modes/components/tool-transcript-format.d.ts +19 -0
  9. package/dist/types/modes/controllers/input-controller.d.ts +8 -2
  10. package/dist/types/sdk/broker/broker.d.ts +1 -0
  11. package/dist/types/sdk/broker/discovery.d.ts +10 -0
  12. package/dist/types/sdk/broker/ensure.d.ts +24 -1
  13. package/dist/types/sdk/broker/internal.d.ts +3 -0
  14. package/dist/types/sdk/broker/session-index.d.ts +12 -0
  15. package/dist/types/sdk/broker/state-version.d.ts +2 -1
  16. package/dist/types/tools/ask.d.ts +39 -12
  17. package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
  18. package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
  19. package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
  20. package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
  21. package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
  22. package/dist/types/tools/path-utils.d.ts +9 -14
  23. package/dist/types/tools/render-utils.d.ts +1 -8
  24. package/dist/types/tools/resource-gc.d.ts +14 -0
  25. package/dist/types/utils/image-loading.d.ts +23 -0
  26. package/dist/types/utils/image-resize.d.ts +2 -0
  27. package/dist/types/utils/pasted-image-loading.d.ts +39 -0
  28. package/dist/types/utils/pasted-image-path.d.ts +21 -19
  29. package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
  30. package/dist/types/web/scrapers/types.d.ts +1 -5
  31. package/package.json +24 -8
  32. package/scripts/run-threat-model-check.ts +60 -0
  33. package/src/cli/worktree-cli.ts +9 -287
  34. package/src/cli.ts +10 -9
  35. package/src/commands/gc.ts +11 -1
  36. package/src/commands/sdk.ts +3 -2
  37. package/src/commands/worktree.ts +10 -53
  38. package/src/config/model-profiles.ts +15 -15
  39. package/src/eval/js/shared/runtime.ts +1 -1
  40. package/src/gjc-runtime/gc-render.ts +19 -1
  41. package/src/gjc-runtime/gc-runtime.ts +68 -3
  42. package/src/gjc-runtime/state-argv.ts +292 -0
  43. package/src/gjc-runtime/state-runtime.ts +42 -210
  44. package/src/modes/components/custom-editor.ts +103 -32
  45. package/src/modes/components/custom-provider-wizard.ts +26 -1
  46. package/src/modes/components/session-observer-overlay.ts +10 -23
  47. package/src/modes/components/tool-transcript-format.ts +72 -0
  48. package/src/modes/components/transcript-viewer-overlay.ts +42 -2
  49. package/src/modes/controllers/input-controller.ts +98 -37
  50. package/src/modes/controllers/selector-controller.ts +1 -1
  51. package/src/modes/interactive-mode.ts +13 -2
  52. package/src/sdk/broker/broker.ts +139 -32
  53. package/src/sdk/broker/discovery.ts +125 -0
  54. package/src/sdk/broker/ensure.ts +103 -19
  55. package/src/sdk/broker/internal.ts +10 -0
  56. package/src/sdk/broker/session-index.ts +228 -15
  57. package/src/sdk/broker/state-version.ts +5 -2
  58. package/src/sdk/bus/telegram-daemon.ts +50 -10
  59. package/src/session/internal/managed-session-scope.ts +15 -1
  60. package/src/tools/ask.ts +216 -41
  61. package/src/tools/bash-allowed-prefixes.ts +79 -29
  62. package/src/tools/browser/launch.ts +3 -1
  63. package/src/tools/browser/readable.ts +1 -1
  64. package/src/tools/browser/screenshot-format.ts +34 -0
  65. package/src/tools/browser/tab-protocol.ts +3 -0
  66. package/src/tools/browser/tab-supervisor.ts +125 -122
  67. package/src/tools/browser/tab-worker-entry.ts +33 -14
  68. package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
  69. package/src/tools/browser/tab-worker-smoke.ts +87 -0
  70. package/src/tools/browser/tab-worker.ts +7 -5
  71. package/src/tools/path-utils.ts +42 -35
  72. package/src/tools/read.ts +7 -1
  73. package/src/tools/render-utils.ts +1 -27
  74. package/src/tools/resource-gc.ts +123 -34
  75. package/src/utils/image-loading.ts +85 -34
  76. package/src/utils/image-resize.ts +16 -9
  77. package/src/utils/pasted-image-loading.ts +514 -0
  78. package/src/utils/pasted-image-path.ts +223 -98
  79. package/src/web/scrapers/html-to-markdown.ts +68 -0
  80. package/src/web/scrapers/readthedocs.ts +1 -1
  81. package/src/web/scrapers/types.ts +1 -68
  82. package/src/web/search/providers/openai-compatible.ts +76 -23
@@ -42,6 +42,7 @@ import {
42
42
  SessionResolutionError,
43
43
  writeSessionActivityMarker,
44
44
  } from "./session-resolution";
45
+ import { classifyStateArgv, firstStateFlagValue, type StateAction, type StateArgvClassification } from "./state-argv";
45
46
  import { renderStateGraph, type StateGraphFormat } from "./state-graph";
46
47
  import { migrateAndPersistLegacyState, migrateWorkflowState } from "./state-migrations";
47
48
  import {
@@ -72,7 +73,7 @@ import {
72
73
  writeGuardedWorkflowEnvelopeAtomic,
73
74
  } from "./state-writer";
74
75
  import { assertSafePathComponent, CommandError, flagValue, hasFlag, isPlainObject } from "./workflow-cli-common";
75
- import { getSkillManifest, isKnownWorkflowState, isValidTransition, typedArgsFor } from "./workflow-manifest";
76
+ import { getSkillManifest, isKnownWorkflowState, isValidTransition } from "./workflow-manifest";
76
77
 
77
78
  /**
78
79
  * Native implementation of the `gjc state read|write|clear` command surface.
@@ -99,136 +100,9 @@ class StateCommandError extends CommandError {
99
100
  }
100
101
 
101
102
  const GRAPH_FORMATS = new Set(["ascii", "mermaid", "dot"]);
102
- const FLAGS_WITH_VALUES = new Set([
103
- "--input",
104
- "--mode",
105
- "--session-id",
106
- "--thread-id",
107
- "--turn-id",
108
- "--to",
109
- "--skill",
110
- "--format",
111
- "--older-than",
112
- "--status",
113
- "--fields",
114
- "--since",
115
- "--limit",
116
- ]);
117
- const ACTION_NAMES = new Set([
118
- "read",
119
- "write",
120
- "clear",
121
- "contract",
122
- "handoff",
123
- "graph",
124
- "prune",
125
- "gc",
126
- "migrate",
127
- "status",
128
- "doctor",
129
- ]);
130
- const BOOLEAN_FLAGS = new Set([
131
- "--json",
132
- "--replace",
133
- "--hard",
134
- "--dry-run",
135
- "--migrate",
136
- "--compact",
137
- "--history",
138
- "--force",
139
- ]);
140
- const VERB_SPECIFIC_FLAGS = new Set([
141
- "--skill",
142
- "--format",
143
- "--older-than",
144
- "--status",
145
- "--fields",
146
- "--since",
147
- "--limit",
148
- "--history",
149
- ]);
150
-
151
- function flagName(arg: string): string | undefined {
152
- if (!arg.startsWith("--")) return undefined;
153
- const equalsIndex = arg.indexOf("=");
154
- return equalsIndex >= 0 ? arg.slice(0, equalsIndex) : arg;
155
- }
156
-
157
- function manifestFlagNames(action: ParsedInvocation["action"], positionalSkill: string | undefined): Set<string> {
158
- const names = new Set<string>();
159
- const skills =
160
- positionalSkill && KNOWN_MODES.includes(positionalSkill)
161
- ? [positionalSkill as CanonicalGjcWorkflowSkill]
162
- : CANONICAL_GJC_WORKFLOW_SKILLS;
163
- for (const skill of skills) {
164
- for (const arg of typedArgsFor(skill, action)) names.add(`--${arg.name}`);
165
- }
166
- return names;
167
- }
168
-
169
- function assertKnownFlags(args: readonly string[], parsed: ParsedInvocation): void {
170
- const manifestFlags = manifestFlagNames(parsed.action, parsed.positionalSkill);
171
- for (const arg of args) {
172
- const flag = flagName(arg);
173
- if (!flag) continue;
174
- if (
175
- FLAGS_WITH_VALUES.has(flag) ||
176
- BOOLEAN_FLAGS.has(flag) ||
177
- VERB_SPECIFIC_FLAGS.has(flag) ||
178
- manifestFlags.has(flag)
179
- ) {
180
- continue;
181
- }
182
- throw new StateCommandError(2, `unknown gjc state flag: ${flag}`);
183
- }
184
- }
185
-
186
- interface ParsedInvocation {
187
- action:
188
- | "read"
189
- | "write"
190
- | "clear"
191
- | "contract"
192
- | "handoff"
193
- | "graph"
194
- | "prune"
195
- | "gc"
196
- | "migrate"
197
- | "status"
198
- | "doctor";
199
- positionalSkill?: string;
200
- }
201
-
202
- function parsePositionalArgs(args: readonly string[]): ParsedInvocation {
203
- let skipNext = false;
204
- const positional: string[] = [];
205
- for (const arg of args) {
206
- if (skipNext) {
207
- skipNext = false;
208
- continue;
209
- }
210
- if (FLAGS_WITH_VALUES.has(arg)) {
211
- skipNext = true;
212
- continue;
213
- }
214
- if (!arg.startsWith("-")) positional.push(arg);
215
- }
216
- // Documented argv shapes:
217
- // gjc state read|write|clear|contract ...
218
- // gjc state <skill> read|write|contract ...
219
- const first = positional[0];
220
- const second = positional[1];
221
- if (first && ACTION_NAMES.has(first)) {
222
- return { action: first as ParsedInvocation["action"], positionalSkill: second };
223
- }
224
- if (first && second && ACTION_NAMES.has(second)) {
225
- return { action: second as ParsedInvocation["action"], positionalSkill: first };
226
- }
227
- // `gjc state <skill>` alone defaults to read for that skill.
228
- if (first && !second) {
229
- return { action: "read", positionalSkill: first };
230
- }
231
- return { action: "read" };
103
+ function assertKnownFlags(classification: StateArgvClassification): void {
104
+ const [unknownFlag] = classification.unknownFlags;
105
+ if (unknownFlag) throw new StateCommandError(2, `unknown gjc state flag: ${unknownFlag}`);
232
106
  }
233
107
 
234
108
  function isKnownMode(mode: string): mode is CanonicalGjcWorkflowSkill {
@@ -279,19 +153,16 @@ interface ResolvedSelectors {
279
153
  // `clear` resolves like a read (explicit -> payload -> env -> latest-activity marker)
280
154
  // per the spec: read/status/clear may fall back to the most-recent session. Commands
281
155
  // that create or mutate new state roots still require an explicit/env session id.
282
- const WRITE_SESSION_ACTIONS = new Set<ParsedInvocation["action"]>(["write", "handoff", "prune", "migrate"]);
156
+ const WRITE_SESSION_ACTIONS = new Set<StateAction>(["write", "handoff", "prune", "migrate"]);
283
157
 
284
- async function resolveSelectors(
285
- args: readonly string[],
286
- cwd: string,
287
- positionalSkill: string | undefined,
288
- action: ParsedInvocation["action"],
289
- ): Promise<ResolvedSelectors> {
290
- const payload = await readInputJson(flagValue(args, "--input"), cwd);
158
+ async function resolveSelectors(args: readonly string[], cwd: string, action: StateAction): Promise<ResolvedSelectors> {
159
+ const classification = classifyStateArgv(args);
160
+ const payload = await readInputJson(firstStateFlagValue(classification, "--input"), cwd);
291
161
 
162
+ const [modeCandidate, positionalCandidate] = classification.runtimeSelectorCandidates;
292
163
  const candidates: Array<string | undefined> = [
293
- flagValue(args, "--mode")?.trim() || undefined,
294
- positionalSkill?.trim() || undefined,
164
+ modeCandidate?.value,
165
+ positionalCandidate?.value,
295
166
  typeof payload?.mode === "string" ? (payload.mode as string).trim() || undefined : undefined,
296
167
  typeof payload?.skill === "string" ? (payload.skill as string).trim() || undefined : undefined,
297
168
  ];
@@ -1232,12 +1103,8 @@ export async function readWorkflowStateJson(
1232
1103
  return (await readJsonFile(modeStateFile(cwd, skill, session.gjcSessionId))) ?? {};
1233
1104
  }
1234
1105
 
1235
- async function handleRead(
1236
- args: readonly string[],
1237
- cwd: string,
1238
- positionalSkill: string | undefined,
1239
- ): Promise<StateCommandResult> {
1240
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
1106
+ async function handleRead(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1107
+ const selectors = await resolveSelectors(args, cwd, "read");
1241
1108
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
1242
1109
  const fields = parseFieldsFlag(args);
1243
1110
  if (mode) {
@@ -1276,12 +1143,8 @@ async function handleRead(
1276
1143
  return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n` };
1277
1144
  }
1278
1145
 
1279
- async function handleStatus(
1280
- args: readonly string[],
1281
- cwd: string,
1282
- positionalSkill: string | undefined,
1283
- ): Promise<StateCommandResult> {
1284
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
1146
+ async function handleStatus(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1147
+ const selectors = await resolveSelectors(args, cwd, "read");
1285
1148
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
1286
1149
  if (!mode) {
1287
1150
  throw new StateCommandError(
@@ -1303,12 +1166,8 @@ async function handleStatus(
1303
1166
  };
1304
1167
  }
1305
1168
 
1306
- async function handleWrite(
1307
- args: readonly string[],
1308
- cwd: string,
1309
- positionalSkill: string | undefined,
1310
- ): Promise<StateCommandResult> {
1311
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "write");
1169
+ async function handleWrite(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1170
+ const selectors = await resolveSelectors(args, cwd, "write");
1312
1171
  const { gjcSessionId: sessionId, threadId, turnId, payload } = selectors;
1313
1172
  if (!payload) throw new StateCommandError(2, "gjc state write requires --input '<json>'");
1314
1173
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
@@ -1465,12 +1324,8 @@ async function handleWrite(
1465
1324
  );
1466
1325
  }
1467
1326
 
1468
- async function handleClear(
1469
- args: readonly string[],
1470
- cwd: string,
1471
- positionalSkill: string | undefined,
1472
- ): Promise<StateCommandResult> {
1473
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "clear");
1327
+ async function handleClear(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1328
+ const selectors = await resolveSelectors(args, cwd, "clear");
1474
1329
  const { gjcSessionId: sessionId, threadId, turnId } = selectors;
1475
1330
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
1476
1331
  if (!mode)
@@ -1631,12 +1486,8 @@ async function assertDeepInterviewHandoffReady(state: Record<string, unknown>):
1631
1486
  * the phase remains in `skill-active-state.json` until a chain call (or
1632
1487
  * explicit `clear`) demotes it.
1633
1488
  */
1634
- async function handleHandoffUnlocked(
1635
- args: readonly string[],
1636
- cwd: string,
1637
- positionalSkill: string | undefined,
1638
- ): Promise<StateCommandResult> {
1639
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "handoff");
1489
+ async function handleHandoffUnlocked(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1490
+ const selectors = await resolveSelectors(args, cwd, "handoff");
1640
1491
  const { gjcSessionId: sessionId, threadId, turnId } = selectors;
1641
1492
  const caller = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
1642
1493
  if (!caller) {
@@ -1944,12 +1795,8 @@ async function handleHandoffUnlocked(
1944
1795
  };
1945
1796
  }
1946
1797
 
1947
- async function handleHandoff(
1948
- args: readonly string[],
1949
- cwd: string,
1950
- positionalSkill: string | undefined,
1951
- ): Promise<StateCommandResult> {
1952
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "handoff");
1798
+ async function handleHandoff(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1799
+ const selectors = await resolveSelectors(args, cwd, "handoff");
1953
1800
  // Serialize concurrent handoffs on a dedicated sentinel lock, NOT on the
1954
1801
  // derived `skill-active-state.json` cache. The inner transaction
1955
1802
  // (applyHandoffToActiveState / syncSkillActiveState -> rebuildActiveSnapshot)
@@ -1959,15 +1806,11 @@ async function handleHandoff(
1959
1806
  // `{ cwd }` so the sentinel resolves against the handoff cwd rather than
1960
1807
  // `process.cwd()`.
1961
1808
  const handoffLock = path.join(sessionStateDir(cwd, selectors.gjcSessionId), "handoff");
1962
- return withWorkflowStateLock(handoffLock, async () => handleHandoffUnlocked(args, cwd, positionalSkill), { cwd });
1809
+ return withWorkflowStateLock(handoffLock, async () => handleHandoffUnlocked(args, cwd), { cwd });
1963
1810
  }
1964
1811
 
1965
- async function handleContract(
1966
- args: readonly string[],
1967
- cwd: string,
1968
- positionalSkill: string | undefined,
1969
- ): Promise<StateCommandResult> {
1970
- const { mode } = await resolveSelectors(args, cwd, positionalSkill, "read");
1812
+ async function handleContract(args: readonly string[], cwd: string): Promise<StateCommandResult> {
1813
+ const { mode } = await resolveSelectors(args, cwd, "read");
1971
1814
  if (!mode) {
1972
1815
  throw new StateCommandError(2, "gjc state contract requires --mode <skill>, positional <skill>, or input.skill");
1973
1816
  }
@@ -2195,12 +2038,8 @@ async function handleGraph(
2195
2038
  };
2196
2039
  }
2197
2040
 
2198
- async function handlePrune(
2199
- args: readonly string[],
2200
- cwd: string,
2201
- positionalSkill: string | undefined,
2202
- ): Promise<StateCommandResult> {
2203
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "prune");
2041
+ async function handlePrune(args: readonly string[], cwd: string): Promise<StateCommandResult> {
2042
+ const selectors = await resolveSelectors(args, cwd, "prune");
2204
2043
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
2205
2044
  if (!mode) {
2206
2045
  throw new StateCommandError(
@@ -2265,12 +2104,8 @@ async function handleGc(
2265
2104
  return { status: 0, stdout: `${JSON.stringify(summary, null, 2)}\n` };
2266
2105
  }
2267
2106
 
2268
- async function handleMigrate(
2269
- args: readonly string[],
2270
- cwd: string,
2271
- positionalSkill: string | undefined,
2272
- ): Promise<StateCommandResult> {
2273
- const selectors = await resolveSelectors(args, cwd, positionalSkill, "migrate");
2107
+ async function handleMigrate(args: readonly string[], cwd: string): Promise<StateCommandResult> {
2108
+ const selectors = await resolveSelectors(args, cwd, "migrate");
2274
2109
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
2275
2110
  if (!mode) {
2276
2111
  throw new StateCommandError(
@@ -2301,34 +2136,31 @@ async function handleMigrate(
2301
2136
 
2302
2137
  export async function runNativeStateCommand(args: string[], cwd = process.cwd()): Promise<StateCommandResult> {
2303
2138
  try {
2304
- const parsed = parsePositionalArgs(args);
2305
- assertKnownFlags(args, parsed);
2306
- switch (parsed.action) {
2139
+ const parsed = classifyStateArgv(args);
2140
+ assertKnownFlags(parsed);
2141
+ switch (parsed.effectiveAction) {
2307
2142
  case "read":
2308
- if (hasFlag(args, "--migrate")) return await handleMigrate(args, cwd, parsed.positionalSkill);
2309
- return await handleRead(args, cwd, parsed.positionalSkill);
2143
+ return await handleRead(args, cwd);
2310
2144
  case "write":
2311
- return await handleWrite(args, cwd, parsed.positionalSkill);
2145
+ return await handleWrite(args, cwd);
2312
2146
  case "clear":
2313
- return await handleClear(args, cwd, parsed.positionalSkill);
2147
+ return await handleClear(args, cwd);
2314
2148
  case "contract":
2315
- return await handleContract(args, cwd, parsed.positionalSkill);
2149
+ return await handleContract(args, cwd);
2316
2150
  case "status":
2317
- return await handleStatus(args, cwd, parsed.positionalSkill);
2151
+ return await handleStatus(args, cwd);
2318
2152
  case "doctor":
2319
2153
  return await handleDoctor(args, cwd, parsed.positionalSkill);
2320
2154
  case "handoff":
2321
- return await handleHandoff(args, cwd, parsed.positionalSkill);
2155
+ return await handleHandoff(args, cwd);
2322
2156
  case "graph":
2323
2157
  return await handleGraph(args, cwd, parsed.positionalSkill);
2324
2158
  case "prune":
2325
- return await handlePrune(args, cwd, parsed.positionalSkill);
2159
+ return await handlePrune(args, cwd);
2326
2160
  case "gc":
2327
2161
  return await handleGc(args, cwd, parsed.positionalSkill);
2328
2162
  case "migrate":
2329
- return await handleMigrate(args, cwd, parsed.positionalSkill);
2330
- default:
2331
- return { status: 2, stderr: `Unknown gjc state command: ${parsed.action}\n` };
2163
+ return await handleMigrate(args, cwd);
2332
2164
  }
2333
2165
  } catch (error) {
2334
2166
  if (error instanceof CommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
@@ -59,9 +59,22 @@ const DEFAULT_ACTION_KEYS = Object.fromEntries(
59
59
 
60
60
  const PASTE_DECISION_TIMEOUT_MS = 5_000;
61
61
  const PENDING_PASTE_INPUT_MAX = 64;
62
+ const PENDING_PASTE_INPUT_MAX_BYTES = 256 * 1024;
62
63
 
63
64
  type PastePendingClearReason = "timeout" | "queue-limit";
64
65
 
66
+ export interface PasteTextContext {
67
+ readonly signal: AbortSignal;
68
+ /** Run one synchronous composer mutation only while this paste transaction is current. */
69
+ commit(commit: () => boolean): boolean;
70
+ }
71
+
72
+ interface PendingPasteDecision {
73
+ readonly controller: AbortController;
74
+ readonly pasteContent: string;
75
+ readonly remaining: string;
76
+ }
77
+
65
78
  /**
66
79
  * Custom editor that handles configurable app-level shortcuts for coding-agent.
67
80
  */
@@ -94,9 +107,9 @@ export class CustomEditor extends Editor {
94
107
  /** Called when the configured image-paste shortcut is pressed. */
95
108
  onPasteImage?: () => Promise<boolean>;
96
109
  /** Called before bracketed paste content is inserted. Return true to consume it. */
97
- onPasteText?: (text: string) => boolean | Promise<boolean>;
98
- /** Called when async paste handling drops queued input instead of replaying it. */
99
- onPastePendingInputCleared?: (reason: PastePendingClearReason, droppedInputCount: number) => void;
110
+ onPasteText?: (text: string, context: PasteTextContext) => boolean | Promise<boolean>;
111
+ /** Called when pending paste handling is cancelled after restoring queued input. */
112
+ onPastePendingInputCleared?: (reason: PastePendingClearReason, restoredInputCount: number) => void;
100
113
  /** Called when the configured dequeue shortcut is pressed. */
101
114
  onDequeue?: () => void;
102
115
  /** Called when the configured queue shortcut is pressed. */
@@ -117,7 +130,9 @@ export class CustomEditor extends Editor {
117
130
  #pasteDecisionPending = false;
118
131
  #pasteDecisionToken = 0;
119
132
  #pasteDecisionTimeout: NodeJS.Timeout | undefined;
133
+ #pendingPasteDecision: PendingPasteDecision | undefined;
120
134
  #pendingPasteInput: string[] = [];
135
+ #pendingPasteInputBytes = 0;
121
136
 
122
137
  setActionKeys(action: ConfigurableEditorAction, keys: KeyId[]): void {
123
138
  this.#actionKeys.set(action, [...keys]);
@@ -165,75 +180,127 @@ export class CustomEditor extends Editor {
165
180
  }
166
181
  }
167
182
 
168
- #clearPendingPasteState(): number {
183
+ #resetPendingPasteState(abortReason?: Error): string[] {
169
184
  this.#clearPasteDecisionTimeout();
185
+ if (abortReason) this.#pendingPasteDecision?.controller.abort(abortReason);
170
186
  this.#pasteDecisionPending = false;
171
187
  this.#pasteDecisionToken += 1;
172
- const droppedInputCount = this.#pendingPasteInput.length;
188
+ this.#pendingPasteDecision = undefined;
189
+ const queuedInput = this.#pendingPasteInput;
173
190
  this.#pendingPasteInput = [];
174
- return droppedInputCount;
191
+ this.#pendingPasteInputBytes = 0;
192
+ return queuedInput;
193
+ }
194
+
195
+ #pendingPasteEventCount(): number {
196
+ return this.#pendingPasteInput.length + (this.#pendingPasteDecision?.remaining ? 1 : 0);
197
+ }
198
+
199
+ #restoreCancelledPaste(reason: PastePendingClearReason, overflowInput?: string): void {
200
+ const decision = this.#pendingPasteDecision;
201
+ const queuedInput = this.#resetPendingPasteState(new Error(`Paste handling cancelled: ${reason}`));
202
+ if (decision) {
203
+ super.handleInput(`\x1b[200~${decision.pasteContent}\x1b[201~`);
204
+ this.#drainPendingPasteInput(decision.remaining, queuedInput);
205
+ if (overflowInput !== undefined) this.handleInput(overflowInput);
206
+ }
207
+ this.onPastePendingInputCleared?.(
208
+ reason,
209
+ queuedInput.length + (decision?.remaining ? 1 : 0) + (overflowInput === undefined ? 0 : 1),
210
+ );
175
211
  }
176
212
 
177
213
  #startPasteDecisionTimeout(token: number): void {
178
214
  this.#clearPasteDecisionTimeout();
179
215
  this.#pasteDecisionTimeout = setTimeout(() => {
180
216
  if (token !== this.#pasteDecisionToken) return;
181
- const droppedInputCount = this.#clearPendingPasteState();
182
- this.onPastePendingInputCleared?.("timeout", droppedInputCount);
217
+ this.#restoreCancelledPaste("timeout");
183
218
  }, PASTE_DECISION_TIMEOUT_MS);
184
219
  this.#pasteDecisionTimeout.unref?.();
185
220
  }
186
221
 
187
222
  dispose(): void {
188
- this.#clearPendingPasteState();
223
+ this.#resetPendingPasteState(new Error("Editor disposed during paste handling"));
189
224
  this.#pasteHandler = new BracketedPasteHandler();
190
225
  super.dispose();
191
226
  }
192
227
 
193
- #drainPendingPasteInput(initialInput?: string): void {
228
+ #drainPendingPasteInput(initialInput: string | undefined, queuedInput: readonly string[]): void {
194
229
  if (initialInput && initialInput.length > 0) {
195
230
  this.handleInput(initialInput);
196
231
  }
197
- while (!this.#pasteDecisionPending) {
198
- const nextInput = this.#pendingPasteInput.shift();
199
- if (nextInput === undefined) break;
200
- this.handleInput(nextInput);
232
+ for (const input of queuedInput) {
233
+ this.handleInput(input);
201
234
  }
202
235
  }
203
236
 
204
237
  #handleBracketedPaste(pasteContent: string, remaining: string): void {
205
- const applyPasteResult = (token: number, handled: boolean | undefined) => {
238
+ const remainingBytes = Buffer.byteLength(remaining);
239
+ if (remainingBytes > PENDING_PASTE_INPUT_MAX_BYTES) {
240
+ super.handleInput(`\x1b[200~${pasteContent}\x1b[201~`);
241
+ if (remaining.length > 0) this.handleInput(remaining);
242
+ this.onPastePendingInputCleared?.("queue-limit", 1);
243
+ return;
244
+ }
245
+ const token = this.#pasteDecisionToken + 1;
246
+ const controller = new AbortController();
247
+ this.#pasteDecisionToken = token;
248
+ this.#pasteDecisionPending = true;
249
+ this.#pendingPasteDecision = { controller, pasteContent, remaining };
250
+ this.#pendingPasteInputBytes = remainingBytes;
251
+ let commitUsed = false;
252
+
253
+ const applyPasteResult = (handled: boolean | undefined) => {
206
254
  if (token !== this.#pasteDecisionToken) return;
207
- this.#clearPasteDecisionTimeout();
255
+ const queuedInput = this.#resetPendingPasteState();
208
256
  if (!handled) {
209
257
  super.handleInput(`\x1b[200~${pasteContent}\x1b[201~`);
210
258
  }
211
- this.#pasteDecisionPending = false;
212
- this.#drainPendingPasteInput(remaining);
259
+ this.#drainPendingPasteInput(remaining, queuedInput);
213
260
  };
214
- const pasteResult = this.onPasteText?.(pasteContent);
261
+
262
+ let pasteResult: boolean | Promise<boolean> | undefined;
263
+ try {
264
+ pasteResult = this.onPasteText?.(pasteContent, {
265
+ signal: controller.signal,
266
+ commit: commit => {
267
+ if (
268
+ commitUsed ||
269
+ token !== this.#pasteDecisionToken ||
270
+ !this.#pasteDecisionPending ||
271
+ controller.signal.aborted
272
+ ) {
273
+ return false;
274
+ }
275
+ commitUsed = true;
276
+ return commit();
277
+ },
278
+ });
279
+ } catch {
280
+ applyPasteResult(false);
281
+ return;
282
+ }
215
283
 
216
284
  if (pasteResult instanceof Promise) {
217
- const token = this.#pasteDecisionToken + 1;
218
- this.#pasteDecisionToken = token;
219
- this.#pasteDecisionPending = true;
220
285
  this.#startPasteDecisionTimeout(token);
221
- void pasteResult.then(
222
- handled => applyPasteResult(token, handled),
223
- () => applyPasteResult(token, false),
224
- );
286
+ void pasteResult.then(applyPasteResult, () => applyPasteResult(false));
225
287
  } else {
226
- applyPasteResult(this.#pasteDecisionToken, pasteResult);
288
+ applyPasteResult(pasteResult);
227
289
  }
228
290
  }
229
291
 
230
292
  handleInput(data: string): void {
231
293
  if (this.#pasteDecisionPending) {
232
- this.#pendingPasteInput.push(data);
233
- if (this.#pendingPasteInput.length > PENDING_PASTE_INPUT_MAX) {
234
- const droppedInputCount = this.#clearPendingPasteState();
235
- this.onPastePendingInputCleared?.("queue-limit", droppedInputCount);
294
+ const inputBytes = Buffer.byteLength(data);
295
+ if (
296
+ this.#pendingPasteEventCount() >= PENDING_PASTE_INPUT_MAX ||
297
+ this.#pendingPasteInputBytes + inputBytes > PENDING_PASTE_INPUT_MAX_BYTES
298
+ ) {
299
+ this.#restoreCancelledPaste("queue-limit", data);
300
+ return;
236
301
  }
302
+ this.#pendingPasteInput.push(data);
303
+ this.#pendingPasteInputBytes += inputBytes;
237
304
  return;
238
305
  }
239
306
 
@@ -245,8 +312,12 @@ export class CustomEditor extends Editor {
245
312
  }
246
313
 
247
314
  if (this.onPasteText) {
248
- const paste = this.#pasteHandler.process(data);
315
+ const paste =
316
+ data === "\x1b" && !this.#pasteHandler.hasPendingFrame
317
+ ? ({ handled: false } as const)
318
+ : this.#pasteHandler.process(data);
249
319
  if (paste.handled) {
320
+ if (paste.leading.length > 0) this.handleInput(paste.leading);
250
321
  if (paste.pasteContent !== undefined) {
251
322
  this.onViewportFollowLive?.();
252
323
  this.#handleBracketedPaste(paste.pasteContent, paste.remaining);
@@ -41,6 +41,7 @@ export class CustomProviderWizardComponent extends Container {
41
41
  credential: "",
42
42
  models: "",
43
43
  };
44
+ #submitInFlight = false;
44
45
  #onSubmit: (input: CustomProviderWizardSubmit) => void;
45
46
  #onCancel: () => void;
46
47
  #onRender: () => void;
@@ -294,7 +295,7 @@ export class CustomProviderWizardComponent extends Container {
294
295
  this.#step = "credential";
295
296
  } else if (this.#step === "confirm" || this.#step === "force-confirm") {
296
297
  if (this.#selectedIndex === 0) {
297
- this.#onSubmit(this.#buildInput(this.#step === "force-confirm"));
298
+ this.#submit();
298
299
  return;
299
300
  }
300
301
  this.#step = "models";
@@ -303,6 +304,30 @@ export class CustomProviderWizardComponent extends Container {
303
304
  this.#onRender();
304
305
  }
305
306
 
307
+ #submit(): void {
308
+ if (this.#submitInFlight) return;
309
+ this.#submitInFlight = true;
310
+ let submission: unknown;
311
+ try {
312
+ submission = this.#onSubmit(this.#buildInput(this.#step === "force-confirm"));
313
+ } catch (error) {
314
+ this.#submitInFlight = false;
315
+ throw error;
316
+ }
317
+ if (!(submission instanceof Promise)) {
318
+ this.#submitInFlight = false;
319
+ return;
320
+ }
321
+ void submission.then(
322
+ () => {
323
+ this.#submitInFlight = false;
324
+ },
325
+ () => {
326
+ this.#submitInFlight = false;
327
+ },
328
+ );
329
+ }
330
+
306
331
  #buildInput(force: boolean): CustomProviderWizardSubmit {
307
332
  const input = {
308
333
  compatibility: this.#state.compatibility,