@aragon-agent/cli 0.6.2 → 0.6.4
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 +321 -0
- package/README.md +297 -2
- package/dist/agent/controller.d.ts +193 -1
- package/dist/agent/controller.js +475 -2
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +189 -0
- package/dist/agent/reducer.js +301 -2
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +297 -16
- package/dist/commands/builtins.js +165 -24
- package/dist/commands/registry.d.ts +27 -0
- package/dist/compaction/archive.d.ts +175 -0
- package/dist/compaction/archive.js +267 -0
- package/dist/compaction/child.d.ts +97 -0
- package/dist/compaction/child.js +118 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +355 -0
- package/dist/compaction/compactor.d.ts +302 -0
- package/dist/compaction/compactor.js +810 -0
- package/dist/compaction/digest.d.ts +62 -0
- package/dist/compaction/digest.js +164 -0
- package/dist/compaction/limits.d.ts +165 -0
- package/dist/compaction/limits.js +172 -0
- package/dist/compaction/pressure.d.ts +135 -0
- package/dist/compaction/pressure.js +167 -0
- package/dist/compaction/summarize-call.d.ts +70 -0
- package/dist/compaction/summarize-call.js +86 -0
- package/dist/compaction/summary-prompt.d.ts +85 -0
- package/dist/compaction/summary-prompt.js +197 -0
- package/dist/compaction/tail-budget.d.ts +52 -0
- package/dist/compaction/tail-budget.js +67 -0
- package/dist/compaction/types.d.ts +215 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +264 -0
- package/dist/compaction/wiring.js +577 -0
- package/dist/config/app-paths.d.ts +14 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/cli-commands.d.ts +41 -0
- package/dist/config/cli-commands.js +77 -1
- package/dist/config/env.d.ts +5 -2
- package/dist/config/env.js +140 -3
- package/dist/config/load.d.ts +42 -0
- package/dist/config/load.js +101 -1
- package/dist/config/schema.d.ts +244 -0
- package/dist/config/schema.js +170 -0
- package/dist/config/store.js +37 -1
- package/dist/config/ui-state.d.ts +28 -1
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/runner.js +42 -0
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +53 -1
- package/dist/logging/install.js +81 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +55 -1
- package/dist/team/report.js +7 -1
- package/dist/team/runtime.d.ts +10 -0
- package/dist/team/runtime.js +7 -0
- package/dist/team/subagent.d.ts +31 -1
- package/dist/team/subagent.js +42 -15
- package/dist/team/types.d.ts +8 -0
- package/dist/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +62 -2
- package/dist/ui/App.js +546 -16
- package/dist/ui/Composer.d.ts +66 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +82 -3
- package/dist/ui/PromptInput.js +232 -49
- package/dist/ui/StatusBar.d.ts +59 -0
- package/dist/ui/StatusBar.js +16 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +61 -3
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- package/dist/ui/frame-differ.d.ts +46 -0
- package/dist/ui/frame-differ.js +94 -10
- package/dist/ui/gauge.d.ts +16 -1
- package/dist/ui/gauge.js +25 -6
- package/dist/ui/glyphs.d.ts +11 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/ScrollViewport.d.ts +43 -6
- package/dist/ui/layout/ScrollViewport.js +128 -21
- package/dist/ui/layout/budget.d.ts +44 -9
- package/dist/ui/layout/budget.js +42 -10
- package/dist/ui/layout/follow-state.d.ts +137 -0
- package/dist/ui/layout/follow-state.js +150 -0
- package/dist/ui/layout/virtual-window.d.ts +11 -0
- package/dist/ui/layout/virtual-window.js +85 -1
- package/dist/ui/overlays/HelpOverlay.js +9 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +74 -2
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +38 -0
- package/dist/ui/screen.js +113 -8
- package/dist/ui/selection/highlight.d.ts +39 -0
- package/dist/ui/selection/highlight.js +64 -0
- package/dist/ui/selection/screen-mirror.d.ts +24 -0
- package/dist/ui/selection/screen-mirror.js +35 -0
- package/dist/ui/selection/selection-controller.d.ts +88 -0
- package/dist/ui/selection/selection-controller.js +282 -0
- package/dist/ui/selection/selection.d.ts +90 -0
- package/dist/ui/selection/selection.js +223 -0
- package/dist/ui/stdout-frame-writer.d.ts +33 -0
- package/dist/ui/stdout-frame-writer.js +15 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +2 -0
- package/dist/ui/transcript-text.js +72 -0
- package/dist/ui/use-startup-notices.d.ts +27 -1
- package/dist/ui/use-startup-notices.js +43 -11
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/dist/ui/use-wheel-routing.js +8 -0
- package/package.json +4 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -42
- package/dist/input/stdin-mouse-filter.js +0 -153
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading paste frames back out of an Ink `input` string
|
|
3
|
+
* (tui-paste-handling section 5.4) -- pure.
|
|
4
|
+
*
|
|
5
|
+
* The stdin filter writes `PASTE_OPEN + payload + PASTE_CLOSE` inline, in stream
|
|
6
|
+
* order (I-5). This module is the only place that takes those markers apart.
|
|
7
|
+
*/
|
|
8
|
+
export interface FrameTextSegment {
|
|
9
|
+
readonly kind: 'text';
|
|
10
|
+
readonly text: string;
|
|
11
|
+
}
|
|
12
|
+
export interface FramePasteSegment {
|
|
13
|
+
readonly kind: 'paste';
|
|
14
|
+
readonly text: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A run of one `input` string.
|
|
18
|
+
*
|
|
19
|
+
* NO `id` HERE. The paste id is allocated by `PromptInput` while it builds the
|
|
20
|
+
* reducer's action, never by this function and never by the reducer (P1-5), so
|
|
21
|
+
* the split stays a pure function of its argument and `paste-frames.test.ts` can
|
|
22
|
+
* assert on its output.
|
|
23
|
+
*/
|
|
24
|
+
export type FrameSegment = FrameTextSegment | FramePasteSegment;
|
|
25
|
+
/**
|
|
26
|
+
* Remove every C0 byte, DEL and C1 byte from text that arrived OUTSIDE a frame
|
|
27
|
+
* (I-14). NO CR -> LF conversion, and that omission is the point.
|
|
28
|
+
*
|
|
29
|
+
* WHY THIS IS NOT OPTIONAL. Ink drains its whole buffer in one size-less
|
|
30
|
+
* `read()` (`App.js:135`), so the `input` string carrying a frame can also carry
|
|
31
|
+
* the keystroke bytes that arrived beside it -- most often the `\r` of a user who
|
|
32
|
+
* pastes and immediately presses Enter, since Ink's 32 ms render throttle and a
|
|
33
|
+
* large transcript's Yoga pass both outlast the filter's 15 ms burst window.
|
|
34
|
+
* Splicing that run in verbatim puts a raw CR back into the draft and re-opens
|
|
35
|
+
* the corruption this whole feature exists to remove.
|
|
36
|
+
*
|
|
37
|
+
* Note what it does NOT do. It does not turn a coalesced `\r` into a newline and
|
|
38
|
+
* it does not submit: a keystroke that merged with a frame has already lost its
|
|
39
|
+
* identity as a key (`parseKeypress` saw the concatenation, not the key), so the
|
|
40
|
+
* only two honest options are "insert something wrong" and "drop it". Dropping is
|
|
41
|
+
* recoverable and benign -- the user presses Enter again -- while inserting a
|
|
42
|
+
* control byte is not. Printable characters survive untouched.
|
|
43
|
+
*/
|
|
44
|
+
export declare function sanitiseTyped(text: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Split an `input` string into ordered text runs and paste runs.
|
|
47
|
+
*
|
|
48
|
+
* Text runs are returned already through `sanitiseTyped` (I-14); paste runs are
|
|
49
|
+
* returned through `sanitisePaste`, which keeps TAB and `\n` because they are
|
|
50
|
+
* content inside a payload.
|
|
51
|
+
*
|
|
52
|
+
* RE-SANITISING THE PAYLOAD IS DEFENCE IN DEPTH, NOT A SECOND OPINION. The
|
|
53
|
+
* filter already ran `sanitisePaste` and it is idempotent, so the normal path is
|
|
54
|
+
* byte-identical. What it buys is that G2 ("no pasted byte is ever executed as a
|
|
55
|
+
* terminal control") becomes a property of the COMPOSER rather than a property
|
|
56
|
+
* of whoever produced the frame -- which matters for the branch below, where an
|
|
57
|
+
* unmatched `PASTE_OPEN` turns arbitrary trailing bytes into a paste segment.
|
|
58
|
+
*
|
|
59
|
+
* An unmatched `PASTE_OPEN` -- impossible from our own filter, but cheap to
|
|
60
|
+
* survive -- yields its remainder as a paste segment rather than leaking NULs.
|
|
61
|
+
*/
|
|
62
|
+
export declare function splitPasteFrames(input: string): FrameSegment[];
|
|
63
|
+
/** True when this `input` carries at least one frame -- the branch guard. */
|
|
64
|
+
export declare function hasPasteFrame(input: string): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Turn a possibly-framed `input` into plain text for a consumer that has no
|
|
67
|
+
* paste model of its own: unwrap the frames, keep the payload, drop the markers,
|
|
68
|
+
* and run BOTH halves through `sanitiseTyped`.
|
|
69
|
+
*
|
|
70
|
+
* ANY `useInput` CONSUMER THAT APPENDS `input` TO A STRING MUST CALL THIS (I-12),
|
|
71
|
+
* and the list is pinned by a source scan rather than by review discipline
|
|
72
|
+
* because the failure is invisible: NUL renders as nothing.
|
|
73
|
+
*
|
|
74
|
+
* `useInput` is a BROADCAST -- `use-input.js` re-emits every chunk to every
|
|
75
|
+
* mounted handler -- so `SettingsScreen` and `QuestionOverlay` see the frame the
|
|
76
|
+
* composer was sent. An API key is ~100 characters with no line break, which trips
|
|
77
|
+
* Tier 2's burst rule and gets framed; nothing downstream removes the NULs
|
|
78
|
+
* (`String.prototype.trim()` does not treat U+0000 as whitespace), the field is
|
|
79
|
+
* rendered through `maskDot` so the damage is invisible, and the value is handed
|
|
80
|
+
* to `registerSecret()` and `controller.setApiKey()`. The user is told the
|
|
81
|
+
* settings were saved and every request afterwards fails to authenticate with
|
|
82
|
+
* nothing on screen to explain it.
|
|
83
|
+
*
|
|
84
|
+
* This is strictly better than v0.6.3 for both consumers: a pasted key carrying a
|
|
85
|
+
* stray CR or an ANSI sequence is now cleaned rather than stored raw.
|
|
86
|
+
*/
|
|
87
|
+
export declare function stripPasteFrames(input: string): string;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading paste frames back out of an Ink `input` string
|
|
3
|
+
* (tui-paste-handling section 5.4) -- pure.
|
|
4
|
+
*
|
|
5
|
+
* The stdin filter writes `PASTE_OPEN + payload + PASTE_CLOSE` inline, in stream
|
|
6
|
+
* order (I-5). This module is the only place that takes those markers apart.
|
|
7
|
+
*/
|
|
8
|
+
import { PASTE_CLOSE, PASTE_OPEN } from '../input/limits.js';
|
|
9
|
+
import { sanitisePaste } from '../input/paste-parse.js';
|
|
10
|
+
/**
|
|
11
|
+
* Remove every C0 byte, DEL and C1 byte from text that arrived OUTSIDE a frame
|
|
12
|
+
* (I-14). NO CR -> LF conversion, and that omission is the point.
|
|
13
|
+
*
|
|
14
|
+
* WHY THIS IS NOT OPTIONAL. Ink drains its whole buffer in one size-less
|
|
15
|
+
* `read()` (`App.js:135`), so the `input` string carrying a frame can also carry
|
|
16
|
+
* the keystroke bytes that arrived beside it -- most often the `\r` of a user who
|
|
17
|
+
* pastes and immediately presses Enter, since Ink's 32 ms render throttle and a
|
|
18
|
+
* large transcript's Yoga pass both outlast the filter's 15 ms burst window.
|
|
19
|
+
* Splicing that run in verbatim puts a raw CR back into the draft and re-opens
|
|
20
|
+
* the corruption this whole feature exists to remove.
|
|
21
|
+
*
|
|
22
|
+
* Note what it does NOT do. It does not turn a coalesced `\r` into a newline and
|
|
23
|
+
* it does not submit: a keystroke that merged with a frame has already lost its
|
|
24
|
+
* identity as a key (`parseKeypress` saw the concatenation, not the key), so the
|
|
25
|
+
* only two honest options are "insert something wrong" and "drop it". Dropping is
|
|
26
|
+
* recoverable and benign -- the user presses Enter again -- while inserting a
|
|
27
|
+
* control byte is not. Printable characters survive untouched.
|
|
28
|
+
*/
|
|
29
|
+
export function sanitiseTyped(text) {
|
|
30
|
+
return text.replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Split an `input` string into ordered text runs and paste runs.
|
|
34
|
+
*
|
|
35
|
+
* Text runs are returned already through `sanitiseTyped` (I-14); paste runs are
|
|
36
|
+
* returned through `sanitisePaste`, which keeps TAB and `\n` because they are
|
|
37
|
+
* content inside a payload.
|
|
38
|
+
*
|
|
39
|
+
* RE-SANITISING THE PAYLOAD IS DEFENCE IN DEPTH, NOT A SECOND OPINION. The
|
|
40
|
+
* filter already ran `sanitisePaste` and it is idempotent, so the normal path is
|
|
41
|
+
* byte-identical. What it buys is that G2 ("no pasted byte is ever executed as a
|
|
42
|
+
* terminal control") becomes a property of the COMPOSER rather than a property
|
|
43
|
+
* of whoever produced the frame -- which matters for the branch below, where an
|
|
44
|
+
* unmatched `PASTE_OPEN` turns arbitrary trailing bytes into a paste segment.
|
|
45
|
+
*
|
|
46
|
+
* An unmatched `PASTE_OPEN` -- impossible from our own filter, but cheap to
|
|
47
|
+
* survive -- yields its remainder as a paste segment rather than leaking NULs.
|
|
48
|
+
*/
|
|
49
|
+
export function splitPasteFrames(input) {
|
|
50
|
+
const segments = [];
|
|
51
|
+
const pushText = (raw) => {
|
|
52
|
+
const text = sanitiseTyped(raw);
|
|
53
|
+
if (text.length > 0)
|
|
54
|
+
segments.push({ kind: 'text', text });
|
|
55
|
+
};
|
|
56
|
+
const pushPaste = (raw) => {
|
|
57
|
+
const text = sanitisePaste(raw);
|
|
58
|
+
if (text.length > 0)
|
|
59
|
+
segments.push({ kind: 'paste', text });
|
|
60
|
+
};
|
|
61
|
+
let i = 0;
|
|
62
|
+
while (i < input.length) {
|
|
63
|
+
const open = input.indexOf(PASTE_OPEN, i);
|
|
64
|
+
if (open === -1) {
|
|
65
|
+
pushText(input.slice(i));
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
pushText(input.slice(i, open));
|
|
69
|
+
const bodyStart = open + PASTE_OPEN.length;
|
|
70
|
+
const close = input.indexOf(PASTE_CLOSE, bodyStart);
|
|
71
|
+
if (close === -1) {
|
|
72
|
+
pushPaste(input.slice(bodyStart));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
pushPaste(input.slice(bodyStart, close));
|
|
76
|
+
i = close + PASTE_CLOSE.length;
|
|
77
|
+
}
|
|
78
|
+
return segments;
|
|
79
|
+
}
|
|
80
|
+
/** True when this `input` carries at least one frame -- the branch guard. */
|
|
81
|
+
export function hasPasteFrame(input) {
|
|
82
|
+
return input.includes(PASTE_OPEN);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Turn a possibly-framed `input` into plain text for a consumer that has no
|
|
86
|
+
* paste model of its own: unwrap the frames, keep the payload, drop the markers,
|
|
87
|
+
* and run BOTH halves through `sanitiseTyped`.
|
|
88
|
+
*
|
|
89
|
+
* ANY `useInput` CONSUMER THAT APPENDS `input` TO A STRING MUST CALL THIS (I-12),
|
|
90
|
+
* and the list is pinned by a source scan rather than by review discipline
|
|
91
|
+
* because the failure is invisible: NUL renders as nothing.
|
|
92
|
+
*
|
|
93
|
+
* `useInput` is a BROADCAST -- `use-input.js` re-emits every chunk to every
|
|
94
|
+
* mounted handler -- so `SettingsScreen` and `QuestionOverlay` see the frame the
|
|
95
|
+
* composer was sent. An API key is ~100 characters with no line break, which trips
|
|
96
|
+
* Tier 2's burst rule and gets framed; nothing downstream removes the NULs
|
|
97
|
+
* (`String.prototype.trim()` does not treat U+0000 as whitespace), the field is
|
|
98
|
+
* rendered through `maskDot` so the damage is invisible, and the value is handed
|
|
99
|
+
* to `registerSecret()` and `controller.setApiKey()`. The user is told the
|
|
100
|
+
* settings were saved and every request afterwards fails to authenticate with
|
|
101
|
+
* nothing on screen to explain it.
|
|
102
|
+
*
|
|
103
|
+
* This is strictly better than v0.6.3 for both consumers: a pasted key carrying a
|
|
104
|
+
* stray CR or an ANSI sequence is now cleaned rather than stored raw.
|
|
105
|
+
*/
|
|
106
|
+
export function stripPasteFrames(input) {
|
|
107
|
+
if (!hasPasteFrame(input))
|
|
108
|
+
return sanitiseTyped(input);
|
|
109
|
+
return splitPasteFrames(input)
|
|
110
|
+
.map((segment) => (segment.kind === 'paste' ? sanitiseTyped(segment.text) : segment.text))
|
|
111
|
+
.join('');
|
|
112
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paste token vocabulary (tui-paste-handling section 5.3) -- pure. No Ink,
|
|
3
|
+
* no React, no I/O.
|
|
4
|
+
*
|
|
5
|
+
* A large paste is replaced IN THE BUFFER by a display token,
|
|
6
|
+
* `[Pasted text #1 +15 lines]`, and its payload is held beside the buffer in the
|
|
7
|
+
* editor's `pastes` map until the message is sent (D-7). An inline token rather
|
|
8
|
+
* than an out-of-band attachment is what lets the user place a paste relative to
|
|
9
|
+
* their own words -- "explain this: [Pasted text #1 +218 lines] -- why does it
|
|
10
|
+
* retry?" -- which is also the shape the requirement's example asks for.
|
|
11
|
+
*
|
|
12
|
+
* The token is ATOMIC FOR EDITING and NOT for cursor movement (D-8). Delete and
|
|
13
|
+
* kill ranges snap outward to cover a whole token and the caret snaps out before
|
|
14
|
+
* an insert, which removes every realistic way to corrupt one; arrows still step
|
|
15
|
+
* by character, because re-deriving `moveVertical`, `home`/`end` and word jumps
|
|
16
|
+
* around a synthetic unit buys a correctness the user cannot notice.
|
|
17
|
+
*/
|
|
18
|
+
export interface PasteRecord {
|
|
19
|
+
readonly id: number;
|
|
20
|
+
/** The sanitised payload, byte-for-byte what `expandPastes` sends. */
|
|
21
|
+
readonly text: string;
|
|
22
|
+
readonly lines: number;
|
|
23
|
+
readonly chars: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A FACTORY, NOT A SHARED CONSTANT (P2-2).
|
|
27
|
+
*
|
|
28
|
+
* A module-level `/g` RegExp carries `lastIndex` between calls, so `re.test(x)`
|
|
29
|
+
* returns false on every second call with the same input -- a bug that presents
|
|
30
|
+
* as "token detection works, then randomly does not". Callers must use
|
|
31
|
+
* `matchAll` or `replace` (both safe) on a FRESH instance; nothing in this module
|
|
32
|
+
* may call `.test()` or `.exec()` on a `/g` regex it did not just create.
|
|
33
|
+
*
|
|
34
|
+
* Strict, anchored to its own literal shape, ASCII-only. Group 1 is the id.
|
|
35
|
+
*/
|
|
36
|
+
export declare function pasteTokenRe(): RegExp;
|
|
37
|
+
export declare function allocatePasteId(): number;
|
|
38
|
+
/** Test-only reset. Never called from application code. */
|
|
39
|
+
export declare function resetPasteIdsForTest(): void;
|
|
40
|
+
export declare function makePasteRecord(id: number, text: string): PasteRecord;
|
|
41
|
+
/**
|
|
42
|
+
* `[Pasted text #3 +218 lines]`, or `[Pasted text #4 +1204 chars]` for a paste
|
|
43
|
+
* that is one very long line.
|
|
44
|
+
*
|
|
45
|
+
* "+1 lines" is not a sentence, and for a one-liner the number the user cares
|
|
46
|
+
* about is its length rather than its line count.
|
|
47
|
+
*/
|
|
48
|
+
export declare function formatPasteToken(record: PasteRecord): string;
|
|
49
|
+
/** Above EITHER inline bound the paste is collapsed into a token (G3 / G4). */
|
|
50
|
+
export declare function shouldCollapse(text: string): boolean;
|
|
51
|
+
interface TokenSpan {
|
|
52
|
+
readonly start: number;
|
|
53
|
+
readonly end: number;
|
|
54
|
+
readonly id: number;
|
|
55
|
+
}
|
|
56
|
+
/** Every token in the buffer, in order. */
|
|
57
|
+
export declare function tokenSpans(buffer: string): TokenSpan[];
|
|
58
|
+
/** The token containing or touching `index`, or `null`. */
|
|
59
|
+
export declare function tokenAt(buffer: string, index: number): TokenSpan | null;
|
|
60
|
+
/**
|
|
61
|
+
* Move a caret that landed STRICTLY INSIDE a token to its nearer edge (D-8).
|
|
62
|
+
*
|
|
63
|
+
* An index at either edge, or outside every token, is returned unchanged -- so
|
|
64
|
+
* this is a no-op on a buffer with no tokens, which is every buffer today.
|
|
65
|
+
*/
|
|
66
|
+
export declare function snapOutOfToken(buffer: string, cursor: number): number;
|
|
67
|
+
/**
|
|
68
|
+
* Grow `[from, to)` so it covers every token it partially overlaps (G5).
|
|
69
|
+
*
|
|
70
|
+
* This is what makes `Backspace` immediately after `...lines]` remove the whole
|
|
71
|
+
* token in ONE keystroke, and what stops `Ctrl+W` / `Ctrl+U` / `Ctrl+K` leaving
|
|
72
|
+
* half a token -- and therefore an unparseable label -- behind.
|
|
73
|
+
*/
|
|
74
|
+
export declare function expandRangeOverTokens(buffer: string, from: number, to: number): {
|
|
75
|
+
from: number;
|
|
76
|
+
to: number;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Replace every token whose id is present in `pastes`; leave the rest verbatim.
|
|
80
|
+
*
|
|
81
|
+
* A user who literally TYPES `[Pasted text #3 +9 lines]` while paste #3 exists in
|
|
82
|
+
* the same draft will have it expanded. That is bounded (ids are per process, the
|
|
83
|
+
* map is per draft) and harmless (they get text that is already in their own
|
|
84
|
+
* draft), and the alternative -- an unguessable nonce in the label -- would trade
|
|
85
|
+
* a case nobody hits for a label nobody can read.
|
|
86
|
+
*/
|
|
87
|
+
export declare function expandPastes(buffer: string, pastes: ReadonlyMap<number, PasteRecord>): string;
|
|
88
|
+
/** Ids still referenced by the buffer -- the input to I-7's prune. */
|
|
89
|
+
export declare function referencedIds(buffer: string): Set<number>;
|
|
90
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paste token vocabulary (tui-paste-handling section 5.3) -- pure. No Ink,
|
|
3
|
+
* no React, no I/O.
|
|
4
|
+
*
|
|
5
|
+
* A large paste is replaced IN THE BUFFER by a display token,
|
|
6
|
+
* `[Pasted text #1 +15 lines]`, and its payload is held beside the buffer in the
|
|
7
|
+
* editor's `pastes` map until the message is sent (D-7). An inline token rather
|
|
8
|
+
* than an out-of-band attachment is what lets the user place a paste relative to
|
|
9
|
+
* their own words -- "explain this: [Pasted text #1 +218 lines] -- why does it
|
|
10
|
+
* retry?" -- which is also the shape the requirement's example asks for.
|
|
11
|
+
*
|
|
12
|
+
* The token is ATOMIC FOR EDITING and NOT for cursor movement (D-8). Delete and
|
|
13
|
+
* kill ranges snap outward to cover a whole token and the caret snaps out before
|
|
14
|
+
* an insert, which removes every realistic way to corrupt one; arrows still step
|
|
15
|
+
* by character, because re-deriving `moveVertical`, `home`/`end` and word jumps
|
|
16
|
+
* around a synthetic unit buys a correctness the user cannot notice.
|
|
17
|
+
*/
|
|
18
|
+
import { PASTE_INLINE_MAX_CHARS, PASTE_INLINE_MAX_LINES } from './composer-limits.js';
|
|
19
|
+
/**
|
|
20
|
+
* A FACTORY, NOT A SHARED CONSTANT (P2-2).
|
|
21
|
+
*
|
|
22
|
+
* A module-level `/g` RegExp carries `lastIndex` between calls, so `re.test(x)`
|
|
23
|
+
* returns false on every second call with the same input -- a bug that presents
|
|
24
|
+
* as "token detection works, then randomly does not". Callers must use
|
|
25
|
+
* `matchAll` or `replace` (both safe) on a FRESH instance; nothing in this module
|
|
26
|
+
* may call `.test()` or `.exec()` on a `/g` regex it did not just create.
|
|
27
|
+
*
|
|
28
|
+
* Strict, anchored to its own literal shape, ASCII-only. Group 1 is the id.
|
|
29
|
+
*/
|
|
30
|
+
export function pasteTokenRe() {
|
|
31
|
+
return /\[Pasted text #(\d+) \+\d+ (?:lines|chars)\]/g;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The process-monotonic id counter (D-14).
|
|
35
|
+
*
|
|
36
|
+
* CALLED FROM THE COMPONENT, NEVER FROM THE REDUCER (P1-5). React is explicitly
|
|
37
|
+
* allowed to run a reducer more than once for one action -- eager evaluation in
|
|
38
|
+
* `dispatchSetState`, base-queue replay after a bailout, StrictMode double
|
|
39
|
+
* invocation -- and a counter mutated inside one would skip ids and produce two
|
|
40
|
+
* states that differ in the token TEXT, not merely in identity. Allocating in the
|
|
41
|
+
* handler also makes the rendered token deterministic for a unit test.
|
|
42
|
+
*
|
|
43
|
+
* Monotonic for the PROCESS rather than per draft, so two drafts never show `#1`
|
|
44
|
+
* for different content in one session.
|
|
45
|
+
*/
|
|
46
|
+
let nextPasteId = 1;
|
|
47
|
+
export function allocatePasteId() {
|
|
48
|
+
const id = nextPasteId;
|
|
49
|
+
nextPasteId += 1;
|
|
50
|
+
return id;
|
|
51
|
+
}
|
|
52
|
+
/** Test-only reset. Never called from application code. */
|
|
53
|
+
export function resetPasteIdsForTest() {
|
|
54
|
+
nextPasteId = 1;
|
|
55
|
+
}
|
|
56
|
+
export function makePasteRecord(id, text) {
|
|
57
|
+
return { id, text, lines: text.split('\n').length, chars: [...text].length };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `[Pasted text #3 +218 lines]`, or `[Pasted text #4 +1204 chars]` for a paste
|
|
61
|
+
* that is one very long line.
|
|
62
|
+
*
|
|
63
|
+
* "+1 lines" is not a sentence, and for a one-liner the number the user cares
|
|
64
|
+
* about is its length rather than its line count.
|
|
65
|
+
*/
|
|
66
|
+
export function formatPasteToken(record) {
|
|
67
|
+
return record.lines > 1
|
|
68
|
+
? `[Pasted text #${record.id} +${record.lines} lines]`
|
|
69
|
+
: `[Pasted text #${record.id} +${record.chars} chars]`;
|
|
70
|
+
}
|
|
71
|
+
/** Above EITHER inline bound the paste is collapsed into a token (G3 / G4). */
|
|
72
|
+
export function shouldCollapse(text) {
|
|
73
|
+
return text.split('\n').length > PASTE_INLINE_MAX_LINES || [...text].length > PASTE_INLINE_MAX_CHARS;
|
|
74
|
+
}
|
|
75
|
+
/** Every token in the buffer, in order. */
|
|
76
|
+
export function tokenSpans(buffer) {
|
|
77
|
+
const spans = [];
|
|
78
|
+
for (const m of buffer.matchAll(pasteTokenRe())) {
|
|
79
|
+
const start = m.index ?? 0;
|
|
80
|
+
spans.push({ start, end: start + m[0].length, id: Number(m[1]) });
|
|
81
|
+
}
|
|
82
|
+
return spans;
|
|
83
|
+
}
|
|
84
|
+
/** The token containing or touching `index`, or `null`. */
|
|
85
|
+
export function tokenAt(buffer, index) {
|
|
86
|
+
for (const span of tokenSpans(buffer)) {
|
|
87
|
+
if (index >= span.start && index <= span.end)
|
|
88
|
+
return span;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Move a caret that landed STRICTLY INSIDE a token to its nearer edge (D-8).
|
|
94
|
+
*
|
|
95
|
+
* An index at either edge, or outside every token, is returned unchanged -- so
|
|
96
|
+
* this is a no-op on a buffer with no tokens, which is every buffer today.
|
|
97
|
+
*/
|
|
98
|
+
export function snapOutOfToken(buffer, cursor) {
|
|
99
|
+
for (const span of tokenSpans(buffer)) {
|
|
100
|
+
if (cursor <= span.start || cursor >= span.end)
|
|
101
|
+
continue;
|
|
102
|
+
return cursor - span.start <= span.end - cursor ? span.start : span.end;
|
|
103
|
+
}
|
|
104
|
+
return cursor;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Grow `[from, to)` so it covers every token it partially overlaps (G5).
|
|
108
|
+
*
|
|
109
|
+
* This is what makes `Backspace` immediately after `...lines]` remove the whole
|
|
110
|
+
* token in ONE keystroke, and what stops `Ctrl+W` / `Ctrl+U` / `Ctrl+K` leaving
|
|
111
|
+
* half a token -- and therefore an unparseable label -- behind.
|
|
112
|
+
*/
|
|
113
|
+
export function expandRangeOverTokens(buffer, from, to) {
|
|
114
|
+
let lo = Math.min(from, to);
|
|
115
|
+
let hi = Math.max(from, to);
|
|
116
|
+
for (const span of tokenSpans(buffer)) {
|
|
117
|
+
if (span.start < hi && lo < span.end) {
|
|
118
|
+
lo = Math.min(lo, span.start);
|
|
119
|
+
hi = Math.max(hi, span.end);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { from: lo, to: hi };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Replace every token whose id is present in `pastes`; leave the rest verbatim.
|
|
126
|
+
*
|
|
127
|
+
* A user who literally TYPES `[Pasted text #3 +9 lines]` while paste #3 exists in
|
|
128
|
+
* the same draft will have it expanded. That is bounded (ids are per process, the
|
|
129
|
+
* map is per draft) and harmless (they get text that is already in their own
|
|
130
|
+
* draft), and the alternative -- an unguessable nonce in the label -- would trade
|
|
131
|
+
* a case nobody hits for a label nobody can read.
|
|
132
|
+
*/
|
|
133
|
+
export function expandPastes(buffer, pastes) {
|
|
134
|
+
if (pastes.size === 0)
|
|
135
|
+
return buffer;
|
|
136
|
+
return buffer.replace(pasteTokenRe(), (match, idText) => {
|
|
137
|
+
const record = pastes.get(Number(idText));
|
|
138
|
+
return record ? record.text : match;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** Ids still referenced by the buffer -- the input to I-7's prune. */
|
|
142
|
+
export function referencedIds(buffer) {
|
|
143
|
+
const ids = new Set();
|
|
144
|
+
for (const m of buffer.matchAll(pasteTokenRe()))
|
|
145
|
+
ids.add(Number(m[1]));
|
|
146
|
+
return ids;
|
|
147
|
+
}
|
package/dist/ui/screen.d.ts
CHANGED
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
export interface ScreenHandle {
|
|
15
15
|
/** Leave the alternate screen. Idempotent — safe from every exit path. */
|
|
16
16
|
restore(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Hand the mouse back to the terminal for this session, or take it again
|
|
19
|
+
* (tui-selection-and-scroll-follow §4.4.6 / G3). A no-op when this session
|
|
20
|
+
* never had a filter, and idempotent in both directions.
|
|
21
|
+
*
|
|
22
|
+
* This is the honest answer to "a user who wants their terminal's own
|
|
23
|
+
* selection", and it is reversible in one command instead of a restart plus a
|
|
24
|
+
* config edit (D-9).
|
|
25
|
+
*/
|
|
26
|
+
setMouseCapture(on: boolean): void;
|
|
17
27
|
}
|
|
18
28
|
export interface AltScreenOptions {
|
|
19
29
|
/**
|
|
@@ -27,6 +37,34 @@ export interface AltScreenOptions {
|
|
|
27
37
|
* only caller is `cli.tsx::runInteractive`, and it passes `filter !== null`.
|
|
28
38
|
*/
|
|
29
39
|
readonly mouse: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to request motion reporting (`?1002h`) alongside the two modes
|
|
42
|
+
* above — i.e. whether drag-select is on for this session.
|
|
43
|
+
*
|
|
44
|
+
* SAME "ALREADY DECIDED ELSEWHERE" DISCIPLINE AS `mouse`: this module must
|
|
45
|
+
* never derive it from config. `cli.tsx` passes
|
|
46
|
+
* `mouseFilter !== null && cfg.mouseSelect`.
|
|
47
|
+
*
|
|
48
|
+
* Optional, defaulting to `false`, so every pre-existing caller and every
|
|
49
|
+
* pre-existing test emits exactly the bytes it emitted before (AC-8 / T-34).
|
|
50
|
+
*/
|
|
51
|
+
readonly motion?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* TRUE MEANS "A FILTER THAT CONSUMES `\x1b[200~` IS ALREADY INSTALLED", NOT
|
|
54
|
+
* "the user wants paste support" — the same discipline `mouse` carries (I-1 /
|
|
55
|
+
* I-8).
|
|
56
|
+
*
|
|
57
|
+
* Enabling DEC 2004 without a filter in front of Ink does not degrade the
|
|
58
|
+
* feature: it types `[200~` and `[201~` into the user's message on EVERY paste,
|
|
59
|
+
* which is strictly worse than the bug being fixed. `cli.tsx` passes
|
|
60
|
+
* `pasteOn && mode === 'fullscreen'`, and NOT `filter !== null && config.paste`
|
|
61
|
+
* — after the gate widened, the handle no longer answers any question except
|
|
62
|
+
* "is a stream wrapped?" (D-17 / I-11).
|
|
63
|
+
*
|
|
64
|
+
* Optional, defaulting to `false`, so every pre-existing caller and every
|
|
65
|
+
* pre-existing assertion emits exactly the bytes it emitted before.
|
|
66
|
+
*/
|
|
67
|
+
readonly bracketedPaste?: boolean;
|
|
30
68
|
}
|
|
31
69
|
/**
|
|
32
70
|
* Enter the alternate screen. A non-TTY stdout is a hard no-op (tests, pipes,
|
package/dist/ui/screen.js
CHANGED
|
@@ -20,12 +20,24 @@ const SHOW_CURSOR = '\x1b[?25h';
|
|
|
20
20
|
/**
|
|
21
21
|
* `?1000` is NORMAL TRACKING (press / release), the minimum mode that reports
|
|
22
22
|
* wheel buttons; `?1006` selects SGR encoding so coordinates arrive as decimal
|
|
23
|
-
* text rather than raw bytes above 0x7F that `setEncoding('utf8')` would
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* text rather than raw bytes above 0x7F that `setEncoding('utf8')` would mangle.
|
|
24
|
+
*
|
|
25
|
+
* `?1002` is BUTTON-EVENT TRACKING: motion is reported ONLY WHILE A BUTTON IS
|
|
26
|
+
* HELD, which is what drag-select needs and costs exactly zero reports while the
|
|
27
|
+
* pointer is idle. This comment used to lump it together with `?1003` and
|
|
28
|
+
* dismiss both as "floods stdin on every pointer move" — true of `?1003`
|
|
29
|
+
* (any-event), false of `?1002`, and the sentence had to go in the same commit
|
|
30
|
+
* that started asking for it or it would stand as a documented reason not to
|
|
31
|
+
* (AC-11 / D-10).
|
|
32
|
+
*
|
|
33
|
+
* IT IS GATED ON `motion`, i.e. on the resolved `mouseSelect` capability, and
|
|
34
|
+
* that is what makes "S3 is revertible" a byte-level fact rather than a claim:
|
|
35
|
+
* with drag-select off the sequences below are byte-identical to the constants
|
|
36
|
+
* they replaced (AC-8 / D-15). `disableMouse` is the exact reverse of
|
|
37
|
+
* `enableMouse` for both values (I-1 / AC-7).
|
|
26
38
|
*/
|
|
27
|
-
const
|
|
28
|
-
const
|
|
39
|
+
const enableMouse = (motion) => `\x1b[?1000h${motion ? '\x1b[?1002h' : ''}\x1b[?1006h`;
|
|
40
|
+
const disableMouse = (motion) => `\x1b[?1006l${motion ? '\x1b[?1002l' : ''}\x1b[?1000l`;
|
|
29
41
|
/**
|
|
30
42
|
* Alternate scroll (DEC private mode 1007) is what turns a wheel notch into a
|
|
31
43
|
* burst of arrow keys, which `PromptInput` reads as prompt-history recall. With
|
|
@@ -43,7 +55,33 @@ const DISABLE_MOUSE = '\x1b[?1006l\x1b[?1000l';
|
|
|
43
55
|
*/
|
|
44
56
|
const SAVE_ALT_SCROLL_OFF = '\x1b[?1007s\x1b[?1007l';
|
|
45
57
|
const RESTORE_ALT_SCROLL = '\x1b[?1007r';
|
|
46
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Bracketed paste (DEC private mode 2004) — the terminal wraps a paste in
|
|
60
|
+
* `\x1b[200~` / `\x1b[201~`, which makes Tier 1 detection EXACT rather than a
|
|
61
|
+
* heuristic on chunk shape (tui-paste-handling D-6 / L2).
|
|
62
|
+
*
|
|
63
|
+
* WHY THIS IS FORCED OFF ON EXIT RATHER THAN SAVED AND RESTORED (P1-8). DEC 1007
|
|
64
|
+
* above is the same class of state — global, not ours — and gets the OPPOSITE
|
|
65
|
+
* treatment, so the asymmetry has to be written down or the next reader will
|
|
66
|
+
* "fix" the inconsistency:
|
|
67
|
+
*
|
|
68
|
+
* - `\x1b[?2004r` on a terminal that does not implement XTSAVE is a NO-OP, which
|
|
69
|
+
* leaves 2004 ON. A shell that does not itself speak bracketed paste then
|
|
70
|
+
* shows a literal `[200~` on every paste until the user runs `reset` — the
|
|
71
|
+
* exact I-1 failure, arriving from the restore path.
|
|
72
|
+
* - Forcing `?2004l` has no such failure mode. Every shell that DOES use
|
|
73
|
+
* bracketed paste (bash >= 4.4 readline, zsh ZLE, fish, PSReadLine) re-arms it
|
|
74
|
+
* when it draws its next prompt, so the state the user sees after `aragon`
|
|
75
|
+
* exits is the state they had. Every shell that does not use it was already
|
|
76
|
+
* off.
|
|
77
|
+
*
|
|
78
|
+
* So: 1007 is saved because getting it wrong silently changes a preference; 2004
|
|
79
|
+
* is forced because getting it wrong visibly breaks pasting, and the forced value
|
|
80
|
+
* is the one every consumer re-establishes for itself.
|
|
81
|
+
*/
|
|
82
|
+
const ENABLE_BRACKETED_PASTE = '\x1b[?2004h';
|
|
83
|
+
const DISABLE_BRACKETED_PASTE = '\x1b[?2004l';
|
|
84
|
+
const NOOP_HANDLE = { restore() { }, setMouseCapture() { } };
|
|
47
85
|
function safeWrite(stdout, data) {
|
|
48
86
|
try {
|
|
49
87
|
stdout.write(data);
|
|
@@ -60,7 +98,61 @@ export function enterAltScreen(stdout, options = { mouse: false }) {
|
|
|
60
98
|
if (!stdout || !stdout.isTTY || typeof stdout.write !== 'function')
|
|
61
99
|
return NOOP_HANDLE;
|
|
62
100
|
const mouse = options.mouse === true;
|
|
63
|
-
|
|
101
|
+
const motion = options.motion === true;
|
|
102
|
+
const bracketedPaste = options.bracketedPaste === true;
|
|
103
|
+
safeWrite(stdout, ENTER_ALT +
|
|
104
|
+
CLEAR_VISIBLE +
|
|
105
|
+
CURSOR_HOME +
|
|
106
|
+
(mouse ? enableMouse(motion) : SAVE_ALT_SCROLL_OFF) +
|
|
107
|
+
(bracketedPaste ? ENABLE_BRACKETED_PASTE : ''));
|
|
108
|
+
/**
|
|
109
|
+
* THE CURRENT capture state, not the one the session started in (I-10).
|
|
110
|
+
*
|
|
111
|
+
* `restore()` used to close over the immutable `mouse` boolean. After
|
|
112
|
+
* `/mouse` exists that boolean is no longer the truth, and the exit path is
|
|
113
|
+
* the one place where being wrong leaves the user's shell broken until they
|
|
114
|
+
* run `reset`.
|
|
115
|
+
*/
|
|
116
|
+
let captured = mouse;
|
|
117
|
+
/**
|
|
118
|
+
* Whether OUR `XTSAVE` of DEC 1007 is outstanding.
|
|
119
|
+
*
|
|
120
|
+
* `XTSAVE` HAS ONE SLOT PER MODE, so the save and the restore must stay
|
|
121
|
+
* STRICTLY PAIRED: an `off` that saved while our own disable was already in
|
|
122
|
+
* effect would save OUR value, and the following restore would hand the user
|
|
123
|
+
* back a preference that was never theirs — permanently.
|
|
124
|
+
*/
|
|
125
|
+
let savedAltScroll = mouse ? false : true;
|
|
126
|
+
const setMouseCapture = (on) => {
|
|
127
|
+
// IT MUST NOT TOUCH DEC 2004 (I-16 / P2-7). `/mouse off` releases the
|
|
128
|
+
// pointer; it says nothing about pasting, and a user who turns the mouse off
|
|
129
|
+
// mid-session to select text with their terminal must not silently lose Tier
|
|
130
|
+
// 1 detection as well. The two modes are written by different call sites and
|
|
131
|
+
// unwound by different flags in `restore()`.
|
|
132
|
+
//
|
|
133
|
+
// A session that never installed a filter has nothing to release, and
|
|
134
|
+
// enabling reporting without one types `[<0;12;5M` into the user's message
|
|
135
|
+
// on every click (I-8). It stays a no-op in both directions.
|
|
136
|
+
if (!mouse || on === captured)
|
|
137
|
+
return;
|
|
138
|
+
captured = on;
|
|
139
|
+
if (on) {
|
|
140
|
+
// Alternate scroll goes back to the user's own preference BEFORE reporting
|
|
141
|
+
// resumes, so the two never overlap in the "both disabled" state.
|
|
142
|
+
safeWrite(stdout, (savedAltScroll ? RESTORE_ALT_SCROLL : '') + enableMouse(motion));
|
|
143
|
+
savedAltScroll = false;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// DEC 1007 IS THE HALF THAT MUST NOT BE DROPPED (P1-1). With reporting off,
|
|
147
|
+
// alternate scroll turns every wheel notch back into a burst of arrow keys,
|
|
148
|
+
// which `PromptInput` reads as prompt-history recall — replacing the user's
|
|
149
|
+
// draft with an old prompt. A `/mouse off` that wrote only the mouse
|
|
150
|
+
// sequences would hand that defect back on request, in the one command whose
|
|
151
|
+
// entire purpose is to make things better. Saved rather than force-set, so a
|
|
152
|
+
// user who deliberately disabled alternate scroll keeps their choice.
|
|
153
|
+
safeWrite(stdout, disableMouse(motion) + SAVE_ALT_SCROLL_OFF);
|
|
154
|
+
savedAltScroll = true;
|
|
155
|
+
};
|
|
64
156
|
let restored = false;
|
|
65
157
|
return {
|
|
66
158
|
restore() {
|
|
@@ -74,8 +166,21 @@ export function enterAltScreen(stdout, options = { mouse: false }) {
|
|
|
74
166
|
// here and not in `App.tsx`, a component that is on none of them.
|
|
75
167
|
// Leaving reporting on prints `[<0;12;5M` in the user's shell on every
|
|
76
168
|
// click, forever, until they run `reset`.
|
|
77
|
-
|
|
169
|
+
//
|
|
170
|
+
// IT UNWINDS THE CURRENT STATE (I-10): `captured` and `savedAltScroll` are
|
|
171
|
+
// read here rather than the startup `mouse` flag, so a session that ran
|
|
172
|
+
// `/mouse off` exits with exactly one outstanding save restored and
|
|
173
|
+
// nothing left reporting.
|
|
174
|
+
//
|
|
175
|
+
// DEC 2004 IS UNWOUND FROM THE STARTUP FLAG, not from a runtime one, and
|
|
176
|
+
// that is deliberate: `setMouseCapture` never touches it (I-16), so this
|
|
177
|
+
// session either enabled it at entry or never did.
|
|
178
|
+
const tail = (captured ? disableMouse(motion) : '') +
|
|
179
|
+
(bracketedPaste ? DISABLE_BRACKETED_PASTE : '') +
|
|
180
|
+
(savedAltScroll ? RESTORE_ALT_SCROLL : '');
|
|
181
|
+
safeWrite(stdout, tail + SHOW_CURSOR + LEAVE_ALT);
|
|
78
182
|
},
|
|
183
|
+
setMouseCapture,
|
|
79
184
|
};
|
|
80
185
|
}
|
|
81
186
|
/**
|