@aragon-agent/cli 0.6.8 → 0.6.10
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 +27 -0
- package/README.md +89 -44
- package/dist/agent/controller.d.ts +1 -1
- package/dist/agent/reducer.d.ts +31 -113
- package/dist/agent/reducer.js +41 -63
- package/dist/cli.js +53 -155
- package/dist/commands/builtins.d.ts +2 -1
- package/dist/commands/builtins.js +47 -2
- package/dist/commands/perf.d.ts +32 -2
- package/dist/commands/perf.js +32 -2
- package/dist/commands/registry.d.ts +2 -2
- package/dist/config/env.js +0 -130
- package/dist/config/load.d.ts +0 -2
- package/dist/config/load.js +2 -31
- package/dist/config/schema.d.ts +1 -14
- package/dist/config/schema.js +0 -1
- package/dist/config/store.js +2 -0
- package/dist/input/enter-sequences.d.ts +63 -0
- package/dist/input/enter-sequences.js +151 -0
- package/dist/input/limits.d.ts +11 -0
- package/dist/input/limits.js +11 -0
- package/dist/input/stdin-filter.js +19 -1
- package/dist/logging/install.d.ts +2 -2
- package/dist/logging/install.js +2 -2
- package/dist/session/persist.js +20 -0
- package/dist/todo/follow-through.d.ts +0 -4
- package/dist/todo/follow-through.js +0 -4
- package/dist/todo/limits.d.ts +0 -10
- package/dist/todo/limits.js +0 -10
- package/dist/todo/prompt.d.ts +0 -15
- package/dist/ui/ActivityLine.d.ts +26 -34
- package/dist/ui/ActivityLine.js +30 -19
- package/dist/ui/App.d.ts +1 -41
- package/dist/ui/App.js +94 -129
- package/dist/ui/BottomStatusRow.d.ts +1 -3
- package/dist/ui/BottomStatusRow.js +4 -8
- package/dist/ui/Composer.d.ts +28 -1
- package/dist/ui/Composer.js +46 -17
- package/dist/ui/Header.d.ts +2 -16
- package/dist/ui/Header.js +3 -18
- package/dist/ui/Logo.d.ts +1 -17
- package/dist/ui/Logo.js +0 -16
- package/dist/ui/ModeChip.d.ts +7 -18
- package/dist/ui/ModeChip.js +9 -0
- package/dist/ui/PromptInput.d.ts +2 -28
- package/dist/ui/PromptInput.js +38 -9
- package/dist/ui/StatusBar.d.ts +0 -11
- package/dist/ui/ToastStack.d.ts +2 -18
- package/dist/ui/ToastStack.js +4 -11
- package/dist/ui/Transcript.d.ts +5 -71
- package/dist/ui/Transcript.js +15 -199
- package/dist/ui/enter-frames.d.ts +63 -0
- package/dist/ui/enter-frames.js +136 -0
- package/dist/ui/entries/AssistantEntry.d.ts +0 -23
- package/dist/ui/entries/AssistantEntry.js +3 -4
- package/dist/ui/entries/DiffView.d.ts +1 -7
- package/dist/ui/entries/DiffView.js +2 -2
- package/dist/ui/entries/QueuedEntry.d.ts +27 -0
- package/dist/ui/entries/QueuedEntry.js +30 -0
- package/dist/ui/entries/ToolCard.d.ts +0 -6
- package/dist/ui/entries/ToolCard.js +4 -13
- package/dist/ui/frame-differ.d.ts +13 -0
- package/dist/ui/frame-differ.js +36 -6
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/ink-runtime.d.ts +1 -0
- package/dist/ui/ink-runtime.js +21 -0
- package/dist/ui/layout/AppShell.d.ts +1 -10
- package/dist/ui/layout/AppShell.js +2 -5
- package/dist/ui/layout/OverlayFrame.d.ts +1 -25
- package/dist/ui/layout/OverlayFrame.js +0 -24
- package/dist/ui/layout/ScrollViewport.d.ts +3 -0
- package/dist/ui/layout/ScrollViewport.js +17 -5
- package/dist/ui/layout/frame.d.ts +0 -22
- package/dist/ui/layout/frame.js +0 -36
- package/dist/ui/layout/todo-layout.d.ts +1 -3
- package/dist/ui/layout/todo-layout.js +7 -8
- package/dist/ui/layout/viewport-geometry.d.ts +1 -1
- package/dist/ui/layout/viewport-geometry.js +1 -1
- package/dist/ui/live-clamp.d.ts +1 -29
- package/dist/ui/live-clamp.js +1 -29
- package/dist/ui/overlays/HelpOverlay.js +3 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +5 -1
- package/dist/ui/overlays/QuestionOverlay.js +7 -2
- package/dist/ui/overlays/SettingsScreen.js +8 -1
- package/dist/ui/run-status-row.d.ts +61 -0
- package/dist/ui/run-status-row.js +70 -0
- package/dist/ui/selection/selection-controller.d.ts +23 -7
- package/dist/ui/selection/selection-controller.js +68 -16
- package/dist/ui/transcript-text.d.ts +0 -8
- package/dist/ui/transcript-text.js +0 -8
- package/dist/ui/use-startup-notices.d.ts +6 -58
- package/dist/ui/use-startup-notices.js +10 -57
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/package.json +2 -2
- package/dist/ui/TodoStrip.d.ts +0 -36
- package/dist/ui/TodoStrip.js +0 -32
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enter-family sequence recognition (tui-shift-enter-copy-queue §3.3) -- pure,
|
|
3
|
+
* stream-free, no node/React imports, callable from any test environment.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS LIVES BELOW INK. Ink 5's `parse-keypress.js` calls `parseKeypress`
|
|
6
|
+
* ONCE per stdin chunk and hard-codes `shift: false` for the exact string
|
|
7
|
+
* `'\r'`, so a component can never see `key.return && key.shift` -- the
|
|
8
|
+
* `PromptInput` branch for it is dead code. Worse, terminals that DO encode a
|
|
9
|
+
* distinct Shift+Enter send CSI-u (`\x1b[13;2u`), which Ink's `fnKeyRe` does
|
|
10
|
+
* not match: the ESC is stripped by `use-input.js` and `'[13;2u'` falls through
|
|
11
|
+
* to the PRINTABLE branch, typing garbage into the draft. The only layer that
|
|
12
|
+
* sees the bytes before Ink mangles them is the stdin filter, which is where
|
|
13
|
+
* the mouse-report and paste-marker dialects are already translated.
|
|
14
|
+
*
|
|
15
|
+
* THE CONTRACT: rewrite every recognized sequence to ONE normalized token --
|
|
16
|
+
* `ENTER_NEWLINE_FRAME` for "this was a modified Enter, the user wants a
|
|
17
|
+
* newline" and `'\r'` for the unmodified CSI-u Enter some other program left
|
|
18
|
+
* behind. No state, no partial-output: `rewriteEnterSequences` is idempotent
|
|
19
|
+
* because its output contains none of its inputs.
|
|
20
|
+
*/
|
|
21
|
+
import { ENTER_NEWLINE_FRAME } from './limits.js';
|
|
22
|
+
/**
|
|
23
|
+
* The recognition table (§3.3). CSI-u modifiers 2-8 cover Shift(2), Alt(3),
|
|
24
|
+
* Shift+Alt(4), Ctrl(5), Ctrl+Shift(6), Ctrl+Alt(7), Ctrl+Alt+Shift(8) as
|
|
25
|
+
* emitted by kitty/WezTerm keyboard-enhancement mode and by
|
|
26
|
+
* `modifyOtherKeys=2`; every one of them means "Enter with a modifier", and a
|
|
27
|
+
* modified Enter must NEVER submit the draft. `\x1b\r` / `\x1b\n` are the
|
|
28
|
+
* classic Alt+Enter encodings. `\x1b[13u` (no modifier field) is what a
|
|
29
|
+
* leftover kitty mode sends for a PLAIN Enter: it must submit, so it maps to
|
|
30
|
+
* `'\r'` rather than to a newline.
|
|
31
|
+
*/
|
|
32
|
+
export const ENTER_SEQUENCES = [
|
|
33
|
+
{
|
|
34
|
+
seq: '\x1b[13;2u',
|
|
35
|
+
meaning: 'shift+enter',
|
|
36
|
+
to: ENTER_NEWLINE_FRAME,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
seq: '\x1b[13;3u',
|
|
40
|
+
meaning: 'alt+enter',
|
|
41
|
+
to: ENTER_NEWLINE_FRAME,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
seq: '\x1b[13;4u',
|
|
45
|
+
meaning: 'shift+alt+enter',
|
|
46
|
+
to: ENTER_NEWLINE_FRAME,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
seq: '\x1b[13;5u',
|
|
50
|
+
meaning: 'ctrl+enter',
|
|
51
|
+
to: ENTER_NEWLINE_FRAME,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
seq: '\x1b[13;6u',
|
|
55
|
+
meaning: 'ctrl+shift+enter',
|
|
56
|
+
to: ENTER_NEWLINE_FRAME,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
seq: '\x1b[13;7u',
|
|
60
|
+
meaning: 'ctrl+alt+enter',
|
|
61
|
+
to: ENTER_NEWLINE_FRAME,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
seq: '\x1b[13;8u',
|
|
65
|
+
meaning: 'ctrl+alt+shift+enter',
|
|
66
|
+
to: ENTER_NEWLINE_FRAME,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
seq: '\x1b\r',
|
|
70
|
+
meaning: 'alt+enter',
|
|
71
|
+
to: ENTER_NEWLINE_FRAME,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
seq: '\x1b\n',
|
|
75
|
+
meaning: 'alt+enter',
|
|
76
|
+
to: ENTER_NEWLINE_FRAME,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
seq: '\x1b[13u',
|
|
80
|
+
meaning: 'enter',
|
|
81
|
+
to: '\r',
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
/**
|
|
85
|
+
* Longest strict prefix any recognized sequence can have. Derived from the
|
|
86
|
+
* table rather than hard-coded so adding a sequence can never leave the hold
|
|
87
|
+
* ceiling behind: the longest sequence is 7 bytes, so a torn sequence can hold
|
|
88
|
+
* up to 6 of them.
|
|
89
|
+
*/
|
|
90
|
+
const MAX_ENTER_PREFIX = Math.max(...ENTER_SEQUENCES.map((s) => s.seq.length)) - 1;
|
|
91
|
+
/** `\x1b[13;2u` .. `\x1b[13;8u`, or `\x1b[13u` when the modifier is absent. */
|
|
92
|
+
const CSI_U_ENTER = /\x1b\[13(?:;[2-8])?u/g;
|
|
93
|
+
/**
|
|
94
|
+
* Rewrite every recognized Enter-family sequence to its normalized token.
|
|
95
|
+
*
|
|
96
|
+
* ONE ANCHORED REGEX, not a per-sequence scan and not a state machine: the
|
|
97
|
+
* CSI-u family shares one shape and the two literal Alt+Enter encodings cannot
|
|
98
|
+
* nest inside it, so a single left-to-right pass is unambiguous. `\x1b[13;9u`
|
|
99
|
+
* and other unknown modifiers are deliberately left untouched -- rewriting an
|
|
100
|
+
* unrecognized sequence would be guessing at a terminal dialect we have not
|
|
101
|
+
* seen.
|
|
102
|
+
*/
|
|
103
|
+
export function rewriteEnterSequences(text) {
|
|
104
|
+
if (!text.includes('\x1b'))
|
|
105
|
+
return text;
|
|
106
|
+
// ONE PASS, TWO TARGETS: a MODIFIED Enter is a newline intent, while the
|
|
107
|
+
// modifier-less CSI-u Enter a leftover kitty mode emits is a PLAIN Enter
|
|
108
|
+
// and must still submit (`\r`). The `;` is the discriminator the table
|
|
109
|
+
// above records in each entry's `to` field.
|
|
110
|
+
let out = text.replace(CSI_U_ENTER, (seq) => (seq.includes(';') ? ENTER_NEWLINE_FRAME : '\r'));
|
|
111
|
+
// Only the two literal Alt+Enter encodings remain. `\x1b\n` is checked
|
|
112
|
+
// before `\x1b\r` is replaced; they share no bytes, so order is free, but
|
|
113
|
+
// splitting the escapes keeps each replacement self-evident.
|
|
114
|
+
if (out.includes('\x1b\n'))
|
|
115
|
+
out = out.split('\x1b\n').join(ENTER_NEWLINE_FRAME);
|
|
116
|
+
if (out.includes('\x1b\r'))
|
|
117
|
+
out = out.split('\x1b\r').join(ENTER_NEWLINE_FRAME);
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
/** True when `s` is a PROPER prefix of some recognized sequence (never whole). */
|
|
121
|
+
function isEnterPrefix(s) {
|
|
122
|
+
if (s.length === 0 || s.length > MAX_ENTER_PREFIX)
|
|
123
|
+
return false;
|
|
124
|
+
for (const spec of ENTER_SEQUENCES) {
|
|
125
|
+
// `length >`, not `startsWith` alone: `startsWith` accepts the WHOLE
|
|
126
|
+
// sequence too, and holding a complete sequence back would add 12 ms
|
|
127
|
+
// of latency to a key that is ready to be rewritten right now.
|
|
128
|
+
if (spec.seq.length > s.length && spec.seq.startsWith(s))
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Length of the trailing run of `text` that is a proper prefix of an
|
|
135
|
+
* Enter-family sequence, or 0.
|
|
136
|
+
*
|
|
137
|
+
* LONGEST MATCH, not shortest: unlike the mouse and paste families, a torn
|
|
138
|
+
* CSI-u sequence diverges from its siblings only at the LAST byte
|
|
139
|
+
* (`\x1b[13;` + `2u`), so every byte before the divergence point must be held
|
|
140
|
+
* back for the next chunk or the sequence is destroyed a byte early. Feeding
|
|
141
|
+
* the result into the filter's `keep = max(...)` alongside the other two
|
|
142
|
+
* families keeps every family's hold intact.
|
|
143
|
+
*/
|
|
144
|
+
export function trailingEnterPrefixLength(text) {
|
|
145
|
+
const max = Math.min(text.length, MAX_ENTER_PREFIX);
|
|
146
|
+
for (let k = max; k >= 1; k -= 1) {
|
|
147
|
+
if (isEnterPrefix(text.slice(text.length - k)))
|
|
148
|
+
return k;
|
|
149
|
+
}
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
package/dist/input/limits.d.ts
CHANGED
|
@@ -53,6 +53,17 @@ export declare const PASTE_ASSEMBLY_MAX_MS = 2000;
|
|
|
53
53
|
*/
|
|
54
54
|
export declare const PASTE_OPEN = "\0[";
|
|
55
55
|
export declare const PASTE_CLOSE = "\0]";
|
|
56
|
+
/**
|
|
57
|
+
* The inline frame that carries ONE newline intent from the stdin filter to the
|
|
58
|
+
* composer (tui-shift-enter-copy-queue 3.2).
|
|
59
|
+
*
|
|
60
|
+
* SAME FAMILY AS `PASTE_OPEN`, with the same guarantee and for the same reason:
|
|
61
|
+
* NUL is the delimiter because no terminal delivers it for any key, and because
|
|
62
|
+
* `sanitisePaste` strips every NUL out of every payload -- so pasted text can
|
|
63
|
+
* never forge this frame either. A newline intent is single, atomic and carries
|
|
64
|
+
* no payload, so one constant replaces the OPEN/CLOSE pair.
|
|
65
|
+
*/
|
|
66
|
+
export declare const ENTER_NEWLINE_FRAME = "\0n";
|
|
56
67
|
/**
|
|
57
68
|
* The one channel from the stdin filter to `App`'s notice dispatch (P1-3 / I-15).
|
|
58
69
|
*
|
package/dist/input/limits.js
CHANGED
|
@@ -53,6 +53,17 @@ export const PASTE_ASSEMBLY_MAX_MS = 2_000;
|
|
|
53
53
|
*/
|
|
54
54
|
export const PASTE_OPEN = '\u0000[';
|
|
55
55
|
export const PASTE_CLOSE = '\u0000]';
|
|
56
|
+
/**
|
|
57
|
+
* The inline frame that carries ONE newline intent from the stdin filter to the
|
|
58
|
+
* composer (tui-shift-enter-copy-queue 3.2).
|
|
59
|
+
*
|
|
60
|
+
* SAME FAMILY AS `PASTE_OPEN`, with the same guarantee and for the same reason:
|
|
61
|
+
* NUL is the delimiter because no terminal delivers it for any key, and because
|
|
62
|
+
* `sanitisePaste` strips every NUL out of every payload -- so pasted text can
|
|
63
|
+
* never forge this frame either. A newline intent is single, atomic and carries
|
|
64
|
+
* no payload, so one constant replaces the OPEN/CLOSE pair.
|
|
65
|
+
*/
|
|
66
|
+
export const ENTER_NEWLINE_FRAME = '\u0000n';
|
|
56
67
|
/** Human-readable byte size for the refusal toast (`4.1 MB`, `812.0 KB`, `7 B`). */
|
|
57
68
|
export function formatPasteSize(bytes) {
|
|
58
69
|
if (bytes >= 1024 * 1024)
|
|
@@ -35,6 +35,7 @@ import { PassThrough } from 'node:stream';
|
|
|
35
35
|
import { MAX_PENDING_MOUSE_CHARS, splitMouseEvents, isMousePrefix, } from './mouse-events.js';
|
|
36
36
|
import { 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 { rewriteEnterSequences, trailingEnterPrefixLength } from './enter-sequences.js';
|
|
38
39
|
/**
|
|
39
40
|
* How long a possible-prefix tail is held before being flushed as text.
|
|
40
41
|
*
|
|
@@ -192,6 +193,17 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
192
193
|
const handleOutsidePaste = (text, holdTail) => {
|
|
193
194
|
if (text.length === 0)
|
|
194
195
|
return;
|
|
196
|
+
// ENTER-FAMILY REWRITE, AND IT IS FIRST FOR A REASON
|
|
197
|
+
// (tui-shift-enter-copy-queue 3.4). The Alt+Enter encoding contains a CR,
|
|
198
|
+
// and `classifyChunk`'s primary rule -- "longer than one character AND
|
|
199
|
+
// contains a line break" -- would buffer that two-byte key as a Tier 2
|
|
200
|
+
// paste body whose sanitiser then strips the ESC, silently swallowing the
|
|
201
|
+
// keystroke. After the rewrite the frame is NUL-led and classifies as
|
|
202
|
+
// `keys` under both rules. The cost is that an UNBRACKETED paste whose
|
|
203
|
+
// body happens to contain the literal Alt+Enter bytes loses that one
|
|
204
|
+
// pair's newline; a bracketed body never reaches this function at all
|
|
205
|
+
// (I-3), and losing two bytes of a heuristic tier beats eating the key.
|
|
206
|
+
text = rewriteEnterSequences(text);
|
|
195
207
|
if (paste.kind === 'burst') {
|
|
196
208
|
paste = { kind: 'burst', body: paste.body + text, bytes: paste.bytes + byteLength(text) };
|
|
197
209
|
burstTimer = clearTimer(burstTimer);
|
|
@@ -266,7 +278,13 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
266
278
|
const strayEnd = rest.indexOf(PASTE_END_MARK);
|
|
267
279
|
const first = begin === -1 ? strayEnd : strayEnd === -1 ? begin : Math.min(begin, strayEnd);
|
|
268
280
|
if (first === -1) {
|
|
269
|
-
|
|
281
|
+
// THIRD PREFIX FAMILY: a torn CSI-u Enter sequence
|
|
282
|
+
// (tui-shift-enter-copy-queue 3.4). `keep = max(...)` holds the longest
|
|
283
|
+
// tail any family might still complete; the three families share only
|
|
284
|
+
// the ESC / ESC-[ head bytes, which the max keeps for whoever needs
|
|
285
|
+
// them. The existing 12 ms flush and the 32-char ceiling bound the
|
|
286
|
+
// hold, so no new timer is needed.
|
|
287
|
+
const keep = Math.max(trailingPastePrefixLength(rest), features.mouse ? trailingMousePrefixLength(rest) : 0, trailingEnterPrefixLength(rest));
|
|
270
288
|
const tail = rest.slice(rest.length - keep);
|
|
271
289
|
handleOutsidePaste(rest.slice(0, rest.length - keep), true);
|
|
272
290
|
pending += tail;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* Adding a listener suppresses it, turning a crash into a silent hang.
|
|
26
26
|
*
|
|
27
27
|
* `SIGINT` / `SIGTERM` / `SIGHUP` — the handlers in `runInteractive()` exist
|
|
28
|
-
* ONLY in its full-screen branch, so
|
|
28
|
+
* ONLY in its full-screen branch, so `-p` and every
|
|
29
29
|
* subcommand had no handler at all and took Node's default termination,
|
|
30
30
|
* which does not run `'exit'` listeners: the whole queue was lost. But a
|
|
31
31
|
* signal listener registered here runs BEFORE the one `runInteractive`
|
|
@@ -45,7 +45,7 @@ import { Logger, type LogScope } from './logger.js';
|
|
|
45
45
|
export declare function setScreenRestore(fn: () => void): void;
|
|
46
46
|
/**
|
|
47
47
|
* Replace what a signal does after the flush. The default (a bare
|
|
48
|
-
* `exit(128 + signo)`) is correct for headless
|
|
48
|
+
* `exit(128 + signo)`) is correct for headless and every subcommand;
|
|
49
49
|
* full-screen substitutes one that restores the terminal first.
|
|
50
50
|
*
|
|
51
51
|
* A TERMINATOR MAY NOW RETURN WITHOUT EXITING, AND THAT IS A CONTRACT CHANGE
|
package/dist/logging/install.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* Adding a listener suppresses it, turning a crash into a silent hang.
|
|
26
26
|
*
|
|
27
27
|
* `SIGINT` / `SIGTERM` / `SIGHUP` — the handlers in `runInteractive()` exist
|
|
28
|
-
* ONLY in its full-screen branch, so
|
|
28
|
+
* ONLY in its full-screen branch, so `-p` and every
|
|
29
29
|
* subcommand had no handler at all and took Node's default termination,
|
|
30
30
|
* which does not run `'exit'` listeners: the whole queue was lost. But a
|
|
31
31
|
* signal listener registered here runs BEFORE the one `runInteractive`
|
|
@@ -63,7 +63,7 @@ export function setScreenRestore(fn) {
|
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* Replace what a signal does after the flush. The default (a bare
|
|
66
|
-
* `exit(128 + signo)`) is correct for headless
|
|
66
|
+
* `exit(128 + signo)`) is correct for headless and every subcommand;
|
|
67
67
|
* full-screen substitutes one that restores the terminal first.
|
|
68
68
|
*
|
|
69
69
|
* A TERMINATOR MAY NOW RETURN WITHOUT EXITING, AND THAT IS A CONTRACT CHANGE
|
package/dist/session/persist.js
CHANGED
|
@@ -193,6 +193,26 @@ export function normalizeLoadedEntries(entries) {
|
|
|
193
193
|
(entry.status === 'starting' || entry.status === 'ready' || entry.status === 'running')) {
|
|
194
194
|
return { ...entry, status: 'stopped', rows: [] };
|
|
195
195
|
}
|
|
196
|
+
// THE SEVENTH CLAUSE (tui-shift-enter-copy-queue 5.2.5). Core's steering
|
|
197
|
+
// queue is memory-only, so a `queued` entry on disk is a promise this
|
|
198
|
+
// process can no longer keep: the message was never accepted into any
|
|
199
|
+
// conversation. Downgrade to a WARN NOTICE carrying the first line
|
|
200
|
+
// rather than to a `user` entry -- pretending it was sent is the lie
|
|
201
|
+
// that loses the user's words with a straight face.
|
|
202
|
+
//
|
|
203
|
+
// THE ID IS KEPT: `restoreEntries` splices these positions into the
|
|
204
|
+
// live transcript, and a fresh id here would collide with the next
|
|
205
|
+
// `e<n>` the reducer allocates.
|
|
206
|
+
if (entry.kind === 'queued') {
|
|
207
|
+
const firstLine = entry.text.split('\n', 1)[0] ?? '';
|
|
208
|
+
const downgraded = {
|
|
209
|
+
id: entry.id,
|
|
210
|
+
kind: 'notice',
|
|
211
|
+
level: 'warn',
|
|
212
|
+
text: `Queued but never sent: ${firstLine}`,
|
|
213
|
+
};
|
|
214
|
+
return downgraded;
|
|
215
|
+
}
|
|
196
216
|
return entry;
|
|
197
217
|
});
|
|
198
218
|
}
|
|
@@ -84,10 +84,6 @@ export type FollowThroughDecision = {
|
|
|
84
84
|
};
|
|
85
85
|
/** The zero budget. A fresh App, a fresh headless run, and every cleared list. */
|
|
86
86
|
export declare function emptyBudget(): FollowThroughBudget;
|
|
87
|
-
/**
|
|
88
|
-
* The decision. EVALUATED IN THIS ORDER, AND THE ORDER IS SEMANTICS, NOT STYLE —
|
|
89
|
-
* the same sentence `decideRenderMode` carries.
|
|
90
|
-
*/
|
|
91
87
|
export declare function decideFollowThrough(input: FollowThroughInput): FollowThroughDecision;
|
|
92
88
|
/**
|
|
93
89
|
* Fold a finished run into the budget. Called BEFORE `decideFollowThrough`, on
|
|
@@ -36,10 +36,6 @@ function legacyUnfinishedNotice(left) {
|
|
|
36
36
|
function advise(interactive, text) {
|
|
37
37
|
return interactive ? ` ${text}` : '';
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
40
|
-
* The decision. EVALUATED IN THIS ORDER, AND THE ORDER IS SEMANTICS, NOT STYLE —
|
|
41
|
-
* the same sentence `decideRenderMode` carries.
|
|
42
|
-
*/
|
|
43
39
|
export function decideFollowThrough(input) {
|
|
44
40
|
const { mode, snapshot, runEnd, budget, interactive } = input;
|
|
45
41
|
// 1. Nothing is unfinished. `beginUserTurn` already drops a fully-completed
|
package/dist/todo/limits.d.ts
CHANGED
|
@@ -38,16 +38,6 @@ export declare const TODO_LIMITS: {
|
|
|
38
38
|
readonly panelFullRows: 6;
|
|
39
39
|
/** Below this many terminal columns the status-bar counter degrades to `[3/7]`. */
|
|
40
40
|
readonly statusCompactCols: 100;
|
|
41
|
-
/**
|
|
42
|
-
* Below this many columns the inline strip drops its `+N done` suffix
|
|
43
|
-
* (todo-plan-followthrough §3.7 / D-19).
|
|
44
|
-
*
|
|
45
|
-
* NAMED RATHER THAN INLINED, and the reason is that it sits between two other
|
|
46
|
-
* real bounds — `statusCompactCols` (100) above and `MIN_FULLSCREEN_COLS` (40)
|
|
47
|
-
* in `frame.ts` — with nothing to tell a reader whether a bare `80` in the
|
|
48
|
-
* component was a third policy or a typo for one of them.
|
|
49
|
-
*/
|
|
50
|
-
readonly stripDoneCols: 80;
|
|
51
41
|
/**
|
|
52
42
|
* Consecutive user turns an UNFINISHED list survives without a `todo_write`
|
|
53
43
|
* before `beginUserTurn()` drops it (§3.2 / P1-7). Three keeps the common
|
package/dist/todo/limits.js
CHANGED
|
@@ -38,16 +38,6 @@ export const TODO_LIMITS = {
|
|
|
38
38
|
panelFullRows: 6,
|
|
39
39
|
/** Below this many terminal columns the status-bar counter degrades to `[3/7]`. */
|
|
40
40
|
statusCompactCols: 100,
|
|
41
|
-
/**
|
|
42
|
-
* Below this many columns the inline strip drops its `+N done` suffix
|
|
43
|
-
* (todo-plan-followthrough §3.7 / D-19).
|
|
44
|
-
*
|
|
45
|
-
* NAMED RATHER THAN INLINED, and the reason is that it sits between two other
|
|
46
|
-
* real bounds — `statusCompactCols` (100) above and `MIN_FULLSCREEN_COLS` (40)
|
|
47
|
-
* in `frame.ts` — with nothing to tell a reader whether a bare `80` in the
|
|
48
|
-
* component was a third policy or a typo for one of them.
|
|
49
|
-
*/
|
|
50
|
-
stripDoneCols: 80,
|
|
51
41
|
/**
|
|
52
42
|
* Consecutive user turns an UNFINISHED list survives without a `todo_write`
|
|
53
43
|
* before `beginUserTurn()` drops it (§3.2 / P1-7). Three keeps the common
|
package/dist/todo/prompt.d.ts
CHANGED
|
@@ -22,21 +22,6 @@
|
|
|
22
22
|
import { TODO_BLOCK_VERSION } from './limits.js';
|
|
23
23
|
export { TODO_BLOCK_VERSION };
|
|
24
24
|
export interface TodoBlockParams {
|
|
25
|
-
/**
|
|
26
|
-
* Whether this session can actually render the rail.
|
|
27
|
-
*
|
|
28
|
-
* FOUR SUPPORTED PATHS HAVE THE TOOL AND NO PANEL — `-p` (§3.12), inline mode
|
|
29
|
-
* (non-goal 4), `--no-todo-panel` (§4.3), and a terminal under 80 columns or 6
|
|
30
|
-
* viewport rows (§6.4) — and in the first three the answer is fixed for the
|
|
31
|
-
* whole session, before the prompt is composed. Telling the model about a
|
|
32
|
-
* panel the user does not have is the same error `composeSystemPrompt` already
|
|
33
|
-
* guards against for team mode with its TWO flags (P1-6 / D-24).
|
|
34
|
-
*
|
|
35
|
-
* Dropping the block instead would be wrong: the planning discipline is worth
|
|
36
|
-
* having without the column, which is precisely why `enabled` and `panel` are
|
|
37
|
-
* separate config keys, and a screen-reader user is the case that argument was
|
|
38
|
-
* written for.
|
|
39
|
-
*/
|
|
40
25
|
panelVisible: boolean;
|
|
41
26
|
}
|
|
42
27
|
/**
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
import React from 'react';
|
|
42
42
|
import type { Theme } from './theme.js';
|
|
43
43
|
import type { TermCapabilities } from './capabilities.js';
|
|
44
|
+
import { pickGlyphs } from './glyphs.js';
|
|
45
|
+
import type { RunActivity } from './run-status-row.js';
|
|
44
46
|
/**
|
|
45
47
|
* The animated spinner this row draws while it is live, or `null` when it would
|
|
46
48
|
* be STATIC — an ASCII terminal, or a user who asked for stillness.
|
|
@@ -57,42 +59,32 @@ import type { TermCapabilities } from './capabilities.js';
|
|
|
57
59
|
* reader has to reason about; one more caller of this function is not.
|
|
58
60
|
*/
|
|
59
61
|
export declare function liveSpinner(reducedMotion: boolean, caps: TermCapabilities): React.ReactElement | null;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* is right for that gap; a UI that says "Pondering" while `npm test` runs is
|
|
74
|
-
* not calm, it is wrong.
|
|
75
|
-
*/
|
|
76
|
-
runningTool?: string;
|
|
62
|
+
/**
|
|
63
|
+
* The label ladder, MOST SPECIFIC FIRST: `Compacting context` outranks a tool name
|
|
64
|
+
* because compaction can be the only thing happening, and the tool name outranks
|
|
65
|
+
* the rotating phrase for the reason `runningTool` exists at all. One
|
|
66
|
+
* implementation, because the fixed bottom row and the run status row above the
|
|
67
|
+
* input must never disagree about what the agent is doing.
|
|
68
|
+
*
|
|
69
|
+
* `Running ${name}` rather than a per-tool verb table: a table would be a SECOND
|
|
70
|
+
* registry of tool names, and `toolGlyph` is already the one place that knows
|
|
71
|
+
* them. ASCII only, and the ellipsis comes from `pickGlyphs` like every other one.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveActivityLabel(activity: RunActivity, glyphs: ReturnType<typeof pickGlyphs>): string;
|
|
74
|
+
export interface ActivityLabelProps extends RunActivity {
|
|
77
75
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* when `status` is `'running'` but no tool is in flight — precisely the window
|
|
83
|
-
* in which the phrase rotation would say something false, and it can last
|
|
84
|
-
* tens of seconds. `runningTool` already established the principle one step
|
|
85
|
-
* back: a UI that says "Pondering" while `npm test` runs is not calm, it is
|
|
86
|
-
* wrong, and one that says it while a 30 k-token summarization runs is the same
|
|
87
|
-
* error.
|
|
88
|
-
*
|
|
89
|
-
* STILL NO SECOND SPINNER. `single-spinner-while-running`'s D-1 stands: this
|
|
90
|
-
* row owns the only animation while it is mounted, and the compaction card
|
|
91
|
-
* follows `FastCard` in taking a `reducedMotion` that `App` has already
|
|
92
|
-
* widened.
|
|
76
|
+
* False draws the STATIC glyph even when motion is allowed. The run status row
|
|
77
|
+
* uses it when it has scrolled out of the viewport (the fixed bottom row owns
|
|
78
|
+
* the animation then) or an overlay covers the chat. The phrase still rotates
|
|
79
|
+
* from the RAW `reducedMotion`, so a still icon never freezes the wording.
|
|
93
80
|
*/
|
|
94
|
-
|
|
81
|
+
spinnerLive?: boolean;
|
|
82
|
+
/** Prefix one space (the fixed bottom row's indent); the run row indents itself. */
|
|
83
|
+
leadingSpace?: boolean;
|
|
95
84
|
theme: Theme;
|
|
96
85
|
caps: TermCapabilities;
|
|
97
86
|
}
|
|
98
|
-
|
|
87
|
+
/** `<spinner> <label>` as ONE truncating `<Text>`; the only place that composes them. */
|
|
88
|
+
export declare function ActivityLabel({ spinnerLive, leadingSpace, theme, caps, ...activity }: ActivityLabelProps): React.ReactElement;
|
|
89
|
+
export type ActivityLineProps = Omit<ActivityLabelProps, 'spinnerLive' | 'leadingSpace'>;
|
|
90
|
+
export declare function ActivityLine(props: ActivityLineProps): React.ReactElement;
|
package/dist/ui/ActivityLine.js
CHANGED
|
@@ -21,32 +21,43 @@ import { pickActivityPhrase } from './activity-phrases.js';
|
|
|
21
21
|
export function liveSpinner(reducedMotion, caps) {
|
|
22
22
|
return caps.unicode && !reducedMotion ? _jsx(Spinner, { type: "dots" }) : null;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The label ladder, MOST SPECIFIC FIRST: `Compacting context` outranks a tool name
|
|
26
|
+
* because compaction can be the only thing happening, and the tool name outranks
|
|
27
|
+
* the rotating phrase for the reason `runningTool` exists at all. One
|
|
28
|
+
* implementation, because the fixed bottom row and the run status row above the
|
|
29
|
+
* input must never disagree about what the agent is doing.
|
|
30
|
+
*
|
|
31
|
+
* `Running ${name}` rather than a per-tool verb table: a table would be a SECOND
|
|
32
|
+
* registry of tool names, and `toolGlyph` is already the one place that knows
|
|
33
|
+
* them. ASCII only, and the ellipsis comes from `pickGlyphs` like every other one.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveActivityLabel(activity, glyphs) {
|
|
36
|
+
if (activity.compacting)
|
|
37
|
+
return `Compacting context${glyphs.ellipsis}`;
|
|
38
|
+
if (activity.runningTool)
|
|
39
|
+
return `Running ${activity.runningTool}`;
|
|
26
40
|
// PURE AND DERIVED FROM `(startedAt, now)`, so the sequence does not need to
|
|
27
41
|
// keep rotating underneath the tool label: when the tool settles the phrase
|
|
28
42
|
// resumes at the position the clock implies, with no state anywhere.
|
|
29
|
-
const phrase = pickActivityPhrase(startedAt, startedAt + elapsedMs, !reducedMotion);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// phrase for the reason `runningTool` does. ASCII only, and the ellipsis comes
|
|
37
|
-
// from `pickGlyphs` like every other one on this row.
|
|
38
|
-
const label = compacting
|
|
39
|
-
? `Compacting context${glyphs.ellipsis}`
|
|
40
|
-
: runningTool
|
|
41
|
-
? `Running ${runningTool}`
|
|
42
|
-
: `${phrase}${glyphs.ellipsis}`;
|
|
43
|
+
const phrase = pickActivityPhrase(activity.startedAt, activity.startedAt + activity.elapsedMs, !activity.reducedMotion);
|
|
44
|
+
return `${phrase}${glyphs.ellipsis}`;
|
|
45
|
+
}
|
|
46
|
+
/** `<spinner> <label>` as ONE truncating `<Text>`; the only place that composes them. */
|
|
47
|
+
export function ActivityLabel({ spinnerLive = true, leadingSpace = false, theme, caps, ...activity }) {
|
|
48
|
+
const glyphs = pickGlyphs(caps);
|
|
49
|
+
const label = resolveActivityLabel(activity, glyphs);
|
|
43
50
|
// The IDENTICAL branch `Transcript.tsx` uses for the assistant role marker, and
|
|
44
51
|
// it must stay identical: braille dots are both an animation AND a Unicode-only
|
|
45
52
|
// glyph, so reduced motion and an ASCII terminal fall back to the same marker.
|
|
46
53
|
//
|
|
47
54
|
// Through `liveSpinner` rather than inline, so this row and the glyph
|
|
48
55
|
// `BottomStatusRow` composes beside a toast can never disagree about WHEN the
|
|
49
|
-
// spinner is live
|
|
50
|
-
const spinner = liveSpinner(reducedMotion, caps) ??
|
|
51
|
-
|
|
56
|
+
// spinner is live - the same reason `App` names its mount condition once.
|
|
57
|
+
const spinner = (spinnerLive ? liveSpinner(activity.reducedMotion, caps) : null) ??
|
|
58
|
+
glyphs.spinnerStill;
|
|
59
|
+
return (_jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [leadingSpace ? ' ' : '', spinner, " ", label] }));
|
|
60
|
+
}
|
|
61
|
+
export function ActivityLine(props) {
|
|
62
|
+
return (_jsx(Box, { flexShrink: 0, children: _jsx(ActivityLabel, { ...props, leadingSpace: true }) }));
|
|
52
63
|
}
|
package/dist/ui/App.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ import type { AgentController } from '../agent/controller.js';
|
|
|
10
10
|
import type { ConfirmRequest } from '../tools/index.js';
|
|
11
11
|
import { type Overlay, type ToastLevel } from '../agent/reducer.js';
|
|
12
12
|
import type { UpdateBridge } from '../update/types.js';
|
|
13
|
-
import { type RenderMode } from './layout/frame.js';
|
|
14
13
|
import type { MouseSource } from '../input/stdin-filter.js';
|
|
15
14
|
import type { PasteBridge } from '../input/limits.js';
|
|
16
15
|
import type { SelectionBridge } from './selection/selection-controller.js';
|
|
@@ -43,15 +42,11 @@ export interface AppProps {
|
|
|
43
42
|
controller: AgentController;
|
|
44
43
|
version: string;
|
|
45
44
|
/** Decided once in `cli.tsx::runInteractive()`; never switched at run time. */
|
|
46
|
-
mode: RenderMode;
|
|
47
45
|
initialOverlay?: Overlay;
|
|
48
46
|
initialPrompt?: string;
|
|
49
47
|
confirmBridge?: ConfirmBridge;
|
|
50
48
|
/** The plan-mode human channel; absent in tests that do not exercise it. */
|
|
51
49
|
humanInputBridge?: HumanInputBridge;
|
|
52
|
-
/** Wheel events, already stripped out of stdin. Absent whenever mouse support
|
|
53
|
-
* is off: inline mode, `--no-mouse`, a non-TTY, or a filter that failed to
|
|
54
|
-
* build (mouse-wheel-region-routing §4.2). */
|
|
55
50
|
mouseSource?: MouseSource;
|
|
56
51
|
/**
|
|
57
52
|
* Present ONLY on a console that can deliver neither `CSI Z` nor a mouse
|
|
@@ -87,28 +82,8 @@ export interface AppProps {
|
|
|
87
82
|
* on a non-TTY, and in every test that does not exercise it.
|
|
88
83
|
*/
|
|
89
84
|
pasteBridge?: PasteBridge;
|
|
90
|
-
/**
|
|
91
|
-
* The terminal-facing services `cli.tsx` owns and this component cannot build
|
|
92
|
-
* (tui-selection-and-scroll-follow §4.4).
|
|
93
|
-
*
|
|
94
|
-
* ONE PROP RATHER THAN FOUR, and every FIELD of it is independently optional,
|
|
95
|
-
* so `--no-mouse`, `--no-diff-render` and inline mode each drop exactly the
|
|
96
|
-
* services they do not have. Absent altogether on a non-TTY and in every test
|
|
97
|
-
* that does not exercise selection, which is what keeps those trees
|
|
98
|
-
* byte-identical.
|
|
99
|
-
*/
|
|
100
85
|
terminal?: TerminalBridge;
|
|
101
86
|
}
|
|
102
|
-
/**
|
|
103
|
-
* Terminal-level services, almost all of which exist only in a full-screen TTY
|
|
104
|
-
* session — `writeForeign` is the exception, because `/copy` is available in
|
|
105
|
-
* inline mode too and OSC 52 is what makes it work over SSH (§4.4.5).
|
|
106
|
-
*
|
|
107
|
-
* `selection` is a LATE-BOUND BRIDGE for the reason `updateBridge` above is one:
|
|
108
|
-
* the controller is constructed before `render()` and must not hold React state,
|
|
109
|
-
* while the toast sink and the redraw nonce only exist once this component has
|
|
110
|
-
* mounted. The effect below assigns both and clears them on unmount.
|
|
111
|
-
*/
|
|
112
87
|
export interface TerminalBridge {
|
|
113
88
|
scrollbar?: ScrollbarBridge;
|
|
114
89
|
/** Whether drag-select is on for this session (`mouseSelect` + a filter). */
|
|
@@ -117,22 +92,7 @@ export interface TerminalBridge {
|
|
|
117
92
|
setMouseCapture?: (on: boolean) => void;
|
|
118
93
|
/** Whether the mouse is captured right now — read at command time. */
|
|
119
94
|
isMouseCaptured?: () => boolean;
|
|
120
|
-
/**
|
|
121
|
-
* Write bytes that are ours but are not a frame (OSC 52).
|
|
122
|
-
*
|
|
123
|
-
* IT MUST NOT BE `stdout.write` WITH THE DIFFER IN FRONT OF STDOUT (P1-6):
|
|
124
|
-
* that goes through the frame-writer proxy, which does not recognise an OSC 52
|
|
125
|
-
* chunk, counts it as a foreign write and prints `FRAME_FALLBACK_NOTICE` at
|
|
126
|
-
* the user the first time they copy anything. Without a differ — inline mode,
|
|
127
|
-
* `--no-diff-render` — stdout IS the real stream and `cli.tsx` supplies a
|
|
128
|
-
* direct write instead.
|
|
129
|
-
*
|
|
130
|
-
* Absent only when there is no terminal to accept it at all. It is never a
|
|
131
|
-
* function that writes nowhere: `copyText` reports `'osc52'` whenever its door
|
|
132
|
-
* took the text, so a no-op door would make the toast name a mechanism that
|
|
133
|
-
* was never attempted.
|
|
134
|
-
*/
|
|
135
95
|
writeForeign?: (text: string) => void;
|
|
136
96
|
selection?: SelectionBridge;
|
|
137
97
|
}
|
|
138
|
-
export declare function App({ controller, version,
|
|
98
|
+
export declare function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, }: AppProps): React.ReactElement;
|