@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,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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CompactionCard — one context compaction in the transcript
|
|
3
|
+
* (context-auto-compaction §6.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: every glyph comes from `pickGlyphs`, INCLUDING THE ARROW. The mock
|
|
6
|
+
* in the design shows `112 -> 9 messages`; that arrow is `glyphs.arrowRight`, the
|
|
7
|
+
* one `StatusBar` already uses, not a literal — `ui/` is inside
|
|
8
|
+
* `glyphs.test.ts::inScope`, so a literal here fails the scan (P2-7).
|
|
9
|
+
*
|
|
10
|
+
* EXPANDABLE, UNLIKE `FastCard`. A critique is already clamped to
|
|
11
|
+
* `fast.reviewMaxChars` before it reaches its card, so there is nothing hidden
|
|
12
|
+
* for Ctrl+O to reveal; a summary is up to `summaryMaxChars` and there genuinely
|
|
13
|
+
* is more to see. That is why `App`'s Ctrl+O predicate had to learn about this
|
|
14
|
+
* kind — without it the card renders a hint that does nothing and expands an
|
|
15
|
+
* older tool card instead (C-15 / P1-8 / AC-21).
|
|
16
|
+
*
|
|
17
|
+
* EVERY STRING DESCRIBES THE HISTORY, NEVER THE SCREEN (§6.4 / P2-9). `/clear`
|
|
18
|
+
* empties the transcript view but not `messages`, so this card can legitimately
|
|
19
|
+
* report "112 -> 9 messages" with nine visible rows above it. Nothing here says
|
|
20
|
+
* "above" or "earlier in this transcript".
|
|
21
|
+
*/
|
|
22
|
+
import React from 'react';
|
|
23
|
+
import type { Theme } from '../theme.js';
|
|
24
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
25
|
+
import type { CompactionMode, CompactionUiTrigger } from '../../compaction/types.js';
|
|
26
|
+
export interface CompactionCardProps {
|
|
27
|
+
index: number;
|
|
28
|
+
trigger: CompactionUiTrigger;
|
|
29
|
+
mode: CompactionMode;
|
|
30
|
+
applied: boolean;
|
|
31
|
+
reason?: string;
|
|
32
|
+
messagesBefore: number;
|
|
33
|
+
messagesAfter: number;
|
|
34
|
+
tokensBefore: number;
|
|
35
|
+
tokensAfter: number;
|
|
36
|
+
summary?: string;
|
|
37
|
+
model: string;
|
|
38
|
+
durationMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Wall-clock since the card opened, LIVE ONLY
|
|
41
|
+
* (context-auto-compaction-hardening §3.6 / W5).
|
|
42
|
+
*
|
|
43
|
+
* Rendered in the position `durationMs` occupies once settled, so the number
|
|
44
|
+
* does not move when the card settles. Only the single live card receives it,
|
|
45
|
+
* so exactly one memoized card re-renders per second.
|
|
46
|
+
*/
|
|
47
|
+
elapsedMs?: number;
|
|
48
|
+
/** Set when tail relief clipped the retained turns (W2). */
|
|
49
|
+
tailRelief?: {
|
|
50
|
+
messages: number;
|
|
51
|
+
charsRemoved: number;
|
|
52
|
+
};
|
|
53
|
+
live: boolean;
|
|
54
|
+
expanded?: boolean;
|
|
55
|
+
reducedMotion?: boolean;
|
|
56
|
+
theme: Theme;
|
|
57
|
+
caps: TermCapabilities;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The card's colour.
|
|
61
|
+
*
|
|
62
|
+
* `truncated` is a WARNING rather than the accent, because it is a real and
|
|
63
|
+
* bounded data loss the user is entitled to notice; `applied: false` is an error
|
|
64
|
+
* because the run is still at the occupancy that triggered it.
|
|
65
|
+
*/
|
|
66
|
+
export declare function compactionCardColor(props: Pick<CompactionCardProps, 'live' | 'applied' | 'mode'>, theme: Theme): string | undefined;
|
|
67
|
+
declare function CompactionCardImpl(props: CompactionCardProps): React.ReactElement;
|
|
68
|
+
/**
|
|
69
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
70
|
+
* `mapEntry` already preserves object identity for untouched entries, so the
|
|
71
|
+
* props below are stable references on a settled card, and `theme` / `caps` are
|
|
72
|
+
* `useMemo`d in `App.tsx` (I-L2-1).
|
|
73
|
+
*/
|
|
74
|
+
export declare const CompactionCard: React.MemoExoticComponent<typeof CompactionCardImpl>;
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* CompactionCard — one context compaction in the transcript
|
|
4
|
+
* (context-auto-compaction §6.3).
|
|
5
|
+
*
|
|
6
|
+
* ASCII ONLY: every glyph comes from `pickGlyphs`, INCLUDING THE ARROW. The mock
|
|
7
|
+
* in the design shows `112 -> 9 messages`; that arrow is `glyphs.arrowRight`, the
|
|
8
|
+
* one `StatusBar` already uses, not a literal — `ui/` is inside
|
|
9
|
+
* `glyphs.test.ts::inScope`, so a literal here fails the scan (P2-7).
|
|
10
|
+
*
|
|
11
|
+
* EXPANDABLE, UNLIKE `FastCard`. A critique is already clamped to
|
|
12
|
+
* `fast.reviewMaxChars` before it reaches its card, so there is nothing hidden
|
|
13
|
+
* for Ctrl+O to reveal; a summary is up to `summaryMaxChars` and there genuinely
|
|
14
|
+
* is more to see. That is why `App`'s Ctrl+O predicate had to learn about this
|
|
15
|
+
* kind — without it the card renders a hint that does nothing and expands an
|
|
16
|
+
* older tool card instead (C-15 / P1-8 / AC-21).
|
|
17
|
+
*
|
|
18
|
+
* EVERY STRING DESCRIBES THE HISTORY, NEVER THE SCREEN (§6.4 / P2-9). `/clear`
|
|
19
|
+
* empties the transcript view but not `messages`, so this card can legitimately
|
|
20
|
+
* report "112 -> 9 messages" with nine visible rows above it. Nothing here says
|
|
21
|
+
* "above" or "earlier in this transcript".
|
|
22
|
+
*/
|
|
23
|
+
import React from 'react';
|
|
24
|
+
import { Box, Text } from 'ink';
|
|
25
|
+
import Spinner from 'ink-spinner';
|
|
26
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
27
|
+
import { railBorderProps } from '../layout/Gutter.js';
|
|
28
|
+
import { formatDuration, formatTokens } from '../../agent/usage.js';
|
|
29
|
+
import { COMPACTION_LIMITS } from '../../compaction/limits.js';
|
|
30
|
+
/**
|
|
31
|
+
* The card's colour.
|
|
32
|
+
*
|
|
33
|
+
* `truncated` is a WARNING rather than the accent, because it is a real and
|
|
34
|
+
* bounded data loss the user is entitled to notice; `applied: false` is an error
|
|
35
|
+
* because the run is still at the occupancy that triggered it.
|
|
36
|
+
*/
|
|
37
|
+
export function compactionCardColor(props, theme) {
|
|
38
|
+
if (props.live)
|
|
39
|
+
return theme.toolRunning;
|
|
40
|
+
if (!props.applied)
|
|
41
|
+
return theme.toolError;
|
|
42
|
+
if (props.mode === 'truncated' || props.mode === 'relieved')
|
|
43
|
+
return theme.noticeWarn;
|
|
44
|
+
return theme.accent;
|
|
45
|
+
}
|
|
46
|
+
/** The one-line summary of what the compaction did, or why it did not. */
|
|
47
|
+
function headline(props, arrow, dot) {
|
|
48
|
+
const { index, live } = props;
|
|
49
|
+
// The model is `''` when no summarizer resolves (no key for the provider), and
|
|
50
|
+
// the separator has to go with it — a headline ending in a dangling ` - `
|
|
51
|
+
// reads as a rendering fault rather than as the missing model it is.
|
|
52
|
+
if (live) {
|
|
53
|
+
// ELAPSED IN THE SAME POSITION `durationMs` TAKES ONCE SETTLED, so the number
|
|
54
|
+
// does not jump across the line when the card settles. Between second 0 and
|
|
55
|
+
// second 45 this was the only changing information on the card, and there was
|
|
56
|
+
// none.
|
|
57
|
+
const elapsed = props.elapsedMs === undefined ? '' : `${dot}${formatDuration(props.elapsedMs)}`;
|
|
58
|
+
return props.model
|
|
59
|
+
? `compacting context #${index}${dot}${props.model}${elapsed}`
|
|
60
|
+
: `compacting context #${index}${elapsed}`;
|
|
61
|
+
}
|
|
62
|
+
if (!props.applied) {
|
|
63
|
+
const why = props.reason ? `: ${props.reason}` : '';
|
|
64
|
+
return `context not compacted #${index}${why}`;
|
|
65
|
+
}
|
|
66
|
+
const messages = `${props.messagesBefore} ${arrow} ${props.messagesAfter} messages`;
|
|
67
|
+
const tokens = `${formatTokens(props.tokensBefore)} ${arrow} ${formatTokens(props.tokensAfter)} tokens`;
|
|
68
|
+
const duration = props.durationMs === undefined ? '' : `${dot}${formatDuration(props.durationMs)}`;
|
|
69
|
+
return `context compacted${dot}${messages}${dot}${tokens}${duration}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The muted second line: how it was done, and with what.
|
|
73
|
+
*
|
|
74
|
+
* THE LIVE BRANCH NAMES THE ESCAPE (§3.6 / E-10). `AgentController.abort()` has
|
|
75
|
+
* always called `this.compaction?.abort()` before `agent.abort()`, so Esc has
|
|
76
|
+
* always cancelled an in-flight summarization - and no surface has ever said so
|
|
77
|
+
* during the forty-five seconds it matters. The hint goes with the model for the
|
|
78
|
+
* same reason the separator does: with no summarizer resolved there is no call to
|
|
79
|
+
* cancel.
|
|
80
|
+
*
|
|
81
|
+
* THE `relieved` BRANCH COMES BEFORE `truncated`, AND IT EXISTS BECAUSE THE
|
|
82
|
+
* `summarized` LINE WOULD OTHERWISE LIE. Relief drops no messages, so the
|
|
83
|
+
* fall-through would render "summarized 0 messages with <model>" for a
|
|
84
|
+
* compaction in which no model was called - in the rare, alarming case the user
|
|
85
|
+
* most needs to read correctly.
|
|
86
|
+
*/
|
|
87
|
+
function detailLine(props) {
|
|
88
|
+
if (props.live) {
|
|
89
|
+
if (!props.model)
|
|
90
|
+
return '';
|
|
91
|
+
return `summarizing with ${props.model} - esc to cancel`;
|
|
92
|
+
}
|
|
93
|
+
if (!props.applied)
|
|
94
|
+
return props.mode === 'none' && !props.reason ? 'nothing was changed' : '';
|
|
95
|
+
const dropped = Math.max(0, props.messagesBefore - props.messagesAfter);
|
|
96
|
+
if (props.mode === 'relieved') {
|
|
97
|
+
const n = props.tailRelief?.messages ?? 0;
|
|
98
|
+
return `clipped ${n} tool ${n === 1 ? 'result' : 'results'} in the retained turns - nothing could be dropped`;
|
|
99
|
+
}
|
|
100
|
+
if (props.mode === 'truncated') {
|
|
101
|
+
return `dropped ${dropped} messages WITHOUT a summary - summarization failed`;
|
|
102
|
+
}
|
|
103
|
+
const via = props.trigger === 'overflow' ? ' after a context overflow' : '';
|
|
104
|
+
return `summarized ${dropped} messages with ${props.model}${via}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The relief disclosure, on a settled card that ALSO summarized.
|
|
108
|
+
*
|
|
109
|
+
* A BOUNDED, ANNOUNCED DATA LOSS THE USER IS ENTITLED TO NOTICE, in the same
|
|
110
|
+
* voice §6.4's honesty rules use for `truncated`. Suppressed on the `relieved`
|
|
111
|
+
* card, where `detailLine` has already said it and a second line would be the
|
|
112
|
+
* same sentence twice.
|
|
113
|
+
*/
|
|
114
|
+
function reliefLine(props) {
|
|
115
|
+
if (props.live || !props.applied || !props.tailRelief || props.mode === 'relieved')
|
|
116
|
+
return '';
|
|
117
|
+
const n = props.tailRelief.messages;
|
|
118
|
+
return (`clipped ${n} tool ${n === 1 ? 'result' : 'results'} in the retained turns ` +
|
|
119
|
+
`(${formatTokens(props.tailRelief.charsRemoved)} characters) - ` +
|
|
120
|
+
'the recent turns alone exceeded the window');
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Split the summary into rows.
|
|
124
|
+
*
|
|
125
|
+
* A ROW budget, not a character one: `summaryMaxChars` already bounded the text,
|
|
126
|
+
* and this only decides how much of it the COLLAPSED card shows. Ink wraps the
|
|
127
|
+
* rest.
|
|
128
|
+
*/
|
|
129
|
+
function summaryRows(text, expanded) {
|
|
130
|
+
const lines = text.split('\n').filter((l) => l.trim().length > 0);
|
|
131
|
+
return expanded ? lines : lines.slice(0, COMPACTION_LIMITS.cardTextRows);
|
|
132
|
+
}
|
|
133
|
+
function CompactionCardImpl(props) {
|
|
134
|
+
const { live, summary, expanded, reducedMotion, theme, caps } = props;
|
|
135
|
+
const glyphs = pickGlyphs(caps);
|
|
136
|
+
const color = compactionCardColor(props, theme);
|
|
137
|
+
const dot = ` ${glyphs.midDot} `;
|
|
138
|
+
const head = headline(props, glyphs.arrowRight, dot);
|
|
139
|
+
const detail = detailLine(props);
|
|
140
|
+
const relief = reliefLine(props);
|
|
141
|
+
const rows = summary ? summaryRows(summary, expanded === true) : [];
|
|
142
|
+
const totalRows = summary ? summary.split('\n').filter((l) => l.trim().length > 0).length : 0;
|
|
143
|
+
const hidden = Math.max(0, totalRows - rows.length);
|
|
144
|
+
// COLOUR TRACKS `live`, MOTION TRACKS `animate` — one headline, two separate
|
|
145
|
+
// decisions, and the split is the one `FastCard` records at length: `App`
|
|
146
|
+
// widens `reducedMotion` to "this frame does not animate here" for the whole of
|
|
147
|
+
// every run, and folding the two together would grey out every live card for
|
|
148
|
+
// the whole run, losing the status colour along with the motion.
|
|
149
|
+
const animate = live && reducedMotion !== true && caps.unicode;
|
|
150
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "compact" }), _jsxs(Text, { color: live ? color : theme.muted, wrap: "truncate", children: [' ', animate ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), ' '] })) : live ? (`${glyphs.spinnerStill} `) : (''), head] })] }), detail.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: color, wrap: "truncate", children: detail }) })), relief.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: theme.noticeWarn, children: relief }) })), rows.length > 0 && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [rows.map((line, index) => (_jsx(Text, { color: theme.primary, children: line }, index))), hidden > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " +", hidden, " more lines (ctrl+o to expand)"] })), hidden === 0 && expanded === true && totalRows > COMPACTION_LIMITS.cardTextRows && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " ctrl+o to collapse"] }))] }))] }));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
154
|
+
* `mapEntry` already preserves object identity for untouched entries, so the
|
|
155
|
+
* props below are stable references on a settled card, and `theme` / `caps` are
|
|
156
|
+
* `useMemo`d in `App.tsx` (I-L2-1).
|
|
157
|
+
*/
|
|
158
|
+
export const CompactionCard = React.memo(CompactionCardImpl);
|