@iinm/plain-agent 1.7.13 → 1.7.14

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.
package/README.md CHANGED
@@ -191,7 +191,7 @@ Create the configuration.
191
191
  },
192
192
  {
193
193
  "name": "claude-sonnet-4-6",
194
- "variant": "thinking-16k-bedrock-jp",
194
+ "variant": "thinking-high-bedrock-jp",
195
195
  "platform": {
196
196
  "name": "bedrock",
197
197
  "variant": "jp"
@@ -201,7 +201,8 @@ Create the configuration.
201
201
  "config": {
202
202
  "model": "jp.anthropic.claude-sonnet-4-6",
203
203
  "max_tokens": 32768,
204
- "thinking": { "type": "enabled", "budget_tokens": 16384 }
204
+ "thinking": { "type": "adaptive" },
205
+ "output_config": { "effort": "high" }
205
206
  }
206
207
  },
207
208
  "cost": {
@@ -239,6 +240,18 @@ plain
239
240
  plain -m <model+variant>
240
241
  ```
241
242
 
243
+ (Optional) Set up a sandbox for your project with the `sandbox-configurator` agent.
244
+
245
+ ```
246
+ /agents:sandbox-configurator Set up a sandbox for this project
247
+ ```
248
+
249
+ After the agent finishes, run the generated setup script once to build the sandbox image and install dependencies.
250
+
251
+ ```sh
252
+ ./.plain-agent/setup.sh
253
+ ```
254
+
242
255
  Run in batch mode (non-interactive).
243
256
  In batch mode, config files are not loaded automatically. Only the files specified with `--config` are loaded.
244
257
 
@@ -331,7 +344,7 @@ The agent loads configuration files in the following order. Settings in later fi
331
344
  },
332
345
  "sandbox": {
333
346
  "command": "plain-sandbox",
334
- "args": ["--dockerfile", ".plain-agent/sandbox/Dockerfile", "--allow-write", "--skip-build", "--keep-alive", "30"],
347
+ "args": ["--allow-write", "--skip-build", "--keep-alive", "30"],
335
348
  "separator": "--",
336
349
  "rules": [
337
350
  {
@@ -394,7 +407,7 @@ The agent loads configuration files in the following order. Settings in later fi
394
407
  // https://github.com/iinm/plain-agent/tree/main/sandbox
395
408
  "sandbox": {
396
409
  "command": "plain-sandbox",
397
- "args": ["--dockerfile", ".plain-agent/sandbox/Dockerfile", "--allow-write", "--skip-build", "--keep-alive", "30"],
410
+ "args": ["--allow-write", "--skip-build", "--keep-alive", "30"],
398
411
  // separator is inserted between sandbox flags and the user command to prevent bypasses
399
412
  "separator": "--",
400
413
 
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: Analyzes the project and builds sandbox configuration files (Dockerfile, run.sh, env, setup.sh) tailored to the project's needs.
2
+ description: Analyzes the project and generates sandbox configuration files (run.sh, setup.sh) tailored to the project's needs.
3
3
  ---
4
4
 
5
- You are a sandbox builder. You analyze the project and generate sandbox configuration files so that commands run in an isolated Docker container.
5
+ You are a sandbox builder. You analyze the project and generate sandbox configuration files so that commands run in an isolated Docker container using the `plain-sandbox` preset image.
6
6
 
7
7
  ## Overview
8
8
 
@@ -19,48 +19,40 @@ Before generating anything, analyze the project to determine:
19
19
 
20
20
  ### 1a. Runtime & Tools
21
21
 
22
- Detect the project type and determine which runtimes to install via mise:
22
+ Detect the project type and determine which runtimes to install via mise. Use the runtime's bundled package managers instead of installing them separately via mise (e.g. Node.js ships with npm; use `corepack enable` for yarn/pnpm).
23
23
 
24
- | File found | mise install commands |
25
- |---|---|
26
- | `package.json` | `mise use node@<version>` (check `.nvmrc` or `.node-version`, else use LTS) |
27
- | `package.json` + `package-lock.json` | Add `mise use npm@latest` |
28
- | `package.json` + `yarn.lock` | Add `mise use yarn@latest` |
29
- | `package.json` + `pnpm-lock.yaml` | Add `mise use pnpm@latest` |
30
- | `requirements.txt` or `pyproject.toml` | `mise use python@<version>` (check `.python-version`, else 3.12) |
31
- | `go.mod` | `mise use go@<version>` (check `go.mod` for version directive) |
32
- | `Cargo.toml` | `mise use rust@latest` |
33
- | Multiple of the above | All detected runtimes |
24
+ | File found | mise install commands | Version source |
25
+ |---|---|---|
26
+ | `package.json` | `mise use node@<version>` | `.nvmrc` / `.node-version` / `package.json` (`engines.node`) |
27
+ | `requirements.txt` or `pyproject.toml` | `mise use python@<version>` | `.python-version` / `pyproject.toml` (`requires-python`) |
34
28
 
35
29
  Also check for common dev tools:
36
- - `terraform/` directory or `*.tf` files → `mise use terraform@<version>`
37
- - `.terraform-version` → `mise use terraform@<version>`
30
+ - `*.tf` files or `.terraform-version` → `mise use terraform@<version>` (version source: `.terraform-version`)
31
+
32
+ If a version cannot be determined from the files above, **ask the user which version to use** rather than falling back to a default.
38
33
 
39
34
  ### 1b. Volume Candidates
40
35
 
41
- Detect directories that should use Docker volumes (for performance with large directories):
36
+ Detect directories that should use Docker volumes. A Docker volume is preferred over a host bind mount for `node_modules` because:
37
+
38
+ - `node_modules` contains many thousands of small files, and bind-mounting it into the container is slow on macOS/Windows (file sync overhead).
39
+ - Native modules compiled for the host OS/arch can be incompatible with the Linux container, so keeping container-side `node_modules` isolated avoids conflicts.
42
40
 
43
41
  | Project type | Cache volumes | Dependency volumes |
44
42
  |---|---|---|
45
- | Node.js | `plain-sandbox--global--home-npm:/home/sandbox/.npm` | `node_modules` (per package.json dir if monorepo) |
43
+ | Node.js | `plain-sandbox--global--home-npm:/home/sandbox/.npm` | `node_modules` (per `package.json` dir if monorepo) |
46
44
  | Python | `plain-sandbox--global--home-pip:/home/sandbox/.cache/pip` | — |
47
- | Go | `plain-sandbox--global--home-go-pkg:/home/sandbox/go/pkg/mod` | — |
48
- | Rust | `plain-sandbox--global--home-cargo:/home/sandbox/.cargo/registry` | — |
49
45
 
50
- For monorepo detection: if multiple `package.json` files exist (excluding `node_modules`), treat as monorepo and create a volume per `node_modules` directory.
46
+ For monorepo detection: if multiple `package.json` files exist (excluding `node_modules`), treat as a monorepo and create a volume per `node_modules` directory.
51
47
 
52
48
  ### 1c. Setup Install Commands
53
49
 
54
50
  | Project type | Install command |
55
51
  |---|---|
56
52
  | Node.js (npm) | `npm ci` (or `npm install` if no lockfile) |
57
- | Node.js (yarn) | `yarn install --frozen-lockfile` |
58
- | Node.js (pnpm) | `pnpm install --frozen-lockfile` |
53
+ | Node.js (yarn) | `corepack enable && yarn install --frozen-lockfile` |
54
+ | Node.js (pnpm) | `corepack enable && pnpm install --frozen-lockfile` |
59
55
  | Python | `pip install -r requirements.txt` or `pip install .` |
60
- | Go | `go mod download` |
61
- | Rust | `cargo build` |
62
-
63
- If multiple project types, include all relevant commands.
64
56
 
65
57
  ## Step 2: Confirm with User
66
58
 
@@ -77,45 +69,7 @@ Ask only one additional question:
77
69
 
78
70
  ## Step 3: Generate run.sh
79
71
 
80
- Generate `.plain-agent/sandbox/run.sh`. The structure varies by project type.
81
-
82
- ### Monorepo handling:
83
-
84
- If multiple `package.json` files exist, dynamically create volumes for each `node_modules`:
85
-
86
- ```bash
87
- # Create volumes for each node_modules directory
88
- for path in $(fd package.json --max-depth 3 | sed -E 's,package.json$,node_modules,'); do
89
- mkdir -p "$path"
90
- options+=("--volume" "$path")
91
- done
92
- ```
93
-
94
- ### Git worktree handling:
95
-
96
- Always include this block after the options array, before `plain-sandbox`:
97
-
98
- ```bash
99
- # Mount main worktree if using git worktrees
100
- git_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
101
- if test -n "$git_root" && test -f "$git_root/.git"; then
102
- main_worktree_path=$(sed -E 's,^gitdir: (.+)/.git/.+,\1,' < "$git_root/.git")
103
- options+=("--mount-writable" "$main_worktree_path:$main_worktree_path")
104
- fi
105
- ```
106
-
107
- ### gitconfig handling:
108
-
109
- Include this block only if the user confirmed:
110
-
111
- ```bash
112
- # Mount gitconfig
113
- if test -f "$HOME/.gitconfig"; then
114
- options+=("--mount-readonly" "$HOME/.gitconfig:/home/sandbox/.gitconfig")
115
- fi
116
- ```
117
-
118
- ### Complete run.sh example (Node.js project):
72
+ Generate `.plain-agent/sandbox/run.sh`. Use the following Node.js example as the template and adapt volumes for other runtimes from the table in Step 1b.
119
73
 
120
74
  ```bash
121
75
  #!/usr/bin/env bash
@@ -128,6 +82,13 @@ options=(
128
82
  --volume node_modules
129
83
  )
130
84
 
85
+ # Monorepo: create a volume for each node_modules directory.
86
+ # Include only when multiple package.json files exist.
87
+ # for path in $(fd package.json --max-depth 3 | sed -E 's,package.json$,node_modules,'); do
88
+ # mkdir -p "$path"
89
+ # options+=("--volume" "$path")
90
+ # done
91
+
131
92
  # Mount main worktree if using git worktrees
132
93
  git_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
133
94
  if test -n "$git_root" && test -f "$git_root/.git"; then
@@ -135,7 +96,7 @@ if test -n "$git_root" && test -f "$git_root/.git"; then
135
96
  options+=("--mount-writable" "$main_worktree_path:$main_worktree_path")
136
97
  fi
137
98
 
138
- # Mount gitconfig
99
+ # Mount gitconfig (include only if the user confirmed)
139
100
  if test -f "$HOME/.gitconfig"; then
140
101
  options+=("--mount-readonly" "$HOME/.gitconfig:/home/sandbox/.gitconfig")
141
102
  fi
@@ -145,7 +106,7 @@ plain-sandbox "${options[@]}" "$@"
145
106
 
146
107
  ## Step 4: Generate setup.sh
147
108
 
148
- Generate `.plain-agent/setup.sh`:
109
+ Generate `.plain-agent/setup.sh`. Use the following Node.js example and replace `node@lts` / `npm ci` with the commands chosen in Step 1.
149
110
 
150
111
  ```bash
151
112
  #!/usr/bin/env bash
@@ -154,24 +115,31 @@ set -eu -o pipefail
154
115
 
155
116
  this_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
156
117
 
157
- # Setup sandbox (install dependencies inside container with full network access)
118
+ # Setup sandbox (install runtime and dependencies with network access)
158
119
  "$this_dir/sandbox/run.sh" --verbose --allow-net 0.0.0.0/0 mise use node@lts
159
120
  "$this_dir/sandbox/run.sh" --verbose --allow-net 0.0.0.0/0 npm ci
160
121
 
161
- # Setup host (install dependencies on host)
122
+ # Setup host
162
123
  npm ci
163
124
  ```
164
125
 
165
- The `--allow-net 0.0.0.0/0` is needed only during setup for downloading packages. It should NOT be in run.sh for normal usage.
126
+ `--allow-net 0.0.0.0/0` is needed only during setup for downloading packages. It should NOT be in run.sh for normal usage.
166
127
 
167
128
  ## Step 5: Show config.json Example
168
129
 
169
- After generating all files, display the following example and instruct the user to add it to their `.plain-agent/config.json`:
170
-
171
- ```
172
- Add the following to your .plain-agent/config.json:
130
+ After generating all files, instruct the user to add the following to their `.plain-agent/config.json`:
173
131
 
132
+ ```json
174
133
  {
134
+ "autoApproval": {
135
+ "patterns": [
136
+ {
137
+ "toolName": "exec_command",
138
+ "input": { "command": { "$regex": "^(gh|docker)$" } },
139
+ "action": "ask"
140
+ }
141
+ ]
142
+ },
175
143
  "sandbox": {
176
144
  "command": ".plain-agent/sandbox/run.sh",
177
145
  "args": ["--skip-build", "--keep-alive", "30"],
@@ -186,7 +154,7 @@ Add the following to your .plain-agent/config.json:
186
154
  }
187
155
  ```
188
156
 
189
- If the project already has a `.plain-agent/config.json`, show only the `sandbox` key that should be added/merged. Remind the user:
157
+ If the project already has a `.plain-agent/config.json`, show only the keys that should be added/merged. Remind the user:
190
158
  - `--skip-build` assumes the image is already built (run `setup.sh` first to build)
191
159
  - `--keep-alive 30` reuses the container for 30 seconds between commands for performance
192
- - `rules` for `gh` and `docker` should typically run unsandboxed (host access needed)
160
+ - `gh` and `docker` run unsandboxed (host access needed), so they should also be set to `ask` in `autoApproval` to avoid being auto-approved alongside other shell commands. Place this `ask` pattern before any broad `allow` pattern for `exec_command`, since `autoApproval` patterns are evaluated in order and the first match wins.
@@ -181,7 +181,7 @@
181
181
  },
182
182
  {
183
183
  "name": "claude-sonnet-4-6",
184
- "variant": "thinking-16k",
184
+ "variant": "thinking-high",
185
185
  "platform": {
186
186
  "name": "anthropic",
187
187
  "variant": "default",
@@ -192,7 +192,8 @@
192
192
  "config": {
193
193
  "model": "claude-sonnet-4-6",
194
194
  "max_tokens": 32768,
195
- "thinking": { "type": "enabled", "budget_tokens": 16384 }
195
+ "thinking": { "type": "adaptive" },
196
+ "output_config": { "effort": "high" }
196
197
  }
197
198
  },
198
199
  "cost": {
@@ -208,7 +209,7 @@
208
209
  },
209
210
  {
210
211
  "name": "claude-sonnet-4-6",
211
- "variant": "thinking-32k",
212
+ "variant": "thinking-max",
212
213
  "platform": {
213
214
  "name": "anthropic",
214
215
  "variant": "default",
@@ -219,7 +220,8 @@
219
220
  "config": {
220
221
  "model": "claude-sonnet-4-6",
221
222
  "max_tokens": 64000,
222
- "thinking": { "type": "enabled", "budget_tokens": 32768 }
223
+ "thinking": { "type": "adaptive" },
224
+ "output_config": { "effort": "max" }
223
225
  }
224
226
  },
225
227
  "cost": {
@@ -234,8 +236,8 @@
234
236
  }
235
237
  },
236
238
  {
237
- "name": "claude-opus-4-6",
238
- "variant": "thinking-16k",
239
+ "name": "claude-opus-4-7",
240
+ "variant": "thinking-high",
239
241
  "platform": {
240
242
  "name": "anthropic",
241
243
  "variant": "default",
@@ -244,9 +246,10 @@
244
246
  "model": {
245
247
  "format": "anthropic",
246
248
  "config": {
247
- "model": "claude-opus-4-6",
249
+ "model": "claude-opus-4-7",
248
250
  "max_tokens": 32768,
249
- "thinking": { "type": "enabled", "budget_tokens": 16384 }
251
+ "thinking": { "type": "adaptive" },
252
+ "output_config": { "effort": "high" }
250
253
  }
251
254
  },
252
255
  "cost": {
@@ -261,8 +264,8 @@
261
264
  }
262
265
  },
263
266
  {
264
- "name": "claude-opus-4-6",
265
- "variant": "thinking-32k",
267
+ "name": "claude-opus-4-7",
268
+ "variant": "thinking-max",
266
269
  "platform": {
267
270
  "name": "anthropic",
268
271
  "variant": "default",
@@ -271,9 +274,10 @@
271
274
  "model": {
272
275
  "format": "anthropic",
273
276
  "config": {
274
- "model": "claude-opus-4-6",
277
+ "model": "claude-opus-4-7",
275
278
  "max_tokens": 64000,
276
- "thinking": { "type": "enabled", "budget_tokens": 32768 }
279
+ "thinking": { "type": "adaptive" },
280
+ "output_config": { "effort": "max" }
277
281
  }
278
282
  },
279
283
  "cost": {
@@ -342,7 +346,7 @@
342
346
  },
343
347
  {
344
348
  "name": "claude-sonnet-4-6",
345
- "variant": "thinking-16k-bedrock",
349
+ "variant": "thinking-high-bedrock",
346
350
  "platform": {
347
351
  "name": "bedrock",
348
352
  "variant": "default"
@@ -352,7 +356,8 @@
352
356
  "config": {
353
357
  "model": "global.anthropic.claude-sonnet-4-6",
354
358
  "max_tokens": 32768,
355
- "thinking": { "type": "enabled", "budget_tokens": 16384 }
359
+ "thinking": { "type": "adaptive" },
360
+ "output_config": { "effort": "high" }
356
361
  }
357
362
  },
358
363
  "cost": {
@@ -368,7 +373,7 @@
368
373
  },
369
374
  {
370
375
  "name": "claude-sonnet-4-6",
371
- "variant": "thinking-32k-bedrock",
376
+ "variant": "thinking-max-bedrock",
372
377
  "platform": {
373
378
  "name": "bedrock",
374
379
  "variant": "default"
@@ -378,7 +383,8 @@
378
383
  "config": {
379
384
  "model": "global.anthropic.claude-sonnet-4-6",
380
385
  "max_tokens": 64000,
381
- "thinking": { "type": "enabled", "budget_tokens": 32768 }
386
+ "thinking": { "type": "adaptive" },
387
+ "output_config": { "effort": "max" }
382
388
  }
383
389
  },
384
390
  "cost": {
@@ -393,8 +399,8 @@
393
399
  }
394
400
  },
395
401
  {
396
- "name": "claude-opus-4-6",
397
- "variant": "thinking-16k-bedrock",
402
+ "name": "claude-opus-4-7",
403
+ "variant": "thinking-high-bedrock",
398
404
  "platform": {
399
405
  "name": "bedrock",
400
406
  "variant": "default"
@@ -402,9 +408,10 @@
402
408
  "model": {
403
409
  "format": "anthropic",
404
410
  "config": {
405
- "model": "global.anthropic.claude-opus-4-6-v1",
411
+ "model": "global.anthropic.claude-opus-4-7",
406
412
  "max_tokens": 32768,
407
- "thinking": { "type": "enabled", "budget_tokens": 16384 }
413
+ "thinking": { "type": "adaptive" },
414
+ "output_config": { "effort": "high" }
408
415
  }
409
416
  },
410
417
  "cost": {
@@ -419,8 +426,8 @@
419
426
  }
420
427
  },
421
428
  {
422
- "name": "claude-opus-4-6",
423
- "variant": "thinking-32k-bedrock",
429
+ "name": "claude-opus-4-7",
430
+ "variant": "thinking-max-bedrock",
424
431
  "platform": {
425
432
  "name": "bedrock",
426
433
  "variant": "default"
@@ -428,9 +435,10 @@
428
435
  "model": {
429
436
  "format": "anthropic",
430
437
  "config": {
431
- "model": "global.anthropic.claude-opus-4-6-v1",
438
+ "model": "global.anthropic.claude-opus-4-7",
432
439
  "max_tokens": 64000,
433
- "thinking": { "type": "enabled", "budget_tokens": 32768 }
440
+ "thinking": { "type": "adaptive" },
441
+ "output_config": { "effort": "max" }
434
442
  }
435
443
  },
436
444
  "cost": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.7.13",
3
+ "version": "1.7.14",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -52,6 +52,12 @@ export function resolvePastePlaceholders(input) {
52
52
  return text;
53
53
  }
54
54
 
55
+ // Time to wait for a continuation paste chunk before flushing the paste buffer.
56
+ // Some terminals split large pastes into multiple bracketed paste sequences
57
+ // (e.g. `\x1b[200~...\x1b[201~\x1b[200~...\x1b[201~`) that arrive back-to-back.
58
+ // Holding the paste briefly lets us merge them into a single placeholder.
59
+ const PASTE_MERGE_WINDOW_MS = 20;
60
+
55
61
  /**
56
62
  * Create a Transform stream to handle bracketed paste before readline.
57
63
  * @param {() => void} onCtrlC - Called when Ctrl-C or Ctrl-D is detected
@@ -60,8 +66,43 @@ export function resolvePastePlaceholders(input) {
60
66
  export function createPasteTransform(onCtrlC) {
61
67
  let inPasteMode = false;
62
68
  let pasteBuffer = "";
69
+ // True when a paste just ended and we are waiting to see if the next data
70
+ // continues it (i.e. starts with another BRACKETED_PASTE_START).
71
+ let awaitingMerge = false;
72
+ /** @type {NodeJS.Timeout | null} */
73
+ let mergeTimer = null;
74
+ /** @type {Transform} */
75
+ let transform;
76
+
77
+ const clearMergeTimer = () => {
78
+ if (mergeTimer) {
79
+ clearTimeout(mergeTimer);
80
+ mergeTimer = null;
81
+ }
82
+ };
83
+
84
+ const flushPaste = () => {
85
+ clearMergeTimer();
86
+ awaitingMerge = false;
87
+ if (pasteBuffer) {
88
+ // Remove trailing newline for single-line paste detection
89
+ const trimmedPaste = pasteBuffer.replace(/\n$/, "");
90
+
91
+ // For single-line paste, insert directly without placeholder
92
+ if (!trimmedPaste.includes("\n")) {
93
+ transform.push(trimmedPaste);
94
+ } else {
95
+ // For multi-line paste, use placeholder
96
+ const hash = generatePasteHash(pasteBuffer);
97
+ pastedContentStore.set(hash, pasteBuffer);
98
+ const lines = pasteBuffer.split("\n");
99
+ transform.push(`[Pasted text #${hash}, ${lines.length} lines]`);
100
+ }
101
+ }
102
+ pasteBuffer = "";
103
+ };
63
104
 
64
- return new Transform({
105
+ transform = new Transform({
65
106
  transform(chunk, _encoding, callback) {
66
107
  /** @type {string} */
67
108
  let data = chunk.toString("utf8");
@@ -77,32 +118,28 @@ export function createPasteTransform(onCtrlC) {
77
118
  if (inPasteMode) {
78
119
  const endIdx = data.indexOf(BRACKETED_PASTE_END);
79
120
  if (endIdx !== -1) {
80
- // End of paste
121
+ // End of (this chunk of) paste. Hold the buffer briefly in case
122
+ // another paste chunk follows immediately and should be merged.
81
123
  pasteBuffer += data.slice(0, endIdx);
82
124
  data = data.slice(endIdx + BRACKETED_PASTE_END.length);
83
125
  inPasteMode = false;
84
-
85
- // Handle paste content
86
- if (pasteBuffer) {
87
- // Remove trailing newline for single-line paste detection
88
- const trimmedPaste = pasteBuffer.replace(/\n$/, "");
89
-
90
- // For single-line paste, insert directly without placeholder
91
- if (!trimmedPaste.includes("\n")) {
92
- this.push(trimmedPaste);
93
- } else {
94
- // For multi-line paste, use placeholder
95
- const hash = generatePasteHash(pasteBuffer);
96
- pastedContentStore.set(hash, pasteBuffer);
97
- const lines = pasteBuffer.split("\n");
98
- this.push(`[Pasted text #${hash}, ${lines.length} lines]`);
99
- }
100
- }
101
- pasteBuffer = "";
126
+ awaitingMerge = true;
102
127
  } else {
103
128
  // Still in paste mode
104
129
  pasteBuffer += data;
105
- break;
130
+ data = "";
131
+ }
132
+ } else if (awaitingMerge) {
133
+ // If the next data starts with another paste start marker, treat it
134
+ // as a continuation of the previous paste and merge.
135
+ if (data.startsWith(BRACKETED_PASTE_START)) {
136
+ data = data.slice(BRACKETED_PASTE_START.length);
137
+ inPasteMode = true;
138
+ awaitingMerge = false;
139
+ clearMergeTimer();
140
+ } else {
141
+ // Not a continuation; flush pending paste, then process this data.
142
+ flushPaste();
106
143
  }
107
144
  } else {
108
145
  const startIdx = data.indexOf(BRACKETED_PASTE_START);
@@ -118,12 +155,30 @@ export function createPasteTransform(onCtrlC) {
118
155
  } else {
119
156
  // Normal data
120
157
  this.push(data);
121
- break;
158
+ data = "";
122
159
  }
123
160
  }
124
161
  }
125
162
 
163
+ // If the chunk ended while still awaiting a continuation, schedule a
164
+ // short timer to flush the pending paste if nothing else arrives.
165
+ if (awaitingMerge && !mergeTimer) {
166
+ mergeTimer = setTimeout(() => {
167
+ mergeTimer = null;
168
+ flushPaste();
169
+ }, PASTE_MERGE_WINDOW_MS);
170
+ }
171
+
172
+ callback();
173
+ },
174
+
175
+ flush(callback) {
176
+ if (awaitingMerge) {
177
+ flushPaste();
178
+ }
126
179
  callback();
127
180
  },
128
181
  });
182
+
183
+ return transform;
129
184
  }