@pugi/cli 0.1.0-beta.44 → 0.1.0-beta.46
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/dist/runtime/version.js +1 -1
- package/dist/tui/input-box.js +120 -1
- package/package.json +2 -2
package/dist/runtime/version.js
CHANGED
|
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
|
|
|
44
44
|
* during import). When bumping the CLI version BOTH literals must be
|
|
45
45
|
* updated; the release smoke-test (`pack:smoke`) verifies they agree.
|
|
46
46
|
*/
|
|
47
|
-
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.
|
|
47
|
+
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.46');
|
|
48
48
|
/**
|
|
49
49
|
* Outbound: the CLI's installed semver. Read at request time by
|
|
50
50
|
* `version-interceptor.ts` and injected on every `fetch` call.
|
package/dist/tui/input-box.js
CHANGED
|
@@ -238,7 +238,9 @@ export function InputBox(props) {
|
|
|
238
238
|
setCursor(draftBeforeSearch.length);
|
|
239
239
|
return;
|
|
240
240
|
}
|
|
241
|
-
|
|
241
|
+
// Bare LF accepts the focused match, same as CR (`key.return`).
|
|
242
|
+
// See the post-search block below for the rationale.
|
|
243
|
+
if (key.return || (input === '\n' && !key.meta && !key.ctrl && !key.shift)) {
|
|
242
244
|
const picked = currentBrief(search);
|
|
243
245
|
setSearch(undefined);
|
|
244
246
|
if (picked !== null) {
|
|
@@ -257,6 +259,11 @@ export function InputBox(props) {
|
|
|
257
259
|
return;
|
|
258
260
|
}
|
|
259
261
|
if (input && !key.meta && !key.ctrl) {
|
|
262
|
+
// Drop a bare LF from the search query — the Enter-accept
|
|
263
|
+
// branch above already handled it; falling through here would
|
|
264
|
+
// splice a newline into the search string.
|
|
265
|
+
if (input === '\n')
|
|
266
|
+
return;
|
|
260
267
|
const nextQuery = search.query + input;
|
|
261
268
|
setSearch(applyQuery(search, nextQuery, history));
|
|
262
269
|
return;
|
|
@@ -279,6 +286,118 @@ export function InputBox(props) {
|
|
|
279
286
|
setSearch(initialSearchState(history));
|
|
280
287
|
return;
|
|
281
288
|
}
|
|
289
|
+
// P0 fix (CEO 2026-05-29 dogfood, second iteration): bare LF (`\n`)
|
|
290
|
+
// MUST submit the brief, same as bare CR (`\r`). Ink's parseKeypress
|
|
291
|
+
// maps `\r` to `key.return` and `\n` to `key.name === 'enter'`
|
|
292
|
+
// WITHOUT setting `key.return`. Most real terminals deliver CR for
|
|
293
|
+
// Enter (ICRNL on by default), so the `key.return` branch below
|
|
294
|
+
// catches them. But when stdin is a PTY whose parent writes raw
|
|
295
|
+
// `\n` (Python's `pty.fork` + `os.write(fd, b"\n")`, automation
|
|
296
|
+
// harnesses, certain SSH multiplexers), the LF arrives as a
|
|
297
|
+
// printable char.
|
|
298
|
+
//
|
|
299
|
+
// PR #697 (beta.45) fixed the case where `input === '\n'` exactly.
|
|
300
|
+
// CEO PTY smoke 2026-05-29 surfaced the REAL shape: when the parent
|
|
301
|
+
// writes the brief AND the Enter as separate `os.write` calls (or
|
|
302
|
+
// even when it doesn't), Node's stdin buffer COALESCES them into
|
|
303
|
+
// ONE chunk before Ink delivers the `useInput` event. The repro
|
|
304
|
+
// confirmed via stderr instrumentation: typing `hi\n` arrives in
|
|
305
|
+
// input-box as `bytes=[68 69 0a] len=3 flags=-` — a SINGLE 3-char
|
|
306
|
+
// chunk "hi\n" with no key flags. The PR #697 branch (`input ===
|
|
307
|
+
// '\n'`) does not match, so `hi\n` falls through to the printable-
|
|
308
|
+
// char branch and the literal newline lands in the buffer as
|
|
309
|
+
// `› hi\n █` (multi-line composer, brief never dispatches, status
|
|
310
|
+
// stays `idle` forever).
|
|
311
|
+
//
|
|
312
|
+
// Fix: detect a TRAILING `\n` in a printable chunk with no
|
|
313
|
+
// modifiers — type the prefix into the buffer, then submit. The
|
|
314
|
+
// discriminator that keeps multi-line paste working: the chunk
|
|
315
|
+
// must contain EXACTLY ONE `\n` (the trailing one) and no other
|
|
316
|
+
// newlines. Multi-line pastes have ≥2 `\n` characters (or arrive
|
|
317
|
+
// wrapped in bracketed-paste markers handled below), so they
|
|
318
|
+
// still preserve interior newlines via the printable-char branch.
|
|
319
|
+
//
|
|
320
|
+
// Detection contract:
|
|
321
|
+
// - `input` ends with `\n`
|
|
322
|
+
// - no Ctrl / Meta / Shift modifiers
|
|
323
|
+
// - exactly ONE `\n` in the chunk (the trailing one)
|
|
324
|
+
// - chunk is not bracketed-paste wrapped (markers stripped below)
|
|
325
|
+
//
|
|
326
|
+
// Edge cases covered by `test/input-box-lf-submit.spec.tsx`:
|
|
327
|
+
// - bare `\n` → submit empty (no-op on empty buf)
|
|
328
|
+
// - `hi\n` → splice `hi` + submit
|
|
329
|
+
// - `hi\nthere\n` (multi-line) → printable branch, preserves \n
|
|
330
|
+
// - `\r` (CR) → key.return branch unchanged
|
|
331
|
+
// - `hi\r\n` (CRLF) → key.return branch (CR wins first)
|
|
332
|
+
const endsWithLf = input.length > 0 && input.charCodeAt(input.length - 1) === 0x0a;
|
|
333
|
+
const newlineCount = (input.match(/\n/g) || []).length;
|
|
334
|
+
if (endsWithLf
|
|
335
|
+
&& newlineCount === 1
|
|
336
|
+
&& !key.meta
|
|
337
|
+
&& !key.ctrl
|
|
338
|
+
&& !key.shift) {
|
|
339
|
+
// Splice the prefix (everything before the trailing `\n`) into
|
|
340
|
+
// the buffer at the cursor, then run the canonical submit path.
|
|
341
|
+
// Refs (cursorRef / lineRef) hold the latest committed values so
|
|
342
|
+
// the splice runs against the operator's most recent edits even
|
|
343
|
+
// if a previous async paste / setState is still mid-flight.
|
|
344
|
+
const prefix = input.slice(0, -1);
|
|
345
|
+
let mergedLine = lineRef.current;
|
|
346
|
+
let mergedCursor = cursorRef.current;
|
|
347
|
+
if (prefix.length > 0) {
|
|
348
|
+
// Same sanitisation as the printable-char branch below — strip
|
|
349
|
+
// bracketed-paste markers so a stray escape sequence never
|
|
350
|
+
// lands in the submitted brief.
|
|
351
|
+
const stripped = prefix
|
|
352
|
+
.replace(/\x1b\[200~/g, '')
|
|
353
|
+
.replace(/\x1b\[201~/g, '')
|
|
354
|
+
.replace(/\[200~/g, '')
|
|
355
|
+
.replace(/\[201~/g, '');
|
|
356
|
+
if (stripped.length > 0) {
|
|
357
|
+
mergedLine =
|
|
358
|
+
mergedLine.slice(0, mergedCursor) + stripped + mergedLine.slice(mergedCursor);
|
|
359
|
+
mergedCursor = mergedCursor + stripped.length;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Synthesise the same payload-shape the `key.return` branch
|
|
363
|
+
// below uses so palette completion + history dedup + onSubmit
|
|
364
|
+
// dispatch all run identically.
|
|
365
|
+
const paletteHere = !paletteSuppressed
|
|
366
|
+
? filterPalette(mergedLine)
|
|
367
|
+
: { rows: [], totalBeforeLimit: 0 };
|
|
368
|
+
const paletteOpenHere = paletteHere.rows.length > 0;
|
|
369
|
+
const paletteFocusedIndexHere = paletteHere.rows.length === 0
|
|
370
|
+
? 0
|
|
371
|
+
: Math.min(paletteIndex, paletteHere.rows.length - 1);
|
|
372
|
+
let payload = mergedLine;
|
|
373
|
+
if (paletteOpenHere) {
|
|
374
|
+
const completed = completePalette(mergedLine, paletteHere.rows, paletteFocusedIndexHere);
|
|
375
|
+
if (completed !== null)
|
|
376
|
+
payload = completed;
|
|
377
|
+
}
|
|
378
|
+
const trimmed = payload.trim();
|
|
379
|
+
if (trimmed.length > 0) {
|
|
380
|
+
setHistory((prev) => {
|
|
381
|
+
if (prev[prev.length - 1] === trimmed)
|
|
382
|
+
return prev;
|
|
383
|
+
return [...prev, trimmed];
|
|
384
|
+
});
|
|
385
|
+
setHistoryIndex(-1);
|
|
386
|
+
if (props.workspaceSlug) {
|
|
387
|
+
appendHistory({
|
|
388
|
+
home: props.historyHome,
|
|
389
|
+
workspaceSlug: props.workspaceSlug,
|
|
390
|
+
brief: trimmed,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
props.onSubmit(trimmed);
|
|
394
|
+
}
|
|
395
|
+
setLine('');
|
|
396
|
+
setCursor(0);
|
|
397
|
+
setPaletteSuppressed(false);
|
|
398
|
+
setPaletteIndex(0);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
282
401
|
// Readline-style kill ring shortcuts. All four kills push the
|
|
283
402
|
// removed slice onto the ring; Ctrl+Y yanks the most recent.
|
|
284
403
|
if (key.ctrl && input === 'u') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pugi/cli",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.46",
|
|
4
4
|
"description": "Pugi CLI - terminal-native software execution system",
|
|
5
5
|
"homepage": "https://pugi.io",
|
|
6
6
|
"repository": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"undici": "^8.3.0",
|
|
56
56
|
"zod": "^3.23.0",
|
|
57
57
|
"@pugi/personas": "0.1.2",
|
|
58
|
-
"@pugi/sdk": "0.1.0-beta.
|
|
58
|
+
"@pugi/sdk": "0.1.0-beta.46"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^22.0.0",
|