@levu/snap 0.3.8 → 0.3.9

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,13 @@ 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.9] - 2026-02-28
9
+
10
+ ### Fixed
11
+ - Added multiline paste recovery for cases where readline submit flow only exposes the last pasted line.
12
+ - Recovery now uses recent raw paste chunks to reconstruct full multi-line content when the submit payload is unexpectedly single-line.
13
+ - Added regression coverage for the “last line only after multiline paste” scenario.
14
+
8
15
  ## [0.3.8] - 2026-02-26
9
16
 
10
17
  ### 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);
@@ -114,8 +116,35 @@ export const createMultilineTextPrompt = () => {
114
116
  const normalizeGhosttyInlineTokens = (raw) => {
115
117
  return raw.replace(/(?:\u001b\[13;2u|\[13;2u|\u001b\[27;2;13~|\[27;2;13~|13~|~13)/g, '\n');
116
118
  };
119
+ const stripAnsiControls = (raw) => {
120
+ return String(raw || '')
121
+ .replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '')
122
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '');
123
+ };
124
+ const recoverMultilineFromRawPaste = (primary) => {
125
+ if (!sawPasteLikeRawInput)
126
+ return '';
127
+ if (primary.includes('\n'))
128
+ return '';
129
+ if (!recentPasteRawBuffer)
130
+ return '';
131
+ const recoveredLines = stripAnsiControls(recentPasteRawBuffer)
132
+ .replace(/\r/g, '\n')
133
+ .split('\n')
134
+ .map((line) => line.trim())
135
+ .filter(Boolean);
136
+ if (recoveredLines.length < 2)
137
+ return '';
138
+ const normalizedPrimary = String(primary || '').trim();
139
+ const recoveredLastLine = recoveredLines[recoveredLines.length - 1] || '';
140
+ if (normalizedPrimary && recoveredLastLine !== normalizedPrimary)
141
+ return '';
142
+ return recoveredLines.join('\n');
143
+ };
117
144
  const buildSubmitValue = () => {
118
- return normalizeGhosttyInlineTokens(lines.concat(getLiveLine()).join('\n'));
145
+ const primary = normalizeGhosttyInlineTokens(lines.concat(getLiveLine()).join('\n'));
146
+ const recovered = recoverMultilineFromRawPaste(primary);
147
+ return recovered || primary;
119
148
  };
120
149
  const absorbLine = (line) => {
121
150
  if (line.trim() === '') {
@@ -143,6 +172,7 @@ export const createMultilineTextPrompt = () => {
143
172
  const BRACKET_PASTE_END = '\u001b[201~';
144
173
  const BRACKET_PASTE_PROBE_MAX = 96;
145
174
  const PASTE_BURST_WINDOW_MS = 70;
175
+ const RAW_PASTE_BUFFER_MAX = 8192;
146
176
  const updateBracketPasteState = (chunk) => {
147
177
  if (!chunk)
148
178
  return;
@@ -173,6 +203,8 @@ export const createMultilineTextPrompt = () => {
173
203
  // In raw mode, normal typing usually arrives as single-byte chunks.
174
204
  // Multi-byte chunks and newlines are strong signals that input is a paste burst.
175
205
  if (normalized.length > 1 || /[\r\n]/.test(normalized)) {
206
+ sawPasteLikeRawInput = true;
207
+ recentPasteRawBuffer = `${recentPasteRawBuffer}${normalized}`.slice(-RAW_PASTE_BUFFER_MAX);
176
208
  recentPasteBurstUntil = Math.max(recentPasteBurstUntil, Date.now() + PASTE_BURST_WINDOW_MS);
177
209
  }
178
210
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levu/snap",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "snap": "./dist/cli-entry.js"