@aragon-agent/cli 0.6.3 → 0.6.7
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 +142 -0
- package/README.md +203 -13
- package/dist/agent/controller.d.ts +158 -2
- package/dist/agent/controller.js +400 -1
- package/dist/agent/reducer.d.ts +102 -19
- package/dist/agent/reducer.js +162 -71
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/agent/usage.d.ts +34 -0
- package/dist/agent/usage.js +43 -0
- package/dist/cli.js +132 -23
- package/dist/commands/builtins.js +101 -1
- package/dist/compaction/child.js +5 -0
- package/dist/compaction/command.js +14 -1
- package/dist/compaction/compactor.d.ts +94 -24
- package/dist/compaction/compactor.js +247 -98
- package/dist/compaction/context-command.d.ts +33 -0
- package/dist/compaction/context-command.js +166 -0
- package/dist/compaction/meter.d.ts +251 -0
- package/dist/compaction/meter.js +428 -0
- package/dist/compaction/pressure.d.ts +6 -0
- package/dist/compaction/pressure.js +4 -0
- package/dist/compaction/types.d.ts +77 -0
- package/dist/compaction/wiring.d.ts +56 -0
- package/dist/compaction/wiring.js +107 -25
- package/dist/config/cli-commands.js +5 -0
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.js +63 -2
- package/dist/config/load.d.ts +28 -0
- package/dist/config/load.js +83 -1
- package/dist/config/schema.d.ts +129 -0
- package/dist/config/schema.js +69 -0
- package/dist/config/store.js +47 -1
- package/dist/exec/events.d.ts +138 -1
- package/dist/exec/events.js +42 -0
- package/dist/exec/index.js +43 -6
- package/dist/exec/progress-reporter.d.ts +23 -0
- package/dist/exec/progress-reporter.js +88 -0
- package/dist/exec/runner.d.ts +34 -0
- package/dist/exec/runner.js +137 -25
- package/dist/fast/reviewer.d.ts +19 -1
- package/dist/fast/reviewer.js +22 -0
- package/dist/fast/types.d.ts +15 -0
- package/dist/fast/wiring.d.ts +10 -1
- package/dist/fast/wiring.js +18 -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/todo/limits.d.ts +4 -17
- package/dist/todo/limits.js +4 -17
- package/dist/todo/panel-rows.d.ts +8 -6
- package/dist/todo/panel-rows.js +19 -6
- 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 +330 -69
- package/dist/ui/AutocompletePopup.d.ts +3 -1
- package/dist/ui/AutocompletePopup.js +9 -11
- package/dist/ui/BottomStatusRow.d.ts +1 -1
- package/dist/ui/Composer.d.ts +56 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +52 -3
- package/dist/ui/PromptInput.js +201 -55
- package/dist/ui/StatusBar.d.ts +40 -15
- package/dist/ui/StatusBar.js +51 -9
- package/dist/ui/TeamPanel.d.ts +3 -1
- package/dist/ui/TeamPanel.js +13 -9
- package/dist/ui/TodoPanel.d.ts +5 -5
- package/dist/ui/TodoPanel.js +56 -24
- package/dist/ui/Transcript.js +29 -1
- package/dist/ui/UpdateLine.d.ts +1 -1
- package/dist/ui/capabilities.js +5 -5
- 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/exit-snapshot.d.ts +9 -0
- package/dist/ui/layout/ScrollViewport.d.ts +1 -1
- package/dist/ui/layout/autocomplete.d.ts +18 -0
- package/dist/ui/layout/autocomplete.js +26 -0
- package/dist/ui/layout/budget.d.ts +34 -6
- package/dist/ui/layout/budget.js +32 -7
- package/dist/ui/layout/rail.d.ts +4 -53
- package/dist/ui/layout/rail.js +10 -62
- package/dist/ui/layout/team-panel.d.ts +18 -0
- package/dist/ui/layout/team-panel.js +22 -0
- package/dist/ui/layout/todo-layout.d.ts +24 -0
- package/dist/ui/layout/todo-layout.js +27 -0
- 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 +55 -2
- 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,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer layout (tui-paste-handling section 5.5) -- pure. No Ink, no React.
|
|
3
|
+
*
|
|
4
|
+
* THE ONE PLACE THAT MAPS A BUFFER INDEX TO A SCREEN CELL (I-9). The composer
|
|
5
|
+
* used to hand its buffer to Ink one `<Text>` per logical line and let Yoga wrap
|
|
6
|
+
* it, which is fine until the draft is taller than the frame: `AppShell` gives
|
|
7
|
+
* the bottom chrome `flexShrink={0}`, so a tall composer does not grow the frame
|
|
8
|
+
* -- Yoga takes the rows out of the transcript and `overflow: hidden` clips them
|
|
9
|
+
* away, while `layout/budget.ts` keeps telling five consumers the composer is
|
|
10
|
+
* three rows tall. Four hundred presses of `Shift+Enter` do it without pasting
|
|
11
|
+
* anything.
|
|
12
|
+
*
|
|
13
|
+
* So the wrap happens HERE, the row count is a number this module returns, and
|
|
14
|
+
* `budget.ts` is told that number (I-8).
|
|
15
|
+
*
|
|
16
|
+
* HARD WRAP BY DISPLAY COLUMN, NEVER `layout/wrap-rows.ts::wrapToRows` (D-15).
|
|
17
|
+
* That function is a PROSE wrapper: it splits on `/\s+/` and rejoins with single
|
|
18
|
+
* spaces, so it destroys indentation and makes the character-index-to-screen-cell
|
|
19
|
+
* mapping the caret depends on impossible to state, let alone compute.
|
|
20
|
+
*/
|
|
21
|
+
export interface ComposerSegment {
|
|
22
|
+
readonly text: string;
|
|
23
|
+
readonly kind: 'text' | 'token';
|
|
24
|
+
}
|
|
25
|
+
export interface ComposerRow {
|
|
26
|
+
readonly segments: ComposerSegment[];
|
|
27
|
+
}
|
|
28
|
+
export interface ComposerLayout {
|
|
29
|
+
/** At most `maxRows`, and always containing the caret when `active`. */
|
|
30
|
+
readonly rows: ComposerRow[];
|
|
31
|
+
/** Index into `rows`, or -1 when the composer is not focused. */
|
|
32
|
+
readonly cursorRow: number;
|
|
33
|
+
/** Display column within that row. */
|
|
34
|
+
readonly cursorCol: number;
|
|
35
|
+
/** Rows the buffer occupies BEFORE the window was applied. */
|
|
36
|
+
readonly totalRows: number;
|
|
37
|
+
readonly hiddenAbove: number;
|
|
38
|
+
readonly hiddenBelow: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ComposerLayoutInput {
|
|
41
|
+
readonly buffer: string;
|
|
42
|
+
readonly cursor: number;
|
|
43
|
+
readonly cols: number;
|
|
44
|
+
readonly maxRows: number;
|
|
45
|
+
readonly active: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** Total display width of a run, using the same table the layout used. */
|
|
48
|
+
export declare function displayWidth(text: string): number;
|
|
49
|
+
/**
|
|
50
|
+
* Wrap `buffer` into rows, locate the caret, and select a window of at most
|
|
51
|
+
* `maxRows` rows that contains it.
|
|
52
|
+
*
|
|
53
|
+
* The window is pinned to the BOTTOM -- where the user is typing -- and pulled up
|
|
54
|
+
* only far enough to keep the caret visible. That is deterministic, which matters
|
|
55
|
+
* because this function holds no state between calls: a scroll offset would have
|
|
56
|
+
* to live in the reducer and would then need its own invalidation rules.
|
|
57
|
+
*/
|
|
58
|
+
export declare function layoutComposer(input: ComposerLayoutInput): ComposerLayout;
|
|
59
|
+
export interface RowSplit {
|
|
60
|
+
readonly before: ComposerSegment[];
|
|
61
|
+
/** The one cell the caret sits on, or `null` when it sits past the last one. */
|
|
62
|
+
readonly at: ComposerSegment | null;
|
|
63
|
+
readonly after: ComposerSegment[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Split one row at a display column so the renderer can draw an inverse caret on
|
|
67
|
+
* exactly one cell.
|
|
68
|
+
*
|
|
69
|
+
* Lives here rather than in the component because it walks the SAME width table
|
|
70
|
+
* `layoutComposer` walked (I-9): a second, subtly different measurement is how
|
|
71
|
+
* the caret ends up drawn on a cell that does not hold the character it is on.
|
|
72
|
+
*/
|
|
73
|
+
export declare function splitRowAtColumn(row: ComposerRow, column: number): RowSplit;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer layout (tui-paste-handling section 5.5) -- pure. No Ink, no React.
|
|
3
|
+
*
|
|
4
|
+
* THE ONE PLACE THAT MAPS A BUFFER INDEX TO A SCREEN CELL (I-9). The composer
|
|
5
|
+
* used to hand its buffer to Ink one `<Text>` per logical line and let Yoga wrap
|
|
6
|
+
* it, which is fine until the draft is taller than the frame: `AppShell` gives
|
|
7
|
+
* the bottom chrome `flexShrink={0}`, so a tall composer does not grow the frame
|
|
8
|
+
* -- Yoga takes the rows out of the transcript and `overflow: hidden` clips them
|
|
9
|
+
* away, while `layout/budget.ts` keeps telling five consumers the composer is
|
|
10
|
+
* three rows tall. Four hundred presses of `Shift+Enter` do it without pasting
|
|
11
|
+
* anything.
|
|
12
|
+
*
|
|
13
|
+
* So the wrap happens HERE, the row count is a number this module returns, and
|
|
14
|
+
* `budget.ts` is told that number (I-8).
|
|
15
|
+
*
|
|
16
|
+
* HARD WRAP BY DISPLAY COLUMN, NEVER `layout/wrap-rows.ts::wrapToRows` (D-15).
|
|
17
|
+
* That function is a PROSE wrapper: it splits on `/\s+/` and rejoins with single
|
|
18
|
+
* spaces, so it destroys indentation and makes the character-index-to-screen-cell
|
|
19
|
+
* mapping the caret depends on impossible to state, let alone compute.
|
|
20
|
+
*/
|
|
21
|
+
import stringWidth from 'string-width';
|
|
22
|
+
import { tokenSpans } from './paste-tokens.js';
|
|
23
|
+
/**
|
|
24
|
+
* Columns a TAB advances to.
|
|
25
|
+
*
|
|
26
|
+
* TABS ARE EXPANDED TO SPACES FOR DISPLAY ONLY -- the buffer keeps the tab, so
|
|
27
|
+
* the model receives exactly what was pasted. The alternative is to emit the raw
|
|
28
|
+
* tab and let the terminal expand it, which puts the terminal's tab stop (8) and
|
|
29
|
+
* this module's column arithmetic in disagreement: the row would overflow its box
|
|
30
|
+
* and the terminal would wrap it a second time, which is the corruption the whole
|
|
31
|
+
* feature exists to remove.
|
|
32
|
+
*/
|
|
33
|
+
const TAB_WIDTH = 4;
|
|
34
|
+
/**
|
|
35
|
+
* Display width of one code point.
|
|
36
|
+
*
|
|
37
|
+
* The ASCII fast path matters: `layoutComposer` runs on every keystroke and a
|
|
38
|
+
* recalled prompt-history entry can be thousands of characters, so one
|
|
39
|
+
* `string-width` call per character would be the cost this package spent
|
|
40
|
+
* `tui-render-performance` removing.
|
|
41
|
+
*/
|
|
42
|
+
function charWidth(ch) {
|
|
43
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
44
|
+
if (code >= 0x20 && code < 0x7f)
|
|
45
|
+
return 1;
|
|
46
|
+
return stringWidth(ch);
|
|
47
|
+
}
|
|
48
|
+
/** Total display width of a run, using the same table the layout used. */
|
|
49
|
+
export function displayWidth(text) {
|
|
50
|
+
let w = 0;
|
|
51
|
+
for (const ch of text)
|
|
52
|
+
w += charWidth(ch);
|
|
53
|
+
return w;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Wrap `buffer` into rows, locate the caret, and select a window of at most
|
|
57
|
+
* `maxRows` rows that contains it.
|
|
58
|
+
*
|
|
59
|
+
* The window is pinned to the BOTTOM -- where the user is typing -- and pulled up
|
|
60
|
+
* only far enough to keep the caret visible. That is deterministic, which matters
|
|
61
|
+
* because this function holds no state between calls: a scroll offset would have
|
|
62
|
+
* to live in the reducer and would then need its own invalidation rules.
|
|
63
|
+
*/
|
|
64
|
+
export function layoutComposer(input) {
|
|
65
|
+
const { buffer, cursor, active } = input;
|
|
66
|
+
const cols = Math.max(1, Math.floor(input.cols));
|
|
67
|
+
const maxRows = input.maxRows >= 1 ? Math.floor(input.maxRows) : 1;
|
|
68
|
+
const spans = tokenSpans(buffer);
|
|
69
|
+
let spanIndex = 0;
|
|
70
|
+
/** Monotonic in `index`, which is why the pointer is safe. */
|
|
71
|
+
const kindAt = (index) => {
|
|
72
|
+
while (spanIndex < spans.length && spans[spanIndex].end <= index)
|
|
73
|
+
spanIndex += 1;
|
|
74
|
+
const span = spans[spanIndex];
|
|
75
|
+
return span && index >= span.start && index < span.end ? 'token' : 'text';
|
|
76
|
+
};
|
|
77
|
+
const rows = [];
|
|
78
|
+
let segments = [];
|
|
79
|
+
let col = 0;
|
|
80
|
+
let cursorRowAbs = -1;
|
|
81
|
+
let cursorCol = 0;
|
|
82
|
+
const breakRow = () => {
|
|
83
|
+
rows.push({ segments });
|
|
84
|
+
segments = [];
|
|
85
|
+
col = 0;
|
|
86
|
+
};
|
|
87
|
+
const append = (text, width, kind) => {
|
|
88
|
+
const last = segments[segments.length - 1];
|
|
89
|
+
if (last && last.kind === kind)
|
|
90
|
+
segments[segments.length - 1] = { text: last.text + text, kind };
|
|
91
|
+
else
|
|
92
|
+
segments.push({ text, kind });
|
|
93
|
+
col += width;
|
|
94
|
+
};
|
|
95
|
+
const ensureRoom = (width) => {
|
|
96
|
+
if (col > 0 && col + width > cols)
|
|
97
|
+
breakRow();
|
|
98
|
+
};
|
|
99
|
+
const length = buffer.length;
|
|
100
|
+
let i = 0;
|
|
101
|
+
while (i <= length) {
|
|
102
|
+
const atCursor = active && i === cursor && cursorRowAbs === -1;
|
|
103
|
+
if (i === length) {
|
|
104
|
+
if (atCursor) {
|
|
105
|
+
ensureRoom(1);
|
|
106
|
+
cursorRowAbs = rows.length;
|
|
107
|
+
cursorCol = col;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
const ch = String.fromCodePoint(buffer.codePointAt(i));
|
|
112
|
+
if (ch === '\n') {
|
|
113
|
+
if (atCursor) {
|
|
114
|
+
cursorRowAbs = rows.length;
|
|
115
|
+
cursorCol = col;
|
|
116
|
+
}
|
|
117
|
+
breakRow();
|
|
118
|
+
i += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const kind = kindAt(i);
|
|
122
|
+
if (ch === '\t') {
|
|
123
|
+
let advance = TAB_WIDTH - (col % TAB_WIDTH);
|
|
124
|
+
if (col > 0 && col + advance > cols) {
|
|
125
|
+
breakRow();
|
|
126
|
+
advance = TAB_WIDTH;
|
|
127
|
+
}
|
|
128
|
+
if (atCursor) {
|
|
129
|
+
cursorRowAbs = rows.length;
|
|
130
|
+
cursorCol = col;
|
|
131
|
+
}
|
|
132
|
+
append(' '.repeat(advance), advance, kind);
|
|
133
|
+
i += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const width = charWidth(ch);
|
|
137
|
+
ensureRoom(width);
|
|
138
|
+
if (atCursor) {
|
|
139
|
+
cursorRowAbs = rows.length;
|
|
140
|
+
cursorCol = col;
|
|
141
|
+
}
|
|
142
|
+
append(ch, width, kind);
|
|
143
|
+
i += ch.length;
|
|
144
|
+
}
|
|
145
|
+
rows.push({ segments });
|
|
146
|
+
const totalRows = rows.length;
|
|
147
|
+
const cap = Math.max(1, Math.min(maxRows, totalRows));
|
|
148
|
+
let start = Math.max(0, totalRows - cap);
|
|
149
|
+
if (cursorRowAbs >= 0) {
|
|
150
|
+
if (cursorRowAbs < start)
|
|
151
|
+
start = cursorRowAbs;
|
|
152
|
+
else if (cursorRowAbs >= start + cap)
|
|
153
|
+
start = Math.min(totalRows - cap, cursorRowAbs - cap + 1);
|
|
154
|
+
}
|
|
155
|
+
const windowed = rows.slice(start, start + cap);
|
|
156
|
+
return {
|
|
157
|
+
rows: windowed,
|
|
158
|
+
cursorRow: cursorRowAbs >= 0 ? cursorRowAbs - start : -1,
|
|
159
|
+
cursorCol,
|
|
160
|
+
totalRows,
|
|
161
|
+
hiddenAbove: start,
|
|
162
|
+
hiddenBelow: totalRows - start - windowed.length,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Split one row at a display column so the renderer can draw an inverse caret on
|
|
167
|
+
* exactly one cell.
|
|
168
|
+
*
|
|
169
|
+
* Lives here rather than in the component because it walks the SAME width table
|
|
170
|
+
* `layoutComposer` walked (I-9): a second, subtly different measurement is how
|
|
171
|
+
* the caret ends up drawn on a cell that does not hold the character it is on.
|
|
172
|
+
*/
|
|
173
|
+
export function splitRowAtColumn(row, column) {
|
|
174
|
+
const before = [];
|
|
175
|
+
const after = [];
|
|
176
|
+
let at = null;
|
|
177
|
+
let x = 0;
|
|
178
|
+
const push = (list, text, kind) => {
|
|
179
|
+
const last = list[list.length - 1];
|
|
180
|
+
if (last && last.kind === kind)
|
|
181
|
+
list[list.length - 1] = { text: last.text + text, kind };
|
|
182
|
+
else
|
|
183
|
+
list.push({ text, kind });
|
|
184
|
+
};
|
|
185
|
+
for (const segment of row.segments) {
|
|
186
|
+
if (at !== null) {
|
|
187
|
+
push(after, segment.text, segment.kind);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
for (const ch of segment.text) {
|
|
191
|
+
if (at === null && x === column) {
|
|
192
|
+
at = { text: ch, kind: segment.kind };
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
push(at === null ? before : after, ch, segment.kind);
|
|
196
|
+
}
|
|
197
|
+
x += charWidth(ch);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { before, at, after };
|
|
201
|
+
}
|
|
@@ -22,13 +22,41 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Extracted from the component so it is unit-testable without mounting Ink.
|
|
24
24
|
*/
|
|
25
|
+
import { type PasteRecord } from './paste-tokens.js';
|
|
25
26
|
export interface EditorState {
|
|
26
27
|
buffer: string;
|
|
27
28
|
cursor: number;
|
|
28
29
|
historyIndex: number | null;
|
|
29
30
|
dismissed: boolean;
|
|
30
31
|
sel: number;
|
|
32
|
+
/**
|
|
33
|
+
* Payloads for the collapse tokens currently in `buffer`
|
|
34
|
+
* (tui-paste-handling section 5.3).
|
|
35
|
+
*
|
|
36
|
+
* Pruned to `referencedIds(buffer)` on EVERY buffer-changing action and
|
|
37
|
+
* emptied by `clear` (I-7). Without both, a payload outlives the draft that
|
|
38
|
+
* referenced it, unbounded, for the whole session.
|
|
39
|
+
*/
|
|
40
|
+
pastes: ReadonlyMap<number, PasteRecord>;
|
|
31
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* One run of an `input` chunk that mixed typing and pasting.
|
|
44
|
+
*
|
|
45
|
+
* THE `id` IS ALLOCATED BY THE CALLER (P1-5). React may run a reducer more than
|
|
46
|
+
* once for a single action -- eager evaluation in `dispatchSetState`, base-queue
|
|
47
|
+
* replay after a bailout, StrictMode -- so a counter read and written inside one
|
|
48
|
+
* would skip ids and produce two computed states differing in the token TEXT.
|
|
49
|
+
* `PromptInput` calls `allocatePasteId()` while building these segments, and the
|
|
50
|
+
* reducer stays a pure function of `(state, action)`.
|
|
51
|
+
*/
|
|
52
|
+
export type InputSegment = {
|
|
53
|
+
readonly kind: 'text';
|
|
54
|
+
readonly text: string;
|
|
55
|
+
} | {
|
|
56
|
+
readonly kind: 'paste';
|
|
57
|
+
readonly text: string;
|
|
58
|
+
readonly id: number;
|
|
59
|
+
};
|
|
32
60
|
/**
|
|
33
61
|
* A `type` and not an `interface` because it is a discriminated union — the one
|
|
34
62
|
* documented exception to this package's interface-first convention.
|
|
@@ -57,6 +85,15 @@ export type EditorAction = {
|
|
|
57
85
|
type: 'dismiss';
|
|
58
86
|
} | {
|
|
59
87
|
type: 'clear';
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* ONE chunk of stdin, already split into ordered runs. The only new action,
|
|
91
|
+
* and it is what keeps the one-dispatch-per-key rule true for a chunk that
|
|
92
|
+
* carried both a paste and the keystrokes beside it.
|
|
93
|
+
*/
|
|
94
|
+
| {
|
|
95
|
+
type: 'input';
|
|
96
|
+
segments: InputSegment[];
|
|
60
97
|
};
|
|
61
98
|
export declare const INITIAL_EDITOR_STATE: EditorState;
|
|
62
99
|
export declare function editorReducer(state: EditorState, action: EditorAction): EditorState;
|
|
@@ -22,12 +22,15 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Extracted from the component so it is unit-testable without mounting Ink.
|
|
24
24
|
*/
|
|
25
|
+
import { expandRangeOverTokens, formatPasteToken, makePasteRecord, referencedIds, shouldCollapse, snapOutOfToken, } from './paste-tokens.js';
|
|
26
|
+
const NO_PASTES = new Map();
|
|
25
27
|
export const INITIAL_EDITOR_STATE = {
|
|
26
28
|
buffer: '',
|
|
27
29
|
cursor: 0,
|
|
28
30
|
historyIndex: null,
|
|
29
31
|
dismissed: false,
|
|
30
32
|
sel: 0,
|
|
33
|
+
pastes: NO_PASTES,
|
|
31
34
|
};
|
|
32
35
|
/**
|
|
33
36
|
* The three flags `resetDraftFlags()` used to clear with three separate
|
|
@@ -40,26 +43,99 @@ export const INITIAL_EDITOR_STATE = {
|
|
|
40
43
|
* (the candidate list is about to be recomputed).
|
|
41
44
|
*/
|
|
42
45
|
const DRAFT_FLAGS = { historyIndex: null, dismissed: false, sel: 0 };
|
|
46
|
+
/**
|
|
47
|
+
* Drop payloads the buffer no longer references (I-7).
|
|
48
|
+
*
|
|
49
|
+
* A WRAPPER, NOT A LINE IN ONE BRANCH (P1-6). v1 of the design pruned only inside
|
|
50
|
+
* `input`, which leaves `replace` (the action behind `Ctrl+U` / `Ctrl+K` /
|
|
51
|
+
* `Ctrl+W` and every `applyEdit`), `backspace` and `recall` able to delete a token
|
|
52
|
+
* without releasing its payload. A rule the body contradicts is worse than no
|
|
53
|
+
* rule, because the next reader trusts the invariant.
|
|
54
|
+
*
|
|
55
|
+
* `moveCursor`, `select` and `dismiss` do not change the buffer and are left
|
|
56
|
+
* alone, which keeps their "return the same object so React bails out" property.
|
|
57
|
+
*/
|
|
58
|
+
function withPrune(next) {
|
|
59
|
+
if (next.pastes.size === 0)
|
|
60
|
+
return next;
|
|
61
|
+
const live = referencedIds(next.buffer);
|
|
62
|
+
// EVERY held id is tested, rather than `live.size === next.pastes.size`.
|
|
63
|
+
// Equal counts do not mean equal sets: `referencedIds` reads the BUFFER, which
|
|
64
|
+
// can name an id this map never held — a user who types `[Pasted text #99 +2
|
|
65
|
+
// lines]` by hand, or edits the number inside a real token. One such id
|
|
66
|
+
// balances out one deleted token and the cheap check then returns early with a
|
|
67
|
+
// payload the buffer no longer references, which is the leak I-7 forbids.
|
|
68
|
+
let stale = false;
|
|
69
|
+
for (const id of next.pastes.keys()) {
|
|
70
|
+
if (!live.has(id)) {
|
|
71
|
+
stale = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!stale)
|
|
76
|
+
return next;
|
|
77
|
+
const kept = new Map();
|
|
78
|
+
for (const [id, record] of next.pastes)
|
|
79
|
+
if (live.has(id))
|
|
80
|
+
kept.set(id, record);
|
|
81
|
+
return { ...next, pastes: kept };
|
|
82
|
+
}
|
|
43
83
|
export function editorReducer(state, action) {
|
|
44
84
|
switch (action.type) {
|
|
45
|
-
case 'insert':
|
|
46
|
-
|
|
85
|
+
case 'insert': {
|
|
86
|
+
// D-8: typing can never land INSIDE a token. A no-op on a buffer with no
|
|
87
|
+
// tokens, which is every buffer until the first collapse.
|
|
88
|
+
const cursor = snapOutOfToken(state.buffer, state.cursor);
|
|
89
|
+
return withPrune({
|
|
47
90
|
...state,
|
|
48
91
|
...DRAFT_FLAGS,
|
|
49
|
-
buffer: state.buffer.slice(0,
|
|
50
|
-
cursor:
|
|
51
|
-
};
|
|
92
|
+
buffer: state.buffer.slice(0, cursor) + action.text + state.buffer.slice(cursor),
|
|
93
|
+
cursor: cursor + action.text.length,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
52
96
|
case 'replace':
|
|
53
|
-
return {
|
|
97
|
+
return withPrune({
|
|
98
|
+
...state,
|
|
99
|
+
...DRAFT_FLAGS,
|
|
100
|
+
buffer: action.buffer,
|
|
101
|
+
cursor: action.cursor,
|
|
102
|
+
});
|
|
54
103
|
case 'backspace': {
|
|
55
104
|
if (state.cursor <= 0)
|
|
56
105
|
return state;
|
|
57
|
-
|
|
106
|
+
// G5: one `Backspace` immediately after `...lines]` removes the whole
|
|
107
|
+
// token and releases its payload, in ONE dispatch.
|
|
108
|
+
const range = expandRangeOverTokens(state.buffer, state.cursor - 1, state.cursor);
|
|
109
|
+
return withPrune({
|
|
110
|
+
...state,
|
|
111
|
+
...DRAFT_FLAGS,
|
|
112
|
+
buffer: state.buffer.slice(0, range.from) + state.buffer.slice(range.to),
|
|
113
|
+
cursor: range.from,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
case 'input': {
|
|
117
|
+
let buffer = state.buffer;
|
|
118
|
+
let cursor = snapOutOfToken(buffer, state.cursor);
|
|
119
|
+
let pastes = null;
|
|
120
|
+
for (const segment of action.segments) {
|
|
121
|
+
const collapse = segment.kind === 'paste' && shouldCollapse(segment.text);
|
|
122
|
+
let text = segment.text;
|
|
123
|
+
if (collapse) {
|
|
124
|
+
const record = makePasteRecord(segment.id, segment.text);
|
|
125
|
+
text = formatPasteToken(record);
|
|
126
|
+
pastes = pastes ?? new Map(state.pastes);
|
|
127
|
+
pastes.set(record.id, record);
|
|
128
|
+
}
|
|
129
|
+
buffer = buffer.slice(0, cursor) + text + buffer.slice(cursor);
|
|
130
|
+
cursor += text.length;
|
|
131
|
+
}
|
|
132
|
+
return withPrune({
|
|
58
133
|
...state,
|
|
59
134
|
...DRAFT_FLAGS,
|
|
60
|
-
buffer
|
|
61
|
-
cursor
|
|
62
|
-
|
|
135
|
+
buffer,
|
|
136
|
+
cursor,
|
|
137
|
+
...(pastes ? { pastes } : {}),
|
|
138
|
+
});
|
|
63
139
|
}
|
|
64
140
|
case 'moveCursor':
|
|
65
141
|
// NO DRAFT-FLAG RESET. Moving the caret is not an edit: clearing
|
|
@@ -74,12 +150,12 @@ export function editorReducer(state, action) {
|
|
|
74
150
|
// an Esc-dismissed popup that reopened when the user stepped through their
|
|
75
151
|
// history would be a behaviour change smuggled in by a refactor whose whole
|
|
76
152
|
// claim is that input semantics are identical (K-7).
|
|
77
|
-
return {
|
|
153
|
+
return withPrune({
|
|
78
154
|
...state,
|
|
79
155
|
buffer: action.buffer,
|
|
80
156
|
cursor: action.cursor,
|
|
81
157
|
historyIndex: action.historyIndex,
|
|
82
|
-
};
|
|
158
|
+
});
|
|
83
159
|
case 'select':
|
|
84
160
|
return state.sel === action.sel ? state : { ...state, sel: action.sel };
|
|
85
161
|
case 'dismiss':
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ServiceCard — one supervised background service in the transcript
|
|
3
|
+
* (background-service-supervision §3.8).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: every glyph comes from `pickGlyphs`. `ui/` is inside
|
|
6
|
+
* `glyphs.test.ts::inScope`, so a literal here fails the scan.
|
|
7
|
+
*
|
|
8
|
+
* NO ANIMATED SPINNER, IN ANY STATE (D-14 / P2-7). That is STRICTER than
|
|
9
|
+
* `single-spinner-while-running` D-1, which suppresses animation only while the
|
|
10
|
+
* activity line is mounted — and a service can sit at `starting` while the agent
|
|
11
|
+
* is IDLE, where D-1's "one owner on screen" argument does not reach because
|
|
12
|
+
* there is no owner on screen. A card that animated only when the agent happened
|
|
13
|
+
* to be idle would be the worst of both. It uses `glyphs.spinnerStill`
|
|
14
|
+
* throughout and still accepts `reducedMotion` for consistency with
|
|
15
|
+
* `EntryView`'s comparator.
|
|
16
|
+
*
|
|
17
|
+
* TWO SHAPES, ONE COMPONENT. `terminal: true` is the one-row record a service's
|
|
18
|
+
* exit appends (D-11); everything else is the live card. They share a component
|
|
19
|
+
* because they share a colour ladder and a status vocabulary, and splitting them
|
|
20
|
+
* would be two places to keep those in step.
|
|
21
|
+
*/
|
|
22
|
+
import React from 'react';
|
|
23
|
+
import type { Theme } from '../theme.js';
|
|
24
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
25
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
26
|
+
import type { ServiceStatus } from '../../proc/types.js';
|
|
27
|
+
export interface ServiceCardProps {
|
|
28
|
+
serviceId: string;
|
|
29
|
+
command: string;
|
|
30
|
+
status: ServiceStatus;
|
|
31
|
+
url?: string;
|
|
32
|
+
port?: number;
|
|
33
|
+
exitCode: number | null;
|
|
34
|
+
startedAt: number;
|
|
35
|
+
readyAt?: number;
|
|
36
|
+
endedAt?: number;
|
|
37
|
+
rows: readonly string[];
|
|
38
|
+
/** The one-row record a terminal transition appends. */
|
|
39
|
+
terminal?: boolean;
|
|
40
|
+
killIncomplete?: boolean;
|
|
41
|
+
expanded?: boolean;
|
|
42
|
+
/** Accepted for comparator consistency; this card never animates (D-14). */
|
|
43
|
+
reducedMotion?: boolean;
|
|
44
|
+
theme: Theme;
|
|
45
|
+
caps: TermCapabilities;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The card's colour.
|
|
49
|
+
*
|
|
50
|
+
* `running` is `toolRunning` rather than the accent: it is alive and listening
|
|
51
|
+
* on nothing, which is a real third state and not a lesser `ready`. A clean
|
|
52
|
+
* `exited(0)` is muted, because a command that finished on purpose is not a
|
|
53
|
+
* failure.
|
|
54
|
+
*/
|
|
55
|
+
export declare function serviceCardColor(props: Pick<ServiceCardProps, 'status' | 'exitCode'>, theme: Theme): string | undefined;
|
|
56
|
+
/** The status glyph, by the same ladder as the colour. */
|
|
57
|
+
export declare function serviceGlyph(props: Pick<ServiceCardProps, 'status' | 'exitCode'>, glyphs: ReturnType<typeof pickGlyphs>): string;
|
|
58
|
+
declare function ServiceCardImpl(props: ServiceCardProps): React.ReactElement;
|
|
59
|
+
/**
|
|
60
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
61
|
+
* `mapEntry` already preserves object identity for untouched entries, so the
|
|
62
|
+
* props below are stable references on a settled card, and `theme` / `caps` are
|
|
63
|
+
* `useMemo`d in `App.tsx` (I-L2-1).
|
|
64
|
+
*/
|
|
65
|
+
export declare const ServiceCard: React.MemoExoticComponent<typeof ServiceCardImpl>;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ServiceCard — one supervised background service in the transcript
|
|
4
|
+
* (background-service-supervision §3.8).
|
|
5
|
+
*
|
|
6
|
+
* ASCII ONLY: every glyph comes from `pickGlyphs`. `ui/` is inside
|
|
7
|
+
* `glyphs.test.ts::inScope`, so a literal here fails the scan.
|
|
8
|
+
*
|
|
9
|
+
* NO ANIMATED SPINNER, IN ANY STATE (D-14 / P2-7). That is STRICTER than
|
|
10
|
+
* `single-spinner-while-running` D-1, which suppresses animation only while the
|
|
11
|
+
* activity line is mounted — and a service can sit at `starting` while the agent
|
|
12
|
+
* is IDLE, where D-1's "one owner on screen" argument does not reach because
|
|
13
|
+
* there is no owner on screen. A card that animated only when the agent happened
|
|
14
|
+
* to be idle would be the worst of both. It uses `glyphs.spinnerStill`
|
|
15
|
+
* throughout and still accepts `reducedMotion` for consistency with
|
|
16
|
+
* `EntryView`'s comparator.
|
|
17
|
+
*
|
|
18
|
+
* TWO SHAPES, ONE COMPONENT. `terminal: true` is the one-row record a service's
|
|
19
|
+
* exit appends (D-11); everything else is the live card. They share a component
|
|
20
|
+
* because they share a colour ladder and a status vocabulary, and splitting them
|
|
21
|
+
* would be two places to keep those in step.
|
|
22
|
+
*/
|
|
23
|
+
import React from 'react';
|
|
24
|
+
import { Box, Text } from 'ink';
|
|
25
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
26
|
+
import { railBorderProps } from '../layout/Gutter.js';
|
|
27
|
+
import { formatDuration } from '../../agent/usage.js';
|
|
28
|
+
/** Tail rows the collapsed card draws. Mirrored as a NUMBER in `virtual-window.ts`. */
|
|
29
|
+
const COLLAPSED_TAIL_ROWS = 2;
|
|
30
|
+
/** Tail rows the expanded card draws — the snapshot's own cap. */
|
|
31
|
+
const EXPANDED_TAIL_ROWS = 8;
|
|
32
|
+
/**
|
|
33
|
+
* The card's colour.
|
|
34
|
+
*
|
|
35
|
+
* `running` is `toolRunning` rather than the accent: it is alive and listening
|
|
36
|
+
* on nothing, which is a real third state and not a lesser `ready`. A clean
|
|
37
|
+
* `exited(0)` is muted, because a command that finished on purpose is not a
|
|
38
|
+
* failure.
|
|
39
|
+
*/
|
|
40
|
+
export function serviceCardColor(props, theme) {
|
|
41
|
+
switch (props.status) {
|
|
42
|
+
case 'starting':
|
|
43
|
+
return theme.muted;
|
|
44
|
+
case 'ready':
|
|
45
|
+
return theme.accent;
|
|
46
|
+
case 'running':
|
|
47
|
+
return theme.toolRunning;
|
|
48
|
+
case 'failed':
|
|
49
|
+
return theme.toolError;
|
|
50
|
+
case 'exited':
|
|
51
|
+
return props.exitCode === 0 ? theme.muted : theme.toolError;
|
|
52
|
+
case 'stopped':
|
|
53
|
+
return theme.muted;
|
|
54
|
+
default:
|
|
55
|
+
return theme.muted;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The status glyph, by the same ladder as the colour. */
|
|
59
|
+
export function serviceGlyph(props, glyphs) {
|
|
60
|
+
switch (props.status) {
|
|
61
|
+
case 'starting':
|
|
62
|
+
return glyphs.toolPending;
|
|
63
|
+
case 'ready':
|
|
64
|
+
return glyphs.toolDone;
|
|
65
|
+
case 'running':
|
|
66
|
+
return glyphs.toolRunning;
|
|
67
|
+
case 'failed':
|
|
68
|
+
return glyphs.toolError;
|
|
69
|
+
case 'exited':
|
|
70
|
+
return props.exitCode === 0 ? glyphs.toolDone : glyphs.toolError;
|
|
71
|
+
case 'stopped':
|
|
72
|
+
return glyphs.toolPending;
|
|
73
|
+
default:
|
|
74
|
+
return glyphs.toolPending;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** The right-hand status clause: what happened, and how long it took. */
|
|
78
|
+
function statusClause(props, dot) {
|
|
79
|
+
const { status, exitCode, startedAt, readyAt, endedAt } = props;
|
|
80
|
+
if (status === 'ready') {
|
|
81
|
+
const took = readyAt === undefined ? '' : `${dot}${formatDuration(readyAt - startedAt)}`;
|
|
82
|
+
return `ready${took}`;
|
|
83
|
+
}
|
|
84
|
+
if (status === 'running')
|
|
85
|
+
return 'running';
|
|
86
|
+
if (status === 'starting')
|
|
87
|
+
return 'starting';
|
|
88
|
+
const lived = endedAt === undefined ? '' : `${dot}${formatDuration(endedAt - startedAt)}`;
|
|
89
|
+
if (status === 'stopped')
|
|
90
|
+
return `stopped${lived}`;
|
|
91
|
+
if (status === 'failed')
|
|
92
|
+
return `failed${lived}`;
|
|
93
|
+
return `exited${exitCode === null ? '' : ` code ${exitCode}`}${lived}`;
|
|
94
|
+
}
|
|
95
|
+
function ServiceCardImpl(props) {
|
|
96
|
+
const { serviceId, command, url, rows, expanded, terminal, theme, caps } = props;
|
|
97
|
+
const glyphs = pickGlyphs(caps);
|
|
98
|
+
const color = serviceCardColor(props, theme);
|
|
99
|
+
const dot = ` ${glyphs.midDot} `;
|
|
100
|
+
const clause = statusClause(props, dot);
|
|
101
|
+
// THE TERMINAL RECORD IS ONE ROW, ALWAYS. It is a SECOND event at a SECOND
|
|
102
|
+
// time — the only thing `<Static>` permits once the live card is printed — so
|
|
103
|
+
// it names what happened and nothing else (D-11).
|
|
104
|
+
if (terminal) {
|
|
105
|
+
return (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: color, wrap: "truncate", children: ["service ", serviceId, " ", clause, props.killIncomplete ? ' (may have left a detached child)' : ''] }) }));
|
|
106
|
+
}
|
|
107
|
+
const shown = rows.slice(-(expanded ? EXPANDED_TAIL_ROWS : COLLAPSED_TAIL_ROWS));
|
|
108
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: theme.accent, bold: true, children: ["service ", serviceId] }), _jsxs(Text, { color: theme.muted, wrap: "truncate", children: [' ', command] }), _jsxs(Text, { color: color, wrap: "truncate", children: [dot, clause] })] }), url !== undefined && (_jsxs(Box, { flexDirection: "row", flexShrink: 0, paddingLeft: 1, children: [_jsx(Text, { color: theme.primary, children: url }), _jsxs(Text, { color: theme.muted, children: [dot, "ctrl+c stop", dot, "ctrl+o log"] })] })), shown.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: shown.map((line, index) => (_jsx(Text, { color: theme.muted, wrap: "truncate", children: line }, index))) }))] }));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
112
|
+
* `mapEntry` already preserves object identity for untouched entries, so the
|
|
113
|
+
* props below are stable references on a settled card, and `theme` / `caps` are
|
|
114
|
+
* `useMemo`d in `App.tsx` (I-L2-1).
|
|
115
|
+
*/
|
|
116
|
+
export const ServiceCard = React.memo(ServiceCardImpl);
|