@levu/snap 0.3.9 → 0.3.11

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/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.11] - 2026-02-28
9
+
10
+ ### Fixed
11
+ - Added direct handling of bracketed paste payloads from raw input stream to preserve multiline content in Ghostty-like terminals.
12
+ - Kept compatibility with readline `line`-event paste flow while avoiding duplicate line ingestion when raw bracketed payload is available.
13
+ - Added regression coverage for bracketed paste payload capture without `line` events.
14
+
15
+ ## [0.3.10] - 2026-02-28
16
+
17
+ ### Fixed
18
+ - Seeded multiline prompt readline buffer from `initialValue`, so editing existing values supports character-by-character backspace/delete behavior.
19
+ - Improved multiline paste recovery when terminal submit flow collapses newline-separated URLs into a concatenated single line.
20
+ - Added regression coverage for concatenated submit recovery and initial-value editability.
21
+
8
22
  ## [0.3.9] - 2026-02-28
9
23
 
10
24
  ### Fixed
@@ -33,6 +33,9 @@ export const createMultilineTextPrompt = () => {
33
33
  let expectingGhosttySequenceEcho = false;
34
34
  let bracketPasteActive = false;
35
35
  let bracketPasteProbe = '';
36
+ let bracketPasteBuffer = '';
37
+ let bracketPasteHasRawPayload = false;
38
+ let suppressLineEventsUntil = 0;
36
39
  let pendingEnterSubmit = false;
37
40
  let pendingEnterSubmitTimer;
38
41
  let recentPasteBurstUntil = 0;
@@ -73,6 +76,10 @@ export const createMultilineTextPrompt = () => {
73
76
  output.write(pc.dim(` Press Enter to submit; Shift+Enter for newline (Alt+Enter fallback)\n`));
74
77
  const lines = value.split('\n');
75
78
  let currentLine = lines.length > 0 ? lines.pop() : '';
79
+ rl.line = currentLine;
80
+ if (typeof rl.cursor === 'number') {
81
+ rl.cursor = currentLine.length;
82
+ }
76
83
  const getLiveLine = () => {
77
84
  const rlLine = typeof rl.line === 'string' ? rl.line : '';
78
85
  if (rlLine.length > 0)
@@ -137,7 +144,10 @@ export const createMultilineTextPrompt = () => {
137
144
  return '';
138
145
  const normalizedPrimary = String(primary || '').trim();
139
146
  const recoveredLastLine = recoveredLines[recoveredLines.length - 1] || '';
140
- if (normalizedPrimary && recoveredLastLine !== normalizedPrimary)
147
+ const recoveredJoined = recoveredLines.join('');
148
+ if (normalizedPrimary &&
149
+ recoveredLastLine !== normalizedPrimary &&
150
+ recoveredJoined !== normalizedPrimary)
141
151
  return '';
142
152
  return recoveredLines.join('\n');
143
153
  };
@@ -168,6 +178,21 @@ export const createMultilineTextPrompt = () => {
168
178
  const showPrompt = () => {
169
179
  output.write(`\n${pc.dim('> ')}${getLiveLine()}`);
170
180
  };
181
+ const applyPastedText = (rawPasted) => {
182
+ const normalized = String(rawPasted || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
183
+ if (!normalized)
184
+ return;
185
+ const merged = `${getLiveLine()}${normalized}`;
186
+ const pastedLines = merged.split('\n');
187
+ if (pastedLines.length > 1) {
188
+ lines.push(...pastedLines.slice(0, -1));
189
+ currentLine = pastedLines[pastedLines.length - 1];
190
+ }
191
+ else {
192
+ currentLine = merged;
193
+ }
194
+ rl.line = currentLine;
195
+ };
171
196
  const BRACKET_PASTE_START = '\u001b[200~';
172
197
  const BRACKET_PASTE_END = '\u001b[201~';
173
198
  const BRACKET_PASTE_PROBE_MAX = 96;
@@ -208,6 +233,38 @@ export const createMultilineTextPrompt = () => {
208
233
  recentPasteBurstUntil = Math.max(recentPasteBurstUntil, Date.now() + PASTE_BURST_WINDOW_MS);
209
234
  }
210
235
  };
236
+ const ingestBracketedPasteChunk = (chunk) => {
237
+ if (!chunk)
238
+ return;
239
+ let rest = chunk;
240
+ while (rest.length > 0) {
241
+ if (!bracketPasteActive) {
242
+ const startIndex = rest.indexOf(BRACKET_PASTE_START);
243
+ if (startIndex < 0)
244
+ return;
245
+ bracketPasteActive = true;
246
+ rest = rest.slice(startIndex + BRACKET_PASTE_START.length);
247
+ continue;
248
+ }
249
+ const endIndex = rest.indexOf(BRACKET_PASTE_END);
250
+ if (endIndex < 0) {
251
+ if (rest.length > 0)
252
+ bracketPasteHasRawPayload = true;
253
+ bracketPasteBuffer += rest;
254
+ return;
255
+ }
256
+ if (endIndex > 0)
257
+ bracketPasteHasRawPayload = true;
258
+ bracketPasteBuffer += rest.slice(0, endIndex);
259
+ bracketPasteActive = false;
260
+ applyPastedText(bracketPasteBuffer);
261
+ bracketPasteBuffer = '';
262
+ bracketPasteHasRawPayload = false;
263
+ suppressLineEventsUntil = Math.max(suppressLineEventsUntil, Date.now() + 60);
264
+ showPrompt();
265
+ rest = rest.slice(endIndex + BRACKET_PASTE_END.length);
266
+ }
267
+ };
211
268
  showPrompt();
212
269
  // Handle paste from clipboard
213
270
  const handlePaste = async () => {
@@ -248,6 +305,8 @@ export const createMultilineTextPrompt = () => {
248
305
  rl.on('line', (line) => {
249
306
  if (cancelled)
250
307
  return;
308
+ if ((bracketPasteActive && bracketPasteHasRawPayload) || Date.now() < suppressLineEventsUntil)
309
+ return;
251
310
  const now = Date.now();
252
311
  if (pendingEnterSubmit) {
253
312
  const likelyPasteFlow = now < recentPasteBurstUntil;
@@ -315,6 +374,7 @@ export const createMultilineTextPrompt = () => {
315
374
  if (cancelled)
316
375
  return;
317
376
  const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
377
+ ingestBracketedPasteChunk(text);
318
378
  updateBracketPasteState(text);
319
379
  notePossiblePasteBurst(text);
320
380
  };
@@ -355,17 +415,7 @@ export const createMultilineTextPrompt = () => {
355
415
  if (pasted) {
356
416
  // Clear current line and show pasted content
357
417
  output.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
358
- const pastedLines = pasted.split('\n');
359
- if (pastedLines.length > 1) {
360
- // Multiline paste
361
- lines.push(...pastedLines.slice(0, -1));
362
- currentLine = pastedLines[pastedLines.length - 1];
363
- }
364
- else {
365
- // Single line paste
366
- currentLine += pasted;
367
- }
368
- rl.line = currentLine;
418
+ applyPastedText(pasted);
369
419
  output.write(`${pc.dim('> ')}${currentLine}`);
370
420
  }
371
421
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levu/snap",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "snap": "./dist/cli-entry.js"