@aragon-agent/cli 0.6.27 → 0.6.29
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 +164 -0
- package/README.md +2 -2
- package/dist/agent/reducer.js +47 -2
- package/dist/input/control-sequences.d.ts +71 -0
- package/dist/input/control-sequences.js +222 -0
- package/dist/input/limits.d.ts +20 -0
- package/dist/input/limits.js +20 -0
- package/dist/input/paste-parse.d.ts +21 -9
- package/dist/input/paste-parse.js +23 -11
- package/dist/input/stdin-filter.js +123 -9
- package/dist/tools/terminal-output.d.ts +8 -0
- package/dist/tools/terminal-output.js +10 -7
- package/dist/ui/App.js +34 -6
- package/dist/ui/AutocompletePopup.js +1 -1
- package/dist/ui/Header.js +7 -2
- package/dist/ui/Markdown.js +30 -3
- package/dist/ui/PromptInput.js +41 -3
- package/dist/ui/Transcript.js +7 -0
- package/dist/ui/display-tab.d.ts +51 -0
- package/dist/ui/display-tab.js +82 -0
- package/dist/ui/editor-navigation.d.ts +11 -1
- package/dist/ui/editor-navigation.js +14 -3
- package/dist/ui/entries/QueuedEntry.js +4 -1
- package/dist/ui/entries/UserEntry.js +8 -1
- package/dist/ui/layout/Gutter.js +1 -1
- package/dist/ui/layout/OverlayFrame.d.ts +15 -0
- package/dist/ui/layout/OverlayFrame.js +18 -0
- package/dist/ui/layout/ScrollIndicator.js +10 -3
- package/dist/ui/layout/budget.d.ts +15 -0
- package/dist/ui/layout/budget.js +15 -0
- package/dist/ui/layout/header-hints.d.ts +9 -0
- package/dist/ui/layout/header-hints.js +14 -1
- package/dist/ui/layout/status-layout.js +63 -2
- package/dist/ui/layout/virtual-window.js +44 -11
- package/dist/ui/markdown-blocks.d.ts +26 -5
- package/dist/ui/markdown-blocks.js +110 -33
- package/dist/ui/paste-frames.d.ts +5 -1
- package/dist/ui/paste-frames.js +7 -2
- package/dist/ui/transcript-text.js +11 -2
- package/dist/ui/use-wheel-routing.d.ts +10 -7
- package/dist/ui/use-wheel-routing.js +11 -6
- package/package.json +2 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TAB expansion that agrees with the TERMINAL, not with UTF-16 (fix A'-2 of
|
|
3
|
+
* paste-control-sequence-residue).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS A MODULE OF ITS OWN. A TAB has no width; it advances the pen to
|
|
6
|
+
* the next tab stop, so what it occupies depends on where the line currently
|
|
7
|
+
* is. That makes "one TAB" a property of a CURSOR, and the same character was
|
|
8
|
+
* being read three different ways in this package:
|
|
9
|
+
*
|
|
10
|
+
* - `ui/editor-navigation.ts` painted it at FOUR-column stops in the composer,
|
|
11
|
+
* - `entries/UserEntry.tsx` handed the raw byte to `<Text>`, so the terminal
|
|
12
|
+
* painted it at EIGHT-column stops in the transcript,
|
|
13
|
+
* - `tools/terminal-output.ts` expanded it by UTF-16 code-unit count, so a
|
|
14
|
+
* pasted `中文<TAB>X` table came out two columns short.
|
|
15
|
+
*
|
|
16
|
+
* A user who pastes a TAB-indented block or an Excel/Word table therefore saw
|
|
17
|
+
* the draft and the sent message disagree about their own text. One function,
|
|
18
|
+
* used by the composer AND by every renderer of stored text, is what makes the
|
|
19
|
+
* "before send" and "after send" rows character-identical.
|
|
20
|
+
*
|
|
21
|
+
* `input/` is the wrong home for it: `cli/input` may depend on `node:stream`
|
|
22
|
+
* and nothing else (`.claude-index/index.md`), and column arithmetic needs the
|
|
23
|
+
* width table. `ui/` already owns that (`selection/selection.ts` uses
|
|
24
|
+
* `string-width` for exactly this reason).
|
|
25
|
+
*
|
|
26
|
+
* Pure, React-free, Ink-free.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Tab stop width, matching every terminal's default.
|
|
30
|
+
*
|
|
31
|
+
* A COPY OF `tools/terminal-output.ts`'s number, on purpose and in the same
|
|
32
|
+
* shape `composer-limits.ts` copies `budget.ts::HINT_MIN_ROWS`: importing the
|
|
33
|
+
* tools module here would point an arrow from `ui/` into `tools/` for one
|
|
34
|
+
* integer, and the two are asserted equal by a test instead. (`tools/` keeps
|
|
35
|
+
* its own expander because its rows are clipped to 200 columns before anyone
|
|
36
|
+
* looks at them, so a code-unit approximation there cannot reach a user.)
|
|
37
|
+
*/
|
|
38
|
+
export declare const TAB_WIDTH = 8;
|
|
39
|
+
/**
|
|
40
|
+
* Expand every TAB in `text` to spaces up to the next `TAB_WIDTH` stop, given
|
|
41
|
+
* the display column the first character will be painted at.
|
|
42
|
+
*
|
|
43
|
+
* `fromColumn` is a DISPLAY column, not an offset: a caller that has already
|
|
44
|
+
* painted `中文` must pass 4. Lines are independent -- a `\n` resets the column
|
|
45
|
+
* to zero, because the terminal's pen returns to column 0.
|
|
46
|
+
*
|
|
47
|
+
* Non-ASCII text is measured with `string-width`, which is the whole point: a
|
|
48
|
+
* CJK glyph occupies two cells, and counting it as one space is what left
|
|
49
|
+
* CJK-plus-TAB tables misaligned.
|
|
50
|
+
*/
|
|
51
|
+
export declare function expandTabs(text: string, fromColumn?: number): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TAB expansion that agrees with the TERMINAL, not with UTF-16 (fix A'-2 of
|
|
3
|
+
* paste-control-sequence-residue).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS A MODULE OF ITS OWN. A TAB has no width; it advances the pen to
|
|
6
|
+
* the next tab stop, so what it occupies depends on where the line currently
|
|
7
|
+
* is. That makes "one TAB" a property of a CURSOR, and the same character was
|
|
8
|
+
* being read three different ways in this package:
|
|
9
|
+
*
|
|
10
|
+
* - `ui/editor-navigation.ts` painted it at FOUR-column stops in the composer,
|
|
11
|
+
* - `entries/UserEntry.tsx` handed the raw byte to `<Text>`, so the terminal
|
|
12
|
+
* painted it at EIGHT-column stops in the transcript,
|
|
13
|
+
* - `tools/terminal-output.ts` expanded it by UTF-16 code-unit count, so a
|
|
14
|
+
* pasted `中文<TAB>X` table came out two columns short.
|
|
15
|
+
*
|
|
16
|
+
* A user who pastes a TAB-indented block or an Excel/Word table therefore saw
|
|
17
|
+
* the draft and the sent message disagree about their own text. One function,
|
|
18
|
+
* used by the composer AND by every renderer of stored text, is what makes the
|
|
19
|
+
* "before send" and "after send" rows character-identical.
|
|
20
|
+
*
|
|
21
|
+
* `input/` is the wrong home for it: `cli/input` may depend on `node:stream`
|
|
22
|
+
* and nothing else (`.claude-index/index.md`), and column arithmetic needs the
|
|
23
|
+
* width table. `ui/` already owns that (`selection/selection.ts` uses
|
|
24
|
+
* `string-width` for exactly this reason).
|
|
25
|
+
*
|
|
26
|
+
* Pure, React-free, Ink-free.
|
|
27
|
+
*/
|
|
28
|
+
import stringWidth from 'string-width';
|
|
29
|
+
/**
|
|
30
|
+
* Tab stop width, matching every terminal's default.
|
|
31
|
+
*
|
|
32
|
+
* A COPY OF `tools/terminal-output.ts`'s number, on purpose and in the same
|
|
33
|
+
* shape `composer-limits.ts` copies `budget.ts::HINT_MIN_ROWS`: importing the
|
|
34
|
+
* tools module here would point an arrow from `ui/` into `tools/` for one
|
|
35
|
+
* integer, and the two are asserted equal by a test instead. (`tools/` keeps
|
|
36
|
+
* its own expander because its rows are clipped to 200 columns before anyone
|
|
37
|
+
* looks at them, so a code-unit approximation there cannot reach a user.)
|
|
38
|
+
*/
|
|
39
|
+
export const TAB_WIDTH = 8;
|
|
40
|
+
/**
|
|
41
|
+
* Expand every TAB in `text` to spaces up to the next `TAB_WIDTH` stop, given
|
|
42
|
+
* the display column the first character will be painted at.
|
|
43
|
+
*
|
|
44
|
+
* `fromColumn` is a DISPLAY column, not an offset: a caller that has already
|
|
45
|
+
* painted `中文` must pass 4. Lines are independent -- a `\n` resets the column
|
|
46
|
+
* to zero, because the terminal's pen returns to column 0.
|
|
47
|
+
*
|
|
48
|
+
* Non-ASCII text is measured with `string-width`, which is the whole point: a
|
|
49
|
+
* CJK glyph occupies two cells, and counting it as one space is what left
|
|
50
|
+
* CJK-plus-TAB tables misaligned.
|
|
51
|
+
*/
|
|
52
|
+
export function expandTabs(text, fromColumn = 0) {
|
|
53
|
+
if (text.indexOf('\t') === -1)
|
|
54
|
+
return text;
|
|
55
|
+
let out = '';
|
|
56
|
+
let column = Math.max(0, Math.floor(fromColumn));
|
|
57
|
+
let run = '';
|
|
58
|
+
for (const part of graphemesOf(text)) {
|
|
59
|
+
if (part === '\t') {
|
|
60
|
+
out += run;
|
|
61
|
+
run = '';
|
|
62
|
+
const width = TAB_WIDTH - (column % TAB_WIDTH);
|
|
63
|
+
out += ' '.repeat(width);
|
|
64
|
+
column += width;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
run += part;
|
|
68
|
+
if (part === '\n')
|
|
69
|
+
column = 0;
|
|
70
|
+
else
|
|
71
|
+
column += stringWidth(part);
|
|
72
|
+
}
|
|
73
|
+
return out + run;
|
|
74
|
+
}
|
|
75
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
76
|
+
/**
|
|
77
|
+
* Grapheme clusters rather than code units, so a combining mark or an emoji ZWJ
|
|
78
|
+
* sequence advances the column once, exactly as the terminal draws it.
|
|
79
|
+
*/
|
|
80
|
+
function graphemesOf(text) {
|
|
81
|
+
return Array.from(segmenter.segment(text), ({ segment }) => segment);
|
|
82
|
+
}
|
|
@@ -25,7 +25,17 @@ export interface EditorVisualMap {
|
|
|
25
25
|
readonly positions: VisualPosition[];
|
|
26
26
|
readonly cursor: VisualPosition;
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Map graphemes to hard-wrapped rows, sharing tab stops and caret reservation.
|
|
30
|
+
*
|
|
31
|
+
* THE TAB STOP IS THE TERMINAL'S (`TAB_WIDTH`), NOT AN EDITOR-SIZED FOUR. It
|
|
32
|
+
* used to be four, while the transcript handed the raw `\t` to `<Text>` and
|
|
33
|
+
* the terminal painted it at eight: the same pasted table sat one column off
|
|
34
|
+
* from itself before and after sending. A tab that would cross the right edge
|
|
35
|
+
* is still clipped to the row (the caret cannot point past the last cell), so
|
|
36
|
+
* a tab STRADDLING the wrap is the one shape where the draft and the stored
|
|
37
|
+
* text can differ by the remainder of the advance.
|
|
38
|
+
*/
|
|
29
39
|
export declare function buildEditorVisualMap(input: {
|
|
30
40
|
buffer: string;
|
|
31
41
|
cols: number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared UTF-16 grapheme boundaries and terminal-cell layout for the editor. */
|
|
2
2
|
import stringWidth from 'string-width';
|
|
3
|
+
import { TAB_WIDTH } from './display-tab.js';
|
|
3
4
|
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
4
5
|
/** Segment text without changing its UTF-16 offsets or stored whitespace. */
|
|
5
6
|
export function graphemes(text) {
|
|
@@ -31,7 +32,17 @@ export function graphemeWidth(text) {
|
|
|
31
32
|
return 1;
|
|
32
33
|
return stringWidth(text);
|
|
33
34
|
}
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Map graphemes to hard-wrapped rows, sharing tab stops and caret reservation.
|
|
37
|
+
*
|
|
38
|
+
* THE TAB STOP IS THE TERMINAL'S (`TAB_WIDTH`), NOT AN EDITOR-SIZED FOUR. It
|
|
39
|
+
* used to be four, while the transcript handed the raw `\t` to `<Text>` and
|
|
40
|
+
* the terminal painted it at eight: the same pasted table sat one column off
|
|
41
|
+
* from itself before and after sending. A tab that would cross the right edge
|
|
42
|
+
* is still clipped to the row (the caret cannot point past the last cell), so
|
|
43
|
+
* a tab STRADDLING the wrap is the one shape where the draft and the stored
|
|
44
|
+
* text can differ by the remainder of the advance.
|
|
45
|
+
*/
|
|
35
46
|
export function buildEditorVisualMap(input) {
|
|
36
47
|
const cols = Math.max(1, Math.floor(input.cols));
|
|
37
48
|
const cursor = snapGrapheme(input.buffer, input.cursor);
|
|
@@ -50,12 +61,12 @@ export function buildEditorVisualMap(input) {
|
|
|
50
61
|
breakRow();
|
|
51
62
|
continue;
|
|
52
63
|
}
|
|
53
|
-
let width = part.text === '\t' ? Math.min(cols,
|
|
64
|
+
let width = part.text === '\t' ? Math.min(cols, TAB_WIDTH - column % TAB_WIDTH)
|
|
54
65
|
: graphemeWidth(part.text);
|
|
55
66
|
if (column > 0 && column + width > cols) {
|
|
56
67
|
breakRow();
|
|
57
68
|
if (part.text === '\t')
|
|
58
|
-
width = Math.min(cols,
|
|
69
|
+
width = Math.min(cols, TAB_WIDTH);
|
|
59
70
|
}
|
|
60
71
|
locate(part.index);
|
|
61
72
|
rows[rows.length - 1].push({ ...part,
|
|
@@ -13,10 +13,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
13
13
|
import React from 'react';
|
|
14
14
|
import { Text } from 'ink';
|
|
15
15
|
import { pickGlyphs } from '../glyphs.js';
|
|
16
|
+
import { expandTabs } from '../display-tab.js';
|
|
16
17
|
function QueuedEntryImpl({ text, theme, caps, }) {
|
|
17
18
|
const glyphs = pickGlyphs(caps);
|
|
18
19
|
const lines = text.split('\n');
|
|
19
|
-
|
|
20
|
+
// Same reason as `UserEntry`: a queued draft is a draft, so its TABs must
|
|
21
|
+
// occupy the columns the composer showed them at.
|
|
22
|
+
const first = expandTabs(lines[0] ?? '');
|
|
20
23
|
const extra = lines.length - 1;
|
|
21
24
|
const suffix = extra > 0 ? ` ${glyphs.ellipsis}+${extra} line${extra === 1 ? '' : 's'}` : '';
|
|
22
25
|
return (_jsx(Text, { wrap: "truncate", color: theme.muted, children: `Queue: ${first}${suffix}` }));
|
|
@@ -14,9 +14,16 @@ import React from 'react';
|
|
|
14
14
|
import { Box, Text } from 'ink';
|
|
15
15
|
import { pickGlyphs } from '../glyphs.js';
|
|
16
16
|
import { USER_ENTRY_MAX_ROWS } from '../composer-limits.js';
|
|
17
|
+
import { expandTabs } from '../display-tab.js';
|
|
17
18
|
function UserEntryImpl({ text, theme, caps, }) {
|
|
18
19
|
const glyphs = pickGlyphs(caps);
|
|
19
|
-
|
|
20
|
+
// TAB IS EXPANDED HERE, AT THE RENDER BOUNDARY, AND NOWHERE ABOVE IT.
|
|
21
|
+
// The payload keeps its `\t` (the model must receive the original
|
|
22
|
+
// indentation), so `<Text>` used to hand the raw byte to the terminal and
|
|
23
|
+
// the terminal painted an EIGHT-column stop while the composer painted a
|
|
24
|
+
// FOUR-column one. Splitting first and expanding per line keeps this row
|
|
25
|
+
// identical to the row the user saw in the draft.
|
|
26
|
+
const all = text.split('\n').map((line) => expandTabs(line));
|
|
20
27
|
// A submitted 5 000-line paste otherwise asks Yoga to lay out 5 000 nodes
|
|
21
28
|
// inside a measured entry -- the hazard `render-cache.ts:37` already records
|
|
22
29
|
// in the same words ("a multi-megabyte 'code block' is almost always a pasted
|
package/dist/ui/layout/Gutter.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Box, Text } from 'ink';
|
|
|
3
3
|
/** Columns reserved for the rail, glyph included. */
|
|
4
4
|
export const GUTTER_WIDTH = 2;
|
|
5
5
|
export function Gutter({ glyph, color, children }) {
|
|
6
|
-
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, width: GUTTER_WIDTH, children: _jsx(Text, { color: color, children: glyph }) }), _jsx(Box, { flexDirection: "column", flexShrink: 0, children: children })] }));
|
|
6
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, width: GUTTER_WIDTH, children: _jsx(Text, { color: color, children: glyph }) }), _jsx(Box, { flexDirection: "column", flexBasis: 0, flexGrow: 1, flexShrink: 0, children: children })] }));
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* The one-row gap between turns: a dim rail stub rather than a blank line, so
|
|
@@ -35,6 +35,21 @@ interface OverlayFrameSelfManaged extends OverlayFrameBase {
|
|
|
35
35
|
}
|
|
36
36
|
export type OverlayFrameProps = OverlayFrameControlled | OverlayFrameSelfManaged;
|
|
37
37
|
export declare function OverlayFrame(props: OverlayFrameProps): React.ReactElement;
|
|
38
|
+
/**
|
|
39
|
+
* Rows one PgUp/PgDn - and one Shift+wheel notch - turns inside a controlled
|
|
40
|
+
* overlay (defect 11).
|
|
41
|
+
*
|
|
42
|
+
* THIS IS THE OVERLAY'S `scroll.pageSize`, deliberately the same rule: one window
|
|
43
|
+
* minus `PAGE_CONTEXT_LINES` of overlap, so a reader keeps two rows of context
|
|
44
|
+
* across a page turn exactly as `PgDn` in the transcript does. It replaces a
|
|
45
|
+
* constant 8, which was 42% of `/help`'s 19-row window on a 100x24 terminal and
|
|
46
|
+
* made reading a 93-row shortcut list a twelve-keystroke exercise.
|
|
47
|
+
*
|
|
48
|
+
* THE WINDOW HEIGHT COMES FROM `overlayBodyRows`, which is the same expression
|
|
49
|
+
* `OverlayFrame` renders with - computed twice here would be the two-authorities
|
|
50
|
+
* defect B1 is fixing one component over.
|
|
51
|
+
*/
|
|
52
|
+
export declare function overlayPageSize(maxRows: number, cols: number): number;
|
|
38
53
|
/** Body rows shared by controlled overlays and self-managed focused lists. */
|
|
39
54
|
export declare function overlayBodyRows(maxRows: number, cols: number): number;
|
|
40
55
|
export {};
|
|
@@ -4,6 +4,7 @@ import { Box, Text } from 'ink';
|
|
|
4
4
|
import stringWidth from 'string-width';
|
|
5
5
|
import { pickGlyphs } from '../glyphs.js';
|
|
6
6
|
import { sliceWindow } from './overlay-window.js';
|
|
7
|
+
import { pageSize } from './scroll.js';
|
|
7
8
|
/** Below this width the round border costs more than it conveys (§5.2). */
|
|
8
9
|
const BORDERLESS_COLS = 48;
|
|
9
10
|
/** Title + footer + top and bottom border. */
|
|
@@ -68,6 +69,23 @@ export function OverlayFrame(props) {
|
|
|
68
69
|
}
|
|
69
70
|
return (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: theme.primary, paddingX: 1, marginTop: 1, children: inner }));
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Rows one PgUp/PgDn - and one Shift+wheel notch - turns inside a controlled
|
|
74
|
+
* overlay (defect 11).
|
|
75
|
+
*
|
|
76
|
+
* THIS IS THE OVERLAY'S `scroll.pageSize`, deliberately the same rule: one window
|
|
77
|
+
* minus `PAGE_CONTEXT_LINES` of overlap, so a reader keeps two rows of context
|
|
78
|
+
* across a page turn exactly as `PgDn` in the transcript does. It replaces a
|
|
79
|
+
* constant 8, which was 42% of `/help`'s 19-row window on a 100x24 terminal and
|
|
80
|
+
* made reading a 93-row shortcut list a twelve-keystroke exercise.
|
|
81
|
+
*
|
|
82
|
+
* THE WINDOW HEIGHT COMES FROM `overlayBodyRows`, which is the same expression
|
|
83
|
+
* `OverlayFrame` renders with - computed twice here would be the two-authorities
|
|
84
|
+
* defect B1 is fixing one component over.
|
|
85
|
+
*/
|
|
86
|
+
export function overlayPageSize(maxRows, cols) {
|
|
87
|
+
return pageSize(overlayBodyRows(maxRows, cols));
|
|
88
|
+
}
|
|
71
89
|
/** Body rows shared by controlled overlays and self-managed focused lists. */
|
|
72
90
|
export function overlayBodyRows(maxRows, cols) {
|
|
73
91
|
if (!Number.isFinite(maxRows))
|
|
@@ -6,10 +6,17 @@ export const MIN_INDICATOR_COLS = 40;
|
|
|
6
6
|
export function ScrollIndicator({ rows, thumb, dragging = false, theme, caps, }) {
|
|
7
7
|
const glyphs = pickGlyphs(caps);
|
|
8
8
|
const count = Math.max(0, Math.floor(rows) || 0);
|
|
9
|
+
// NO CELLS WITHOUT A THUMB (defect 6). `thumb === null` means the content fits,
|
|
10
|
+
// so there is no position to mark and a full-height column of track glyphs is
|
|
11
|
+
// pure noise next to an empty transcript. The BOX below is still rendered
|
|
12
|
+
// whatever the thumb is — see the feedback-loop note at the top of this file:
|
|
13
|
+
// the reserved column, not the glyphs, is what keeps the width constant.
|
|
9
14
|
const cells = [];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
if (thumb) {
|
|
16
|
+
for (let i = 0; i < count; i += 1) {
|
|
17
|
+
const onThumb = i >= thumb.start && i < thumb.start + thumb.size;
|
|
18
|
+
cells.push(_jsx(Text, { color: onThumb ? theme.primary : theme.muted, bold: onThumb && dragging, children: onThumb ? glyphs.scrollThumb : glyphs.scrollTrack }, i));
|
|
19
|
+
}
|
|
13
20
|
}
|
|
14
21
|
return (_jsx(Box, { flexDirection: "column", flexShrink: 0, width: 1, children: cells }));
|
|
15
22
|
}
|
|
@@ -22,6 +22,21 @@ export interface FrameBudget {
|
|
|
22
22
|
}
|
|
23
23
|
/** Clamp the displayed draft, independently of any transcript or panel content. */
|
|
24
24
|
export declare function clampDraftRows(rows: number, draftRows: number): number;
|
|
25
|
+
/**
|
|
26
|
+
* The share of `popupMaxHeight` a completion menu may actually occupy (defect 10).
|
|
27
|
+
*
|
|
28
|
+
* `popupCapacity` answers "how much COULD the editor slot take", and its floor is
|
|
29
|
+
* one message row - right for a picker that owns the frame, wrong for a menu that
|
|
30
|
+
* opens over a conversation. At 20 rows the whole capacity is 13, which leaves one
|
|
31
|
+
* transcript row and no TODO rail at all (measured, `todo-app-layout.test.tsx`).
|
|
32
|
+
* Half keeps the menu growing with the terminal - 6 rows at 20, 11 at 30, 16 at 40 -
|
|
33
|
+
* while the conversation and the rail keep their rows.
|
|
34
|
+
*
|
|
35
|
+
* A divisor rather than a row count or a fraction of the terminal: it is the only
|
|
36
|
+
* form that stays derived from the frame budget instead of becoming a second
|
|
37
|
+
* authority beside it, and it cannot drift when `popupCapacity` changes.
|
|
38
|
+
*/
|
|
39
|
+
export declare const POPUP_CAPACITY_SHARE = 2;
|
|
25
40
|
/** Total menu capacity, including its borders; reserve at least one message row. */
|
|
26
41
|
export declare function popupCapacity(rows: number, draftRows: number, statusExpanded: boolean): number;
|
|
27
42
|
/** Normalize terminal dimensions once and conserve every visible frame row. */
|
package/dist/ui/layout/budget.js
CHANGED
|
@@ -8,6 +8,21 @@ export function clampDraftRows(rows, draftRows) {
|
|
|
8
8
|
const wanted = Number.isFinite(draftRows) ? Math.floor(draftRows) : 1;
|
|
9
9
|
return Math.max(1, Math.min(wanted, draftMaxRows(rows)));
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* The share of `popupMaxHeight` a completion menu may actually occupy (defect 10).
|
|
13
|
+
*
|
|
14
|
+
* `popupCapacity` answers "how much COULD the editor slot take", and its floor is
|
|
15
|
+
* one message row - right for a picker that owns the frame, wrong for a menu that
|
|
16
|
+
* opens over a conversation. At 20 rows the whole capacity is 13, which leaves one
|
|
17
|
+
* transcript row and no TODO rail at all (measured, `todo-app-layout.test.tsx`).
|
|
18
|
+
* Half keeps the menu growing with the terminal - 6 rows at 20, 11 at 30, 16 at 40 -
|
|
19
|
+
* while the conversation and the rail keep their rows.
|
|
20
|
+
*
|
|
21
|
+
* A divisor rather than a row count or a fraction of the terminal: it is the only
|
|
22
|
+
* form that stays derived from the frame budget instead of becoming a second
|
|
23
|
+
* authority beside it, and it cannot drift when `popupCapacity` changes.
|
|
24
|
+
*/
|
|
25
|
+
export const POPUP_CAPACITY_SHARE = 2;
|
|
11
26
|
/** Total menu capacity, including its borders; reserve at least one message row. */
|
|
12
27
|
export function popupCapacity(rows, draftRows, statusExpanded) {
|
|
13
28
|
return Math.max(0, rows - 1 - 1 - (statusExpanded ? 2 : 1)
|
|
@@ -36,6 +36,15 @@ export declare const HEADER_HINT_LIMITS: {
|
|
|
36
36
|
readonly minLeftCells: 24;
|
|
37
37
|
/** Fixed spacing between two hint clauses. */
|
|
38
38
|
readonly gapCells: 2;
|
|
39
|
+
/**
|
|
40
|
+
* Blank cells between the header's left half and this hint column (defect 12).
|
|
41
|
+
*
|
|
42
|
+
* A SEPARATE CONSTANT FROM `gapCells`. That one spaces two hint clauses INSIDE
|
|
43
|
+
* the right column (`buildPlan` below); this one separates the session
|
|
44
|
+
* description from that column. One constant carrying two meanings is how a
|
|
45
|
+
* later change to clause spacing silently re-lays-out the whole row.
|
|
46
|
+
*/
|
|
47
|
+
readonly separatorCells: 2;
|
|
39
48
|
};
|
|
40
49
|
export type HeaderHintId = 'branch' | 'index' | 'detail';
|
|
41
50
|
export interface HeaderHintClause {
|
|
@@ -36,6 +36,15 @@ export const HEADER_HINT_LIMITS = {
|
|
|
36
36
|
minLeftCells: 24,
|
|
37
37
|
/** Fixed spacing between two hint clauses. */
|
|
38
38
|
gapCells: 2,
|
|
39
|
+
/**
|
|
40
|
+
* Blank cells between the header's left half and this hint column (defect 12).
|
|
41
|
+
*
|
|
42
|
+
* A SEPARATE CONSTANT FROM `gapCells`. That one spaces two hint clauses INSIDE
|
|
43
|
+
* the right column (`buildPlan` below); this one separates the session
|
|
44
|
+
* description from that column. One constant carrying two meanings is how a
|
|
45
|
+
* later change to clause spacing silently re-lays-out the whole row.
|
|
46
|
+
*/
|
|
47
|
+
separatorCells: 2,
|
|
39
48
|
};
|
|
40
49
|
/** The verbatim clause text (spec 3.8, appendix A). */
|
|
41
50
|
const BRANCH_CLAUSE = { collapsed: '^L branch', expanded: '^L new branch' };
|
|
@@ -69,7 +78,11 @@ export function planHeaderHints(input) {
|
|
|
69
78
|
return buildPlan([{ id: 'detail', text: detail }], scrollCells, true);
|
|
70
79
|
}
|
|
71
80
|
const ladder = ladderFor(input.statusExpanded);
|
|
72
|
-
|
|
81
|
+
// THE SEPARATOR IS SPENT OUT OF THE SAME ROW (defect 12): left half + gap + this
|
|
82
|
+
// column have to add up to `columns`, so the gap comes off the budget instead of
|
|
83
|
+
// being assumed free.
|
|
84
|
+
const budget = columns - HEADER_HINT_LIMITS.minLeftCells
|
|
85
|
+
- HEADER_HINT_LIMITS.separatorCells;
|
|
73
86
|
for (const rung of ladder) {
|
|
74
87
|
const plan = buildPlan(rung, scrollCells, false);
|
|
75
88
|
if (plan.cells <= budget)
|
|
@@ -90,6 +90,47 @@ function feedbackLabel(input, form) {
|
|
|
90
90
|
};
|
|
91
91
|
return labels[feedback.kind][form === 'full' ? 0 : form === 'short' ? 1 : 2];
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Kinds that are NOTIFICATIONS, not a phase word (defect 8 / 9).
|
|
95
|
+
*
|
|
96
|
+
* A notification says "something happened". An ACTION kind (`confirm`,
|
|
97
|
+
* `stopping`, `force-stop`, `exit`, `clear-input`, `copy-error`,
|
|
98
|
+
* `copy-cleanup`) says what the NEXT key does - that IS the phase slot's job, so
|
|
99
|
+
* those keep replacing it. Everything in this set gets its own field instead and
|
|
100
|
+
* `phase` is never displaced: `Idle` / `Running` is the one thing this row must
|
|
101
|
+
* always answer, and a notice that lands as a turn ends is exactly when a wrong
|
|
102
|
+
* answer misleads the user about whether the agent is still working.
|
|
103
|
+
*/
|
|
104
|
+
const NOTIFICATION_KINDS = new Set([
|
|
105
|
+
'notice', 'copied', 'copy-sent', 'copying',
|
|
106
|
+
]);
|
|
107
|
+
/**
|
|
108
|
+
* The most cells a notification body may take on the primary row.
|
|
109
|
+
*
|
|
110
|
+
* The row is exactly one row high and measured, so the body is CLIPPED rather
|
|
111
|
+
* than wrapped. 32 cells carries `Theme set to light.` or `Todo panel off.`
|
|
112
|
+
* whole while leaving the phase word, the mode and the context gauge intact; a
|
|
113
|
+
* longer message still has the details row, where it is rendered in full.
|
|
114
|
+
*/
|
|
115
|
+
const FEEDBACK_TEXT_CELLS = 32;
|
|
116
|
+
/**
|
|
117
|
+
* Clip `text` to at most `cells` display columns, never splitting a wide glyph.
|
|
118
|
+
*
|
|
119
|
+
* Local rather than `interaction-hints.clipStatusText`: that module already
|
|
120
|
+
* imports THIS one, and a cycle for a five-line guard is not worth it.
|
|
121
|
+
*/
|
|
122
|
+
function clipFeedbackText(text, cells) {
|
|
123
|
+
const clean = cleanStatusText(text);
|
|
124
|
+
if (stringWidth(clean) <= cells)
|
|
125
|
+
return clean;
|
|
126
|
+
let clipped = '';
|
|
127
|
+
for (const char of clean) {
|
|
128
|
+
if (stringWidth(clipped + char) > cells)
|
|
129
|
+
break;
|
|
130
|
+
clipped += char;
|
|
131
|
+
}
|
|
132
|
+
return clipped;
|
|
133
|
+
}
|
|
93
134
|
const thinkingCodes = { off: 'O', minimal: 'N', low: 'L', medium: 'M', high: 'H', xhigh: 'X' };
|
|
94
135
|
/**
|
|
95
136
|
* The mode word for the primary row: full rungs name the mode with
|
|
@@ -127,15 +168,35 @@ export function planPrimaryStatusFields(input) {
|
|
|
127
168
|
const code = thinkingCodes[input.thinkingLevel] ?? '?';
|
|
128
169
|
const pct = contextPercent(input.context);
|
|
129
170
|
const speed = speedNumber(input);
|
|
171
|
+
// THE RUN'S OWN URGENCY, and the notification's, are two different things
|
|
172
|
+
// (defect 9). `tone` keeps today's meaning for an ACTION kind (it colours the
|
|
173
|
+
// phase word); a notification is painted with its own level and leaves the
|
|
174
|
+
// phase word to describe the run - a copy error must not make `Idle` red.
|
|
130
175
|
const tone = input.feedback?.level === 'error' || input.runOutcome === 'failed'
|
|
131
176
|
? 'error' : input.feedback?.level === 'warn' || input.waitingForConfirmation || input.stopping ? 'warning' : 'normal';
|
|
177
|
+
const notification = !!input.feedback && NOTIFICATION_KINDS.has(input.feedback.kind);
|
|
178
|
+
const phaseTone = notification
|
|
179
|
+
? input.runOutcome === 'failed' ? 'error'
|
|
180
|
+
: input.waitingForConfirmation || input.stopping ? 'warning' : 'normal'
|
|
181
|
+
: tone;
|
|
182
|
+
const feedbackTone = input.feedback?.level === 'error' ? 'error'
|
|
183
|
+
: input.feedback?.level === 'warn' ? 'warning' : 'normal';
|
|
132
184
|
const build = (rung) => {
|
|
133
185
|
const full = rung === 0;
|
|
134
|
-
const
|
|
186
|
+
const form = full ? 'full' : rung >= 4 ? 'tiny' : 'short';
|
|
187
|
+
// THE BODY IS A FIELD OF ITS OWN (defect 8). The text has been in
|
|
188
|
+
// `feedback.text` all along; it only ever reached the screen on the SECOND
|
|
189
|
+
// status row, which is behind Ctrl+G and off by default. Rung 3 and tighter
|
|
190
|
+
// drop it: the ladder exists to keep the phase word, and a body that only fits
|
|
191
|
+
// by hiding `Running` would be the original defect back with extra steps.
|
|
192
|
+
const notice = notification && rung <= 2 && input.feedback
|
|
193
|
+
? clipFeedbackText(input.feedback.text, FEEDBACK_TEXT_CELLS) : '';
|
|
194
|
+
const label = (notification ? undefined : feedbackLabel(input, form))
|
|
135
195
|
?? (full ? labels[1] : rung >= 4 ? labels[0].slice(0, 4) : labels[0]);
|
|
136
196
|
const modeText = modeFieldText(input.mode, input.pendingMode, full);
|
|
137
197
|
return statusLinePlan([
|
|
138
|
-
statusField('phase', glyph + (rung >= 5 ? '' : ' ') + label,
|
|
198
|
+
statusField('phase', glyph + (rung >= 5 ? '' : ' ') + label, phaseTone),
|
|
199
|
+
...(notice ? [statusField('feedback', notice, feedbackTone)] : []),
|
|
139
200
|
...(modeText ? [statusField('mode', modeText, 'accent')] : []),
|
|
140
201
|
statusField('context', (full ? 'Context ' : 'C') + pct),
|
|
141
202
|
statusField('thinking', full ? 'Think ' + (thinkingCodes[input.thinkingLevel] ? input.thinkingLevel : '?')
|
|
@@ -32,9 +32,11 @@
|
|
|
32
32
|
* `selectWindow` takes `offset` as an INPUT, would close a loop from the offset
|
|
33
33
|
* through the mounted set and the measured heights back to the offset.
|
|
34
34
|
*/
|
|
35
|
+
import stringWidth from 'string-width';
|
|
35
36
|
import { FAST_LIMITS } from '../../fast/limits.js';
|
|
36
37
|
import { COMPACTION_LIMITS } from '../../compaction/limits.js';
|
|
37
38
|
import { GUTTER_WIDTH } from './Gutter.js';
|
|
39
|
+
import { expandTabs } from '../display-tab.js';
|
|
38
40
|
import { USER_ENTRY_MAX_ROWS } from '../composer-limits.js';
|
|
39
41
|
export const VIRTUAL_LIMITS = {
|
|
40
42
|
/**
|
|
@@ -159,21 +161,52 @@ export function entryRevision(entry) {
|
|
|
159
161
|
export function heightKey(entry, cols, expanded, density, flags = '') {
|
|
160
162
|
return `${entry.id}|${entryRevision(entry)}|${cols}|${expanded ? 1 : 0}|${density}|${flags}`;
|
|
161
163
|
}
|
|
162
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* Rows a string occupies at `usable` columns, wrapping included. At least 1.
|
|
166
|
+
*
|
|
167
|
+
* COUNTED IN DISPLAY COLUMNS, NOT UTF-16 CODE UNITS, and the difference is a
|
|
168
|
+
* correctness bug rather than a nicety: a CJK glyph is one code unit and two
|
|
169
|
+
* cells, and a TAB is one code unit and up to `TAB_WIDTH` cells. Counting code
|
|
170
|
+
* units therefore UNDER-estimates, which this file's own note names as the
|
|
171
|
+
* unsafe direction ("an over-estimate makes the scroll position slightly
|
|
172
|
+
* conservative and self-corrects ... an under-estimate can put the newest
|
|
173
|
+
* output off the bottom of a viewport the user believes is pinned"). It also
|
|
174
|
+
* had to change together with the renderers: a stored `\t` is now expanded at
|
|
175
|
+
* paint time, so measuring the unexpanded string would keep the estimate short
|
|
176
|
+
* while the painted row got taller.
|
|
177
|
+
*/
|
|
163
178
|
function wrappedRows(text, usable) {
|
|
164
179
|
const width = Math.max(1, usable);
|
|
165
180
|
let rows = 0;
|
|
166
|
-
let
|
|
167
|
-
for (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
181
|
+
let start = 0;
|
|
182
|
+
for (;;) {
|
|
183
|
+
const end = text.indexOf('\n', start);
|
|
184
|
+
const line = end === -1 ? text.slice(start) : text.slice(start, end);
|
|
185
|
+
rows += Math.max(1, Math.ceil(displayColumns(line) / width));
|
|
186
|
+
if (end === -1)
|
|
187
|
+
return rows;
|
|
188
|
+
start = end + 1;
|
|
175
189
|
}
|
|
176
|
-
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Printable ASCII with no TAB: one code unit is one cell, so the width table is
|
|
193
|
+
* not consulted at all.
|
|
194
|
+
*
|
|
195
|
+
* THE FAST PATH IS LOAD-BEARING, not a micro-optimisation. `stringWidth`
|
|
196
|
+
* strips ANSI and walks the East-Asian width table for every code point, and
|
|
197
|
+
* this function runs over EVERY unmeasured entry of a 10 000-entry document
|
|
198
|
+
* (`render-budget.test.tsx` mounts exactly that); measuring them all with the
|
|
199
|
+
* table turned that test into a timeout. Transcript text is overwhelmingly
|
|
200
|
+
* ASCII, so the branch is taken almost always.
|
|
201
|
+
*/
|
|
202
|
+
const PLAIN_ASCII = /^[\x20-\x7e]*$/;
|
|
203
|
+
/** Display columns of one logical line: TABs expanded, CJK counted twice. */
|
|
204
|
+
function displayColumns(line) {
|
|
205
|
+
if (line.indexOf('\t') === -1 && PLAIN_ASCII.test(line))
|
|
206
|
+
return line.length;
|
|
207
|
+
// `expandTabs` mirrors what `entries/*` paints; `stringWidth` is the width
|
|
208
|
+
// table the composer already wraps with, so the two agree on CJK and emoji.
|
|
209
|
+
return stringWidth(expandTabs(line));
|
|
177
210
|
}
|
|
178
211
|
/** Collapsed preview rows in `ToolCard`; duplicated as a NUMBER, not imported. */
|
|
179
212
|
const TOOL_COLLAPSED_LINES = 8;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Markdown parsing layer (spec §4.7). Pure and React-free so the grammar can be
|
|
3
|
-
* unit-tested without rendering anything.
|
|
3
|
+
* unit-tested without rendering anything. `string-width` is the only import and
|
|
4
|
+
* it is a pure function of its argument, so the module stays side-effect free.
|
|
4
5
|
*
|
|
5
6
|
* Scope is deliberately small — this is not CommonMark. It covers what a coding
|
|
6
7
|
* agent's answers actually contain: fenced code, headings, lists, quotes, rules,
|
|
@@ -31,11 +32,31 @@ export declare function parseTableRow(line: string): string[] | null;
|
|
|
31
32
|
export declare function isTableDivider(line: string): boolean;
|
|
32
33
|
export declare function tableAlignments(dividerLine: string): TableBlock['align'];
|
|
33
34
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* How the table is drawn. Both characters are injected by the renderer from
|
|
36
|
+
* `pickGlyphs(caps)` because this file is inside the ASCII-only scan
|
|
37
|
+
* (`glyphs.test.ts::inScope`): a Unicode box-drawing literal here would fail
|
|
38
|
+
* that guard.
|
|
37
39
|
*/
|
|
38
|
-
export
|
|
40
|
+
export interface TableFormatOptions {
|
|
41
|
+
/** Drawn between cells; `Markdown.tsx` passes `glyphs.railVertical`. */
|
|
42
|
+
separator?: string;
|
|
43
|
+
/** Repeated to a column's padded width; `Markdown.tsx` passes `glyphs.hrule`. */
|
|
44
|
+
rule?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Pad a parsed table into fixed-width columns and draw the header rule.
|
|
48
|
+
*
|
|
49
|
+
* Returns `[header, divider, ...bodyRows]`, one string per rendered row, so
|
|
50
|
+
* index 0 is always the header and index 1 always the divider. The divider is
|
|
51
|
+
* rendered rather than swallowed because the parser consumes the source
|
|
52
|
+
* `|---|` row to read alignment: without it, a `--no-color` terminal showed a
|
|
53
|
+
* table as unseparated prose (bold is dropped at colour level 0, and the old
|
|
54
|
+
* two-space join gave no column boundary either).
|
|
55
|
+
*
|
|
56
|
+
* Padding is by DISPLAY columns, not `String.length`: a CJK cell is one code
|
|
57
|
+
* unit but two terminal columns, so `.length` misaligned every such table.
|
|
58
|
+
*/
|
|
59
|
+
export declare function formatTable(rows: string[][], align: TableBlock['align'], options?: TableFormatOptions): string[];
|
|
39
60
|
/**
|
|
40
61
|
* One parsed top-level block.
|
|
41
62
|
*
|