@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,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The selection model (tui-selection-and-scroll-follow §4.4.4). PURE — no React,
|
|
3
|
+
* no I/O, no terminal.
|
|
4
|
+
*
|
|
5
|
+
* Coordinates are SCREEN CELLS, 0-based: frame line `i` is terminal row `i + 1`
|
|
6
|
+
* and column `x - 1`, so an SGR report at `(x, y)` becomes `{ row: y - 1, col: x
|
|
7
|
+
* - 1 }`. Screen coordinates are only sound while the rows hold still, and
|
|
8
|
+
* holding them still is Stage 2's job — `press` sets `hold`, which anchors the
|
|
9
|
+
* content and suspends the resume timer, so the transcript is frozen for the
|
|
10
|
+
* duration of a drag (D-7).
|
|
11
|
+
*
|
|
12
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
* I-12 — `sliceColumns` IS THE ONE COLUMN→CHARACTER MAPPING IN THIS FEATURE,
|
|
14
|
+
* and `stringWidth` is its only width oracle.
|
|
15
|
+
*
|
|
16
|
+
* The painter and the extractor are handed the SAME half-open column range by
|
|
17
|
+
* `rowSpan`. If they turn that range into characters by different rules, the
|
|
18
|
+
* copied text is not the highlighted text — silently, which is the one failure
|
|
19
|
+
* mode I-4 exists to name. That is not hypothetical: `slice-ansi` advances its
|
|
20
|
+
* cursor by `isFullWidth ? 2 : character.length`, i.e. display columns for CJK
|
|
21
|
+
* but UTF-16 CODE UNITS for everything else, so a combining mark costs it a
|
|
22
|
+
* column that occupies none and an astral code point costs two; and the obvious
|
|
23
|
+
* `String.slice` for the extractor is code units throughout. Two mappings from
|
|
24
|
+
* one range agree on ASCII and diverge on exactly the rows a user is most likely
|
|
25
|
+
* to want to copy.
|
|
26
|
+
*
|
|
27
|
+
* So both call the function below, which walks GRAPHEME CLUSTERS and charges
|
|
28
|
+
* each one `stringWidth(cluster)` columns. What is hand-rolled here is only the
|
|
29
|
+
* column arithmetic — the width table is still `string-width`'s, and the escape
|
|
30
|
+
* bookkeeping is `slice-ansi`'s idea re-indexed (R-13).
|
|
31
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
32
|
+
*/
|
|
33
|
+
import stringWidth from 'string-width';
|
|
34
|
+
export function normalize(sel) {
|
|
35
|
+
const { anchor, focus } = sel;
|
|
36
|
+
const anchorFirst = anchor.row < focus.row || (anchor.row === focus.row && anchor.col <= focus.col);
|
|
37
|
+
return anchorFirst ? { start: anchor, end: focus } : { start: focus, end: anchor };
|
|
38
|
+
}
|
|
39
|
+
export function isEmpty(sel) {
|
|
40
|
+
return sel.start.row === sel.end.row && sel.start.col === sel.end.col;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The half-open column range to highlight on `row`, or `null` when the row is
|
|
44
|
+
* outside the selection.
|
|
45
|
+
*
|
|
46
|
+
* LINEAR, NOT RECTANGULAR — what every terminal does: the first row runs from
|
|
47
|
+
* `start.col` to end of line, whole middle rows are taken entirely, and the last
|
|
48
|
+
* row stops at `end.col`. `Infinity` means "to the end of the line"; the painter
|
|
49
|
+
* clamps it to the terminal width so a multi-row selection gets a straight right
|
|
50
|
+
* edge, and the extractor treats it as "no upper bound".
|
|
51
|
+
*/
|
|
52
|
+
export function rowSpan(sel, row) {
|
|
53
|
+
const { start, end } = sel;
|
|
54
|
+
if (row < start.row || row > end.row)
|
|
55
|
+
return null;
|
|
56
|
+
if (start.row === end.row) {
|
|
57
|
+
return end.col > start.col ? { from: start.col, to: end.col } : null;
|
|
58
|
+
}
|
|
59
|
+
if (row === start.row)
|
|
60
|
+
return { from: start.col, to: Number.POSITIVE_INFINITY };
|
|
61
|
+
if (row === end.row)
|
|
62
|
+
return end.col > 0 ? { from: 0, to: end.col } : null;
|
|
63
|
+
return { from: 0, to: Number.POSITIVE_INFINITY };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The text a release would put on the clipboard.
|
|
67
|
+
*
|
|
68
|
+
* `plainRows` are the mirror's ANSI-STRIPPED rows — the same rows the highlight
|
|
69
|
+
* was painted over, which is what makes I-4 hold by construction rather than by
|
|
70
|
+
* a staleness check that can be wrong.
|
|
71
|
+
*/
|
|
72
|
+
export function selectedText(plainRows, sel) {
|
|
73
|
+
const out = [];
|
|
74
|
+
for (let row = sel.start.row; row <= sel.end.row; row += 1) {
|
|
75
|
+
const span = rowSpan(sel, row);
|
|
76
|
+
if (!span)
|
|
77
|
+
continue;
|
|
78
|
+
const line = plainRows[row] ?? '';
|
|
79
|
+
const to = Number.isFinite(span.to) ? span.to : undefined;
|
|
80
|
+
out.push(sliceColumns(line, span.from, to).trimEnd());
|
|
81
|
+
}
|
|
82
|
+
return out.join('\n');
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// sliceColumns — the one mapping (I-12)
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
/**
|
|
88
|
+
* CSI / OSC sequences. They occupy ZERO columns and are copied through so a
|
|
89
|
+
* slice keeps the styling of the text around it.
|
|
90
|
+
*/
|
|
91
|
+
const ANSI_RE = /\x1b(?:\[[0-9;:?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
|
|
92
|
+
/** SGR "all attributes off" — everything carried before it is cancelled. */
|
|
93
|
+
const SGR_RESET_RE = /^\x1b\[0?m$/;
|
|
94
|
+
const segmenter = typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function'
|
|
95
|
+
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
96
|
+
: null;
|
|
97
|
+
/**
|
|
98
|
+
* Width cache. `stringWidth` re-segments and re-classifies on every call, and a
|
|
99
|
+
* repaint slices every row of the selection; the key space is the set of
|
|
100
|
+
* grapheme clusters on screen, which is tiny and bounded by the frame.
|
|
101
|
+
*/
|
|
102
|
+
const widthCache = new Map();
|
|
103
|
+
const WIDTH_CACHE_MAX = 4096;
|
|
104
|
+
function clusterWidth(cluster) {
|
|
105
|
+
const hit = widthCache.get(cluster);
|
|
106
|
+
if (hit !== undefined)
|
|
107
|
+
return hit;
|
|
108
|
+
const w = stringWidth(cluster);
|
|
109
|
+
// A plain ceiling with a wholesale drop, like `virtual-window.ts`'s height
|
|
110
|
+
// cache: an LRU here would cost more bookkeeping than the values are worth.
|
|
111
|
+
if (widthCache.size >= WIDTH_CACHE_MAX)
|
|
112
|
+
widthCache.clear();
|
|
113
|
+
widthCache.set(cluster, w);
|
|
114
|
+
return w;
|
|
115
|
+
}
|
|
116
|
+
/** Split into ANSI sequences and grapheme clusters, in order. */
|
|
117
|
+
function tokenize(text) {
|
|
118
|
+
const tokens = [];
|
|
119
|
+
ANSI_RE.lastIndex = 0;
|
|
120
|
+
let last = 0;
|
|
121
|
+
let m;
|
|
122
|
+
const pushPlain = (chunk) => {
|
|
123
|
+
if (chunk.length === 0)
|
|
124
|
+
return;
|
|
125
|
+
if (segmenter) {
|
|
126
|
+
for (const { segment } of segmenter.segment(chunk))
|
|
127
|
+
tokens.push({ text: segment, ansi: false });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// No `Intl.Segmenter`: code points are a strictly worse but never wrong
|
|
131
|
+
// approximation — a combining mark becomes its own zero-width token, which
|
|
132
|
+
// the walk below already handles.
|
|
133
|
+
for (const cp of chunk)
|
|
134
|
+
tokens.push({ text: cp, ansi: false });
|
|
135
|
+
};
|
|
136
|
+
while ((m = ANSI_RE.exec(text)) !== null) {
|
|
137
|
+
pushPlain(text.slice(last, m.index));
|
|
138
|
+
tokens.push({ text: m[0], ansi: true });
|
|
139
|
+
last = m.index + m[0].length;
|
|
140
|
+
}
|
|
141
|
+
pushPlain(text.slice(last));
|
|
142
|
+
return tokens;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Slice `text` by SCREEN COLUMNS, half-open `[from, to)`.
|
|
146
|
+
*
|
|
147
|
+
* Three properties are load-bearing:
|
|
148
|
+
*
|
|
149
|
+
* - **Escape codes opened before `from` are carried into the result.** Without
|
|
150
|
+
* it the painter's tail — `sliceColumns(line, to)` — would lose the colour
|
|
151
|
+
* `Markdown` and `cli-highlight` gave it, on every row a selection touches.
|
|
152
|
+
* - **A wide cluster straddling either boundary becomes spaces for the part
|
|
153
|
+
* that is inside.** That keeps the arithmetic EXACT in columns, which is the
|
|
154
|
+
* whole point of I-12: half a CJK glyph is not a character, and inventing one
|
|
155
|
+
* would put the painter and the extractor back out of step.
|
|
156
|
+
* - **`to` is optional and means "to the end of the line".**
|
|
157
|
+
*/
|
|
158
|
+
export function sliceColumns(text, from, to) {
|
|
159
|
+
const start = Math.max(0, Math.floor(from));
|
|
160
|
+
const end = to === undefined || !Number.isFinite(to) ? Number.POSITIVE_INFINITY : Math.floor(to);
|
|
161
|
+
if (end <= start)
|
|
162
|
+
return '';
|
|
163
|
+
let carried = '';
|
|
164
|
+
let out = '';
|
|
165
|
+
let col = 0;
|
|
166
|
+
let started = false;
|
|
167
|
+
const open = () => {
|
|
168
|
+
if (started)
|
|
169
|
+
return;
|
|
170
|
+
started = true;
|
|
171
|
+
out += carried;
|
|
172
|
+
};
|
|
173
|
+
for (const token of tokenize(text)) {
|
|
174
|
+
if (token.ansi) {
|
|
175
|
+
if (started)
|
|
176
|
+
out += token.text;
|
|
177
|
+
else {
|
|
178
|
+
if (SGR_RESET_RE.test(token.text))
|
|
179
|
+
carried = '';
|
|
180
|
+
carried += token.text;
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (col >= end)
|
|
185
|
+
break;
|
|
186
|
+
const w = clusterWidth(token.text);
|
|
187
|
+
if (w === 0) {
|
|
188
|
+
// A zero-width cluster attaches to the cell before it, so it belongs to the
|
|
189
|
+
// slice only when that cell is already inside.
|
|
190
|
+
if (started)
|
|
191
|
+
out += token.text;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const cellEnd = col + w;
|
|
195
|
+
if (cellEnd <= start) {
|
|
196
|
+
col = cellEnd;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
open();
|
|
200
|
+
if (col < start) {
|
|
201
|
+
// Straddles the LEFT boundary: only `cellEnd - start` columns are ours.
|
|
202
|
+
out += ' '.repeat(Math.min(cellEnd, end) - start);
|
|
203
|
+
}
|
|
204
|
+
else if (cellEnd > end) {
|
|
205
|
+
// Straddles the RIGHT boundary.
|
|
206
|
+
out += ' '.repeat(end - col);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
out += token.text;
|
|
210
|
+
}
|
|
211
|
+
col = cellEnd;
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
/** Display width in columns, using the same oracle `sliceColumns` charges with. */
|
|
216
|
+
export function columnsOf(text) {
|
|
217
|
+
let total = 0;
|
|
218
|
+
for (const token of tokenize(text)) {
|
|
219
|
+
if (!token.ansi)
|
|
220
|
+
total += clusterWidth(token.text);
|
|
221
|
+
}
|
|
222
|
+
return total;
|
|
223
|
+
}
|
|
@@ -35,6 +35,39 @@ export interface FrameWriterHandle {
|
|
|
35
35
|
stdout: NodeJS.WriteStream;
|
|
36
36
|
/** Hand this to `setFrameStatsProvider`. */
|
|
37
37
|
stats(): FrameWriterStats;
|
|
38
|
+
/**
|
|
39
|
+
* Re-paint the last frame through the differ's `decorate` hook — how a
|
|
40
|
+
* selection drag reaches the screen without a React commit.
|
|
41
|
+
*
|
|
42
|
+
* Returns whether anything was written. `false` means the differ had nothing
|
|
43
|
+
* safe to do (no cache, or a geometry stand-down), which is the caller's cue to
|
|
44
|
+
* fall back to `App`'s `redrawNonce`.
|
|
45
|
+
*
|
|
46
|
+
* I-3 / I-13 — IT WRITES TO THE REAL STREAM, NEVER THROUGH THE PROXY. Through
|
|
47
|
+
* the proxy the payload would be handed to `transform`, which would not
|
|
48
|
+
* recognise it and would `invalidate()`, turning every drag frame into a full
|
|
49
|
+
* repaint — the flicker `tui-input-flicker-fix` removed. It is also not a
|
|
50
|
+
* fallback and not a frame, so it touches neither counter.
|
|
51
|
+
*/
|
|
52
|
+
repaint(): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Write bytes that are ours but are not a frame — today, the OSC 52 clipboard
|
|
55
|
+
* sequence (§4.4.5).
|
|
56
|
+
*
|
|
57
|
+
* The differ is invalidated first, because absolute addressing is only valid
|
|
58
|
+
* while nothing else writes to stdout, and one full repaint after a copy is the
|
|
59
|
+
* correct price.
|
|
60
|
+
*
|
|
61
|
+
* IT IS NOT A FALLBACK (P1-6 / I-13), and that is the whole reason this door
|
|
62
|
+
* exists rather than the caller reaching for `stdout.write`. `transform` cannot
|
|
63
|
+
* recognise an OSC 52 chunk, so through the proxy it would take
|
|
64
|
+
* `passThrough(true)` — which increments `fallbacks` and, on the first one,
|
|
65
|
+
* raises `onFirstFallback`, wired to `console.warn(FRAME_FALLBACK_NOTICE)`.
|
|
66
|
+
* The first copy of every session would print a diagnostic at a user who did
|
|
67
|
+
* nothing wrong, and `fallbacks` — documented to mean "something wrote to
|
|
68
|
+
* stdout behind Ink's back" — would stop being true for the rest of the run.
|
|
69
|
+
*/
|
|
70
|
+
writeForeign(text: string): void;
|
|
38
71
|
/** Removes the `'resize'` subscription of I-8. Idempotent. */
|
|
39
72
|
dispose(): void;
|
|
40
73
|
}
|
|
@@ -92,6 +92,21 @@ export function wrapStdoutForFrames(stdout, differ) {
|
|
|
92
92
|
return {
|
|
93
93
|
stdout: proxy,
|
|
94
94
|
stats: () => differ.stats(),
|
|
95
|
+
repaint() {
|
|
96
|
+
if (disposed)
|
|
97
|
+
return false;
|
|
98
|
+
const payload = differ.repaint();
|
|
99
|
+
if (payload.length === 0)
|
|
100
|
+
return false;
|
|
101
|
+
writeReal(payload);
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
writeForeign(text) {
|
|
105
|
+
differ.invalidate();
|
|
106
|
+
if (disposed || text.length === 0)
|
|
107
|
+
return;
|
|
108
|
+
writeReal(text);
|
|
109
|
+
},
|
|
95
110
|
dispose() {
|
|
96
111
|
if (disposed)
|
|
97
112
|
return;
|
package/dist/ui/theme.d.ts
CHANGED
|
@@ -50,6 +50,17 @@ export interface Theme {
|
|
|
50
50
|
hintFg: Color;
|
|
51
51
|
/** Single-color wordmark fallback below ansi-256. */
|
|
52
52
|
logoShadow: Color;
|
|
53
|
+
/**
|
|
54
|
+
* Drag-selection highlight (tui-selection-and-scroll-follow §4.4.4).
|
|
55
|
+
*
|
|
56
|
+
* Degraded like every other colour here, which means that below ansi-256 these
|
|
57
|
+
* are chalk colour NAMES rather than hex triples. `highlight.ts` tests for a
|
|
58
|
+
* hex shape and falls back to plain reverse video otherwise — the fallback a
|
|
59
|
+
* terminal that shallow would render anyway, and what the emulator's own
|
|
60
|
+
* selection looks like.
|
|
61
|
+
*/
|
|
62
|
+
selectionBg: Color;
|
|
63
|
+
selectionFg: Color;
|
|
53
64
|
gradient: string[];
|
|
54
65
|
gauge: {
|
|
55
66
|
track: Color;
|
package/dist/ui/theme.js
CHANGED
|
@@ -101,6 +101,8 @@ export function getTheme(name, caps) {
|
|
|
101
101
|
idleBorder: d(palette.idleBorder),
|
|
102
102
|
hintFg: d(palette.hintFg),
|
|
103
103
|
logoShadow: d(palette.logoShadow),
|
|
104
|
+
selectionBg: d(palette.selectionBg),
|
|
105
|
+
selectionFg: d(palette.selectionFg),
|
|
104
106
|
gradient: level >= 2 ? [...palette.gradient] : [],
|
|
105
107
|
gauge: {
|
|
106
108
|
track: d(palette.gauge.track),
|
|
@@ -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':
|
|
@@ -111,6 +139,50 @@ function renderEntry(entry, glyphs) {
|
|
|
111
139
|
const detail = entry.detail ? `: ${entry.detail}` : '';
|
|
112
140
|
return [`${head} ${entry.status}${detail}`];
|
|
113
141
|
}
|
|
142
|
+
case 'compaction': {
|
|
143
|
+
// A compaction is real spend AND a real, permanent loss of history, and an
|
|
144
|
+
// exported transcript that omitted it would be unaccountable for both —
|
|
145
|
+
// the argument the `team`, `retry` and `fast` cases above each make in
|
|
146
|
+
// their own words. It matters most here: this is the one entry kind whose
|
|
147
|
+
// presence explains why the messages the reader is looking for are gone.
|
|
148
|
+
const glyph = entry.applied ? glyphs.compaction : glyphs.toolError;
|
|
149
|
+
const duration = entry.durationMs === undefined ? '' : ` (${entry.durationMs}ms)`;
|
|
150
|
+
if (!entry.applied) {
|
|
151
|
+
const why = entry.reason ? `: ${entry.reason}` : '';
|
|
152
|
+
return [`${TOOL_INDENT}${glyph} context not compacted #${entry.index}${why}`];
|
|
153
|
+
}
|
|
154
|
+
const head = `${TOOL_INDENT}${glyph} context compacted #${entry.index} ` +
|
|
155
|
+
`${entry.messagesBefore} ${glyphs.arrowRight} ${entry.messagesAfter} messages, ` +
|
|
156
|
+
`${entry.tokensBefore} ${glyphs.arrowRight} ${entry.tokensAfter} tokens ` +
|
|
157
|
+
`[${entry.mode}] ${entry.model}${duration}`;
|
|
158
|
+
if (!entry.summary)
|
|
159
|
+
return [head];
|
|
160
|
+
return [
|
|
161
|
+
head,
|
|
162
|
+
...entry.summary.split('\n').map((l) => `${TOOL_INDENT}${CONTINUATION_INDENT}${l}`),
|
|
163
|
+
];
|
|
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
|
+
}
|
|
114
186
|
case 'notice': {
|
|
115
187
|
const glyph = entry.level === 'error'
|
|
116
188
|
? glyphs.error
|
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
* not otherwise see — rather than belonging to the view.
|
|
13
13
|
*/
|
|
14
14
|
import type { ViewAction } from '../agent/reducer.js';
|
|
15
|
+
/**
|
|
16
|
+
* The revision of the mouse notice a user must have seen for it to stay quiet
|
|
17
|
+
* (`state.json::mouseNoticeVersion`, `0` when absent).
|
|
18
|
+
*
|
|
19
|
+
* BUMP IT WHENEVER THE ADVICE CHANGES, never for a rewording that leaves the
|
|
20
|
+
* recommended actions alone — the same rule `VT_INPUT_NOTICE_VERSION` below
|
|
21
|
+
* carries, and for the same reason.
|
|
22
|
+
*
|
|
23
|
+
* `1` = the first text that names DRAG-SELECT and `/mouse`. `0` = the 0.6.2 text
|
|
24
|
+
* that taught the Shift bypass, recorded back when this was a boolean.
|
|
25
|
+
*/
|
|
26
|
+
export declare const MOUSE_NOTICE_VERSION = 1;
|
|
15
27
|
/**
|
|
16
28
|
* The revision of the text below that a user must have seen for this notice to
|
|
17
29
|
* stay quiet (`state.json::vtInputNoticeVersion`, `0` when absent).
|
|
@@ -100,7 +112,21 @@ export interface MouseNoticeOptions {
|
|
|
100
112
|
* re-deriving either one here would put them back out of step.
|
|
101
113
|
*/
|
|
102
114
|
enabled: boolean;
|
|
103
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Whether DRAG-SELECT is running too, i.e. the resolved `mouseSelect`.
|
|
117
|
+
*
|
|
118
|
+
* A SECOND FLAG RATHER THAN AN INFERENCE, for the reason the field above
|
|
119
|
+
* records about its own two halves: "the mouse is captured" and "dragging
|
|
120
|
+
* selects text" are not the same claim, and a session that turned drag-select
|
|
121
|
+
* off must not be handed advice about a mode that is not running.
|
|
122
|
+
*/
|
|
123
|
+
selectEnabled: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Record that `MOUSE_NOTICE_VERSION` was shown (`state.json`). Called once,
|
|
126
|
+
* after dispatch — and it must write that constant, not `true`: the gate
|
|
127
|
+
* compares against it, so a caller storing anything else either replays the
|
|
128
|
+
* notice forever or silences a future revision of the text.
|
|
129
|
+
*/
|
|
104
130
|
onSeen: () => void;
|
|
105
131
|
}
|
|
106
132
|
export interface VtInputNoticeOptions {
|
|
@@ -14,18 +14,42 @@
|
|
|
14
14
|
import { useEffect, useRef } from 'react';
|
|
15
15
|
import { MODE_TOGGLE_KEYS } from '../agent/agent-mode.js';
|
|
16
16
|
import { readConfigFile } from '../config/store.js';
|
|
17
|
-
import {
|
|
17
|
+
import { getMouseNoticeVersion, getVtInputNoticeVersion } from '../config/ui-state.js';
|
|
18
18
|
import { getLogger } from '../logging/logger.js';
|
|
19
19
|
/**
|
|
20
|
-
* The
|
|
20
|
+
* The revision of the mouse notice a user must have seen for it to stay quiet
|
|
21
|
+
* (`state.json::mouseNoticeVersion`, `0` when absent).
|
|
21
22
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* BUMP IT WHENEVER THE ADVICE CHANGES, never for a rewording that leaves the
|
|
24
|
+
* recommended actions alone — the same rule `VT_INPUT_NOTICE_VERSION` below
|
|
25
|
+
* carries, and for the same reason.
|
|
26
|
+
*
|
|
27
|
+
* `1` = the first text that names DRAG-SELECT and `/mouse`. `0` = the 0.6.2 text
|
|
28
|
+
* that taught the Shift bypass, recorded back when this was a boolean.
|
|
26
29
|
*/
|
|
27
|
-
const
|
|
28
|
-
|
|
30
|
+
export const MOUSE_NOTICE_VERSION = 1;
|
|
31
|
+
/**
|
|
32
|
+
* The one-shot mouse notice (tui-selection-and-scroll-follow §5.2 row 29).
|
|
33
|
+
*
|
|
34
|
+
* IT NO LONGER TEACHES THE SHIFT WORKAROUND. That text was a workaround told
|
|
35
|
+
* once, in a line that scrolls away, naming a key that is not portable — Shift
|
|
36
|
+
* on xterm / VTE / Windows Terminal, Fn on macOS Terminal, a preference
|
|
37
|
+
* elsewhere — and it collided with our own Shift+wheel binding. The user report
|
|
38
|
+
* this feature exists for is the evidence that it was not enough (D-1).
|
|
39
|
+
*
|
|
40
|
+
* TWO TEXTS, because a `mouseSelect: false` session must not be told about a
|
|
41
|
+
* gesture that is not running — the same rule the vt-input notice follows below,
|
|
42
|
+
* and the reason `MouseNoticeOptions` carries the flag rather than this module
|
|
43
|
+
* re-deriving it.
|
|
44
|
+
*/
|
|
45
|
+
function mouseNoticeText(selectEnabled) {
|
|
46
|
+
if (!selectEnabled) {
|
|
47
|
+
return ('Mouse wheel scrolls the transcript. Drag-select is off (mouseSelect); ' +
|
|
48
|
+
'/mouse off hands the mouse back to your terminal for this session.');
|
|
49
|
+
}
|
|
50
|
+
return ('Mouse wheel scrolls the transcript, and dragging selects text - releasing ' +
|
|
51
|
+
'copies it. /mouse off hands the mouse back to your terminal for this session.');
|
|
52
|
+
}
|
|
29
53
|
/**
|
|
30
54
|
* The §5.1 notice (tui-render-performance). Shown once per run, and only when
|
|
31
55
|
* the user's own `transcriptRetain` was below `transcriptWindow`.
|
|
@@ -142,16 +166,24 @@ export function useStartupNotices(dispatch, mouseNotice, retainNotice, vtInputNo
|
|
|
142
166
|
const noticeRef = useRef(mouseNotice);
|
|
143
167
|
noticeRef.current = mouseNotice;
|
|
144
168
|
const mouseEnabled = mouseNotice?.enabled ?? false;
|
|
169
|
+
const mouseSelectEnabled = mouseNotice?.selectEnabled ?? false;
|
|
145
170
|
const mouseNoticeShown = useRef(false);
|
|
146
171
|
useEffect(() => {
|
|
147
172
|
if (!mouseEnabled || mouseNoticeShown.current)
|
|
148
173
|
return;
|
|
149
|
-
|
|
174
|
+
// A VERSION, NOT THE OLD BOOLEAN (P1-8 / AC-16). The audience for the
|
|
175
|
+
// corrected text is exactly the set of users who already have
|
|
176
|
+
// `mouseNoticeSeen: true` on disk from the text it replaces, so reading that
|
|
177
|
+
// flag would make this change inert for everyone it is for.
|
|
178
|
+
//
|
|
179
|
+
// `>=`, not `!==`: a state file written by a NEWER build (a downgrade, or a
|
|
180
|
+
// shared home directory) has already told this user everything below.
|
|
181
|
+
if (getMouseNoticeVersion() >= MOUSE_NOTICE_VERSION)
|
|
150
182
|
return;
|
|
151
183
|
mouseNoticeShown.current = true;
|
|
152
|
-
dispatch({ type: 'notice', level: 'info', text:
|
|
184
|
+
dispatch({ type: 'notice', level: 'info', text: mouseNoticeText(mouseSelectEnabled) });
|
|
153
185
|
noticeRef.current?.onSeen();
|
|
154
|
-
}, [mouseEnabled, dispatch]);
|
|
186
|
+
}, [mouseEnabled, mouseSelectEnabled, dispatch]);
|
|
155
187
|
// The R-1 notice. `warn`, not `info`: unlike the one above it reports a
|
|
156
188
|
// capability the user was promised and does not have, plus a key that is
|
|
157
189
|
// actively editing their draft.
|
|
@@ -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
|
|
@@ -84,6 +84,14 @@ export function useWheelRouting(options) {
|
|
|
84
84
|
}
|
|
85
85
|
}, [flush]);
|
|
86
86
|
const route = useCallback((event) => {
|
|
87
|
+
// The channel now carries press / drag / release as well
|
|
88
|
+
// (tui-selection-and-scroll-follow §4.4.2). ONE LINE, AT THE TOP, and the
|
|
89
|
+
// rest of this module is untouched: widening the single channel rather
|
|
90
|
+
// than adding a second is what keeps arrival order intact between a wheel
|
|
91
|
+
// notch and a button, and each subscriber ignoring what it does not own is
|
|
92
|
+
// the price of that.
|
|
93
|
+
if (event.kind !== 'wheel')
|
|
94
|
+
return;
|
|
87
95
|
const current = optionsRef.current;
|
|
88
96
|
const overlay = current.getOverlay();
|
|
89
97
|
// THE OVERLAY BRANCH IS EVALUATED FIRST AND RETURNS FOR EVERY OVERLAY IT
|
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",
|
|
@@ -67,6 +67,8 @@
|
|
|
67
67
|
"ink-select-input": "^6.0.0",
|
|
68
68
|
"ink-spinner": "^5.0.0",
|
|
69
69
|
"react": "^18.3.1",
|
|
70
|
+
"string-width": "^7.2.0",
|
|
71
|
+
"strip-ansi": "^7.1.0",
|
|
70
72
|
"tinyglobby": "^0.2.17"
|
|
71
73
|
},
|
|
72
74
|
"devDependencies": {
|
|
@@ -1,42 +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 WheelEvent } from './mouse-events.js';
|
|
22
|
-
export interface MouseSource {
|
|
23
|
-
/** Returns an unsubscribe function. Multiple subscribers are allowed. */
|
|
24
|
-
subscribe(listener: (event: WheelEvent) => void): () => void;
|
|
25
|
-
}
|
|
26
|
-
export interface MouseFilter {
|
|
27
|
-
/** Hand this to Ink: `render(<App/>, { stdin: filter.stdin })`. */
|
|
28
|
-
readonly stdin: NodeJS.ReadStream;
|
|
29
|
-
readonly source: MouseSource;
|
|
30
|
-
/** Idempotent. Detaches from the real stdin and clears the flush timer (I-7). */
|
|
31
|
-
dispose(): void;
|
|
32
|
-
}
|
|
33
|
-
export declare function createMouseFilter(real: NodeJS.ReadStream): MouseFilter;
|
|
34
|
-
/**
|
|
35
|
-
* Build a filter, or `null` if construction throws.
|
|
36
|
-
*
|
|
37
|
-
* The `null` return is the whole point (P0-1): `enterAltScreen` derives whether
|
|
38
|
-
* to enable mouse reporting from `filter !== null`, so a construction failure
|
|
39
|
-
* degrades to "the wheel is inert" — a supported, tested rung of the fail-safe
|
|
40
|
-
* ladder — rather than leaving reporting on with no filter in front of Ink.
|
|
41
|
-
*/
|
|
42
|
-
export declare function tryCreateMouseFilter(real: NodeJS.ReadStream, onError?: (reason: string) => void): MouseFilter | null;
|