@aragon-agent/cli 0.6.11 → 0.6.13
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 +10 -1
- package/README.md +97 -20
- package/dist/agent/controller.d.ts +38 -3
- package/dist/agent/controller.js +151 -60
- package/dist/agent/model-profile-settings.d.ts +31 -0
- package/dist/agent/model-profile-settings.js +199 -0
- package/dist/agent/queued-messages.d.ts +20 -0
- package/dist/agent/queued-messages.js +46 -0
- package/dist/agent/reducer.d.ts +21 -2
- package/dist/agent/reducer.js +65 -37
- package/dist/cli.js +15 -1
- package/dist/commands/builtins.js +83 -42
- package/dist/commands/queue-command.d.ts +3 -0
- package/dist/commands/queue-command.js +5 -0
- package/dist/commands/registry.d.ts +3 -0
- package/dist/compaction/child.d.ts +4 -2
- package/dist/compaction/child.js +4 -1
- package/dist/compaction/compactor.d.ts +7 -2
- package/dist/compaction/compactor.js +13 -11
- package/dist/compaction/context-command.js +5 -0
- package/dist/compaction/wiring.d.ts +4 -3
- package/dist/compaction/wiring.js +6 -1
- package/dist/config/cli-commands.d.ts +2 -0
- package/dist/config/cli-commands.js +16 -4
- package/dist/config/load.d.ts +2 -1
- package/dist/config/load.js +15 -8
- package/dist/config/model-profile-resolution.d.ts +20 -0
- package/dist/config/model-profile-resolution.js +81 -0
- package/dist/config/model-profile-store.d.ts +67 -0
- package/dist/config/model-profile-store.js +153 -0
- package/dist/config/model-profiles.d.ts +44 -0
- package/dist/config/model-profiles.js +121 -0
- package/dist/config/schema.d.ts +9 -0
- package/dist/config/store.d.ts +11 -1
- package/dist/config/store.js +43 -27
- package/dist/fast/resolve.d.ts +2 -1
- package/dist/fast/resolve.js +6 -4
- package/dist/fast/wiring.d.ts +5 -3
- package/dist/fast/wiring.js +8 -4
- package/dist/input/enter-sequences.d.ts +11 -10
- package/dist/input/enter-sequences.js +29 -27
- package/dist/input/stdin-filter.js +93 -28
- package/dist/logging/install.d.ts +3 -0
- package/dist/logging/install.js +4 -2
- package/dist/logging/secret-registry.d.ts +2 -0
- package/dist/logging/secret-registry.js +18 -0
- package/dist/session/persist.d.ts +6 -7
- package/dist/session/persist.js +6 -9
- package/dist/session/store.d.ts +4 -1
- package/dist/session/validate-session.d.ts +3 -0
- package/dist/session/validate-session.js +207 -0
- package/dist/team/runtime.d.ts +2 -1
- package/dist/team/subagent.d.ts +3 -1
- package/dist/team/subagent.js +3 -1
- package/dist/ui/ActivityLine.d.ts +6 -70
- package/dist/ui/ActivityLine.js +12 -34
- package/dist/ui/App.d.ts +1 -0
- package/dist/ui/App.js +161 -226
- package/dist/ui/BottomStatusRow.d.ts +6 -86
- package/dist/ui/BottomStatusRow.js +11 -26
- package/dist/ui/Composer.d.ts +18 -84
- package/dist/ui/Composer.js +9 -100
- package/dist/ui/PromptInput.d.ts +6 -2
- package/dist/ui/PromptInput.js +91 -105
- package/dist/ui/QueuePanel.d.ts +8 -0
- package/dist/ui/QueuePanel.js +9 -0
- package/dist/ui/QueueStatusRow.d.ts +24 -0
- package/dist/ui/QueueStatusRow.js +51 -0
- package/dist/ui/StatusBar.d.ts +15 -149
- package/dist/ui/StatusBar.js +27 -112
- package/dist/ui/clipboard-task.d.ts +29 -0
- package/dist/ui/clipboard-task.js +66 -0
- package/dist/ui/clipboard.d.ts +29 -50
- package/dist/ui/clipboard.js +141 -75
- package/dist/ui/composer-input.d.ts +28 -0
- package/dist/ui/composer-input.js +46 -0
- package/dist/ui/composer-limits.d.ts +3 -4
- package/dist/ui/composer-limits.js +4 -9
- package/dist/ui/composer-rows.d.ts +1 -1
- package/dist/ui/composer-rows.js +24 -137
- package/dist/ui/editor-navigation.d.ts +45 -0
- package/dist/ui/editor-navigation.js +85 -0
- package/dist/ui/editor-reducer.d.ts +9 -0
- package/dist/ui/editor-reducer.js +31 -15
- package/dist/ui/enter-frames.d.ts +15 -31
- package/dist/ui/enter-frames.js +14 -33
- package/dist/ui/interaction-copy.d.ts +77 -0
- package/dist/ui/interaction-copy.js +40 -0
- package/dist/ui/interaction-hints.d.ts +18 -0
- package/dist/ui/interaction-hints.js +92 -0
- package/dist/ui/layout/budget.js +1 -1
- package/dist/ui/layout/queue-layout.d.ts +19 -0
- package/dist/ui/layout/queue-layout.js +26 -0
- package/dist/ui/layout/status-layout.d.ts +55 -0
- package/dist/ui/layout/status-layout.js +109 -0
- package/dist/ui/layout/todo-layout.d.ts +1 -0
- package/dist/ui/layout/todo-layout.js +1 -1
- package/dist/ui/overlays/HelpOverlay.js +11 -3
- package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
- package/dist/ui/overlays/ModelProfileEditor.js +103 -0
- package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
- package/dist/ui/overlays/ModelProfilePicker.js +167 -0
- package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
- package/dist/ui/overlays/QueueOverlay.js +79 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
- package/dist/ui/overlays/SettingsScreen.js +347 -3
- package/dist/ui/overlays/model-profile-state.d.ts +74 -0
- package/dist/ui/overlays/model-profile-state.js +119 -0
- package/dist/ui/run-status-row.d.ts +7 -19
- package/dist/ui/run-status-row.js +2 -18
- package/dist/ui/selection/selection-controller.d.ts +2 -2
- package/dist/ui/selection/selection-controller.js +58 -20
- package/dist/ui/transcript-text.js +20 -12
- package/dist/ui/use-wheel-routing.js +2 -1
- package/package.json +2 -2
|
@@ -22,10 +22,9 @@ export declare const USER_ENTRY_MAX_ROWS = 40;
|
|
|
22
22
|
/**
|
|
23
23
|
* Ceiling on the composer's DRAFT rows at a given terminal height.
|
|
24
24
|
*
|
|
25
|
-
* Non-decreasing in `terminalRows
|
|
26
|
-
*
|
|
27
|
-
* that stage revertible on its own.
|
|
25
|
+
* Non-decreasing in `terminalRows`, with one quarter of the terminal available
|
|
26
|
+
* to the draft and an absolute six-row ceiling shared by rendering and budgets.
|
|
28
27
|
*/
|
|
29
28
|
export declare function draftMaxRows(terminalRows: number): number;
|
|
30
|
-
/**
|
|
29
|
+
/** Compatibility export for the shared hint visibility threshold. */
|
|
31
30
|
export declare const DRAFT_MAX_ROWS_HINT_TIER = 20;
|
|
@@ -33,18 +33,13 @@ const DRAFT_TIER_MIN_ROWS = 20;
|
|
|
33
33
|
/**
|
|
34
34
|
* Ceiling on the composer's DRAFT rows at a given terminal height.
|
|
35
35
|
*
|
|
36
|
-
* Non-decreasing in `terminalRows
|
|
37
|
-
*
|
|
38
|
-
* that stage revertible on its own.
|
|
36
|
+
* Non-decreasing in `terminalRows`, with one quarter of the terminal available
|
|
37
|
+
* to the draft and an absolute six-row ceiling shared by rendering and budgets.
|
|
39
38
|
*/
|
|
40
39
|
export function draftMaxRows(terminalRows) {
|
|
41
40
|
if (!Number.isFinite(terminalRows))
|
|
42
41
|
return 3;
|
|
43
|
-
|
|
44
|
-
return 10;
|
|
45
|
-
if (terminalRows >= DRAFT_TIER_MIN_ROWS)
|
|
46
|
-
return 6;
|
|
47
|
-
return 3;
|
|
42
|
+
return Math.max(1, Math.min(6, Math.floor(terminalRows / 4)));
|
|
48
43
|
}
|
|
49
|
-
/**
|
|
44
|
+
/** Compatibility export for the shared hint visibility threshold. */
|
|
50
45
|
export const DRAFT_MAX_ROWS_HINT_TIER = DRAFT_TIER_MIN_ROWS;
|
|
@@ -44,7 +44,7 @@ export interface ComposerLayoutInput {
|
|
|
44
44
|
readonly maxRows: number;
|
|
45
45
|
readonly active: boolean;
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
47
|
+
/** Width of a displayed run, measured by complete graphemes. */
|
|
48
48
|
export declare function displayWidth(text: string): number;
|
|
49
49
|
/**
|
|
50
50
|
* Wrap `buffer` into rows, locate the caret, and select a window of at most
|
package/dist/ui/composer-rows.js
CHANGED
|
@@ -18,65 +18,11 @@
|
|
|
18
18
|
* spaces, so it destroys indentation and makes the character-index-to-screen-cell
|
|
19
19
|
* mapping the caret depends on impossible to state, let alone compute.
|
|
20
20
|
*/
|
|
21
|
-
import
|
|
21
|
+
import { buildEditorVisualMap, graphemes, graphemeWidth } from './editor-navigation.js';
|
|
22
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. */
|
|
23
|
+
/** Width of a displayed run, measured by complete graphemes. */
|
|
49
24
|
export function displayWidth(text) {
|
|
50
|
-
|
|
51
|
-
for (const ch of text)
|
|
52
|
-
w += charWidth(ch);
|
|
53
|
-
return w;
|
|
54
|
-
}
|
|
55
|
-
/** Normalize only the visual projection; the editor keeps its UTF-16 index. */
|
|
56
|
-
function visualCursor(buffer, cursor) {
|
|
57
|
-
let index = Math.max(0, Math.min(buffer.length, cursor));
|
|
58
|
-
const code = buffer.charCodeAt(index);
|
|
59
|
-
const previous = buffer.charCodeAt(index - 1);
|
|
60
|
-
if (code >= 0xdc00 && code <= 0xdfff && previous >= 0xd800 && previous <= 0xdbff)
|
|
61
|
-
index -= 1;
|
|
62
|
-
if (index === buffer.length || buffer[index] === '\n')
|
|
63
|
-
return index;
|
|
64
|
-
const ch = String.fromCodePoint(buffer.codePointAt(index));
|
|
65
|
-
if (ch === '\t' || charWidth(ch) > 0)
|
|
66
|
-
return index;
|
|
67
|
-
const start = buffer.lastIndexOf('\n', index - 1) + 1;
|
|
68
|
-
let nearest = -1;
|
|
69
|
-
let i = start;
|
|
70
|
-
while (i < buffer.length && buffer[i] !== '\n') {
|
|
71
|
-
const point = String.fromCodePoint(buffer.codePointAt(i));
|
|
72
|
-
if (point === '\t' || charWidth(point) > 0) {
|
|
73
|
-
if (i > index)
|
|
74
|
-
return nearest >= 0 ? nearest : i;
|
|
75
|
-
nearest = i;
|
|
76
|
-
}
|
|
77
|
-
i += point.length;
|
|
78
|
-
}
|
|
79
|
-
return nearest >= 0 ? nearest : i;
|
|
25
|
+
return graphemes(text).reduce((width, part) => width + graphemeWidth(part.text), 0);
|
|
80
26
|
}
|
|
81
27
|
/**
|
|
82
28
|
* Wrap `buffer` into rows, locate the caret, and select a window of at most
|
|
@@ -89,88 +35,29 @@ function visualCursor(buffer, cursor) {
|
|
|
89
35
|
*/
|
|
90
36
|
export function layoutComposer(input) {
|
|
91
37
|
const { buffer, active } = input;
|
|
92
|
-
const cursor = visualCursor(buffer, input.cursor);
|
|
93
|
-
const cols = Math.max(1, Math.floor(input.cols));
|
|
94
38
|
const maxRows = input.maxRows >= 1 ? Math.floor(input.maxRows) : 1;
|
|
39
|
+
const map = buildEditorVisualMap(input);
|
|
40
|
+
const cursorRowAbs = active ? map.cursor.row : -1;
|
|
41
|
+
const cursorCol = map.cursor.column;
|
|
95
42
|
const spans = tokenSpans(buffer);
|
|
96
43
|
let spanIndex = 0;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
spanIndex
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
segments = [];
|
|
112
|
-
col = 0;
|
|
113
|
-
};
|
|
114
|
-
const append = (text, width, kind) => {
|
|
115
|
-
const last = segments[segments.length - 1];
|
|
116
|
-
if (last && last.kind === kind)
|
|
117
|
-
segments[segments.length - 1] = { text: last.text + text, kind };
|
|
118
|
-
else
|
|
119
|
-
segments.push({ text, kind });
|
|
120
|
-
col += width;
|
|
121
|
-
};
|
|
122
|
-
const ensureRoom = (width) => {
|
|
123
|
-
if (col > 0 && col + width > cols)
|
|
124
|
-
breakRow();
|
|
125
|
-
};
|
|
126
|
-
const length = buffer.length;
|
|
127
|
-
let i = 0;
|
|
128
|
-
while (i <= length) {
|
|
129
|
-
const atCursor = active && i === cursor && cursorRowAbs === -1;
|
|
130
|
-
if (i === length) {
|
|
131
|
-
if (atCursor) {
|
|
132
|
-
ensureRoom(1);
|
|
133
|
-
cursorRowAbs = rows.length;
|
|
134
|
-
cursorCol = col;
|
|
135
|
-
}
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
const ch = String.fromCodePoint(buffer.codePointAt(i));
|
|
139
|
-
if (ch === '\n') {
|
|
140
|
-
if (atCursor) {
|
|
141
|
-
ensureRoom(1);
|
|
142
|
-
cursorRowAbs = rows.length;
|
|
143
|
-
cursorCol = col;
|
|
144
|
-
}
|
|
145
|
-
breakRow();
|
|
146
|
-
i += 1;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
const kind = kindAt(i);
|
|
150
|
-
if (ch === '\t') {
|
|
151
|
-
let advance = TAB_WIDTH - (col % TAB_WIDTH);
|
|
152
|
-
if (col > 0 && col + advance > cols) {
|
|
153
|
-
breakRow();
|
|
154
|
-
advance = TAB_WIDTH;
|
|
155
|
-
}
|
|
156
|
-
if (atCursor) {
|
|
157
|
-
cursorRowAbs = rows.length;
|
|
158
|
-
cursorCol = col;
|
|
159
|
-
}
|
|
160
|
-
append(' '.repeat(advance), advance, kind);
|
|
161
|
-
i += 1;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
const width = charWidth(ch);
|
|
165
|
-
ensureRoom(width);
|
|
166
|
-
if (atCursor) {
|
|
167
|
-
cursorRowAbs = rows.length;
|
|
168
|
-
cursorCol = col;
|
|
44
|
+
const rows = map.rows.map((cells) => {
|
|
45
|
+
const segments = [];
|
|
46
|
+
for (const cell of cells) {
|
|
47
|
+
while (spanIndex < spans.length && spans[spanIndex].end <= cell.index)
|
|
48
|
+
spanIndex += 1;
|
|
49
|
+
const span = spans[spanIndex];
|
|
50
|
+
const kind = span && cell.index >= span.start && cell.index < span.end ? 'token' : 'text';
|
|
51
|
+
const last = segments[segments.length - 1];
|
|
52
|
+
if (last && last.kind === kind)
|
|
53
|
+
segments[segments.length - 1] = {
|
|
54
|
+
text: last.text + cell.text, kind,
|
|
55
|
+
};
|
|
56
|
+
else
|
|
57
|
+
segments.push({ text: cell.text, kind });
|
|
169
58
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
rows.push({ segments });
|
|
59
|
+
return { segments };
|
|
60
|
+
});
|
|
174
61
|
const totalRows = rows.length;
|
|
175
62
|
const cap = Math.max(1, Math.min(maxRows, totalRows));
|
|
176
63
|
let start = Math.max(0, totalRows - cap);
|
|
@@ -213,8 +100,8 @@ export function splitRowAtColumn(row, column) {
|
|
|
213
100
|
const cells = [];
|
|
214
101
|
let leading = '';
|
|
215
102
|
for (const segment of row.segments) {
|
|
216
|
-
for (const ch of segment.text) {
|
|
217
|
-
if (
|
|
103
|
+
for (const { text: ch } of graphemes(segment.text)) {
|
|
104
|
+
if (graphemeWidth(ch) === 0) {
|
|
218
105
|
const last = cells[cells.length - 1];
|
|
219
106
|
if (last)
|
|
220
107
|
cells[cells.length - 1] = { ...last, text: last.text + ch };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface EditorGrapheme {
|
|
2
|
+
readonly text: string;
|
|
3
|
+
readonly index: number;
|
|
4
|
+
readonly end: number;
|
|
5
|
+
}
|
|
6
|
+
/** Segment text without changing its UTF-16 offsets or stored whitespace. */
|
|
7
|
+
export declare function graphemes(text: string): EditorGrapheme[];
|
|
8
|
+
/** Snap an arbitrary UTF-16 offset to a complete grapheme boundary. */
|
|
9
|
+
export declare function snapGrapheme(text: string, cursor: number, right?: boolean): number;
|
|
10
|
+
/** Move one complete grapheme, leaving the two buffer edges unchanged. */
|
|
11
|
+
export declare function stepGrapheme(text: string, cursor: number, direction: 'left' | 'right'): number;
|
|
12
|
+
/** Width of one grapheme (ASCII avoids the Unicode width-table lookup). */
|
|
13
|
+
export declare function graphemeWidth(text: string): number;
|
|
14
|
+
export interface VisualCell extends EditorGrapheme {
|
|
15
|
+
readonly column: number;
|
|
16
|
+
readonly width: number;
|
|
17
|
+
}
|
|
18
|
+
export interface VisualPosition {
|
|
19
|
+
readonly index: number;
|
|
20
|
+
readonly row: number;
|
|
21
|
+
readonly column: number;
|
|
22
|
+
}
|
|
23
|
+
export interface EditorVisualMap {
|
|
24
|
+
readonly rows: VisualCell[][];
|
|
25
|
+
readonly positions: VisualPosition[];
|
|
26
|
+
readonly cursor: VisualPosition;
|
|
27
|
+
}
|
|
28
|
+
/** Map graphemes to hard-wrapped rows, sharing tab stops and caret reservation. */
|
|
29
|
+
export declare function buildEditorVisualMap(input: {
|
|
30
|
+
buffer: string;
|
|
31
|
+
cols: number;
|
|
32
|
+
cursor: number;
|
|
33
|
+
active?: boolean;
|
|
34
|
+
}): EditorVisualMap;
|
|
35
|
+
/** Move to the closest boundary at or left of the preferred display column. */
|
|
36
|
+
export declare function moveVisualCursor(input: {
|
|
37
|
+
buffer: string;
|
|
38
|
+
cursor: number;
|
|
39
|
+
cols: number;
|
|
40
|
+
direction: 'up' | 'down';
|
|
41
|
+
preferredVisualColumn?: number;
|
|
42
|
+
}): {
|
|
43
|
+
cursor: number;
|
|
44
|
+
preferredVisualColumn: number;
|
|
45
|
+
} | null;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Shared UTF-16 grapheme boundaries and terminal-cell layout for the editor. */
|
|
2
|
+
import stringWidth from 'string-width';
|
|
3
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
4
|
+
/** Segment text without changing its UTF-16 offsets or stored whitespace. */
|
|
5
|
+
export function graphemes(text) {
|
|
6
|
+
return Array.from(segmenter.segment(text), ({ segment, index }) => ({
|
|
7
|
+
text: segment, index, end: index + segment.length,
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
/** Snap an arbitrary UTF-16 offset to a complete grapheme boundary. */
|
|
11
|
+
export function snapGrapheme(text, cursor, right = false) {
|
|
12
|
+
const clamped = Math.max(0, Math.min(text.length, cursor));
|
|
13
|
+
for (const part of segmenter.segment(text)) {
|
|
14
|
+
const end = part.index + part.segment.length;
|
|
15
|
+
if (clamped > part.index && clamped < end)
|
|
16
|
+
return right ? end : part.index;
|
|
17
|
+
if (part.index >= clamped)
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
return clamped;
|
|
21
|
+
}
|
|
22
|
+
/** Move one complete grapheme, leaving the two buffer edges unchanged. */
|
|
23
|
+
export function stepGrapheme(text, cursor, direction) {
|
|
24
|
+
if (direction === 'left')
|
|
25
|
+
return snapGrapheme(text, Math.max(0, cursor - 1));
|
|
26
|
+
return snapGrapheme(text, Math.min(text.length, cursor + 1), true);
|
|
27
|
+
}
|
|
28
|
+
/** Width of one grapheme (ASCII avoids the Unicode width-table lookup). */
|
|
29
|
+
export function graphemeWidth(text) {
|
|
30
|
+
if (text.length === 1 && text >= ' ' && text <= '~')
|
|
31
|
+
return 1;
|
|
32
|
+
return stringWidth(text);
|
|
33
|
+
}
|
|
34
|
+
/** Map graphemes to hard-wrapped rows, sharing tab stops and caret reservation. */
|
|
35
|
+
export function buildEditorVisualMap(input) {
|
|
36
|
+
const cols = Math.max(1, Math.floor(input.cols));
|
|
37
|
+
const cursor = snapGrapheme(input.buffer, input.cursor);
|
|
38
|
+
const rows = [[]];
|
|
39
|
+
const positions = [];
|
|
40
|
+
let column = 0;
|
|
41
|
+
const breakRow = () => { rows.push([]); column = 0; };
|
|
42
|
+
const locate = (index) => {
|
|
43
|
+
positions.push({ index, row: rows.length - 1, column });
|
|
44
|
+
};
|
|
45
|
+
for (const part of graphemes(input.buffer)) {
|
|
46
|
+
if (part.text === '\n') {
|
|
47
|
+
if (input.active !== false && part.index === cursor && column >= cols)
|
|
48
|
+
breakRow();
|
|
49
|
+
locate(part.index);
|
|
50
|
+
breakRow();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
let width = part.text === '\t' ? Math.min(cols, 4 - column % 4)
|
|
54
|
+
: graphemeWidth(part.text);
|
|
55
|
+
if (column > 0 && column + width > cols) {
|
|
56
|
+
breakRow();
|
|
57
|
+
if (part.text === '\t')
|
|
58
|
+
width = Math.min(cols, 4);
|
|
59
|
+
}
|
|
60
|
+
locate(part.index);
|
|
61
|
+
rows[rows.length - 1].push({ ...part,
|
|
62
|
+
text: part.text === '\t' ? ' '.repeat(width) : part.text, column, width });
|
|
63
|
+
column += width;
|
|
64
|
+
}
|
|
65
|
+
if (input.active !== false && cursor === input.buffer.length && column >= cols)
|
|
66
|
+
breakRow();
|
|
67
|
+
locate(input.buffer.length);
|
|
68
|
+
const caret = positions.find((position) => position.index === cursor);
|
|
69
|
+
return { rows, positions, cursor: caret };
|
|
70
|
+
}
|
|
71
|
+
/** Move to the closest boundary at or left of the preferred display column. */
|
|
72
|
+
export function moveVisualCursor(input) {
|
|
73
|
+
const map = buildEditorVisualMap(input);
|
|
74
|
+
const row = map.cursor.row + (input.direction === 'up' ? -1 : 1);
|
|
75
|
+
if (row < 0 || row >= map.rows.length)
|
|
76
|
+
return null;
|
|
77
|
+
const preferredVisualColumn = input.preferredVisualColumn ?? map.cursor.column;
|
|
78
|
+
const candidates = map.positions.filter((position) => position.row === row);
|
|
79
|
+
let target = candidates[0];
|
|
80
|
+
for (const position of candidates) {
|
|
81
|
+
if (position.column <= preferredVisualColumn)
|
|
82
|
+
target = position;
|
|
83
|
+
}
|
|
84
|
+
return target ? { cursor: target.index, preferredVisualColumn } : null;
|
|
85
|
+
}
|
|
@@ -26,6 +26,7 @@ import { type PasteRecord } from './paste-tokens.js';
|
|
|
26
26
|
export interface EditorState {
|
|
27
27
|
buffer: string;
|
|
28
28
|
cursor: number;
|
|
29
|
+
preferredVisualColumn?: number;
|
|
29
30
|
historyIndex: number | null;
|
|
30
31
|
dismissed: boolean;
|
|
31
32
|
sel: number;
|
|
@@ -70,9 +71,12 @@ export type EditorAction = {
|
|
|
70
71
|
cursor: number;
|
|
71
72
|
} | {
|
|
72
73
|
type: 'backspace';
|
|
74
|
+
} | {
|
|
75
|
+
type: 'delete';
|
|
73
76
|
} | {
|
|
74
77
|
type: 'moveCursor';
|
|
75
78
|
cursor: number;
|
|
79
|
+
preferredVisualColumn?: number;
|
|
76
80
|
} | {
|
|
77
81
|
type: 'recall';
|
|
78
82
|
buffer: string;
|
|
@@ -86,6 +90,11 @@ export type EditorAction = {
|
|
|
86
90
|
} | {
|
|
87
91
|
type: 'clear';
|
|
88
92
|
}
|
|
93
|
+
/** Only local reducer-planned states may be adopted; prune token payloads again. */
|
|
94
|
+
| {
|
|
95
|
+
type: 'adopt';
|
|
96
|
+
state: EditorState;
|
|
97
|
+
}
|
|
89
98
|
/**
|
|
90
99
|
* ONE chunk of stdin, already split into ordered runs. The only new action,
|
|
91
100
|
* and it is what keeps the one-dispatch-per-key rule true for a chunk that
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* Extracted from the component so it is unit-testable without mounting Ink.
|
|
24
24
|
*/
|
|
25
25
|
import { expandRangeOverTokens, formatPasteToken, makePasteRecord, referencedIds, shouldCollapse, snapOutOfToken, } from './paste-tokens.js';
|
|
26
|
+
import { snapGrapheme, stepGrapheme } from './editor-navigation.js';
|
|
26
27
|
const NO_PASTES = new Map();
|
|
27
28
|
export const INITIAL_EDITOR_STATE = {
|
|
28
29
|
buffer: '',
|
|
@@ -42,7 +43,9 @@ export const INITIAL_EDITOR_STATE = {
|
|
|
42
43
|
* typing again, so suggestions should come back), and re-homes the selection
|
|
43
44
|
* (the candidate list is about to be recomputed).
|
|
44
45
|
*/
|
|
45
|
-
const DRAFT_FLAGS = {
|
|
46
|
+
const DRAFT_FLAGS = {
|
|
47
|
+
historyIndex: null, dismissed: false, sel: 0, preferredVisualColumn: undefined,
|
|
48
|
+
};
|
|
46
49
|
/**
|
|
47
50
|
* Drop payloads the buffer no longer references (I-7).
|
|
48
51
|
*
|
|
@@ -82,15 +85,18 @@ function withPrune(next) {
|
|
|
82
85
|
}
|
|
83
86
|
export function editorReducer(state, action) {
|
|
84
87
|
switch (action.type) {
|
|
88
|
+
case 'adopt':
|
|
89
|
+
return withPrune(action.state);
|
|
85
90
|
case 'insert': {
|
|
86
91
|
// D-8: typing can never land INSIDE a token. A no-op on a buffer with no
|
|
87
92
|
// tokens, which is every buffer until the first collapse.
|
|
88
|
-
const cursor = snapOutOfToken(state.buffer, state.cursor);
|
|
93
|
+
const cursor = snapOutOfToken(state.buffer, snapGrapheme(state.buffer, state.cursor));
|
|
94
|
+
const buffer = state.buffer.slice(0, cursor) + action.text + state.buffer.slice(cursor);
|
|
89
95
|
return withPrune({
|
|
90
96
|
...state,
|
|
91
97
|
...DRAFT_FLAGS,
|
|
92
|
-
buffer
|
|
93
|
-
cursor: cursor + action.text.length,
|
|
98
|
+
buffer,
|
|
99
|
+
cursor: snapGrapheme(buffer, cursor + action.text.length, true),
|
|
94
100
|
});
|
|
95
101
|
}
|
|
96
102
|
case 'replace':
|
|
@@ -98,24 +104,29 @@ export function editorReducer(state, action) {
|
|
|
98
104
|
...state,
|
|
99
105
|
...DRAFT_FLAGS,
|
|
100
106
|
buffer: action.buffer,
|
|
101
|
-
cursor: action.cursor,
|
|
107
|
+
cursor: snapGrapheme(action.buffer, action.cursor),
|
|
102
108
|
});
|
|
103
|
-
case 'backspace':
|
|
104
|
-
|
|
109
|
+
case 'backspace':
|
|
110
|
+
case 'delete': {
|
|
111
|
+
const cursor = snapGrapheme(state.buffer, state.cursor);
|
|
112
|
+
const backwards = action.type === 'backspace';
|
|
113
|
+
if (backwards ? cursor <= 0 : cursor >= state.buffer.length)
|
|
105
114
|
return state;
|
|
106
115
|
// G5: one `Backspace` immediately after `...lines]` removes the whole
|
|
107
116
|
// token and releases its payload, in ONE dispatch.
|
|
108
|
-
const
|
|
117
|
+
const other = stepGrapheme(state.buffer, cursor, backwards ? 'left' : 'right');
|
|
118
|
+
const range = expandRangeOverTokens(state.buffer, backwards ? other : cursor, backwards ? cursor : other);
|
|
119
|
+
const buffer = state.buffer.slice(0, range.from) + state.buffer.slice(range.to);
|
|
109
120
|
return withPrune({
|
|
110
121
|
...state,
|
|
111
122
|
...DRAFT_FLAGS,
|
|
112
|
-
buffer
|
|
113
|
-
cursor: range.from,
|
|
123
|
+
buffer,
|
|
124
|
+
cursor: snapGrapheme(buffer, range.from),
|
|
114
125
|
});
|
|
115
126
|
}
|
|
116
127
|
case 'input': {
|
|
117
128
|
let buffer = state.buffer;
|
|
118
|
-
let cursor = snapOutOfToken(buffer, state.cursor);
|
|
129
|
+
let cursor = snapOutOfToken(buffer, snapGrapheme(buffer, state.cursor));
|
|
119
130
|
let pastes = null;
|
|
120
131
|
for (const segment of action.segments) {
|
|
121
132
|
const collapse = segment.kind === 'paste' && shouldCollapse(segment.text);
|
|
@@ -127,7 +138,7 @@ export function editorReducer(state, action) {
|
|
|
127
138
|
pastes.set(record.id, record);
|
|
128
139
|
}
|
|
129
140
|
buffer = buffer.slice(0, cursor) + text + buffer.slice(cursor);
|
|
130
|
-
cursor
|
|
141
|
+
cursor = snapGrapheme(buffer, cursor + text.length, true);
|
|
131
142
|
}
|
|
132
143
|
return withPrune({
|
|
133
144
|
...state,
|
|
@@ -137,11 +148,15 @@ export function editorReducer(state, action) {
|
|
|
137
148
|
...(pastes ? { pastes } : {}),
|
|
138
149
|
});
|
|
139
150
|
}
|
|
140
|
-
case 'moveCursor':
|
|
151
|
+
case 'moveCursor': {
|
|
141
152
|
// NO DRAFT-FLAG RESET. Moving the caret is not an edit: clearing
|
|
142
153
|
// `historyIndex` here would end a history walk the moment the user pressed
|
|
143
154
|
// ← to look at what they recalled.
|
|
144
|
-
|
|
155
|
+
const cursor = snapGrapheme(state.buffer, action.cursor);
|
|
156
|
+
const preferredVisualColumn = action.preferredVisualColumn;
|
|
157
|
+
return state.cursor === cursor && state.preferredVisualColumn === preferredVisualColumn
|
|
158
|
+
? state : { ...state, cursor, preferredVisualColumn };
|
|
159
|
+
}
|
|
145
160
|
case 'recall':
|
|
146
161
|
// `historyIndex` is SET, not reset, which is why recall cannot reuse
|
|
147
162
|
// `replace` — the two differ in exactly the field that keeps the walk
|
|
@@ -153,7 +168,8 @@ export function editorReducer(state, action) {
|
|
|
153
168
|
return withPrune({
|
|
154
169
|
...state,
|
|
155
170
|
buffer: action.buffer,
|
|
156
|
-
cursor: action.cursor,
|
|
171
|
+
cursor: snapGrapheme(action.buffer, action.cursor),
|
|
172
|
+
preferredVisualColumn: undefined,
|
|
157
173
|
historyIndex: action.historyIndex,
|
|
158
174
|
});
|
|
159
175
|
case 'select':
|
|
@@ -1,37 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* - a bare `\n` -- Ctrl+J, which Ink parses to `name: 'enter'` with
|
|
13
|
-
* `input === '\n'` and NO `key.return`, so it reaches this branch as an
|
|
14
|
-
* ordinary (control) character that must still mean "newline".
|
|
15
|
-
*
|
|
16
|
-
* I-12, THE BROADCAST OBLIGATION, APPLIES TO THESE FRAMES TOO. `useInput`
|
|
17
|
-
* re-emits every chunk to every mounted handler, `stripPasteFrames` does not
|
|
18
|
-
* know this frame, and a NUL appended into `SettingsScreen`'s API-key field
|
|
19
|
-
* renders as nothing and silently breaks authentication -- the exact accident
|
|
20
|
-
* class that function's own doc records. `stripEnterFrames` is the equivalent
|
|
21
|
-
* guard for overlays with no paste model of their own.
|
|
22
|
-
*/
|
|
23
|
-
import { type FrameSegment } from './paste-frames.js';
|
|
1
|
+
export type ComposerFrame = {
|
|
2
|
+
kind: 'text';
|
|
3
|
+
text: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'paste';
|
|
6
|
+
text: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'newline';
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'submit';
|
|
11
|
+
};
|
|
24
12
|
/** True when this `input` carries at least one Enter frame -- the branch guard. */
|
|
25
13
|
export declare function hasEnterFrame(input: string): boolean;
|
|
26
14
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* Text-only by construction (paste frames are `mergeWithPasteRuns`'s job), so
|
|
30
|
-
* the paste id never has to exist here -- `PromptInput` allocates ids for paste
|
|
31
|
-
* segments while building the reducer's action, exactly as the paste branch
|
|
32
|
-
* does (P1-5: the reducer stays pure and this stays a function of its input).
|
|
15
|
+
* Decode paste regions before interpreting CR/LF or newline frames as keys.
|
|
16
|
+
* Paste IDs remain the caller's responsibility.
|
|
33
17
|
*/
|
|
34
|
-
export declare function splitEnterFrames(input: string):
|
|
18
|
+
export declare function splitEnterFrames(input: string): ComposerFrame[];
|
|
35
19
|
/**
|
|
36
20
|
* Merge newline runs with the paste frames that share the same `input`.
|
|
37
21
|
*
|
|
@@ -48,7 +32,7 @@ export declare function splitEnterFrames(input: string): FrameSegment[];
|
|
|
48
32
|
* Paste segments carry NO id here (P1-5); the caller assigns one per segment
|
|
49
33
|
* only when it builds the reducer's action.
|
|
50
34
|
*/
|
|
51
|
-
export declare function mergeWithPasteRuns(input: string, runs:
|
|
35
|
+
export declare function mergeWithPasteRuns(input: string, runs: ComposerFrame[]): ComposerFrame[];
|
|
52
36
|
/**
|
|
53
37
|
* Remove every Enter frame for a consumer that has no newline model of its
|
|
54
38
|
* own, replacing each frame with `'\n'` (tui-shift-enter-copy-queue 3.5, the
|
package/dist/ui/enter-frames.js
CHANGED
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reading Enter frames back out of an Ink `input` string
|
|
3
|
-
* (tui-shift-enter-copy-queue 3.5) -- pure.
|
|
4
|
-
*
|
|
5
|
-
* The stdin filter rewrites a modified Enter into `ENTER_NEWLINE_FRAME` inline,
|
|
6
|
-
* in stream order, exactly the way it frames a paste. This module is the only
|
|
7
|
-
* place that takes those frames apart again, and it handles BOTH newline
|
|
8
|
-
* sources the composer can see in one `input` string:
|
|
9
|
-
*
|
|
10
|
-
* - `ENTER_NEWLINE_FRAME` -- Shift+Enter / Alt+Enter / CSI-u Enter, translated
|
|
11
|
-
* by the filter; and
|
|
12
|
-
* - a bare `\n` -- Ctrl+J, which Ink parses to `name: 'enter'` with
|
|
13
|
-
* `input === '\n'` and NO `key.return`, so it reaches this branch as an
|
|
14
|
-
* ordinary (control) character that must still mean "newline".
|
|
15
|
-
*
|
|
16
|
-
* I-12, THE BROADCAST OBLIGATION, APPLIES TO THESE FRAMES TOO. `useInput`
|
|
17
|
-
* re-emits every chunk to every mounted handler, `stripPasteFrames` does not
|
|
18
|
-
* know this frame, and a NUL appended into `SettingsScreen`'s API-key field
|
|
19
|
-
* renders as nothing and silently breaks authentication -- the exact accident
|
|
20
|
-
* class that function's own doc records. `stripEnterFrames` is the equivalent
|
|
21
|
-
* guard for overlays with no paste model of their own.
|
|
22
|
-
*/
|
|
23
1
|
import { ENTER_NEWLINE_FRAME, PASTE_CLOSE, PASTE_OPEN } from '../input/limits.js';
|
|
24
2
|
import { sanitisePaste } from '../input/paste-parse.js';
|
|
25
3
|
import { sanitiseTyped } from './paste-frames.js';
|
|
@@ -28,10 +6,10 @@ export function hasEnterFrame(input) {
|
|
|
28
6
|
return input.includes(ENTER_NEWLINE_FRAME);
|
|
29
7
|
}
|
|
30
8
|
/**
|
|
31
|
-
* Split one
|
|
9
|
+
* Split one paste-free span into ordered text, newline and submit intents.
|
|
32
10
|
*
|
|
33
11
|
* Every newline source -- frame or bare `\n` -- yields ONE
|
|
34
|
-
* `{ kind: '
|
|
12
|
+
* `{ kind: 'newline' }` segment, so the editor's existing `input`
|
|
35
13
|
* action inserts it through the same path typing uses and the draft's row
|
|
36
14
|
* budget applies unchanged. Non-newline text is accumulated and passed through
|
|
37
15
|
* `sanitiseTyped` (I-14): a chunk that carries a frame can also carry the
|
|
@@ -51,13 +29,19 @@ function splitEnterSpan(span) {
|
|
|
51
29
|
while (i < span.length) {
|
|
52
30
|
if (span.startsWith(ENTER_NEWLINE_FRAME, i)) {
|
|
53
31
|
pushText();
|
|
54
|
-
segments.push({ kind: '
|
|
32
|
+
segments.push({ kind: 'newline' });
|
|
55
33
|
i += ENTER_NEWLINE_FRAME.length;
|
|
56
34
|
continue;
|
|
57
35
|
}
|
|
36
|
+
if (span[i] === '\r') {
|
|
37
|
+
pushText();
|
|
38
|
+
segments.push({ kind: 'submit' });
|
|
39
|
+
i += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
58
42
|
if (span[i] === '\n') {
|
|
59
43
|
pushText();
|
|
60
|
-
segments.push({ kind: '
|
|
44
|
+
segments.push({ kind: 'newline' });
|
|
61
45
|
i += 1;
|
|
62
46
|
continue;
|
|
63
47
|
}
|
|
@@ -68,15 +52,12 @@ function splitEnterSpan(span) {
|
|
|
68
52
|
return segments;
|
|
69
53
|
}
|
|
70
54
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* Text-only by construction (paste frames are `mergeWithPasteRuns`'s job), so
|
|
74
|
-
* the paste id never has to exist here -- `PromptInput` allocates ids for paste
|
|
75
|
-
* segments while building the reducer's action, exactly as the paste branch
|
|
76
|
-
* does (P1-5: the reducer stays pure and this stays a function of its input).
|
|
55
|
+
* Decode paste regions before interpreting CR/LF or newline frames as keys.
|
|
56
|
+
* Paste IDs remain the caller's responsibility.
|
|
77
57
|
*/
|
|
78
58
|
export function splitEnterFrames(input) {
|
|
79
|
-
return
|
|
59
|
+
return input.includes(PASTE_OPEN)
|
|
60
|
+
? mergeWithPasteRuns(input, []) : splitEnterSpan(input);
|
|
80
61
|
}
|
|
81
62
|
/**
|
|
82
63
|
* Merge newline runs with the paste frames that share the same `input`.
|