@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
|
@@ -1,11 +1,13 @@
|
|
|
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,
|
|
7
8
|
* tables, and inline emphasis / code / strikethrough.
|
|
8
9
|
*/
|
|
10
|
+
import stringWidth from 'string-width';
|
|
9
11
|
const HEADING_RE = /^(#{1,6})\s+(.*)$/;
|
|
10
12
|
/** `---`, `***` or `___`, three or more, nothing else on the line. */
|
|
11
13
|
const RULE_RE = /^(?:-{3,}|\*{3,}|_{3,})$/;
|
|
@@ -46,53 +48,128 @@ export function tableAlignments(dividerLine) {
|
|
|
46
48
|
return 'left';
|
|
47
49
|
});
|
|
48
50
|
}
|
|
51
|
+
/** Default column separator: pure ASCII, so this module holds no glyph literal. */
|
|
52
|
+
const DEFAULT_TABLE_SEPARATOR = '|';
|
|
53
|
+
/** Default header rule: pure ASCII for the same reason. */
|
|
54
|
+
const DEFAULT_TABLE_RULE = '-';
|
|
55
|
+
/** One space either side of the separator, so a boundary is unmistakable. */
|
|
56
|
+
const TABLE_CELL_GAP = ' ';
|
|
49
57
|
/**
|
|
50
|
-
* Pad a parsed table into fixed-width columns
|
|
51
|
-
*
|
|
52
|
-
*
|
|
58
|
+
* Pad a parsed table into fixed-width columns and draw the header rule.
|
|
59
|
+
*
|
|
60
|
+
* Returns `[header, divider, ...bodyRows]`, one string per rendered row, so
|
|
61
|
+
* index 0 is always the header and index 1 always the divider. The divider is
|
|
62
|
+
* rendered rather than swallowed because the parser consumes the source
|
|
63
|
+
* `|---|` row to read alignment: without it, a `--no-color` terminal showed a
|
|
64
|
+
* table as unseparated prose (bold is dropped at colour level 0, and the old
|
|
65
|
+
* two-space join gave no column boundary either).
|
|
66
|
+
*
|
|
67
|
+
* Padding is by DISPLAY columns, not `String.length`: a CJK cell is one code
|
|
68
|
+
* unit but two terminal columns, so `.length` misaligned every such table.
|
|
53
69
|
*/
|
|
54
|
-
export function formatTable(rows, align) {
|
|
70
|
+
export function formatTable(rows, align, options = {}) {
|
|
71
|
+
const separator = options.separator ?? DEFAULT_TABLE_SEPARATOR;
|
|
72
|
+
const rule = options.rule ?? DEFAULT_TABLE_RULE;
|
|
55
73
|
const colCount = Math.max(...rows.map((r) => r.length), 0);
|
|
56
74
|
const widths = [];
|
|
57
75
|
for (let c = 0; c < colCount; c += 1) {
|
|
58
|
-
widths[c] = Math.max(...rows.map((r) => (r[c] ?? '')
|
|
76
|
+
widths[c] = Math.max(...rows.map((r) => displayColumns(r[c] ?? '')), 0);
|
|
59
77
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
const gap = `${TABLE_CELL_GAP}${separator}${TABLE_CELL_GAP}`;
|
|
79
|
+
const body = rows.map((row) => padRow(row, widths, align).join(gap).trimEnd());
|
|
80
|
+
if (body.length === 0)
|
|
81
|
+
return [];
|
|
82
|
+
const divider = widths.map((w) => rule.repeat(w)).join(gap).trimEnd();
|
|
83
|
+
return [body[0], divider, ...body.slice(1)];
|
|
84
|
+
}
|
|
85
|
+
function padRow(row, widths, align) {
|
|
86
|
+
return row.map((cell, c) => padCell(cell, widths[c] ?? 0, align[c] ?? 'left'));
|
|
87
|
+
}
|
|
88
|
+
function padCell(cell, width, align) {
|
|
89
|
+
const pad = width - displayColumns(cell);
|
|
90
|
+
if (pad <= 0)
|
|
91
|
+
return cell;
|
|
92
|
+
const filler = ' '.repeat(pad);
|
|
93
|
+
if (align === 'right')
|
|
94
|
+
return filler + cell;
|
|
95
|
+
if (align === 'center') {
|
|
96
|
+
const left = Math.floor(pad / 2);
|
|
97
|
+
return `${' '.repeat(left)}${cell}${' '.repeat(pad - left)}`;
|
|
98
|
+
}
|
|
99
|
+
return cell + filler;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Printable ASCII with no TAB: one code unit is one cell, so the width table is
|
|
103
|
+
* not consulted at all.
|
|
104
|
+
*
|
|
105
|
+
* The same load-bearing fast path `virtual-window.displayColumns` carries, and for
|
|
106
|
+
* the same reason: `stringWidth` strips ANSI and walks the East-Asian width table
|
|
107
|
+
* per code point, while this runs once per CELL of every table row on every frame.
|
|
108
|
+
* Table cells are overwhelmingly ASCII, so the branch is taken almost always -
|
|
109
|
+
* and when it is not, the table is CJK and the table is exactly what must be used.
|
|
110
|
+
*/
|
|
111
|
+
const PLAIN_ASCII = /^[\x20-\x7e]*$/;
|
|
112
|
+
/** Terminal columns occupied by `text` (wide glyphs count two). */
|
|
113
|
+
function displayColumns(text) {
|
|
114
|
+
return PLAIN_ASCII.test(text) ? text.length : stringWidth(text);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* An opening fence is three or more backticks plus an optional info string.
|
|
118
|
+
*
|
|
119
|
+
* The old `(\w*)` info group was too narrow for real answers: ```c++ and
|
|
120
|
+
* ```objective-c did not open a block at all, so the body degraded to prose and
|
|
121
|
+
* the closing fence was then swallowed as a new empty code block. A backtick is
|
|
122
|
+
* the one character CommonMark forbids in an info string; everything else is
|
|
123
|
+
* accepted here and the language is simply the FIRST token.
|
|
124
|
+
*/
|
|
125
|
+
const FENCE_OPEN_RE = /^(`{3,})[ \t]*([^`]*)$/;
|
|
126
|
+
/** A closing fence carries backticks only — no info string. */
|
|
127
|
+
const FENCE_CLOSE_RE = /^(`{3,})[ \t]*$/;
|
|
128
|
+
function parseFenceOpen(line) {
|
|
129
|
+
const m = FENCE_OPEN_RE.exec(line);
|
|
130
|
+
if (!m)
|
|
131
|
+
return null;
|
|
132
|
+
const info = (m[2] ?? '').trim();
|
|
133
|
+
return { count: m[1].length, lang: info.split(/\s+/)[0] ?? '' };
|
|
134
|
+
}
|
|
135
|
+
/** Backtick count of a line that could close a fence; 0 when it could not. */
|
|
136
|
+
function closingFenceLength(line) {
|
|
137
|
+
const m = FENCE_CLOSE_RE.exec(line);
|
|
138
|
+
return m ? m[1].length : 0;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Collect a fence body starting after the opening fence.
|
|
142
|
+
*
|
|
143
|
+
* A closer must carry AT LEAST as many backticks as its opener, so a
|
|
144
|
+
* four-backtick block may contain ``` lines — that is how a nested markdown
|
|
145
|
+
* example survives. A shorter run is content, not the end of the block.
|
|
146
|
+
*/
|
|
147
|
+
function readFenceBody(lines, openIndex, openCount) {
|
|
148
|
+
const body = [];
|
|
149
|
+
let i = openIndex + 1;
|
|
150
|
+
while (i < lines.length) {
|
|
151
|
+
if (closingFenceLength((lines[i] ?? '').trim()) >= openCount) {
|
|
152
|
+
return { code: body.join('\n'), next: i + 1 };
|
|
70
153
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
154
|
+
body.push(lines[i] ?? '');
|
|
155
|
+
i += 1;
|
|
156
|
+
}
|
|
157
|
+
return { code: body.join('\n'), next: i };
|
|
75
158
|
}
|
|
76
|
-
/** A fence line: ```` ```lang ```` or a bare ```` ``` ````. */
|
|
77
|
-
const FENCE_RE = /^```(\w*)\s*$/;
|
|
78
159
|
/** Split a document into fenced code, pipe tables, and everything else. */
|
|
79
160
|
export function parseMarkdownBlocks(text) {
|
|
80
|
-
|
|
161
|
+
// CRLF-safe: a bare `split('\n')` left a '\r' on every code line, which then
|
|
162
|
+
// reached the highlighter and the display-width maths.
|
|
163
|
+
const lines = text.split(/\r?\n/);
|
|
81
164
|
const blocks = [];
|
|
82
165
|
let i = 0;
|
|
83
166
|
while (i < lines.length) {
|
|
84
167
|
const line = lines[i] ?? '';
|
|
85
|
-
const fence =
|
|
168
|
+
const fence = parseFenceOpen(line.trim());
|
|
86
169
|
if (fence) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
i
|
|
90
|
-
while (i < lines.length && !FENCE_RE.test((lines[i] ?? '').trim())) {
|
|
91
|
-
codeLines.push(lines[i] ?? '');
|
|
92
|
-
i += 1;
|
|
93
|
-
}
|
|
94
|
-
i += 1; // skip the closing fence
|
|
95
|
-
blocks.push({ kind: 'code', code: codeLines.join('\n'), lang });
|
|
170
|
+
const { code, next } = readFenceBody(lines, i, fence.count);
|
|
171
|
+
blocks.push({ kind: 'code', code, lang: fence.lang });
|
|
172
|
+
i = next;
|
|
96
173
|
continue;
|
|
97
174
|
}
|
|
98
175
|
// A pipe row followed by a divider row starts a table; consume the run.
|
|
@@ -82,6 +82,10 @@ export declare function hasPasteFrame(input: string): boolean;
|
|
|
82
82
|
* nothing on screen to explain it.
|
|
83
83
|
*
|
|
84
84
|
* This is strictly better than v0.6.3 for both consumers: a pasted key carrying a
|
|
85
|
-
* stray CR or an ANSI sequence is now cleaned rather than stored raw.
|
|
85
|
+
* stray CR or an ANSI sequence is now cleaned rather than stored raw. The
|
|
86
|
+
* unframed branch strips escape SEQUENCES before the control sweep, because
|
|
87
|
+
* deleting only the introducer left `[0m` in the stored value -- and in a
|
|
88
|
+
* settings field that residue is invisible behind `maskDot` while the value
|
|
89
|
+
* handed to `registerSecret()` is silently wrong.
|
|
86
90
|
*/
|
|
87
91
|
export declare function stripPasteFrames(input: string): string;
|
package/dist/ui/paste-frames.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { PASTE_CLOSE, PASTE_OPEN } from '../input/limits.js';
|
|
9
9
|
import { sanitisePaste } from '../input/paste-parse.js';
|
|
10
|
+
import { stripControlSequences } from '../input/control-sequences.js';
|
|
10
11
|
/**
|
|
11
12
|
* Remove every C0 byte, DEL and C1 byte from text that arrived OUTSIDE a frame
|
|
12
13
|
* (I-14). NO CR -> LF conversion, and that omission is the point.
|
|
@@ -101,11 +102,15 @@ export function hasPasteFrame(input) {
|
|
|
101
102
|
* nothing on screen to explain it.
|
|
102
103
|
*
|
|
103
104
|
* This is strictly better than v0.6.3 for both consumers: a pasted key carrying a
|
|
104
|
-
* stray CR or an ANSI sequence is now cleaned rather than stored raw.
|
|
105
|
+
* stray CR or an ANSI sequence is now cleaned rather than stored raw. The
|
|
106
|
+
* unframed branch strips escape SEQUENCES before the control sweep, because
|
|
107
|
+
* deleting only the introducer left `[0m` in the stored value -- and in a
|
|
108
|
+
* settings field that residue is invisible behind `maskDot` while the value
|
|
109
|
+
* handed to `registerSecret()` is silently wrong.
|
|
105
110
|
*/
|
|
106
111
|
export function stripPasteFrames(input) {
|
|
107
112
|
if (!hasPasteFrame(input))
|
|
108
|
-
return sanitiseTyped(input);
|
|
113
|
+
return sanitiseTyped(stripControlSequences(input));
|
|
109
114
|
return splitPasteFrames(input)
|
|
110
115
|
.map((segment) => (segment.kind === 'paste' ? sanitiseTyped(segment.text) : segment.text))
|
|
111
116
|
.join('');
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { formatCost, formatDuration, formatTokens, promptTokensOf } from '../agent/usage.js';
|
|
2
2
|
import { formatRetryLine } from '../agent/retry-view.js';
|
|
3
|
+
import { expandTabs } from './display-tab.js';
|
|
3
4
|
const DEFAULT_MAX_ENTRIES = 200;
|
|
4
5
|
const CONTINUATION_INDENT = ' ';
|
|
5
6
|
const TOOL_INDENT = ' ';
|
|
6
7
|
const MAX_ARG_CHARS = 60;
|
|
7
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Prefix the first line with `marker`, indent the rest to line up under it.
|
|
10
|
+
*
|
|
11
|
+
* `expandTabs` RUNS BEFORE THE PREFIX IS ADDED, so a stored line lands on the
|
|
12
|
+
* same columns the TUI drew it at. Without it this replay -- the only record
|
|
13
|
+
* left once the alternate screen is torn down -- disagreed with the screen
|
|
14
|
+
* about every TAB, because the terminal painted an eight-column stop under text
|
|
15
|
+
* the composer had laid out at four.
|
|
16
|
+
*/
|
|
8
17
|
function block(marker, text) {
|
|
9
|
-
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
18
|
+
const lines = expandTabs(text.replace(/\r\n/g, '\n')).split('\n');
|
|
10
19
|
const head = `${marker} ${lines[0] ?? ''}`.trimEnd();
|
|
11
20
|
const rest = lines.slice(1).map((l) => `${CONTINUATION_INDENT}${l}`.trimEnd());
|
|
12
21
|
return [head, ...rest];
|
|
@@ -22,13 +22,6 @@
|
|
|
22
22
|
import type { Overlay } from '../agent/reducer.js';
|
|
23
23
|
import type { MouseSource } from '../input/stdin-filter.js';
|
|
24
24
|
import type { ScrollIntent } from './layout/scroll.js';
|
|
25
|
-
/**
|
|
26
|
-
* Rows a PgUp/PgDn — and a Shift+wheel notch — moves inside a controlled
|
|
27
|
-
* overlay. Declared here rather than in `App.tsx` because both the keyboard
|
|
28
|
-
* branch and the wheel branch need it and `App` imports this module; the other
|
|
29
|
-
* direction would be a cycle.
|
|
30
|
-
*/
|
|
31
|
-
export declare const OVERLAY_PAGE = 8;
|
|
32
25
|
export interface WheelRoutingOptions {
|
|
33
26
|
isPointerCaptured?: () => boolean;
|
|
34
27
|
isActive?: () => boolean;
|
|
@@ -39,6 +32,16 @@ export interface WheelRoutingOptions {
|
|
|
39
32
|
onScroll: (kind: ScrollIntent, repeat: number) => void;
|
|
40
33
|
/** Signed row delta; the caller clamps at 0 the way the keyboard branch does. */
|
|
41
34
|
onOverlayScroll: (delta: number) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Rows one Shift+wheel notch turns inside a controlled overlay (defect 11).
|
|
37
|
+
*
|
|
38
|
+
* A GETTER, read at event time for the same reason `getOverlay` is: the window
|
|
39
|
+
* height changes with the terminal, with `statusExpanded` and with the rail, and
|
|
40
|
+
* a value captured when the subscription was built is the value that goes stale.
|
|
41
|
+
* `App` answers with `overlayPageSize(...)`, the same function its keyboard
|
|
42
|
+
* branch uses, so the wheel and `PgDn` can never disagree.
|
|
43
|
+
*/
|
|
44
|
+
getOverlayPage?: () => number;
|
|
42
45
|
}
|
|
43
46
|
export interface WheelRouting {
|
|
44
47
|
/**
|
|
@@ -21,12 +21,15 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { useCallback, useEffect, useRef } from 'react';
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* Page rows used ONLY when the host supplies no `getOverlayPage`.
|
|
25
|
+
*
|
|
26
|
+
* `App` always supplies one (defect 11): the step is the overlay's visible window
|
|
27
|
+
* minus two rows of context, so `PgDn` in `/help` turns a page instead of 42% of
|
|
28
|
+
* one. This fallback exists so a host that owns no overlay window - this module's
|
|
29
|
+
* own routing tests are the only such host - still gets a page rather than
|
|
30
|
+
* silently degrading a Shift+wheel to a three-row notch.
|
|
28
31
|
*/
|
|
29
|
-
|
|
32
|
+
const FALLBACK_OVERLAY_PAGE = 8;
|
|
30
33
|
/** Transcript rows per notch. The terminal's own convention. */
|
|
31
34
|
const CONTENT_LINES_PER_NOTCH = 3;
|
|
32
35
|
/** Overlay rows per notch. Overlay content is short; a page needs Shift. */
|
|
@@ -113,7 +116,9 @@ export function useWheelRouting(options) {
|
|
|
113
116
|
// D-3: the row is not consulted here either. Behind an overlay the
|
|
114
117
|
// composer is inactive and the overlay is the only scrollable thing on
|
|
115
118
|
// screen, so a notch anywhere scrolls it.
|
|
116
|
-
const step = event.shift
|
|
119
|
+
const step = event.shift
|
|
120
|
+
? current.getOverlayPage?.() ?? FALLBACK_OVERLAY_PAGE
|
|
121
|
+
: OVERLAY_LINES_PER_NOTCH;
|
|
117
122
|
current.onOverlayScroll(event.dir === 'up' ? -step : step);
|
|
118
123
|
return;
|
|
119
124
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aragon-agent/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.29",
|
|
4
4
|
"description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AragonAgent contributors",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"prepublishOnly": "npm run build"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@aragon-agent/core": "^0.2.
|
|
59
|
+
"@aragon-agent/core": "^0.2.41",
|
|
60
60
|
"chalk": "^5.6.2",
|
|
61
61
|
"cli-highlight": "^2.0.0",
|
|
62
62
|
"commander": "^12.1.0",
|