@aragon-agent/cli 0.6.26 → 0.6.28
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 +51 -0
- package/README.md +33 -4
- package/dist/input/control-sequences.d.ts +71 -0
- package/dist/input/control-sequences.js +222 -0
- package/dist/input/paste-parse.d.ts +21 -9
- package/dist/input/paste-parse.js +23 -11
- package/dist/input/stdin-filter.js +56 -5
- package/dist/proc/terminal-plan.d.ts +20 -2
- package/dist/proc/terminal-plan.js +37 -4
- package/dist/session/branch-invocation.d.ts +53 -9
- package/dist/session/branch-invocation.js +52 -8
- package/dist/session/branch-launch.d.ts +9 -0
- package/dist/session/branch-launch.js +30 -13
- package/dist/tools/terminal-output.d.ts +8 -0
- package/dist/tools/terminal-output.js +10 -7
- package/dist/ui/display-tab.d.ts +51 -0
- package/dist/ui/display-tab.js +82 -0
- package/dist/ui/editor-navigation.d.ts +11 -1
- package/dist/ui/editor-navigation.js +14 -3
- package/dist/ui/entries/QueuedEntry.js +4 -1
- package/dist/ui/entries/UserEntry.js +8 -1
- package/dist/ui/layout/virtual-window.js +44 -11
- package/dist/ui/paste-frames.d.ts +5 -1
- package/dist/ui/paste-frames.js +7 -2
- package/dist/ui/transcript-text.js +11 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -77,6 +77,57 @@ were written.
|
|
|
77
77
|
|
|
78
78
|
### Fixed
|
|
79
79
|
|
|
80
|
+
- **A large paste arrived as runs of digits the moment it was sent - on screen,
|
|
81
|
+
in the model's input, in the prompt history and in the session file.**
|
|
82
|
+
`sanitisePaste` deleted the C0/C1 *introducer byte* and kept the parameter
|
|
83
|
+
bytes, so `ESC[38;2;18;52;86m` reached the transcript as the literal text
|
|
84
|
+
`[38;2;18;52;86m`. The composer collapses a large paste into
|
|
85
|
+
`[Pasted text #N +20 lines]`, so nothing was visible until submit expanded
|
|
86
|
+
it: a measured 20-line coloured log produced 80 runs of digits, and the same
|
|
87
|
+
string was what `/context`, the ↑ history and the session checkpoint stored.
|
|
88
|
+
Cleaning is now SEQUENCE-based and has one owner (new
|
|
89
|
+
`input/control-sequences.ts`, ASCII-only, no new dependency): a complete
|
|
90
|
+
escape goes as a unit, a truncated one loses its introducer and parameters
|
|
91
|
+
but never the character after them, an unterminated OSC/DCS/APC/PM/SOS goes
|
|
92
|
+
to the end of its line and never across it, and bare text that merely looks
|
|
93
|
+
like a sequence body (`[38;2m` with no ESC) is left exactly as it is.
|
|
94
|
+
`tools/terminal-output.ts`'s `stripAnsi` becomes a thin wrapper over the same
|
|
95
|
+
syntax, so a tool row and a paste cannot drift apart (both directions are
|
|
96
|
+
pinned by a cross-check test, and `terminal-output.test.ts` is unchanged).
|
|
97
|
+
A **single-line** paste carrying ESC - which `classifyChunk` sent down the
|
|
98
|
+
keystroke path with no cleaning at all - is now framed and cleaned like any
|
|
99
|
+
other paste; measured on the real component that path either put a live ESC
|
|
100
|
+
plus `[38;2;18;52;86m` into the draft or threw an uncaught `TypeError` inside
|
|
101
|
+
Ink's key parser. win32-input-mode records inside a frame are translated
|
|
102
|
+
rather than deleted, so a `?9001h` paste yields its characters instead of 15
|
|
103
|
+
visible digits per key (or, with cleaning but no translation, nothing at all).
|
|
104
|
+
- **A pasted TAB-indented block or TSV table lined up differently before and
|
|
105
|
+
after sending.** One byte had three readings: the composer painted
|
|
106
|
+
four-column tab stops, the transcript handed the raw `\t` to the terminal
|
|
107
|
+
(eight), and the exit replay counted code units. New `ui/display-tab.ts`
|
|
108
|
+
expands a TAB by DISPLAY column (`string-width`, so a `CJK<TAB>X` line lands
|
|
109
|
+
where the terminal paints it); the composer's stop is the terminal's eight, and
|
|
110
|
+
`UserEntry`, `QueuedEntry` and the exit replay expand at paint time only -
|
|
111
|
+
the stored payload keeps its `\t`, so the model still receives the original
|
|
112
|
+
indentation. `wrappedRows` counts display columns too (with an ASCII fast
|
|
113
|
+
path, without which a 10 000-entry transcript timed out), because an estimate
|
|
114
|
+
that reads a TAB as one column keeps the row short while the painted row
|
|
115
|
+
grows - the under-estimate that file names as the unsafe direction.
|
|
116
|
+
- **`Ctrl+L` opened a window that failed with a PowerShell `ParserError` on
|
|
117
|
+
Windows.** The script handed to `-EncodedCommand` was the argv joined with
|
|
118
|
+
quotes but with no call operator, and PowerShell reads a quoted string in command
|
|
119
|
+
position as an *expression*: with node installed under `C:\Program Files\...`
|
|
120
|
+
the child was never created - `UnexpectedToken` at column 36 - while the parent
|
|
121
|
+
TUI still reported that the window opened. `powerShellScript` now emits
|
|
122
|
+
`& <argv...>`, which is the only form that runs a word of any shape as a
|
|
123
|
+
command. The failure fallback follows: it is now two labelled lines
|
|
124
|
+
(`PowerShell:` / `cmd.exe :`), because one line cannot be correct for both
|
|
125
|
+
parsers - and the `PowerShell:` line is byte-for-byte the script the window
|
|
126
|
+
receives. A leading `@` can no longer be left bare either, since `@name` is
|
|
127
|
+
PowerShell's splat operator and silently swallows an argument. New
|
|
128
|
+
`powershell-script-parse.test.ts` hands the generated scripts to the real
|
|
129
|
+
Windows PowerShell parser (and pins the old shape to its exact error), so this
|
|
130
|
+
class of defect has a guard instead of a green suite.
|
|
80
131
|
- **A conversation saved mid-tool could be resumed into an illegal history.**
|
|
81
132
|
A `tool_call` with no matching `tool_result` makes the next request fail. The
|
|
82
133
|
checkpoint writer closes every dangling call on the copy it writes and re-checks
|
package/README.md
CHANGED
|
@@ -539,7 +539,7 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
|
|
|
539
539
|
| `Ctrl+W`, `Alt+Backspace` | Delete the previous word |
|
|
540
540
|
| `Ctrl+U` / `Ctrl+K` | Kill to line start / end |
|
|
541
541
|
| `Up` / `Down` | 按视觉行移动;空草稿或正在浏览历史时,才在边界召回历史,避免覆盖新草稿 |
|
|
542
|
-
| Paste | Inserted
|
|
542
|
+
| Paste | Inserted up to 6 lines / 400 chars; larger collapses to `[Pasted text #1 +15 lines]`, which deletes as one unit and expands in full when you send. Escape sequences are removed as sequences (so a coloured log arrives as its text, not as `[38;2;18;52;86m`), and a TAB is kept in the payload but laid out at the terminal's 8-column tab stops, so the draft and the echoed message line up. `--no-paste` restores the pre-0.6.3 behaviour |
|
|
543
543
|
| `?` | Open help (empty input) |
|
|
544
544
|
|
|
545
545
|
In **full-screen mode**, PgUp/PgDn, Shift+Up/Down and the right-edge scrollbar
|
|
@@ -1838,16 +1838,45 @@ The platform ladder, in order:
|
|
|
1838
1838
|
The shell is `pwsh.exe` then `powershell.exe` on Windows, and `$SHELL` then `bash`
|
|
1839
1839
|
then `/bin/sh` elsewhere. Windows receives the command as `-EncodedCommand` (base64
|
|
1840
1840
|
of UTF-16LE), because that alphabet has no spaces, quotes or `cmd` metacharacters -
|
|
1841
|
-
it survives `cmd` -> `start` -> PowerShell without a quoting argument.
|
|
1841
|
+
it survives `cmd` -> `start` -> PowerShell without a quoting argument. The payload
|
|
1842
|
+
that gets encoded is `& <argv...>`, and **the leading `&` is load-bearing**: in
|
|
1843
|
+
command position PowerShell reads a quoted string as an *expression*, so with a node
|
|
1844
|
+
install under `C:\Program Files\...` the script `'C:\Program Files\nodejs\node.exe'
|
|
1845
|
+
<script>` is a `ParserError ... UnexpectedToken` and no child is ever created - while
|
|
1846
|
+
the parent still reports that the window opened. `&` is PowerShell's call operator:
|
|
1847
|
+
it takes the next word as a command name whatever its shape.
|
|
1842
1848
|
|
|
1843
1849
|
When no terminal can be opened - an SSH session, a container, a machine with no
|
|
1844
|
-
graphical session - the command opens nothing and prints the fallback instead
|
|
1850
|
+
graphical session - the command opens nothing and prints the fallback instead. On
|
|
1851
|
+
Windows that fallback is **two labelled lines**, because one line cannot be correct
|
|
1852
|
+
for both parsers (`& 'x'` is right for PowerShell and wrong for `cmd.exe`, which
|
|
1853
|
+
keeps the single quotes; `"x"` is right for `cmd.exe` and an expression to
|
|
1854
|
+
PowerShell):
|
|
1855
|
+
|
|
1856
|
+
```
|
|
1857
|
+
No terminal window could be opened here. Run this in another terminal:
|
|
1858
|
+
PowerShell: & aragon --resume a4k2m9
|
|
1859
|
+
cmd.exe : aragon --resume a4k2m9
|
|
1860
|
+
```
|
|
1861
|
+
|
|
1862
|
+
Copy the line that matches the terminal you opened. macOS and Linux keep the single
|
|
1863
|
+
line they have always printed:
|
|
1845
1864
|
|
|
1846
1865
|
```
|
|
1847
1866
|
No terminal window could be opened here. Run this in another terminal:
|
|
1848
1867
|
aragon --resume a4k2m9
|
|
1849
1868
|
```
|
|
1850
1869
|
|
|
1870
|
+
A `node <dist/cli.js>` build prints the same two lines with the full paths, quoted:
|
|
1871
|
+
|
|
1872
|
+
```
|
|
1873
|
+
PowerShell: & 'C:\Program Files\nodejs\node.exe' C:\Users\me\AppData\Roaming\npm\node_modules\@aragon-agent\cli\dist\launcher.js --resume a4k2m9
|
|
1874
|
+
cmd.exe : "C:\Program Files\nodejs\node.exe" C:\Users\me\AppData\Roaming\npm\node_modules\@aragon-agent\cli\dist\launcher.js --resume a4k2m9
|
|
1875
|
+
```
|
|
1876
|
+
|
|
1877
|
+
The `PowerShell:` line is byte-for-byte the script the new window receives, so the
|
|
1878
|
+
fallback can never drift from what the feature actually runs.
|
|
1879
|
+
|
|
1851
1880
|
Two environment variables exist because neither belongs in a config file:
|
|
1852
1881
|
|
|
1853
1882
|
| Variable | Effect |
|
|
@@ -2190,7 +2219,7 @@ env / `.env` → CLI flags**.
|
|
|
2190
2219
|
| `planModeDefault` | `false` | Start every session in plan mode. `--no-plan` still overrides it for a single run. |
|
|
2191
2220
|
| `planModeMaxAskRounds` | `4` | `ask_user` rounds allowed per user message, clamped to `[1, 10]`. The counter resets on your next message; steering does **not** reset it. |
|
|
2192
2221
|
| `planModeHumanTimeoutMs` | `1800000` | Ceiling on a single question / plan wait, clamped to `[60000, 7200000]`. Thirty minutes is far outside "went to get coffee", but a ceiling has to exist so an unattended terminal cannot wedge a CI job forever. |
|
|
2193
|
-
| `paste` | `true` | Recognise pasted text before Ink's key parser sees it: line breaks become buffer newlines instead of fifteen separate messages,
|
|
2222
|
+
| `paste` | `true` | Recognise pasted text before Ink's key parser sees it: line breaks become buffer newlines instead of fifteen separate messages, escape SEQUENCES are removed whole (deleting only the introducer byte is what put `[38;2;18;52;86m` on screen and in the model's input) as are stray control bytes, and a paste above 6 lines or 400 characters collapses to a one-line placeholder that expands when you send. `false` treats pasted bytes as keystrokes, which is what 0.6.2 did. |
|
|
2194
2223
|
| `historyEnabled` | `true` | Record submitted prompts to `prompt-history.jsonl` for `↑` recall. Setting it to `false` stops new writes; it does not delete what is already stored (that is `aragon history clear --yes`) and does not stop reading. See [Prompt history](#prompt-history). |
|
|
2195
2224
|
| `team.enabled` | `true` | Master switch for team subagents. `false` means the `task` tool is never registered — `/team on` in such a session saves the setting for next launch and says so, rather than advertising a tool that is not there. |
|
|
2196
2225
|
| `team.maxSubagents` | `5` | Subagents per dispatch, clamped to `[1, 10]`. The 10 is the hard ceiling and is enforced twice — on the config value and again on what the model asks for — so hand-editing this file cannot raise it. |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control-SEQUENCE removal for text that crosses a boundary where a terminal
|
|
3
|
+
* escape must never become content (paste-control-sequence-residue, fix A'-1).
|
|
4
|
+
*
|
|
5
|
+
* WHY A DELETE-THE-WHOLE-SEQUENCE RULE EXISTS. Deleting only the introducer
|
|
6
|
+
* byte turns an escape into visible text: `ESC [ 3 8 ; 2 m` loses its ESC and
|
|
7
|
+
* leaves `[38;2m`, which is a run of digits and semicolons somebody then reads
|
|
8
|
+
* on screen and sends to the model. The bytes were never content; the sequence
|
|
9
|
+
* has to go as a unit.
|
|
10
|
+
*
|
|
11
|
+
* WHY IT IS NOT `tools/terminal-output.ts`'s REGEXES MOVED HERE. Those four
|
|
12
|
+
* regexes answer "what does this look like" for a terminal row that is still
|
|
13
|
+
* being written, and they are fed text whose unfinished tail has ALREADY been
|
|
14
|
+
* cut off and parked in a carry by the caller (`sanitizeChunk`,
|
|
15
|
+
* `toDisplayRow`). Pasted text is one-shot: there is no next chunk, so a
|
|
16
|
+
* truncated sequence needs a rule of its own. Measured against the stream
|
|
17
|
+
* version of the same input, `ESC [ 3 8 ; 2 ; 1 8 ; 5 2 ; 8 6` followed by
|
|
18
|
+
* prose loses the first letter of the prose (`n`), and an unterminated OSC
|
|
19
|
+
* leaves its whole body behind.
|
|
20
|
+
*
|
|
21
|
+
* THE FOUR DECISIONS THIS MODULE ENCODES:
|
|
22
|
+
*
|
|
23
|
+
* 1. A sequence introduced by a real introducer byte (`0x1B`, `0x9B`, `0x9D`,
|
|
24
|
+
* ...) is removed as a unit; bare text that merely LOOKS like the inside of
|
|
25
|
+
* one (`[38;2;18;52;86m` typed or pasted without an ESC) is left exactly as
|
|
26
|
+
* it is, character for character.
|
|
27
|
+
* 2. A string-type sequence (OSC / DCS / SOS / PM / APC) runs to BEL or ST, and
|
|
28
|
+
* an unterminated one is removed UP TO THE END OF ITS LINE. It never crosses
|
|
29
|
+
* `\n` and never guesses at a terminator: a truncated sequence is the most
|
|
30
|
+
* common shape in a clipboard, and the only safe reading of it is "the rest
|
|
31
|
+
* of this line was never text".
|
|
32
|
+
* 3. A truncated CSI loses its introducer and its parameter bytes and NOTHING
|
|
33
|
+
* ELSE. `ESC [ 3 1 m` split mid-parameter is parameters, not prose, but the
|
|
34
|
+
* byte after them is the user's own character and must survive.
|
|
35
|
+
* 4. A C1 CSI (`0x9B`) with no parameter byte at all is treated as the stray
|
|
36
|
+
* control byte it is most likely to be rather than as a sequence, because a
|
|
37
|
+
* lone `0x9B` followed by one letter is otherwise indistinguishable from a
|
|
38
|
+
* non-UTF-8 file's stray byte followed by content -- and consuming that
|
|
39
|
+
* letter would be the very data loss this module exists to prevent.
|
|
40
|
+
*
|
|
41
|
+
* Pure, ASCII-only, React-free, Ink-free, and TOTAL: it never throws and it
|
|
42
|
+
* never reorders or mangles the bytes it keeps. No new dependency.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Remove every escape/control sequence, keeping all other bytes in order.
|
|
46
|
+
*
|
|
47
|
+
* KEEPS `\t`, `\n` AND `\r`: they are line structure, and `\t` inside pasted
|
|
48
|
+
* indentation is content the model is supposed to receive. Stray C0/C1 bytes
|
|
49
|
+
* (and DEL) are NOT this function's job -- they are removed by the caller that
|
|
50
|
+
* wants them gone, so one rule has one owner.
|
|
51
|
+
*/
|
|
52
|
+
export declare function stripControlSequences(text: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* True when `text` carries a sequence a KEYBOARD cannot physically produce.
|
|
55
|
+
*
|
|
56
|
+
* Used at the keystroke branch of the stdin filter: a single-line paste that
|
|
57
|
+
* carries ESC fails `classifyChunk`'s two rules (no line break, and
|
|
58
|
+
* `HAS_CONTROL_EXCEPT_TAB`) and would otherwise reach Ink as raw bytes --
|
|
59
|
+
* either typing `[38;2;18;52;86m` with a live ESC into the draft, or throwing
|
|
60
|
+
* an uncaught `TypeError` inside Ink's key parser.
|
|
61
|
+
*
|
|
62
|
+
* THREE THINGS MUST NOT MATCH, and the shapes below are why:
|
|
63
|
+
* - SGR mouse reports (`ESC [ < 0 ; 1 ; 2 m`) are stripped by `splitMouseEvents`
|
|
64
|
+
* BEFORE this test, and `<` is not a parameter byte, so the SGR rule cannot
|
|
65
|
+
* see them anyway.
|
|
66
|
+
* - Arrow / function / Home / End keys (`ESC [ A`, `ESC [ 1 ; 5 D`, `ESC [ 3 ~`,
|
|
67
|
+
* `ESC [ Z`) never end in `m`.
|
|
68
|
+
* - `Alt`+letter (`ESC` followed by one character) has no introducer body, so
|
|
69
|
+
* the string-type rule does not fire.
|
|
70
|
+
*/
|
|
71
|
+
export declare function hasPasteOnlySequence(text: string): boolean;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control-SEQUENCE removal for text that crosses a boundary where a terminal
|
|
3
|
+
* escape must never become content (paste-control-sequence-residue, fix A'-1).
|
|
4
|
+
*
|
|
5
|
+
* WHY A DELETE-THE-WHOLE-SEQUENCE RULE EXISTS. Deleting only the introducer
|
|
6
|
+
* byte turns an escape into visible text: `ESC [ 3 8 ; 2 m` loses its ESC and
|
|
7
|
+
* leaves `[38;2m`, which is a run of digits and semicolons somebody then reads
|
|
8
|
+
* on screen and sends to the model. The bytes were never content; the sequence
|
|
9
|
+
* has to go as a unit.
|
|
10
|
+
*
|
|
11
|
+
* WHY IT IS NOT `tools/terminal-output.ts`'s REGEXES MOVED HERE. Those four
|
|
12
|
+
* regexes answer "what does this look like" for a terminal row that is still
|
|
13
|
+
* being written, and they are fed text whose unfinished tail has ALREADY been
|
|
14
|
+
* cut off and parked in a carry by the caller (`sanitizeChunk`,
|
|
15
|
+
* `toDisplayRow`). Pasted text is one-shot: there is no next chunk, so a
|
|
16
|
+
* truncated sequence needs a rule of its own. Measured against the stream
|
|
17
|
+
* version of the same input, `ESC [ 3 8 ; 2 ; 1 8 ; 5 2 ; 8 6` followed by
|
|
18
|
+
* prose loses the first letter of the prose (`n`), and an unterminated OSC
|
|
19
|
+
* leaves its whole body behind.
|
|
20
|
+
*
|
|
21
|
+
* THE FOUR DECISIONS THIS MODULE ENCODES:
|
|
22
|
+
*
|
|
23
|
+
* 1. A sequence introduced by a real introducer byte (`0x1B`, `0x9B`, `0x9D`,
|
|
24
|
+
* ...) is removed as a unit; bare text that merely LOOKS like the inside of
|
|
25
|
+
* one (`[38;2;18;52;86m` typed or pasted without an ESC) is left exactly as
|
|
26
|
+
* it is, character for character.
|
|
27
|
+
* 2. A string-type sequence (OSC / DCS / SOS / PM / APC) runs to BEL or ST, and
|
|
28
|
+
* an unterminated one is removed UP TO THE END OF ITS LINE. It never crosses
|
|
29
|
+
* `\n` and never guesses at a terminator: a truncated sequence is the most
|
|
30
|
+
* common shape in a clipboard, and the only safe reading of it is "the rest
|
|
31
|
+
* of this line was never text".
|
|
32
|
+
* 3. A truncated CSI loses its introducer and its parameter bytes and NOTHING
|
|
33
|
+
* ELSE. `ESC [ 3 1 m` split mid-parameter is parameters, not prose, but the
|
|
34
|
+
* byte after them is the user's own character and must survive.
|
|
35
|
+
* 4. A C1 CSI (`0x9B`) with no parameter byte at all is treated as the stray
|
|
36
|
+
* control byte it is most likely to be rather than as a sequence, because a
|
|
37
|
+
* lone `0x9B` followed by one letter is otherwise indistinguishable from a
|
|
38
|
+
* non-UTF-8 file's stray byte followed by content -- and consuming that
|
|
39
|
+
* letter would be the very data loss this module exists to prevent.
|
|
40
|
+
*
|
|
41
|
+
* Pure, ASCII-only, React-free, Ink-free, and TOTAL: it never throws and it
|
|
42
|
+
* never reorders or mangles the bytes it keeps. No new dependency.
|
|
43
|
+
*/
|
|
44
|
+
/** ESC -- the introducer every terminal shares. */
|
|
45
|
+
const ESC = 0x1b;
|
|
46
|
+
/** LF. No rule in this module may consume it (decision 2). */
|
|
47
|
+
const LF = 0x0a;
|
|
48
|
+
/** BEL terminates an OSC. */
|
|
49
|
+
const BEL = 0x07;
|
|
50
|
+
/** `[` after ESC is CSI, and `\` after ESC is the two-byte ST. */
|
|
51
|
+
const BRACKET = 0x5b;
|
|
52
|
+
const BACKSLASH = 0x5c;
|
|
53
|
+
/** The ASCII second byte of each string-type sequence (`ESC ]`, `ESC P`, ...). */
|
|
54
|
+
const ESC_DCS = 0x50;
|
|
55
|
+
const ESC_SOS = 0x58;
|
|
56
|
+
const ESC_OSC = 0x5d;
|
|
57
|
+
const ESC_PM = 0x5e;
|
|
58
|
+
const ESC_APC = 0x5f;
|
|
59
|
+
/** C1 introducers, as written by a source that is not UTF-8 clean. */
|
|
60
|
+
const C1_CSI = 0x9b;
|
|
61
|
+
const C1_ST = 0x9c;
|
|
62
|
+
const C1_OSC = 0x9d;
|
|
63
|
+
const C1_DCS = 0x90;
|
|
64
|
+
const C1_SOS = 0x98;
|
|
65
|
+
const C1_PM = 0x9e;
|
|
66
|
+
const C1_APC = 0x9f;
|
|
67
|
+
/** Every byte that can START a sequence this module removes. */
|
|
68
|
+
const ANY_INTRODUCER = /[\u001b\u0090\u0098\u009b\u009d\u009e\u009f]/;
|
|
69
|
+
/**
|
|
70
|
+
* `CSI ... m` (SGR), in both encodings: `ESC [ digits ; m` and the single-byte
|
|
71
|
+
* C1 form `0x9B digits ; m`. NO KEY PRESS PRODUCES ONE, which is what makes it
|
|
72
|
+
* usable as the discriminator for an unframed paste in `stdin-filter.ts`.
|
|
73
|
+
*/
|
|
74
|
+
const SGR_SEQUENCE = /\u001b\[[0-9;]*m|\u009b[0-9;]*m/;
|
|
75
|
+
/**
|
|
76
|
+
* A string-type introducer followed by at least one body byte. An `Alt`+key
|
|
77
|
+
* press is exactly two bytes, so "introducer plus a body" is already outside
|
|
78
|
+
* what a keyboard can emit.
|
|
79
|
+
*/
|
|
80
|
+
const STRING_SEQUENCE = /(?:\u001b[\u0050\u0058\u005d\u005e\u005f]|[\u0090\u0098\u009d\u009e\u009f])[\s\S]/;
|
|
81
|
+
/** CSI final byte: `@` through `~`. */
|
|
82
|
+
function isFinalByte(code) {
|
|
83
|
+
return code >= 0x40 && code <= 0x7e;
|
|
84
|
+
}
|
|
85
|
+
/** CSI parameter byte: `0` through `?`. */
|
|
86
|
+
function isParamByte(code) {
|
|
87
|
+
return code >= 0x30 && code <= 0x3f;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* CSI intermediate byte: `!` through `/`.
|
|
91
|
+
*
|
|
92
|
+
* SPACE (`0x20`) IS DELIBERATELY EXCLUDED (decision 3). The standard allows it,
|
|
93
|
+
* but a pasted sequence cut mid-parameter is followed by ` ` and then prose,
|
|
94
|
+
* and the first letter of that prose is a legal final byte -- so honouring the
|
|
95
|
+
* space would silently eat it. `CSI Ps SP q` (cursor style) is the only shape
|
|
96
|
+
* that gets worse, and no child process writes that to its stdout.
|
|
97
|
+
*/
|
|
98
|
+
function isIntermediateByte(code) {
|
|
99
|
+
return code >= 0x21 && code <= 0x2f;
|
|
100
|
+
}
|
|
101
|
+
/** True for the C1 introducers that open a string, not a CSI. */
|
|
102
|
+
function isC1StringIntroducer(code) {
|
|
103
|
+
return code === C1_OSC || code === C1_DCS || code === C1_SOS || code === C1_PM || code === C1_APC;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* End of the string-type sequence whose body starts at `from`.
|
|
107
|
+
*
|
|
108
|
+
* Stops AT the line break rather than past it, and reports "consumed to here"
|
|
109
|
+
* for a body that never terminates (decision 2).
|
|
110
|
+
*/
|
|
111
|
+
function stringEnd(text, from) {
|
|
112
|
+
let i = from;
|
|
113
|
+
while (i < text.length) {
|
|
114
|
+
const code = text.charCodeAt(i);
|
|
115
|
+
if (code === BEL || code === C1_ST)
|
|
116
|
+
return i + 1;
|
|
117
|
+
if (code === LF)
|
|
118
|
+
return i;
|
|
119
|
+
if (code === ESC && text.charCodeAt(i + 1) === BACKSLASH)
|
|
120
|
+
return i + 2;
|
|
121
|
+
i += 1;
|
|
122
|
+
}
|
|
123
|
+
return i;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* End of the CSI whose parameter run starts at `from`.
|
|
127
|
+
*
|
|
128
|
+
* `requireParams` implements decision 4: with no parameter byte at all, a C1
|
|
129
|
+
* CSI is only the introducer byte, and its "final byte" is content.
|
|
130
|
+
*/
|
|
131
|
+
function csiEnd(text, from, requireParams) {
|
|
132
|
+
let i = from;
|
|
133
|
+
let sawParams = false;
|
|
134
|
+
while (i < text.length && isParamByte(text.charCodeAt(i))) {
|
|
135
|
+
sawParams = true;
|
|
136
|
+
i += 1;
|
|
137
|
+
}
|
|
138
|
+
if (requireParams && !sawParams)
|
|
139
|
+
return from;
|
|
140
|
+
while (i < text.length && isIntermediateByte(text.charCodeAt(i)))
|
|
141
|
+
i += 1;
|
|
142
|
+
if (i < text.length && isFinalByte(text.charCodeAt(i)))
|
|
143
|
+
return i + 1;
|
|
144
|
+
return i;
|
|
145
|
+
}
|
|
146
|
+
/** End of the sequence starting with ESC at `index` (decision 1 / 3). */
|
|
147
|
+
function escapeEnd(text, index) {
|
|
148
|
+
if (index + 1 >= text.length)
|
|
149
|
+
return index + 1;
|
|
150
|
+
const next = text.charCodeAt(index + 1);
|
|
151
|
+
if (next === BRACKET)
|
|
152
|
+
return csiEnd(text, index + 2, false);
|
|
153
|
+
if (next === ESC_OSC || next === ESC_DCS || next === ESC_SOS || next === ESC_PM
|
|
154
|
+
|| next === ESC_APC) {
|
|
155
|
+
return stringEnd(text, index + 2);
|
|
156
|
+
}
|
|
157
|
+
if (next === LF)
|
|
158
|
+
return index + 1;
|
|
159
|
+
return index + 2;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* End of the sequence starting at `index`, or `-1` when no sequence starts
|
|
163
|
+
* there.
|
|
164
|
+
*/
|
|
165
|
+
function sequenceEnd(text, index) {
|
|
166
|
+
const code = text.charCodeAt(index);
|
|
167
|
+
if (code === ESC)
|
|
168
|
+
return escapeEnd(text, index);
|
|
169
|
+
if (code === C1_CSI)
|
|
170
|
+
return csiEnd(text, index + 1, true);
|
|
171
|
+
if (isC1StringIntroducer(code)) {
|
|
172
|
+
return index + 1 >= text.length ? index + 1 : stringEnd(text, index + 1);
|
|
173
|
+
}
|
|
174
|
+
return -1;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Remove every escape/control sequence, keeping all other bytes in order.
|
|
178
|
+
*
|
|
179
|
+
* KEEPS `\t`, `\n` AND `\r`: they are line structure, and `\t` inside pasted
|
|
180
|
+
* indentation is content the model is supposed to receive. Stray C0/C1 bytes
|
|
181
|
+
* (and DEL) are NOT this function's job -- they are removed by the caller that
|
|
182
|
+
* wants them gone, so one rule has one owner.
|
|
183
|
+
*/
|
|
184
|
+
export function stripControlSequences(text) {
|
|
185
|
+
if (!ANY_INTRODUCER.test(text))
|
|
186
|
+
return text;
|
|
187
|
+
let out = '';
|
|
188
|
+
let kept = 0;
|
|
189
|
+
let i = 0;
|
|
190
|
+
while (i < text.length) {
|
|
191
|
+
const end = sequenceEnd(text, i);
|
|
192
|
+
if (end === -1) {
|
|
193
|
+
i += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
out += text.slice(kept, i);
|
|
197
|
+
i = end;
|
|
198
|
+
kept = end;
|
|
199
|
+
}
|
|
200
|
+
return kept === 0 ? text : out + text.slice(kept);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* True when `text` carries a sequence a KEYBOARD cannot physically produce.
|
|
204
|
+
*
|
|
205
|
+
* Used at the keystroke branch of the stdin filter: a single-line paste that
|
|
206
|
+
* carries ESC fails `classifyChunk`'s two rules (no line break, and
|
|
207
|
+
* `HAS_CONTROL_EXCEPT_TAB`) and would otherwise reach Ink as raw bytes --
|
|
208
|
+
* either typing `[38;2;18;52;86m` with a live ESC into the draft, or throwing
|
|
209
|
+
* an uncaught `TypeError` inside Ink's key parser.
|
|
210
|
+
*
|
|
211
|
+
* THREE THINGS MUST NOT MATCH, and the shapes below are why:
|
|
212
|
+
* - SGR mouse reports (`ESC [ < 0 ; 1 ; 2 m`) are stripped by `splitMouseEvents`
|
|
213
|
+
* BEFORE this test, and `<` is not a parameter byte, so the SGR rule cannot
|
|
214
|
+
* see them anyway.
|
|
215
|
+
* - Arrow / function / Home / End keys (`ESC [ A`, `ESC [ 1 ; 5 D`, `ESC [ 3 ~`,
|
|
216
|
+
* `ESC [ Z`) never end in `m`.
|
|
217
|
+
* - `Alt`+letter (`ESC` followed by one character) has no introducer body, so
|
|
218
|
+
* the string-type rule does not fire.
|
|
219
|
+
*/
|
|
220
|
+
export function hasPasteOnlySequence(text) {
|
|
221
|
+
return SGR_SEQUENCE.test(text) || STRING_SEQUENCE.test(text);
|
|
222
|
+
}
|
|
@@ -56,16 +56,28 @@ export declare function classifyChunk(text: string, minBurstChars?: number): 'pa
|
|
|
56
56
|
* newline in pasted text, and a lone LF is dropped by Ink's key parser
|
|
57
57
|
* entirely, so both have to be normalised here or a pasted line break either
|
|
58
58
|
* submits the message or silently vanishes.
|
|
59
|
-
* 2. Every
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
59
|
+
* 2. Every escape SEQUENCE is removed AS A SEQUENCE (`stripControlSequences`).
|
|
60
|
+
* Deleting only the introducer byte is what produced the reported defect:
|
|
61
|
+
* `ESC [ 3 8 ; 2 ; 1 8 ; 5 2 ; 8 6 m` minus its ESC is `[38;2;18;52;86m`,
|
|
62
|
+
* a run of digits Ink then writes to the terminal as ordinary text -- and it
|
|
63
|
+
* reached the model, the prompt history and the session file too, because
|
|
64
|
+
* this function is the only cleaning the payload ever gets.
|
|
65
|
+
* 3. Every remaining C0 byte except `\n` and `\t` is removed. Ink writes
|
|
66
|
+
* `<Text>` children to the terminal without escaping anything, so a
|
|
67
|
+
* surviving `\r` overwrites the row it is drawn on and a surviving `ESC`
|
|
68
|
+
* repaints the frame colours. Sequences are gone by this point, so what is
|
|
69
|
+
* left for this step is a stray byte, never half of one.
|
|
70
|
+
* 4. DEL and every C1 byte (U+0080..U+009F) go too, and that is NOT padding:
|
|
71
|
+
* U+009B is the single-byte CSI and U+0085 is NEL, and a terminal that
|
|
72
|
+
* decodes them acts on them exactly as it acts on `ESC [`. A file pasted out
|
|
73
|
+
* of a non-UTF-8 editor walks straight through a "strip C0" rule.
|
|
74
|
+
* 5. TAB is KEPT. Indentation is content, and it is what the model receives.
|
|
75
|
+
* Expanding it is the RENDERERS' job (`ui/display-tab.ts`), so the model
|
|
76
|
+
* keeps the original indentation while the screen, the composer and the
|
|
77
|
+
* terminal all agree on what column a TAB lands on.
|
|
67
78
|
*
|
|
68
79
|
* Applied to PAYLOAD ONLY, never to keystrokes, so `Ctrl+A` and Escape keep
|
|
69
|
-
* working. Non-ASCII text (CJK, emoji) is untouched.
|
|
80
|
+
* working. Non-ASCII text (CJK, emoji) is untouched. Idempotent, so the
|
|
81
|
+
* second pass in `ui/paste-frames.ts` stays a byte-identical no-op.
|
|
70
82
|
*/
|
|
71
83
|
export declare function sanitisePaste(raw: string): string;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* `DECSET 2004`. Both are about what a keyboard can physically emit.
|
|
17
17
|
*/
|
|
18
18
|
import { PASTE_MIN_BURST_CHARS } from './limits.js';
|
|
19
|
+
import { stripControlSequences } from './control-sequences.js';
|
|
19
20
|
/** `DECSET 2004` wraps a paste in these two markers. */
|
|
20
21
|
export const PASTE_BEGIN_MARK = '\x1b[200~';
|
|
21
22
|
export const PASTE_END_MARK = '\x1b[201~';
|
|
@@ -76,20 +77,31 @@ export function classifyChunk(text, minBurstChars = PASTE_MIN_BURST_CHARS) {
|
|
|
76
77
|
* newline in pasted text, and a lone LF is dropped by Ink's key parser
|
|
77
78
|
* entirely, so both have to be normalised here or a pasted line break either
|
|
78
79
|
* submits the message or silently vanishes.
|
|
79
|
-
* 2. Every
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
80
|
+
* 2. Every escape SEQUENCE is removed AS A SEQUENCE (`stripControlSequences`).
|
|
81
|
+
* Deleting only the introducer byte is what produced the reported defect:
|
|
82
|
+
* `ESC [ 3 8 ; 2 ; 1 8 ; 5 2 ; 8 6 m` minus its ESC is `[38;2;18;52;86m`,
|
|
83
|
+
* a run of digits Ink then writes to the terminal as ordinary text -- and it
|
|
84
|
+
* reached the model, the prompt history and the session file too, because
|
|
85
|
+
* this function is the only cleaning the payload ever gets.
|
|
86
|
+
* 3. Every remaining C0 byte except `\n` and `\t` is removed. Ink writes
|
|
87
|
+
* `<Text>` children to the terminal without escaping anything, so a
|
|
88
|
+
* surviving `\r` overwrites the row it is drawn on and a surviving `ESC`
|
|
89
|
+
* repaints the frame colours. Sequences are gone by this point, so what is
|
|
90
|
+
* left for this step is a stray byte, never half of one.
|
|
91
|
+
* 4. DEL and every C1 byte (U+0080..U+009F) go too, and that is NOT padding:
|
|
92
|
+
* U+009B is the single-byte CSI and U+0085 is NEL, and a terminal that
|
|
93
|
+
* decodes them acts on them exactly as it acts on `ESC [`. A file pasted out
|
|
94
|
+
* of a non-UTF-8 editor walks straight through a "strip C0" rule.
|
|
95
|
+
* 5. TAB is KEPT. Indentation is content, and it is what the model receives.
|
|
96
|
+
* Expanding it is the RENDERERS' job (`ui/display-tab.ts`), so the model
|
|
97
|
+
* keeps the original indentation while the screen, the composer and the
|
|
98
|
+
* terminal all agree on what column a TAB lands on.
|
|
87
99
|
*
|
|
88
100
|
* Applied to PAYLOAD ONLY, never to keystrokes, so `Ctrl+A` and Escape keep
|
|
89
|
-
* working. Non-ASCII text (CJK, emoji) is untouched.
|
|
101
|
+
* working. Non-ASCII text (CJK, emoji) is untouched. Idempotent, so the
|
|
102
|
+
* second pass in `ui/paste-frames.ts` stays a byte-identical no-op.
|
|
90
103
|
*/
|
|
91
104
|
export function sanitisePaste(raw) {
|
|
92
|
-
return raw
|
|
93
|
-
.replace(/\r\n?/g, '\n')
|
|
105
|
+
return stripControlSequences(raw.replace(/\r\n?/g, '\n'))
|
|
94
106
|
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
|
|
95
107
|
}
|
|
@@ -33,8 +33,9 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { PassThrough } from 'node:stream';
|
|
35
35
|
import { MAX_PENDING_MOUSE_CHARS, splitMouseEvents, isMousePrefix, } from './mouse-events.js';
|
|
36
|
-
import { ENTER_NEWLINE_FRAME, PASTE_ASSEMBLY_MAX_MS, PASTE_BURST_MS, PASTE_CLOSE, PASTE_MAX_BYTES, PASTE_OPEN, formatPasteSize, } from './limits.js';
|
|
36
|
+
import { ENTER_NEWLINE_FRAME, INDEX_KEY_FRAME, PASTE_ASSEMBLY_MAX_MS, PASTE_BURST_MS, PASTE_CLOSE, PASTE_MAX_BYTES, PASTE_OPEN, formatPasteSize, } from './limits.js';
|
|
37
37
|
import { PASTE_BEGIN_MARK, PASTE_END_MARK, classifyChunk, sanitisePaste, trailingPastePrefixLength, } from './paste-parse.js';
|
|
38
|
+
import { hasPasteOnlySequence } from './control-sequences.js';
|
|
38
39
|
import { splitEnterSequences, trailingEnterPrefixLength } from './enter-sequences.js';
|
|
39
40
|
import { splitCsiUKeys, trailingCsiUPrefixLength, translateCsiUKey } from './csiu-keys.js';
|
|
40
41
|
import { splitWin32KeySequences, trailingWin32PrefixLength, translateWin32Key, } from './win32-input-mode.js';
|
|
@@ -206,8 +207,8 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
206
207
|
// The scan (section 5.1, and the ORDER below is the specification)
|
|
207
208
|
// -----------------------------------------------------------------------
|
|
208
209
|
/**
|
|
209
|
-
* Text that reached the pass-through:
|
|
210
|
-
* stripping, then Ink.
|
|
210
|
+
* Text that reached the pass-through: a sequence a KEYBOARD cannot produce
|
|
211
|
+
* first (R-2), then Tier 2 classification, then mouse stripping, then Ink.
|
|
211
212
|
*
|
|
212
213
|
* MOUSE PARSING IS SUSPENDED FOR THE DURATION OF ANY PASTE BODY (I-3), burst
|
|
213
214
|
* and bracketed alike. The Tier 2 population is precisely the terminals with
|
|
@@ -225,6 +226,23 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
225
226
|
burstTimer = arm(flushBurst, PASTE_BURST_MS);
|
|
226
227
|
return;
|
|
227
228
|
}
|
|
229
|
+
// A SEQUENCE A KEYBOARD CANNOT PRODUCE IS A PASTE, FRAMED OR NOT (R-2).
|
|
230
|
+
//
|
|
231
|
+
// WHY THE CLASSIFIER BELOW IS NOT ENOUGH. `classifyChunk` needs either a
|
|
232
|
+
// line break or a chunk with no control byte but TAB, and a SINGLE-LINE
|
|
233
|
+
// paste carrying ESC satisfies neither -- so it travels the keystroke path
|
|
234
|
+
// with `sanitisePaste` never called. Measured on the real component that
|
|
235
|
+
// cost either a draft holding `[38;2;18;52;86m` ALONGSIDE A LIVE ESC (which
|
|
236
|
+
// then reached the model, the history and the session file) or an uncaught
|
|
237
|
+
// `TypeError` in Ink's parser, because `parse-keypress.js` maps `ESC [ 3 2 m`
|
|
238
|
+
// to a ctrl key whose `name` is undefined. Framing it here routes it through
|
|
239
|
+
// the ONE cleaning path every other paste already takes.
|
|
240
|
+
if (features.paste && hasPasteOnlySequence(text)) {
|
|
241
|
+
paste = { kind: 'burst', body: text, bytes: byteLength(text) };
|
|
242
|
+
burstTimer = clearTimer(burstTimer);
|
|
243
|
+
burstTimer = arm(flushBurst, PASTE_BURST_MS);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
228
246
|
if (features.paste && classifyChunk(text) === 'paste') {
|
|
229
247
|
paste = { kind: 'burst', body: text, bytes: byteLength(text) };
|
|
230
248
|
burstTimer = clearTimer(burstTimer);
|
|
@@ -300,14 +318,47 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
300
318
|
}
|
|
301
319
|
}
|
|
302
320
|
};
|
|
321
|
+
/**
|
|
322
|
+
* win32-input-mode records that arrived INSIDE a paste frame (R-4).
|
|
323
|
+
*
|
|
324
|
+
* DATA FIDELITY, NOT POLISH. Every character of a `?9001h` session arrives as
|
|
325
|
+
* `ESC [ vk ; sc ; char ; down ; mods ; rep _`, and once the frame opens
|
|
326
|
+
* nothing translated them: `sanitisePaste` then removed each record's ESC and
|
|
327
|
+
* left 15 visible characters of digits per key. Translating them is also what
|
|
328
|
+
* keeps the fix honest in the OTHER direction -- a whole-sequence stripper
|
|
329
|
+
* deletes such a record outright, so "clean" would have meant the user's
|
|
330
|
+
* characters silently vanishing. A record carrying a key FRAME (modified
|
|
331
|
+
* Enter, Ctrl+I) becomes the byte that frame stands for, because a frame
|
|
332
|
+
* inside a body is content and never a key.
|
|
333
|
+
*/
|
|
334
|
+
const translateInPaste = (text) => {
|
|
335
|
+
if (!enhancedKeys || !text.includes('\x1b['))
|
|
336
|
+
return text;
|
|
337
|
+
let out = '';
|
|
338
|
+
for (const segment of splitWin32KeySequences(text)) {
|
|
339
|
+
if (segment.kind === 'text') {
|
|
340
|
+
out += segment.text;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const legacy = translateWin32Key(segment.record);
|
|
344
|
+
if (legacy === ENTER_NEWLINE_FRAME)
|
|
345
|
+
out += '\n';
|
|
346
|
+
else if (legacy === INDEX_KEY_FRAME)
|
|
347
|
+
out += '\t';
|
|
348
|
+
else
|
|
349
|
+
out += legacy;
|
|
350
|
+
}
|
|
351
|
+
return out;
|
|
352
|
+
};
|
|
303
353
|
const appendBracketed = (text) => {
|
|
304
354
|
if (paste.kind !== 'bracketed' || text.length === 0 || discardBracketed)
|
|
305
355
|
return;
|
|
306
|
-
const
|
|
356
|
+
const body = translateInPaste(text);
|
|
357
|
+
const bytes = paste.bytes + byteLength(body);
|
|
307
358
|
const overflow = paste.overflow || bytes > PASTE_MAX_BYTES;
|
|
308
359
|
paste = {
|
|
309
360
|
kind: 'bracketed',
|
|
310
|
-
body: overflow ? paste.body : paste.body +
|
|
361
|
+
body: overflow ? paste.body : paste.body + body,
|
|
311
362
|
bytes,
|
|
312
363
|
overflow,
|
|
313
364
|
};
|
|
@@ -65,7 +65,10 @@ export interface TerminalPlanInput {
|
|
|
65
65
|
/** New window title, ASCII, already truncated to the cell budget. */
|
|
66
66
|
title: string;
|
|
67
67
|
cwd: string;
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* `renderManualCommand(command, platform)`'s product: two lines on win32
|
|
70
|
+
* (`PowerShell:` / `cmd.exe :`), one line everywhere else.
|
|
71
|
+
*/
|
|
69
72
|
manual: string;
|
|
70
73
|
}
|
|
71
74
|
/** The env var that overrides the shell the new window runs (D-7). */
|
|
@@ -80,7 +83,22 @@ export declare const ARAGON_BRANCH_SHELL_ENV = "ARAGON_BRANCH_SHELL";
|
|
|
80
83
|
* so there is exactly one place that number lives.
|
|
81
84
|
*/
|
|
82
85
|
export declare function planTerminalLaunch(input: TerminalPlanInput): TerminalPlanResult;
|
|
83
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* The PowerShell statement that runs the aragon command (D-1).
|
|
88
|
+
*
|
|
89
|
+
* THE LEADING `&` IS NOT DECORATION. In command position PowerShell reads a quoted
|
|
90
|
+
* string as an EXPRESSION, so `'C:\Program Files\nodejs\node.exe' <script>` is a
|
|
91
|
+
* parse error - "unexpected token ..." at column 36 - and the child is never
|
|
92
|
+
* created, while the parent still believes the window opened (K-1: `wt`/`start`
|
|
93
|
+
* forward and exit). That is exactly the report this branch exists to fix.
|
|
94
|
+
* `&` is the call operator: it takes the following word as a command name whatever
|
|
95
|
+
* its shape, and it does not change name resolution (`& 'npm.cmd'` still finds
|
|
96
|
+
* `npm` through PATH/PATHEXT, appendix B-8).
|
|
97
|
+
*
|
|
98
|
+
* `posixScript` MUST NOT be given the same treatment: in sh `&` means run in the
|
|
99
|
+
* background, so the window would reach the trailing `exec <shell> -i` immediately
|
|
100
|
+
* and the branch would start behind a prompt that looks idle (I-3 / K-1).
|
|
101
|
+
*/
|
|
84
102
|
export declare function powerShellScript(parts: readonly string[]): string;
|
|
85
103
|
/** Bare words stay bare; everything else becomes a single-quoted literal. */
|
|
86
104
|
export declare function powerShellQuote(value: string): string;
|