@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
|
@@ -50,8 +50,21 @@ const POSIX_CANDIDATES = Object.freeze([
|
|
|
50
50
|
]);
|
|
51
51
|
/** The env var that overrides the shell the new window runs (D-7). */
|
|
52
52
|
export const ARAGON_BRANCH_SHELL_ENV = 'ARAGON_BRANCH_SHELL';
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* A bare word is one PowerShell will not read as anything else.
|
|
55
|
+
*
|
|
56
|
+
* THE FIRST CHARACTER IS THE LOAD-BEARING PART (D-3). `@name` in argument position
|
|
57
|
+
* is the SPLAT operator: `& node.exe -p 'process.argv.length' @foo` hands node an
|
|
58
|
+
* EMPTY argument list, exits 0 and prints nothing (measured on Windows PowerShell
|
|
59
|
+
* 5.1, branch-terminal-powershell-fix appendix B-11). As the only word on the line
|
|
60
|
+
* it is not even parseable - `error col=1 id=SplattingNotPermitted`. Nothing after
|
|
61
|
+
* the first character can do that, which is why the class differs in the first
|
|
62
|
+
* position. `-` stays allowed there so that every flag this feature emits
|
|
63
|
+
* (`--resume`) keeps its bare form, and quoting it would change nothing except the
|
|
64
|
+
* look of the fallback line (appendix B-6 A/B, quoting is transparent to a native
|
|
65
|
+
* exe).
|
|
66
|
+
*/
|
|
67
|
+
const BARE_POWER_SHELL_WORD_RE = /^[A-Za-z0-9._\\/:+=\-][A-Za-z0-9._\\/:+=@\-]*$/;
|
|
55
68
|
/**
|
|
56
69
|
* Build the ladder for this platform, most-preferred rung first.
|
|
57
70
|
*
|
|
@@ -201,9 +214,29 @@ function resolveCandidate(candidate, env) {
|
|
|
201
214
|
function posixWindowScript(cwd, shell, command) {
|
|
202
215
|
return `cd ${posixQuote(cwd)} && ${posixScript(command)} ; exec ${posixQuote(shell.path)} -i`;
|
|
203
216
|
}
|
|
204
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* The PowerShell statement that runs the aragon command (D-1).
|
|
219
|
+
*
|
|
220
|
+
* THE LEADING `&` IS NOT DECORATION. In command position PowerShell reads a quoted
|
|
221
|
+
* string as an EXPRESSION, so `'C:\Program Files\nodejs\node.exe' <script>` is a
|
|
222
|
+
* parse error - "unexpected token ..." at column 36 - and the child is never
|
|
223
|
+
* created, while the parent still believes the window opened (K-1: `wt`/`start`
|
|
224
|
+
* forward and exit). That is exactly the report this branch exists to fix.
|
|
225
|
+
* `&` is the call operator: it takes the following word as a command name whatever
|
|
226
|
+
* its shape, and it does not change name resolution (`& 'npm.cmd'` still finds
|
|
227
|
+
* `npm` through PATH/PATHEXT, appendix B-8).
|
|
228
|
+
*
|
|
229
|
+
* `posixScript` MUST NOT be given the same treatment: in sh `&` means run in the
|
|
230
|
+
* background, so the window would reach the trailing `exec <shell> -i` immediately
|
|
231
|
+
* and the branch would start behind a prompt that looks idle (I-3 / K-1).
|
|
232
|
+
*/
|
|
205
233
|
export function powerShellScript(parts) {
|
|
206
|
-
|
|
234
|
+
// TOTAL, NOT THROWING (D-4): an empty argv cannot arise from
|
|
235
|
+
// `resolveAragonCommand` (T-3 pins that), and an empty script is a parse-valid
|
|
236
|
+
// no-op rather than a throw inside a key handler.
|
|
237
|
+
if (parts.length === 0)
|
|
238
|
+
return '';
|
|
239
|
+
return ['&', ...parts.map(powerShellQuote)].join(' ');
|
|
207
240
|
}
|
|
208
241
|
/** Bare words stay bare; everything else becomes a single-quoted literal. */
|
|
209
242
|
export function powerShellQuote(value) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ASCII ONLY - `src/session/**` is inside `glyphs.test.ts::inScope`.
|
|
5
5
|
*
|
|
6
|
-
* THIS MODULE
|
|
6
|
+
* THIS MODULE PRODUCES NO IO ITSELF. It answers two questions and nothing else:
|
|
7
7
|
* "which argv pieces make up the aragon command" and "how does that sentence read
|
|
8
8
|
* when printed to a human". Spawning lives in `session/branch-launch.ts`, and the
|
|
9
9
|
* terminal ladder lives in `proc/terminal-plan.ts`, so the one part of this
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
* `proc/readiness.ts` and `proc/classify.ts` are pure, `proc/supervisor.ts` is the
|
|
13
13
|
* part that touches processes.
|
|
14
14
|
*
|
|
15
|
+
* IT DOES IMPORT `proc/terminal-plan.ts` (D-5), and that is still the right
|
|
16
|
+
* direction: `session -> proc`. The manual line needs both quoting dialects, and
|
|
17
|
+
* the dialect owner is `proc/terminal-plan.ts`; putting it in `proc/` would mean
|
|
18
|
+
* `proc/` importing `session/`, which is the cycle the header above forbids.
|
|
19
|
+
* What that import brings with it is one `statSync` probe used by the PATH lookup -
|
|
20
|
+
* not a process, not a write - so "no IO on purpose" still describes this module's
|
|
21
|
+
* own behaviour.
|
|
22
|
+
*
|
|
15
23
|
* NAMED `branch-invocation`, NOT `branch-command` (R-22): `commands/branch-command.ts`
|
|
16
24
|
* is the command body, and two files with one basename make every import, stack
|
|
17
25
|
* trace and grep ambiguous. What this module exports is an *invocation*.
|
|
@@ -28,7 +36,15 @@ export interface AragonCommandResolution {
|
|
|
28
36
|
export interface BranchInvocation {
|
|
29
37
|
/** `[...aragon.parts, '--resume', id]`. */
|
|
30
38
|
parts: string[];
|
|
31
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* One line, for the toast and the log.
|
|
41
|
+
*
|
|
42
|
+
* NOT THE PASTEABLE FORM ON WIN32 - and the defect that produced this comment is
|
|
43
|
+
* the proof: a double-quoted command word is a string EXPRESSION to PowerShell, so
|
|
44
|
+
* pasting this line into the shell Windows Terminal opens by default yields the
|
|
45
|
+
* same `ParserError` the new window used to. `renderManualCommand(parts,
|
|
46
|
+
* platform)` is the paste-ready form; on win32 it is two labelled lines.
|
|
47
|
+
*/
|
|
32
48
|
command: string;
|
|
33
49
|
}
|
|
34
50
|
export declare const BRANCH_RESUME_FLAG = "--resume";
|
|
@@ -54,21 +70,49 @@ export declare function resolveAragonCommand(input: {
|
|
|
54
70
|
execPath: string;
|
|
55
71
|
env: Record<string, string | undefined>;
|
|
56
72
|
}): AragonCommandResolution;
|
|
57
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* `parts` + `--resume <id>`, plus the ONE-LINE form for the toast and the log.
|
|
75
|
+
*
|
|
76
|
+
* The paste-ready form is NOT this field (see `BranchInvocation.command` and
|
|
77
|
+
* `renderManualCommand`): "one line for every shell" was the claim this branch
|
|
78
|
+
* disproved, and leaving it here would keep the next reader believing it.
|
|
79
|
+
*/
|
|
58
80
|
export declare function buildResumeInvocation(input: {
|
|
59
81
|
id: string;
|
|
60
82
|
aragon: AragonCommandResolution;
|
|
61
83
|
}): BranchInvocation;
|
|
62
84
|
/**
|
|
63
|
-
* Join argv pieces into one line a human can
|
|
85
|
+
* Join argv pieces into one line a human can read at a glance.
|
|
64
86
|
*
|
|
65
|
-
* NOT A SHELL ESCAPER
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
87
|
+
* NOT A SHELL ESCAPER, and no longer claimed to be a universal paste line: it is
|
|
88
|
+
* the RIGHT line for `cmd.exe` and for POSIX shells, and the WRONG one for
|
|
89
|
+
* PowerShell on win32 (a double-quoted command word is an expression there). Its
|
|
90
|
+
* product is what a POSIX/darwin user pastes, and what the `cmd.exe` half of the
|
|
91
|
+
* win32 manual block shows; the PowerShell half comes from `powerShellScript` via
|
|
92
|
+
* `renderManualCommand`. The real command lines are built by
|
|
93
|
+
* `proc/terminal-plan.ts`, which quotes for the exact parser each rung lands in.
|
|
70
94
|
*/
|
|
71
95
|
export declare function joinCommandLine(parts: readonly string[]): string;
|
|
96
|
+
/**
|
|
97
|
+
* The paste-ready form of the command, per platform (D-5 / D-6 / I-4).
|
|
98
|
+
*
|
|
99
|
+
* ONE LINE CANNOT BE CORRECT FOR BOTH WINDOWS PARSERS. A double-quoted command word
|
|
100
|
+
* is a string EXPRESSION to PowerShell (the defect this branch fixes) while it is
|
|
101
|
+
* exactly right for `cmd.exe`; `& 'x'` is right for PowerShell and wrong for
|
|
102
|
+
* `cmd.exe` (which keeps the single quotes and tries to run `'x'`). So Windows gets
|
|
103
|
+
* two labelled lines and the user copies the one matching the terminal they opened;
|
|
104
|
+
* POSIX and macOS keep the single line they have always printed.
|
|
105
|
+
*
|
|
106
|
+
* The first line is `powerShellScript(parts)` VERBATIM, which is what makes I-1
|
|
107
|
+
* ("one command, one artefact") testable: the fallback the user pastes is the same
|
|
108
|
+
* text the new window receives in its `-EncodedCommand` payload.
|
|
109
|
+
*
|
|
110
|
+
* @param parts argv pieces of the aragon invocation, `--resume <id>` included.
|
|
111
|
+
* @param platform the platform whose parsers the two lines have to satisfy.
|
|
112
|
+
* @returns one line off Windows; two ASCII lines labelled `PowerShell:` and
|
|
113
|
+
* `cmd.exe :` on win32. Both labels are 11 columns wide on purpose.
|
|
114
|
+
*/
|
|
115
|
+
export declare function renderManualCommand(parts: readonly string[], platform: NodeJS.Platform): string;
|
|
72
116
|
/**
|
|
73
117
|
* The second gate on D-9: an id is about to be concatenated into a command line.
|
|
74
118
|
*
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ASCII ONLY - `src/session/**` is inside `glyphs.test.ts::inScope`.
|
|
5
5
|
*
|
|
6
|
-
* THIS MODULE
|
|
6
|
+
* THIS MODULE PRODUCES NO IO ITSELF. It answers two questions and nothing else:
|
|
7
7
|
* "which argv pieces make up the aragon command" and "how does that sentence read
|
|
8
8
|
* when printed to a human". Spawning lives in `session/branch-launch.ts`, and the
|
|
9
9
|
* terminal ladder lives in `proc/terminal-plan.ts`, so the one part of this
|
|
@@ -12,11 +12,20 @@
|
|
|
12
12
|
* `proc/readiness.ts` and `proc/classify.ts` are pure, `proc/supervisor.ts` is the
|
|
13
13
|
* part that touches processes.
|
|
14
14
|
*
|
|
15
|
+
* IT DOES IMPORT `proc/terminal-plan.ts` (D-5), and that is still the right
|
|
16
|
+
* direction: `session -> proc`. The manual line needs both quoting dialects, and
|
|
17
|
+
* the dialect owner is `proc/terminal-plan.ts`; putting it in `proc/` would mean
|
|
18
|
+
* `proc/` importing `session/`, which is the cycle the header above forbids.
|
|
19
|
+
* What that import brings with it is one `statSync` probe used by the PATH lookup -
|
|
20
|
+
* not a process, not a write - so "no IO on purpose" still describes this module's
|
|
21
|
+
* own behaviour.
|
|
22
|
+
*
|
|
15
23
|
* NAMED `branch-invocation`, NOT `branch-command` (R-22): `commands/branch-command.ts`
|
|
16
24
|
* is the command body, and two files with one basename make every import, stack
|
|
17
25
|
* trace and grep ambiguous. What this module exports is an *invocation*.
|
|
18
26
|
*/
|
|
19
27
|
import { isAbsolute } from 'node:path';
|
|
28
|
+
import { powerShellScript } from '../proc/terminal-plan.js';
|
|
20
29
|
import { isValidSessionId } from './store.js';
|
|
21
30
|
export const BRANCH_RESUME_FLAG = '--resume';
|
|
22
31
|
/** The env var that overrides "what executable the new window runs" (D-7 sibling). */
|
|
@@ -65,23 +74,58 @@ export function resolveAragonCommand(input) {
|
|
|
65
74
|
}
|
|
66
75
|
return { parts: ['aragon'], kind: 'bin', origin: 'aragon on PATH' };
|
|
67
76
|
}
|
|
68
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* `parts` + `--resume <id>`, plus the ONE-LINE form for the toast and the log.
|
|
79
|
+
*
|
|
80
|
+
* The paste-ready form is NOT this field (see `BranchInvocation.command` and
|
|
81
|
+
* `renderManualCommand`): "one line for every shell" was the claim this branch
|
|
82
|
+
* disproved, and leaving it here would keep the next reader believing it.
|
|
83
|
+
*/
|
|
69
84
|
export function buildResumeInvocation(input) {
|
|
70
85
|
const parts = [...input.aragon.parts, BRANCH_RESUME_FLAG, input.id];
|
|
71
86
|
return { parts, command: joinCommandLine(parts) };
|
|
72
87
|
}
|
|
73
88
|
/**
|
|
74
|
-
* Join argv pieces into one line a human can
|
|
89
|
+
* Join argv pieces into one line a human can read at a glance.
|
|
75
90
|
*
|
|
76
|
-
* NOT A SHELL ESCAPER
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
91
|
+
* NOT A SHELL ESCAPER, and no longer claimed to be a universal paste line: it is
|
|
92
|
+
* the RIGHT line for `cmd.exe` and for POSIX shells, and the WRONG one for
|
|
93
|
+
* PowerShell on win32 (a double-quoted command word is an expression there). Its
|
|
94
|
+
* product is what a POSIX/darwin user pastes, and what the `cmd.exe` half of the
|
|
95
|
+
* win32 manual block shows; the PowerShell half comes from `powerShellScript` via
|
|
96
|
+
* `renderManualCommand`. The real command lines are built by
|
|
97
|
+
* `proc/terminal-plan.ts`, which quotes for the exact parser each rung lands in.
|
|
81
98
|
*/
|
|
82
99
|
export function joinCommandLine(parts) {
|
|
83
100
|
return parts.map(quoteForDisplay).join(' ');
|
|
84
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* The paste-ready form of the command, per platform (D-5 / D-6 / I-4).
|
|
104
|
+
*
|
|
105
|
+
* ONE LINE CANNOT BE CORRECT FOR BOTH WINDOWS PARSERS. A double-quoted command word
|
|
106
|
+
* is a string EXPRESSION to PowerShell (the defect this branch fixes) while it is
|
|
107
|
+
* exactly right for `cmd.exe`; `& 'x'` is right for PowerShell and wrong for
|
|
108
|
+
* `cmd.exe` (which keeps the single quotes and tries to run `'x'`). So Windows gets
|
|
109
|
+
* two labelled lines and the user copies the one matching the terminal they opened;
|
|
110
|
+
* POSIX and macOS keep the single line they have always printed.
|
|
111
|
+
*
|
|
112
|
+
* The first line is `powerShellScript(parts)` VERBATIM, which is what makes I-1
|
|
113
|
+
* ("one command, one artefact") testable: the fallback the user pastes is the same
|
|
114
|
+
* text the new window receives in its `-EncodedCommand` payload.
|
|
115
|
+
*
|
|
116
|
+
* @param parts argv pieces of the aragon invocation, `--resume <id>` included.
|
|
117
|
+
* @param platform the platform whose parsers the two lines have to satisfy.
|
|
118
|
+
* @returns one line off Windows; two ASCII lines labelled `PowerShell:` and
|
|
119
|
+
* `cmd.exe :` on win32. Both labels are 11 columns wide on purpose.
|
|
120
|
+
*/
|
|
121
|
+
export function renderManualCommand(parts, platform) {
|
|
122
|
+
if (platform !== 'win32')
|
|
123
|
+
return joinCommandLine(parts);
|
|
124
|
+
return [
|
|
125
|
+
`PowerShell: ${powerShellScript(parts)}`,
|
|
126
|
+
`cmd.exe : ${joinCommandLine(parts)}`,
|
|
127
|
+
].join('\n');
|
|
128
|
+
}
|
|
85
129
|
/** Empty and whitespace-or-quote-bearing pieces get double quotes; the rest stay bare. */
|
|
86
130
|
function quoteForDisplay(part) {
|
|
87
131
|
if (part.length === 0)
|
|
@@ -29,6 +29,15 @@ export interface BranchRequest {
|
|
|
29
29
|
}
|
|
30
30
|
export type BranchOpenResult = {
|
|
31
31
|
ok: true;
|
|
32
|
+
/**
|
|
33
|
+
* The paste-ready command block for this platform (D-7).
|
|
34
|
+
*
|
|
35
|
+
* "Always present, so no failure is a dead end" is I-4 for the FAILURE arm;
|
|
36
|
+
* the success arm carries it for the mirror-image reason - "what did the
|
|
37
|
+
* window I just opened actually run" is copyable without retyping it from the
|
|
38
|
+
* toast. Same meaning as the field of the same name below: on win32 it is the
|
|
39
|
+
* two-line PowerShell / `cmd.exe` block, off win32 it is one line.
|
|
40
|
+
*/
|
|
32
41
|
command: string;
|
|
33
42
|
shell: string;
|
|
34
43
|
rung: string;
|
|
@@ -22,7 +22,7 @@ import { spawn } from 'node:child_process';
|
|
|
22
22
|
import { getLogger } from '../logging/logger.js';
|
|
23
23
|
import { defaultIsExecutable, pathDirsOf, planTerminalLaunch, resolveShell, truncateTitle, } from '../proc/terminal-plan.js';
|
|
24
24
|
import { BRANCH_COPY } from '../commands/branch-command.js';
|
|
25
|
-
import { buildResumeInvocation,
|
|
25
|
+
import { buildResumeInvocation, renderManualCommand, resolveAragonCommand, validateBranchId, } from './branch-invocation.js';
|
|
26
26
|
import { BRANCH_TERMINAL_LIMITS } from './limits.js';
|
|
27
27
|
/**
|
|
28
28
|
* Build the port. The only mutable state is this closure's four values, none of
|
|
@@ -41,12 +41,16 @@ export function createBranchLauncher(deps) {
|
|
|
41
41
|
let cachedAragon = null;
|
|
42
42
|
let cachedShell = null;
|
|
43
43
|
/**
|
|
44
|
-
* Resolve WHAT the new window runs, and the command
|
|
44
|
+
* Resolve WHAT the new window runs, and the two command forms built from it.
|
|
45
45
|
*
|
|
46
46
|
* The resolution is cached for the process (R-13 / K-14). It costs no
|
|
47
47
|
* filesystem probe - the three rules are env, `argv[1]` and a literal - so the
|
|
48
48
|
* cache is about identity, not speed: the value that opened the first window is
|
|
49
49
|
* the value the log keeps reporting.
|
|
50
|
+
*
|
|
51
|
+
* `manual` is derived HERE from the same `parts` on purpose (D-7): one source of
|
|
52
|
+
* truth for the command means the paste block and the `-EncodedCommand` payload
|
|
53
|
+
* cannot drift apart (I-1).
|
|
50
54
|
*/
|
|
51
55
|
function planFor(id) {
|
|
52
56
|
cachedAragon ??= resolveAragonCommand({
|
|
@@ -54,9 +58,11 @@ export function createBranchLauncher(deps) {
|
|
|
54
58
|
execPath: deps.execPath,
|
|
55
59
|
env: deps.env,
|
|
56
60
|
});
|
|
61
|
+
const invocation = buildResumeInvocation({ id, aragon: cachedAragon });
|
|
57
62
|
return {
|
|
58
63
|
aragon: cachedAragon,
|
|
59
|
-
invocation
|
|
64
|
+
invocation,
|
|
65
|
+
manual: renderManualCommand(invocation.parts, deps.platform),
|
|
60
66
|
};
|
|
61
67
|
}
|
|
62
68
|
function shellFor() {
|
|
@@ -93,22 +99,25 @@ export function createBranchLauncher(deps) {
|
|
|
93
99
|
return cooldown(command, Math.max(1, windowMs - waited));
|
|
94
100
|
}
|
|
95
101
|
async function openOnce(request) {
|
|
96
|
-
const { aragon, invocation } = planFor(request.id);
|
|
102
|
+
const { aragon, invocation, manual } = planFor(request.id);
|
|
97
103
|
const command = invocation.command;
|
|
98
104
|
// 4.5 BEFORE ANY SPAWN (R-7 / T-46): a command line that `cmd` or AppleScript
|
|
99
105
|
// would truncate is worse than no command line, because the window opens and
|
|
100
|
-
// then fails in a way this process cannot see.
|
|
106
|
+
// then fails in a way this process cannot see. The MEASURED object stays the
|
|
107
|
+
// single unquoted line: it is the lower bound of every dialect (quoting only
|
|
108
|
+
// ever adds characters), so the check is conservative and needs no second
|
|
109
|
+
// source of truth.
|
|
101
110
|
if (command.length > BRANCH_TERMINAL_LIMITS.maxCommandLineCells) {
|
|
102
111
|
// A COMPLETE SENTENCE WITH THE FALLBACK INSIDE IT (I-4 / appendix A): the
|
|
103
112
|
// over-long case is the one failure where the user most needs a command to
|
|
104
113
|
// paste, so it may not report the bare reason "command line too long" - that
|
|
105
114
|
// is an internal code, not an answer, and it would leave a dead end.
|
|
106
|
-
return failure('spawn-failed', BRANCH_COPY.spawnFailed('command line too long',
|
|
115
|
+
return failure('spawn-failed', BRANCH_COPY.spawnFailed('command line too long', manual), manual);
|
|
107
116
|
}
|
|
108
117
|
// D-9's second gate: an id is about to enter a command line.
|
|
109
118
|
const invalid = validateBranchId(request.id);
|
|
110
119
|
if (invalid !== null)
|
|
111
|
-
return failure('spawn-failed', invalid,
|
|
120
|
+
return failure('spawn-failed', invalid, manual);
|
|
112
121
|
// Step 6: flush FIRST, spawn only on `ok` (I-1). `force` skips the fingerprint
|
|
113
122
|
// short circuit and nothing else (D-15 / I-7).
|
|
114
123
|
const flushed = deps.checkpoint({ force: true });
|
|
@@ -118,7 +127,10 @@ export function createBranchLauncher(deps) {
|
|
|
118
127
|
// disk problem, so it must not be printed as "could not checkpoint
|
|
119
128
|
// (no-identity)" - that reads like corruption. It gets `/session`'s sentence.
|
|
120
129
|
const message = reason === 'no-identity' ? BRANCH_COPY.noIdentity : BRANCH_COPY.checkpointFailed(reason);
|
|
121
|
-
|
|
130
|
+
// The THIRD argument changes, never the sentence (parent-R-19): these two
|
|
131
|
+
// failures deliberately do NOT embed the command, because their job is to
|
|
132
|
+
// explain why nothing opened. The command still travels in `result.command`.
|
|
133
|
+
return failure('checkpoint-failed', message, manual);
|
|
122
134
|
}
|
|
123
135
|
const shell = shellFor();
|
|
124
136
|
// Step 7: the TITLE is what gets truncated, never the id in the command line (T-47).
|
|
@@ -132,11 +144,13 @@ export function createBranchLauncher(deps) {
|
|
|
132
144
|
command: invocation.parts,
|
|
133
145
|
title,
|
|
134
146
|
cwd: request.cwd,
|
|
135
|
-
|
|
147
|
+
// The paste block the fallback prints, built by the same dialect owner that
|
|
148
|
+
// encodes the window script (I-1: one command, one artefact).
|
|
149
|
+
manual,
|
|
136
150
|
});
|
|
137
151
|
if (!planned.ok) {
|
|
138
152
|
// Not cached: the next press must be allowed to find a terminal that appeared.
|
|
139
|
-
return failure('no-launcher', BRANCH_COPY.noLauncher(planned.manual),
|
|
153
|
+
return failure('no-launcher', BRANCH_COPY.noLauncher(planned.manual), manual);
|
|
140
154
|
}
|
|
141
155
|
// Step 8: walk the ladder, bounded by maxRungs (T-33).
|
|
142
156
|
const rungs = planned.plans.slice(0, BRANCH_TERMINAL_LIMITS.maxRungs);
|
|
@@ -156,14 +170,14 @@ export function createBranchLauncher(deps) {
|
|
|
156
170
|
aragon: aragon.kind,
|
|
157
171
|
origin: aragon.origin,
|
|
158
172
|
});
|
|
159
|
-
return { ok: true, command, shell: planned.shell.path, rung: plan.rung };
|
|
173
|
+
return { ok: true, command: manual, shell: planned.shell.path, rung: plan.rung };
|
|
160
174
|
}
|
|
161
175
|
logger.warn('rung_failed', { rung: plan.rung, message: outcome.message });
|
|
162
176
|
lastError = outcome;
|
|
163
177
|
}
|
|
164
178
|
// Step 9: every rung failed. `wt`'s own semantic failures land here too only if
|
|
165
179
|
// the error event fired; a forwarded-but-rejected `wt` invocation does not (K-1).
|
|
166
|
-
return failure('spawn-failed', BRANCH_COPY.spawnFailed(lastError?.message ?? 'no rung available',
|
|
180
|
+
return failure('spawn-failed', BRANCH_COPY.spawnFailed(lastError?.message ?? 'no rung available', manual), manual);
|
|
167
181
|
}
|
|
168
182
|
/**
|
|
169
183
|
* One rung, with EXACTLY ONE `'error'` LISTENER THAT IS NEVER REMOVED (R-3 / I-10).
|
|
@@ -230,7 +244,10 @@ export function createBranchLauncher(deps) {
|
|
|
230
244
|
}
|
|
231
245
|
return {
|
|
232
246
|
async open(request) {
|
|
233
|
-
|
|
247
|
+
// The cooldown path carries the same paste block. It never displays it, but the
|
|
248
|
+
// result type has exactly one meaning for `command` and inventing a second one
|
|
249
|
+
// for this arm is how the two forms would drift apart (D-7).
|
|
250
|
+
const blocked = gated(planFor(request.id).manual);
|
|
234
251
|
if (blocked !== null)
|
|
235
252
|
return blocked;
|
|
236
253
|
inFlight = true;
|
|
@@ -65,6 +65,14 @@ export interface SanitizeResult {
|
|
|
65
65
|
/**
|
|
66
66
|
* Remove every escape sequence and control byte except `\t` `\n` `\r`.
|
|
67
67
|
*
|
|
68
|
+
* SEQUENCE SYNTAX HAS ONE OWNER (`input/control-sequences.ts`); this function
|
|
69
|
+
* adds only the control-byte sweep a DISPLAY row wants. Both call sites in this
|
|
70
|
+
* module already cut an unfinished escape off into the carry before calling it
|
|
71
|
+
* (`partialEscapeStart`), so the shared one-shot rule for a truncated sequence
|
|
72
|
+
* is exactly what they need -- and it fixes AC-24's failure in place: the old
|
|
73
|
+
* two-byte rule consumed the `\x1b[` of an unfinished CSI and left the
|
|
74
|
+
* PARAMETERS behind, so `\x1b[31m` split across chunks rendered as `red 3`.
|
|
75
|
+
*
|
|
68
76
|
* Exported for the tests, and used by `toDisplayRow` as the second line of
|
|
69
77
|
* defence over a carry that ends mid-escape.
|
|
70
78
|
*/
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* interpreting cursor motion would mean owning a screen buffer. Neither is a
|
|
23
23
|
* presentation round's job.
|
|
24
24
|
*/
|
|
25
|
+
import { stripControlSequences } from '../input/control-sequences.js';
|
|
25
26
|
/** Columns a live row may occupy before it is clipped. */
|
|
26
27
|
export const LIVE_ROW_MAX_CHARS = 200;
|
|
27
28
|
/** Tab stop width, matching every terminal's default. */
|
|
@@ -57,22 +58,24 @@ export const TRUNCATION_MARK = '...';
|
|
|
57
58
|
* and holding it forever would pin the carry.
|
|
58
59
|
*/
|
|
59
60
|
const MAX_PARTIAL_ESCAPE = 64;
|
|
60
|
-
/** CSI: `\x1b[` params intermediates final. */
|
|
61
|
-
const CSI = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
62
|
-
/** OSC: `\x1b]` ... terminated by BEL or ST. */
|
|
63
|
-
const OSC = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
|
|
64
|
-
/** The two-byte forms: `\x1b` followed by one character. */
|
|
65
|
-
const ESC_PAIR = /\x1b[\s\S]/g;
|
|
66
61
|
/** C1, plus every C0 control except `\t` `\n` `\r`. */
|
|
67
62
|
const CONTROLS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g;
|
|
68
63
|
/**
|
|
69
64
|
* Remove every escape sequence and control byte except `\t` `\n` `\r`.
|
|
70
65
|
*
|
|
66
|
+
* SEQUENCE SYNTAX HAS ONE OWNER (`input/control-sequences.ts`); this function
|
|
67
|
+
* adds only the control-byte sweep a DISPLAY row wants. Both call sites in this
|
|
68
|
+
* module already cut an unfinished escape off into the carry before calling it
|
|
69
|
+
* (`partialEscapeStart`), so the shared one-shot rule for a truncated sequence
|
|
70
|
+
* is exactly what they need -- and it fixes AC-24's failure in place: the old
|
|
71
|
+
* two-byte rule consumed the `\x1b[` of an unfinished CSI and left the
|
|
72
|
+
* PARAMETERS behind, so `\x1b[31m` split across chunks rendered as `red 3`.
|
|
73
|
+
*
|
|
71
74
|
* Exported for the tests, and used by `toDisplayRow` as the second line of
|
|
72
75
|
* defence over a carry that ends mid-escape.
|
|
73
76
|
*/
|
|
74
77
|
export function stripAnsi(text) {
|
|
75
|
-
return text
|
|
78
|
+
return stripControlSequences(text).replace(CONTROLS, '');
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
78
81
|
* Index at which a trailing INCOMPLETE escape begins, or `-1`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TAB expansion that agrees with the TERMINAL, not with UTF-16 (fix A'-2 of
|
|
3
|
+
* paste-control-sequence-residue).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS A MODULE OF ITS OWN. A TAB has no width; it advances the pen to
|
|
6
|
+
* the next tab stop, so what it occupies depends on where the line currently
|
|
7
|
+
* is. That makes "one TAB" a property of a CURSOR, and the same character was
|
|
8
|
+
* being read three different ways in this package:
|
|
9
|
+
*
|
|
10
|
+
* - `ui/editor-navigation.ts` painted it at FOUR-column stops in the composer,
|
|
11
|
+
* - `entries/UserEntry.tsx` handed the raw byte to `<Text>`, so the terminal
|
|
12
|
+
* painted it at EIGHT-column stops in the transcript,
|
|
13
|
+
* - `tools/terminal-output.ts` expanded it by UTF-16 code-unit count, so a
|
|
14
|
+
* pasted `中文<TAB>X` table came out two columns short.
|
|
15
|
+
*
|
|
16
|
+
* A user who pastes a TAB-indented block or an Excel/Word table therefore saw
|
|
17
|
+
* the draft and the sent message disagree about their own text. One function,
|
|
18
|
+
* used by the composer AND by every renderer of stored text, is what makes the
|
|
19
|
+
* "before send" and "after send" rows character-identical.
|
|
20
|
+
*
|
|
21
|
+
* `input/` is the wrong home for it: `cli/input` may depend on `node:stream`
|
|
22
|
+
* and nothing else (`.claude-index/index.md`), and column arithmetic needs the
|
|
23
|
+
* width table. `ui/` already owns that (`selection/selection.ts` uses
|
|
24
|
+
* `string-width` for exactly this reason).
|
|
25
|
+
*
|
|
26
|
+
* Pure, React-free, Ink-free.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Tab stop width, matching every terminal's default.
|
|
30
|
+
*
|
|
31
|
+
* A COPY OF `tools/terminal-output.ts`'s number, on purpose and in the same
|
|
32
|
+
* shape `composer-limits.ts` copies `budget.ts::HINT_MIN_ROWS`: importing the
|
|
33
|
+
* tools module here would point an arrow from `ui/` into `tools/` for one
|
|
34
|
+
* integer, and the two are asserted equal by a test instead. (`tools/` keeps
|
|
35
|
+
* its own expander because its rows are clipped to 200 columns before anyone
|
|
36
|
+
* looks at them, so a code-unit approximation there cannot reach a user.)
|
|
37
|
+
*/
|
|
38
|
+
export declare const TAB_WIDTH = 8;
|
|
39
|
+
/**
|
|
40
|
+
* Expand every TAB in `text` to spaces up to the next `TAB_WIDTH` stop, given
|
|
41
|
+
* the display column the first character will be painted at.
|
|
42
|
+
*
|
|
43
|
+
* `fromColumn` is a DISPLAY column, not an offset: a caller that has already
|
|
44
|
+
* painted `中文` must pass 4. Lines are independent -- a `\n` resets the column
|
|
45
|
+
* to zero, because the terminal's pen returns to column 0.
|
|
46
|
+
*
|
|
47
|
+
* Non-ASCII text is measured with `string-width`, which is the whole point: a
|
|
48
|
+
* CJK glyph occupies two cells, and counting it as one space is what left
|
|
49
|
+
* CJK-plus-TAB tables misaligned.
|
|
50
|
+
*/
|
|
51
|
+
export declare function expandTabs(text: string, fromColumn?: number): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TAB expansion that agrees with the TERMINAL, not with UTF-16 (fix A'-2 of
|
|
3
|
+
* paste-control-sequence-residue).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS A MODULE OF ITS OWN. A TAB has no width; it advances the pen to
|
|
6
|
+
* the next tab stop, so what it occupies depends on where the line currently
|
|
7
|
+
* is. That makes "one TAB" a property of a CURSOR, and the same character was
|
|
8
|
+
* being read three different ways in this package:
|
|
9
|
+
*
|
|
10
|
+
* - `ui/editor-navigation.ts` painted it at FOUR-column stops in the composer,
|
|
11
|
+
* - `entries/UserEntry.tsx` handed the raw byte to `<Text>`, so the terminal
|
|
12
|
+
* painted it at EIGHT-column stops in the transcript,
|
|
13
|
+
* - `tools/terminal-output.ts` expanded it by UTF-16 code-unit count, so a
|
|
14
|
+
* pasted `中文<TAB>X` table came out two columns short.
|
|
15
|
+
*
|
|
16
|
+
* A user who pastes a TAB-indented block or an Excel/Word table therefore saw
|
|
17
|
+
* the draft and the sent message disagree about their own text. One function,
|
|
18
|
+
* used by the composer AND by every renderer of stored text, is what makes the
|
|
19
|
+
* "before send" and "after send" rows character-identical.
|
|
20
|
+
*
|
|
21
|
+
* `input/` is the wrong home for it: `cli/input` may depend on `node:stream`
|
|
22
|
+
* and nothing else (`.claude-index/index.md`), and column arithmetic needs the
|
|
23
|
+
* width table. `ui/` already owns that (`selection/selection.ts` uses
|
|
24
|
+
* `string-width` for exactly this reason).
|
|
25
|
+
*
|
|
26
|
+
* Pure, React-free, Ink-free.
|
|
27
|
+
*/
|
|
28
|
+
import stringWidth from 'string-width';
|
|
29
|
+
/**
|
|
30
|
+
* Tab stop width, matching every terminal's default.
|
|
31
|
+
*
|
|
32
|
+
* A COPY OF `tools/terminal-output.ts`'s number, on purpose and in the same
|
|
33
|
+
* shape `composer-limits.ts` copies `budget.ts::HINT_MIN_ROWS`: importing the
|
|
34
|
+
* tools module here would point an arrow from `ui/` into `tools/` for one
|
|
35
|
+
* integer, and the two are asserted equal by a test instead. (`tools/` keeps
|
|
36
|
+
* its own expander because its rows are clipped to 200 columns before anyone
|
|
37
|
+
* looks at them, so a code-unit approximation there cannot reach a user.)
|
|
38
|
+
*/
|
|
39
|
+
export const TAB_WIDTH = 8;
|
|
40
|
+
/**
|
|
41
|
+
* Expand every TAB in `text` to spaces up to the next `TAB_WIDTH` stop, given
|
|
42
|
+
* the display column the first character will be painted at.
|
|
43
|
+
*
|
|
44
|
+
* `fromColumn` is a DISPLAY column, not an offset: a caller that has already
|
|
45
|
+
* painted `中文` must pass 4. Lines are independent -- a `\n` resets the column
|
|
46
|
+
* to zero, because the terminal's pen returns to column 0.
|
|
47
|
+
*
|
|
48
|
+
* Non-ASCII text is measured with `string-width`, which is the whole point: a
|
|
49
|
+
* CJK glyph occupies two cells, and counting it as one space is what left
|
|
50
|
+
* CJK-plus-TAB tables misaligned.
|
|
51
|
+
*/
|
|
52
|
+
export function expandTabs(text, fromColumn = 0) {
|
|
53
|
+
if (text.indexOf('\t') === -1)
|
|
54
|
+
return text;
|
|
55
|
+
let out = '';
|
|
56
|
+
let column = Math.max(0, Math.floor(fromColumn));
|
|
57
|
+
let run = '';
|
|
58
|
+
for (const part of graphemesOf(text)) {
|
|
59
|
+
if (part === '\t') {
|
|
60
|
+
out += run;
|
|
61
|
+
run = '';
|
|
62
|
+
const width = TAB_WIDTH - (column % TAB_WIDTH);
|
|
63
|
+
out += ' '.repeat(width);
|
|
64
|
+
column += width;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
run += part;
|
|
68
|
+
if (part === '\n')
|
|
69
|
+
column = 0;
|
|
70
|
+
else
|
|
71
|
+
column += stringWidth(part);
|
|
72
|
+
}
|
|
73
|
+
return out + run;
|
|
74
|
+
}
|
|
75
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
76
|
+
/**
|
|
77
|
+
* Grapheme clusters rather than code units, so a combining mark or an emoji ZWJ
|
|
78
|
+
* sequence advances the column once, exactly as the terminal draws it.
|
|
79
|
+
*/
|
|
80
|
+
function graphemesOf(text) {
|
|
81
|
+
return Array.from(segmenter.segment(text), ({ segment }) => segment);
|
|
82
|
+
}
|
|
@@ -25,7 +25,17 @@ export interface EditorVisualMap {
|
|
|
25
25
|
readonly positions: VisualPosition[];
|
|
26
26
|
readonly cursor: VisualPosition;
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Map graphemes to hard-wrapped rows, sharing tab stops and caret reservation.
|
|
30
|
+
*
|
|
31
|
+
* THE TAB STOP IS THE TERMINAL'S (`TAB_WIDTH`), NOT AN EDITOR-SIZED FOUR. It
|
|
32
|
+
* used to be four, while the transcript handed the raw `\t` to `<Text>` and
|
|
33
|
+
* the terminal painted it at eight: the same pasted table sat one column off
|
|
34
|
+
* from itself before and after sending. A tab that would cross the right edge
|
|
35
|
+
* is still clipped to the row (the caret cannot point past the last cell), so
|
|
36
|
+
* a tab STRADDLING the wrap is the one shape where the draft and the stored
|
|
37
|
+
* text can differ by the remainder of the advance.
|
|
38
|
+
*/
|
|
29
39
|
export declare function buildEditorVisualMap(input: {
|
|
30
40
|
buffer: string;
|
|
31
41
|
cols: number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared UTF-16 grapheme boundaries and terminal-cell layout for the editor. */
|
|
2
2
|
import stringWidth from 'string-width';
|
|
3
|
+
import { TAB_WIDTH } from './display-tab.js';
|
|
3
4
|
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
4
5
|
/** Segment text without changing its UTF-16 offsets or stored whitespace. */
|
|
5
6
|
export function graphemes(text) {
|
|
@@ -31,7 +32,17 @@ export function graphemeWidth(text) {
|
|
|
31
32
|
return 1;
|
|
32
33
|
return stringWidth(text);
|
|
33
34
|
}
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Map graphemes to hard-wrapped rows, sharing tab stops and caret reservation.
|
|
37
|
+
*
|
|
38
|
+
* THE TAB STOP IS THE TERMINAL'S (`TAB_WIDTH`), NOT AN EDITOR-SIZED FOUR. It
|
|
39
|
+
* used to be four, while the transcript handed the raw `\t` to `<Text>` and
|
|
40
|
+
* the terminal painted it at eight: the same pasted table sat one column off
|
|
41
|
+
* from itself before and after sending. A tab that would cross the right edge
|
|
42
|
+
* is still clipped to the row (the caret cannot point past the last cell), so
|
|
43
|
+
* a tab STRADDLING the wrap is the one shape where the draft and the stored
|
|
44
|
+
* text can differ by the remainder of the advance.
|
|
45
|
+
*/
|
|
35
46
|
export function buildEditorVisualMap(input) {
|
|
36
47
|
const cols = Math.max(1, Math.floor(input.cols));
|
|
37
48
|
const cursor = snapGrapheme(input.buffer, input.cursor);
|
|
@@ -50,12 +61,12 @@ export function buildEditorVisualMap(input) {
|
|
|
50
61
|
breakRow();
|
|
51
62
|
continue;
|
|
52
63
|
}
|
|
53
|
-
let width = part.text === '\t' ? Math.min(cols,
|
|
64
|
+
let width = part.text === '\t' ? Math.min(cols, TAB_WIDTH - column % TAB_WIDTH)
|
|
54
65
|
: graphemeWidth(part.text);
|
|
55
66
|
if (column > 0 && column + width > cols) {
|
|
56
67
|
breakRow();
|
|
57
68
|
if (part.text === '\t')
|
|
58
|
-
width = Math.min(cols,
|
|
69
|
+
width = Math.min(cols, TAB_WIDTH);
|
|
59
70
|
}
|
|
60
71
|
locate(part.index);
|
|
61
72
|
rows[rows.length - 1].push({ ...part,
|