@levu/snap 0.3.8 → 0.3.10

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.10] - 2026-02-28
9
+
10
+ ### Fixed
11
+ - Seeded multiline prompt readline buffer from `initialValue`, so editing existing values supports character-by-character backspace/delete behavior.
12
+ - Improved multiline paste recovery when terminal submit flow collapses newline-separated URLs into a concatenated single line.
13
+ - Added regression coverage for concatenated submit recovery and initial-value editability.
14
+
15
+ ## [0.3.9] - 2026-02-28
16
+
17
+ ### Fixed
18
+ - Added multiline paste recovery for cases where readline submit flow only exposes the last pasted line.
19
+ - Recovery now uses recent raw paste chunks to reconstruct full multi-line content when the submit payload is unexpectedly single-line.
20
+ - Added regression coverage for the “last line only after multiline paste” scenario.
21
+
8
22
  ## [0.3.8] - 2026-02-26
9
23
 
10
24
  ### Fixed
@@ -36,6 +36,8 @@ export const createMultilineTextPrompt = () => {
36
36
  let pendingEnterSubmit = false;
37
37
  let pendingEnterSubmitTimer;
38
38
  let recentPasteBurstUntil = 0;
39
+ let sawPasteLikeRawInput = false;
40
+ let recentPasteRawBuffer = '';
39
41
  const cleanup = () => {
40
42
  if (pendingEnterSubmitTimer) {
41
43
  clearTimeout(pendingEnterSubmitTimer);
@@ -71,6 +73,10 @@ export const createMultilineTextPrompt = () => {
71
73
  output.write(pc.dim(` Press Enter to submit; Shift+Enter for newline (Alt+Enter fallback)\n`));
72
74
  const lines = value.split('\n');
73
75
  let currentLine = lines.length > 0 ? lines.pop() : '';
76
+ rl.line = currentLine;
77
+ if (typeof rl.cursor === 'number') {
78
+ rl.cursor = currentLine.length;
79
+ }
74
80
  const getLiveLine = () => {
75
81
  const rlLine = typeof rl.line === 'string' ? rl.line : '';
76
82
  if (rlLine.length > 0)
@@ -114,8 +120,38 @@ export const createMultilineTextPrompt = () => {
114
120
  const normalizeGhosttyInlineTokens = (raw) => {
115
121
  return raw.replace(/(?:\u001b\[13;2u|\[13;2u|\u001b\[27;2;13~|\[27;2;13~|13~|~13)/g, '\n');
116
122
  };
123
+ const stripAnsiControls = (raw) => {
124
+ return String(raw || '')
125
+ .replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '')
126
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '');
127
+ };
128
+ const recoverMultilineFromRawPaste = (primary) => {
129
+ if (!sawPasteLikeRawInput)
130
+ return '';
131
+ if (primary.includes('\n'))
132
+ return '';
133
+ if (!recentPasteRawBuffer)
134
+ return '';
135
+ const recoveredLines = stripAnsiControls(recentPasteRawBuffer)
136
+ .replace(/\r/g, '\n')
137
+ .split('\n')
138
+ .map((line) => line.trim())
139
+ .filter(Boolean);
140
+ if (recoveredLines.length < 2)
141
+ return '';
142
+ const normalizedPrimary = String(primary || '').trim();
143
+ const recoveredLastLine = recoveredLines[recoveredLines.length - 1] || '';
144
+ const recoveredJoined = recoveredLines.join('');
145
+ if (normalizedPrimary &&
146
+ recoveredLastLine !== normalizedPrimary &&
147
+ recoveredJoined !== normalizedPrimary)
148
+ return '';
149
+ return recoveredLines.join('\n');
150
+ };
117
151
  const buildSubmitValue = () => {
118
- return normalizeGhosttyInlineTokens(lines.concat(getLiveLine()).join('\n'));
152
+ const primary = normalizeGhosttyInlineTokens(lines.concat(getLiveLine()).join('\n'));
153
+ const recovered = recoverMultilineFromRawPaste(primary);
154
+ return recovered || primary;
119
155
  };
120
156
  const absorbLine = (line) => {
121
157
  if (line.trim() === '') {
@@ -143,6 +179,7 @@ export const createMultilineTextPrompt = () => {
143
179
  const BRACKET_PASTE_END = '\u001b[201~';
144
180
  const BRACKET_PASTE_PROBE_MAX = 96;
145
181
  const PASTE_BURST_WINDOW_MS = 70;
182
+ const RAW_PASTE_BUFFER_MAX = 8192;
146
183
  const updateBracketPasteState = (chunk) => {
147
184
  if (!chunk)
148
185
  return;
@@ -173,6 +210,8 @@ export const createMultilineTextPrompt = () => {
173
210
  // In raw mode, normal typing usually arrives as single-byte chunks.
174
211
  // Multi-byte chunks and newlines are strong signals that input is a paste burst.
175
212
  if (normalized.length > 1 || /[\r\n]/.test(normalized)) {
213
+ sawPasteLikeRawInput = true;
214
+ recentPasteRawBuffer = `${recentPasteRawBuffer}${normalized}`.slice(-RAW_PASTE_BUFFER_MAX);
176
215
  recentPasteBurstUntil = Math.max(recentPasteBurstUntil, Date.now() + PASTE_BURST_WINDOW_MS);
177
216
  }
178
217
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levu/snap",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "snap": "./dist/cli-entry.js"