@aragon-agent/cli 0.6.3 → 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 +105 -0
- package/README.md +95 -2
- package/dist/agent/controller.d.ts +119 -0
- package/dist/agent/controller.js +275 -1
- package/dist/agent/reducer.d.ts +69 -0
- package/dist/agent/reducer.js +100 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +84 -22
- package/dist/commands/builtins.js +92 -1
- package/dist/compaction/child.js +5 -0
- package/dist/compaction/command.js +14 -1
- package/dist/compaction/compactor.d.ts +34 -0
- package/dist/compaction/compactor.js +158 -37
- package/dist/compaction/types.d.ts +36 -0
- package/dist/compaction/wiring.d.ts +9 -0
- package/dist/compaction/wiring.js +22 -1
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.js +48 -1
- package/dist/config/load.d.ts +11 -0
- package/dist/config/load.js +39 -1
- package/dist/config/schema.d.ts +85 -0
- package/dist/config/schema.js +33 -0
- package/dist/config/store.js +21 -1
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- 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 +2 -0
- package/dist/logging/install.js +53 -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 +36 -1
- 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/App.d.ts +15 -2
- package/dist/ui/App.js +256 -7
- package/dist/ui/Composer.d.ts +54 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +49 -3
- package/dist/ui/PromptInput.js +184 -52
- package/dist/ui/StatusBar.d.ts +19 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/Transcript.js +29 -1
- 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/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/layout/budget.d.ts +34 -6
- package/dist/ui/layout/budget.js +32 -7
- package/dist/ui/layout/virtual-window.js +51 -1
- package/dist/ui/overlays/HelpOverlay.js +3 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.js +14 -1
- 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 +16 -0
- package/dist/ui/screen.js +45 -2
- package/dist/ui/selection/selection-controller.d.ts +1 -1
- package/dist/ui/transcript-text.js +49 -0
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/package.json +2 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -47
- package/dist/input/stdin-mouse-filter.js +0 -153
|
@@ -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
|
@@ -49,6 +49,22 @@ export interface AltScreenOptions {
|
|
|
49
49
|
* pre-existing test emits exactly the bytes it emitted before (AC-8 / T-34).
|
|
50
50
|
*/
|
|
51
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;
|
|
52
68
|
}
|
|
53
69
|
/**
|
|
54
70
|
* Enter the alternate screen. A non-TTY stdout is a hard no-op (tests, pipes,
|
package/dist/ui/screen.js
CHANGED
|
@@ -55,6 +55,32 @@ const disableMouse = (motion) => `\x1b[?1006l${motion ? '\x1b[?1002l' : ''}\x1b[
|
|
|
55
55
|
*/
|
|
56
56
|
const SAVE_ALT_SCROLL_OFF = '\x1b[?1007s\x1b[?1007l';
|
|
57
57
|
const RESTORE_ALT_SCROLL = '\x1b[?1007r';
|
|
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';
|
|
58
84
|
const NOOP_HANDLE = { restore() { }, setMouseCapture() { } };
|
|
59
85
|
function safeWrite(stdout, data) {
|
|
60
86
|
try {
|
|
@@ -73,7 +99,12 @@ export function enterAltScreen(stdout, options = { mouse: false }) {
|
|
|
73
99
|
return NOOP_HANDLE;
|
|
74
100
|
const mouse = options.mouse === true;
|
|
75
101
|
const motion = options.motion === true;
|
|
76
|
-
|
|
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 : ''));
|
|
77
108
|
/**
|
|
78
109
|
* THE CURRENT capture state, not the one the session started in (I-10).
|
|
79
110
|
*
|
|
@@ -93,6 +124,12 @@ export function enterAltScreen(stdout, options = { mouse: false }) {
|
|
|
93
124
|
*/
|
|
94
125
|
let savedAltScroll = mouse ? false : true;
|
|
95
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
|
+
//
|
|
96
133
|
// A session that never installed a filter has nothing to release, and
|
|
97
134
|
// enabling reporting without one types `[<0;12;5M` into the user's message
|
|
98
135
|
// on every click (I-8). It stays a no-op in both directions.
|
|
@@ -134,7 +171,13 @@ export function enterAltScreen(stdout, options = { mouse: false }) {
|
|
|
134
171
|
// read here rather than the startup `mouse` flag, so a session that ran
|
|
135
172
|
// `/mouse off` exits with exactly one outstanding save restored and
|
|
136
173
|
// nothing left reporting.
|
|
137
|
-
|
|
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 : '');
|
|
138
181
|
safeWrite(stdout, tail + SHOW_CURSOR + LEAVE_ALT);
|
|
139
182
|
},
|
|
140
183
|
setMouseCapture,
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* same `decorate` call. What you see is what you get by construction, rather
|
|
28
28
|
* than by a staleness check that can be wrong.
|
|
29
29
|
*/
|
|
30
|
-
import type { MouseSource } from '../../input/stdin-
|
|
30
|
+
import type { MouseSource } from '../../input/stdin-filter.js';
|
|
31
31
|
import type { Theme } from '../theme.js';
|
|
32
32
|
import type { TermCapabilities } from '../capabilities.js';
|
|
33
33
|
import type { CopyVia } from '../clipboard.js';
|
|
@@ -51,6 +51,34 @@ function safeParse(raw) {
|
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The replay's glyph for a service, MIRRORING `ServiceCard::serviceGlyph`.
|
|
56
|
+
*
|
|
57
|
+
* The two ladders have to agree, and the cheap-looking shortcut here — "ready or
|
|
58
|
+
* running is a tick, everything else is an error" — disagrees on the two states
|
|
59
|
+
* a user reaches ON PURPOSE: a service they stopped with `Ctrl+C`, and one that
|
|
60
|
+
* exited cleanly. Marking either as a failure in the record the user is left
|
|
61
|
+
* with after the alternate screen is torn down is a lie about their own action.
|
|
62
|
+
*
|
|
63
|
+
* Duplicated rather than imported, as every other case in this file duplicates
|
|
64
|
+
* its card's ladder: this module is deliberately free of React and ink, and
|
|
65
|
+
* `serviceGlyph` lives in a `.tsx`.
|
|
66
|
+
*/
|
|
67
|
+
function serviceTextGlyph(status, exitCode, glyphs) {
|
|
68
|
+
switch (status) {
|
|
69
|
+
case 'ready':
|
|
70
|
+
return glyphs.toolDone;
|
|
71
|
+
case 'running':
|
|
72
|
+
return glyphs.toolRunning;
|
|
73
|
+
case 'starting':
|
|
74
|
+
case 'stopped':
|
|
75
|
+
return glyphs.toolPending;
|
|
76
|
+
case 'exited':
|
|
77
|
+
return exitCode === 0 ? glyphs.toolDone : glyphs.toolError;
|
|
78
|
+
default:
|
|
79
|
+
return glyphs.toolError;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
54
82
|
function renderEntry(entry, glyphs) {
|
|
55
83
|
switch (entry.kind) {
|
|
56
84
|
case 'user':
|
|
@@ -134,6 +162,27 @@ function renderEntry(entry, glyphs) {
|
|
|
134
162
|
...entry.summary.split('\n').map((l) => `${TOOL_INDENT}${CONTINUATION_INDENT}${l}`),
|
|
135
163
|
];
|
|
136
164
|
}
|
|
165
|
+
case 'service': {
|
|
166
|
+
// A service the agent started is real work with a real outcome, and an
|
|
167
|
+
// exported transcript that omitted it would be unaccountable for both —
|
|
168
|
+
// the argument the `team`, `retry`, `fast` and `compaction` cases above
|
|
169
|
+
// each make in their own words. It matters more here than for any of them:
|
|
170
|
+
// this replay is the user's ONLY record once the alternate screen is torn
|
|
171
|
+
// down (`cli.tsx`'s exit snapshot), and a service card dropped from it
|
|
172
|
+
// takes the URL the agent reported with it. This file's
|
|
173
|
+
// `default: return []` means the addition is a CHOICE rather than a
|
|
174
|
+
// compile requirement, which is exactly why it is easy to leave out
|
|
175
|
+
// (P0-3).
|
|
176
|
+
const glyph = serviceTextGlyph(entry.status, entry.exitCode, glyphs);
|
|
177
|
+
const where = entry.url ? ` ${entry.url}` : '';
|
|
178
|
+
const code = entry.exitCode === null || entry.exitCode === undefined
|
|
179
|
+
? ''
|
|
180
|
+
: ` (exit code ${entry.exitCode})`;
|
|
181
|
+
return [
|
|
182
|
+
`${TOOL_INDENT}${glyph} service ${entry.serviceId} ${entry.command} ` +
|
|
183
|
+
`${entry.status}${where}${code}`.trimEnd(),
|
|
184
|
+
];
|
|
185
|
+
}
|
|
137
186
|
case 'notice': {
|
|
138
187
|
const glyph = entry.level === 'error'
|
|
139
188
|
? glyphs.error
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* the wheel spins: one gesture, one `setState`, one render.
|
|
21
21
|
*/
|
|
22
22
|
import type { Overlay } from '../agent/reducer.js';
|
|
23
|
-
import type { MouseSource } from '../input/stdin-
|
|
23
|
+
import type { MouseSource } from '../input/stdin-filter.js';
|
|
24
24
|
import type { ScrollIntent } from './layout/scroll.js';
|
|
25
25
|
/**
|
|
26
26
|
* Rows a PgUp/PgDn — and a Shift+wheel notch — moves inside a controlled
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aragon-agent/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AragonAgent contributors",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"prepublishOnly": "npm run build"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@aragon-agent/core": "^0.2.
|
|
59
|
+
"@aragon-agent/core": "^0.2.16",
|
|
60
60
|
"chalk": "^5.6.2",
|
|
61
61
|
"cli-highlight": "^2.0.0",
|
|
62
62
|
"commander": "^12.1.0",
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stdin mouse filter (mouse-wheel-region-routing §4.2 / §5.2).
|
|
3
|
-
*
|
|
4
|
-
* WHY A WRAPPER AND NOT A SECOND LISTENER. Ink 5.2.1 owns stdin in a way that
|
|
5
|
-
* forbids a second reader: `components/App.js` calls `setEncoding('utf8')`,
|
|
6
|
-
* `ref()`, `setRawMode(true)`, subscribes to `'readable'` and drains with
|
|
7
|
-
* `read()`. Attaching our own `'data'` listener to the same `process.stdin`
|
|
8
|
-
* would switch the stream into flowing mode and RACE Ink's `read()` for chunks
|
|
9
|
-
* — some keystrokes would reach Ink, some would not, non-deterministically
|
|
10
|
-
* (D-3). So the filter REPLACES the stream Ink is given: it reads the real
|
|
11
|
-
* stdin, strips every mouse report, and writes the remainder into a
|
|
12
|
-
* `PassThrough` that carries the surface Ink requires.
|
|
13
|
-
*
|
|
14
|
-
* ORDERING IS AN INVARIANT, NOT A CONVENTION (I-8). The caller must build this
|
|
15
|
-
* filter BEFORE anything writes `\x1b[?1000h`. Reporting enabled while Ink
|
|
16
|
-
* still holds the raw stream does not degrade the feature — it types
|
|
17
|
-
* `[<0;12;5M` into the user's message on every click, which is strictly worse
|
|
18
|
-
* than the bug this feature fixes. `cli.tsx::runInteractive` is the only caller
|
|
19
|
-
* and passes `filter !== null` to `enterAltScreen`.
|
|
20
|
-
*/
|
|
21
|
-
import { type MouseEvent } from './mouse-events.js';
|
|
22
|
-
export interface MouseSource {
|
|
23
|
-
/**
|
|
24
|
-
* Returns an unsubscribe function. Multiple subscribers are allowed — the
|
|
25
|
-
* wheel router and (since tui-selection-and-scroll-follow) the selection
|
|
26
|
-
* controller both take this one channel, and each ignores the kinds it does
|
|
27
|
-
* not own.
|
|
28
|
-
*/
|
|
29
|
-
subscribe(listener: (event: MouseEvent) => void): () => void;
|
|
30
|
-
}
|
|
31
|
-
export interface MouseFilter {
|
|
32
|
-
/** Hand this to Ink: `render(<App/>, { stdin: filter.stdin })`. */
|
|
33
|
-
readonly stdin: NodeJS.ReadStream;
|
|
34
|
-
readonly source: MouseSource;
|
|
35
|
-
/** Idempotent. Detaches from the real stdin and clears the flush timer (I-7). */
|
|
36
|
-
dispose(): void;
|
|
37
|
-
}
|
|
38
|
-
export declare function createMouseFilter(real: NodeJS.ReadStream): MouseFilter;
|
|
39
|
-
/**
|
|
40
|
-
* Build a filter, or `null` if construction throws.
|
|
41
|
-
*
|
|
42
|
-
* The `null` return is the whole point (P0-1): `enterAltScreen` derives whether
|
|
43
|
-
* to enable mouse reporting from `filter !== null`, so a construction failure
|
|
44
|
-
* degrades to "the wheel is inert" — a supported, tested rung of the fail-safe
|
|
45
|
-
* ladder — rather than leaving reporting on with no filter in front of Ink.
|
|
46
|
-
*/
|
|
47
|
-
export declare function tryCreateMouseFilter(real: NodeJS.ReadStream, onError?: (reason: string) => void): MouseFilter | null;
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stdin mouse filter (mouse-wheel-region-routing §4.2 / §5.2).
|
|
3
|
-
*
|
|
4
|
-
* WHY A WRAPPER AND NOT A SECOND LISTENER. Ink 5.2.1 owns stdin in a way that
|
|
5
|
-
* forbids a second reader: `components/App.js` calls `setEncoding('utf8')`,
|
|
6
|
-
* `ref()`, `setRawMode(true)`, subscribes to `'readable'` and drains with
|
|
7
|
-
* `read()`. Attaching our own `'data'` listener to the same `process.stdin`
|
|
8
|
-
* would switch the stream into flowing mode and RACE Ink's `read()` for chunks
|
|
9
|
-
* — some keystrokes would reach Ink, some would not, non-deterministically
|
|
10
|
-
* (D-3). So the filter REPLACES the stream Ink is given: it reads the real
|
|
11
|
-
* stdin, strips every mouse report, and writes the remainder into a
|
|
12
|
-
* `PassThrough` that carries the surface Ink requires.
|
|
13
|
-
*
|
|
14
|
-
* ORDERING IS AN INVARIANT, NOT A CONVENTION (I-8). The caller must build this
|
|
15
|
-
* filter BEFORE anything writes `\x1b[?1000h`. Reporting enabled while Ink
|
|
16
|
-
* still holds the raw stream does not degrade the feature — it types
|
|
17
|
-
* `[<0;12;5M` into the user's message on every click, which is strictly worse
|
|
18
|
-
* than the bug this feature fixes. `cli.tsx::runInteractive` is the only caller
|
|
19
|
-
* and passes `filter !== null` to `enterAltScreen`.
|
|
20
|
-
*/
|
|
21
|
-
import { PassThrough } from 'node:stream';
|
|
22
|
-
import { splitMouseEvents } from './mouse-events.js';
|
|
23
|
-
/**
|
|
24
|
-
* How long a possible-prefix tail is held before being flushed as text.
|
|
25
|
-
*
|
|
26
|
-
* This is what stops a lone `\x1b` — the Esc key, which aborts a run — from
|
|
27
|
-
* being held hostage waiting for a `[` that will never arrive. Worst-case Esc
|
|
28
|
-
* latency becomes 12 ms, and only for the keystroke that is literally a bare
|
|
29
|
-
* ESC; every other key resolves `isMousePrefix` to false immediately.
|
|
30
|
-
*/
|
|
31
|
-
const PENDING_FLUSH_MS = 12;
|
|
32
|
-
/** Assign a method onto the PassThrough without widening its public type. */
|
|
33
|
-
function define(target, name, value) {
|
|
34
|
-
Object.defineProperty(target, name, { value, writable: true, configurable: true });
|
|
35
|
-
}
|
|
36
|
-
export function createMouseFilter(real) {
|
|
37
|
-
const wrapper = new PassThrough();
|
|
38
|
-
const listeners = new Set();
|
|
39
|
-
let pending = '';
|
|
40
|
-
let flushTimer = null;
|
|
41
|
-
let disposed = false;
|
|
42
|
-
// --- The surface Ink reads off the stream it is handed. ------------------
|
|
43
|
-
// `isRawModeSupported()` reads `props.stdin.isTTY` (App.js:34), so this has
|
|
44
|
-
// to be a DELEGATING GETTER rather than a snapshot: a stdin that is not a TTY
|
|
45
|
-
// must still report so through the wrapper, or Ink throws instead of falling
|
|
46
|
-
// back.
|
|
47
|
-
Object.defineProperty(wrapper, 'isTTY', {
|
|
48
|
-
get: () => real.isTTY,
|
|
49
|
-
configurable: true,
|
|
50
|
-
});
|
|
51
|
-
define(wrapper, 'setRawMode', (mode) => {
|
|
52
|
-
real.setRawMode?.(mode);
|
|
53
|
-
return wrapper;
|
|
54
|
-
});
|
|
55
|
-
define(wrapper, 'ref', () => {
|
|
56
|
-
real.ref?.();
|
|
57
|
-
return wrapper;
|
|
58
|
-
});
|
|
59
|
-
define(wrapper, 'unref', () => {
|
|
60
|
-
real.unref?.();
|
|
61
|
-
return wrapper;
|
|
62
|
-
});
|
|
63
|
-
const emit = (event) => {
|
|
64
|
-
for (const listener of [...listeners])
|
|
65
|
-
listener(event);
|
|
66
|
-
};
|
|
67
|
-
const clearFlushTimer = () => {
|
|
68
|
-
if (!flushTimer)
|
|
69
|
-
return;
|
|
70
|
-
clearTimeout(flushTimer);
|
|
71
|
-
flushTimer = null;
|
|
72
|
-
};
|
|
73
|
-
const flushPending = () => {
|
|
74
|
-
clearFlushTimer();
|
|
75
|
-
if (pending.length === 0 || disposed)
|
|
76
|
-
return;
|
|
77
|
-
const text = pending;
|
|
78
|
-
pending = '';
|
|
79
|
-
wrapper.write(text);
|
|
80
|
-
};
|
|
81
|
-
const onData = (chunk) => {
|
|
82
|
-
if (disposed)
|
|
83
|
-
return;
|
|
84
|
-
clearFlushTimer();
|
|
85
|
-
const decoded = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
86
|
-
const split = splitMouseEvents(pending + decoded);
|
|
87
|
-
pending = split.pending;
|
|
88
|
-
if (split.text.length > 0)
|
|
89
|
-
wrapper.write(split.text);
|
|
90
|
-
for (const event of split.events)
|
|
91
|
-
emit(event);
|
|
92
|
-
if (pending.length > 0) {
|
|
93
|
-
// `.unref()` so a held tail can never keep the process alive on its own.
|
|
94
|
-
flushTimer = setTimeout(flushPending, PENDING_FLUSH_MS);
|
|
95
|
-
flushTimer.unref?.();
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
// P2-7: a closed stdin must still terminate Ink's reader, or it waits forever
|
|
99
|
-
// on a stream that will never produce another byte.
|
|
100
|
-
const onEnd = () => {
|
|
101
|
-
flushPending();
|
|
102
|
-
if (!wrapper.writableEnded)
|
|
103
|
-
wrapper.end();
|
|
104
|
-
};
|
|
105
|
-
// `setEncoding('utf8')` on the REAL stream so Node's StringDecoder handles
|
|
106
|
-
// multi-byte boundaries for us — the parser works on characters, and a UTF-8
|
|
107
|
-
// sequence split across two chunks would otherwise become two mojibake
|
|
108
|
-
// characters before it ever reached `splitMouseEvents`.
|
|
109
|
-
real.setEncoding?.('utf8');
|
|
110
|
-
real.on('data', onData);
|
|
111
|
-
real.once('end', onEnd);
|
|
112
|
-
real.once('close', onEnd);
|
|
113
|
-
return {
|
|
114
|
-
stdin: wrapper,
|
|
115
|
-
source: {
|
|
116
|
-
subscribe(listener) {
|
|
117
|
-
listeners.add(listener);
|
|
118
|
-
return () => listeners.delete(listener);
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
dispose() {
|
|
122
|
-
if (disposed)
|
|
123
|
-
return;
|
|
124
|
-
disposed = true;
|
|
125
|
-
clearFlushTimer();
|
|
126
|
-
real.removeListener('data', onData);
|
|
127
|
-
real.removeListener('end', onEnd);
|
|
128
|
-
real.removeListener('close', onEnd);
|
|
129
|
-
listeners.clear();
|
|
130
|
-
// I-7: a live `'data'` listener on stdin keeps the event loop alive, and
|
|
131
|
-
// `aragon` would hang on exit instead of returning to the shell.
|
|
132
|
-
real.pause?.();
|
|
133
|
-
real.unref?.();
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Build a filter, or `null` if construction throws.
|
|
139
|
-
*
|
|
140
|
-
* The `null` return is the whole point (P0-1): `enterAltScreen` derives whether
|
|
141
|
-
* to enable mouse reporting from `filter !== null`, so a construction failure
|
|
142
|
-
* degrades to "the wheel is inert" — a supported, tested rung of the fail-safe
|
|
143
|
-
* ladder — rather than leaving reporting on with no filter in front of Ink.
|
|
144
|
-
*/
|
|
145
|
-
export function tryCreateMouseFilter(real, onError) {
|
|
146
|
-
try {
|
|
147
|
-
return createMouseFilter(real);
|
|
148
|
-
}
|
|
149
|
-
catch (err) {
|
|
150
|
-
onError?.(err instanceof Error ? err.message : String(err));
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
}
|