@aragon-agent/cli 0.6.28 → 0.6.31
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 +2123 -1975
- package/README.md +81 -14
- package/dist/agent/project-guidance-prompt.js +20 -20
- package/dist/agent/reducer.js +47 -2
- package/dist/cli.js +169 -56
- package/dist/commands/builtins.js +4 -0
- package/dist/commands/perf.d.ts +25 -1
- package/dist/commands/perf.js +26 -2
- package/dist/commands/registry.d.ts +6 -0
- package/dist/config/env.js +4 -0
- package/dist/config/load.d.ts +3 -1
- package/dist/config/load.js +12 -1
- package/dist/config/schema.d.ts +27 -0
- package/dist/config/schema.js +9 -0
- package/dist/config/ui-state.d.ts +9 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/input/limits.d.ts +20 -0
- package/dist/input/limits.js +20 -0
- package/dist/input/stdin-filter.js +68 -5
- package/dist/ui/App.d.ts +11 -1
- package/dist/ui/App.js +155 -67
- 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/ScrollbackShell.d.ts +28 -0
- package/dist/ui/ScrollbackShell.js +10 -0
- package/dist/ui/Transcript.js +7 -0
- package/dist/ui/frame-differ.d.ts +12 -0
- 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/markdown-blocks.d.ts +26 -5
- package/dist/ui/markdown-blocks.js +110 -33
- package/dist/ui/overlays/HelpOverlay.d.ts +1 -1
- package/dist/ui/overlays/HelpOverlay.js +21 -3
- package/dist/ui/screen.d.ts +16 -0
- package/dist/ui/screen.js +32 -0
- package/dist/ui/scrollback/budget.d.ts +43 -0
- package/dist/ui/scrollback/budget.js +44 -0
- package/dist/ui/scrollback/drain.d.ts +32 -0
- package/dist/ui/scrollback/drain.js +64 -0
- package/dist/ui/scrollback/exit-summary.d.ts +23 -0
- package/dist/ui/scrollback/exit-summary.js +25 -0
- package/dist/ui/scrollback/limits.d.ts +25 -0
- package/dist/ui/scrollback/limits.js +25 -0
- package/dist/ui/scrollback/live-preview.d.ts +26 -0
- package/dist/ui/scrollback/live-preview.js +106 -0
- package/dist/ui/scrollback/renderer.d.ts +38 -0
- package/dist/ui/scrollback/renderer.js +158 -0
- package/dist/ui/scrollback/scrollback-writer.d.ts +74 -0
- package/dist/ui/scrollback/scrollback-writer.js +263 -0
- package/dist/ui/scrollback/settled-boundary.d.ts +42 -0
- package/dist/ui/scrollback/settled-boundary.js +106 -0
- package/dist/ui/scrollback/use-scrollback-view.d.ts +92 -0
- package/dist/ui/scrollback/use-scrollback-view.js +263 -0
- package/dist/ui/scrollback/writer-shapes.d.ts +61 -0
- package/dist/ui/scrollback/writer-shapes.js +114 -0
- package/dist/ui/stdout-frame-writer.js +12 -2
- package/dist/ui/terminal-plan.d.ts +52 -0
- package/dist/ui/terminal-plan.js +49 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +21 -7
- package/dist/ui/use-startup-notices.d.ts +12 -1
- package/dist/ui/use-startup-notices.js +28 -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
- package/skills/project-indexer/SKILL.md +163 -163
- package/skills/project-indexer/templates/cleancode-overrides.md +46 -46
- package/skills/project-indexer/templates/cleancode-template.md +94 -94
- package/skills/project-indexer/templates/config-template.md +69 -69
- package/skills/project-indexer/templates/index-template.md +129 -129
- package/skills/skill-creator/SKILL.md +96 -96
|
@@ -150,5 +150,17 @@ export interface FrameDiffer {
|
|
|
150
150
|
* appears one frame later instead of never.
|
|
151
151
|
*/
|
|
152
152
|
repaint(): string;
|
|
153
|
+
/**
|
|
154
|
+
* The stream was resized. Optional (tui-native-terminal-scroll §5.1): the
|
|
155
|
+
* full-screen differ does not implement it and is invalidated instead, which is
|
|
156
|
+
* its existing behaviour; the scrollback writer keeps its cache for a reflow
|
|
157
|
+
* estimate.
|
|
158
|
+
*/
|
|
159
|
+
onResize?(): void;
|
|
160
|
+
/**
|
|
161
|
+
* An own write that moves neither a cell nor the cursor (OSC 52 copy). Optional
|
|
162
|
+
* for the same reason: absent, the write invalidates the cache as before.
|
|
163
|
+
*/
|
|
164
|
+
cursorNeutralWrite?(): void;
|
|
153
165
|
}
|
|
154
166
|
export declare function createFrameDiffer(options: FrameDifferOptions): FrameDiffer;
|
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 : '?')
|
|
@@ -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
|
*
|
|
@@ -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.
|
|
@@ -14,4 +14,4 @@ import React from 'react';
|
|
|
14
14
|
import type { Theme } from '../theme.js';
|
|
15
15
|
import type { TermCapabilities } from '../capabilities.js';
|
|
16
16
|
/** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
|
|
17
|
-
export declare function helpRows(theme: Theme, caps: TermCapabilities): React.ReactElement[];
|
|
17
|
+
export declare function helpRows(theme: Theme, caps: TermCapabilities, native?: boolean): React.ReactElement[];
|
|
@@ -2,7 +2,25 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Text } from 'ink';
|
|
3
3
|
import { pickGlyphs } from '../glyphs.js';
|
|
4
4
|
const KEY_PAD = 24;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Rows that only make sense when the APPLICATION scrolls and selects. In native
|
|
7
|
+
* scroll mode (tui-native-terminal-scroll §4.7) the terminal does both, so they are
|
|
8
|
+
* dropped and one honest row replaces them.
|
|
9
|
+
*/
|
|
10
|
+
const APP_SCROLL_ROWS = new Set([
|
|
11
|
+
'Shift+Up / Shift+Down', 'Wheel', 'Shift+Wheel', 'Drag right edge', '^N',
|
|
12
|
+
'Drag text (left button)', 'Ctrl+C (with a selection)',
|
|
13
|
+
]);
|
|
14
|
+
function keyRows(times, native = false) {
|
|
15
|
+
const all = allKeyRows(times);
|
|
16
|
+
if (!native)
|
|
17
|
+
return all;
|
|
18
|
+
return all
|
|
19
|
+
.filter(([key]) => !APP_SCROLL_ROWS.has(key))
|
|
20
|
+
.map(([key, description]) => key === 'PgUp / PgDn' ? [key, 'Page through an open overlay'] : [key, description])
|
|
21
|
+
.concat([['Scroll', 'Use your terminal (wheel, Shift+PgUp, scrollbar, search)']]);
|
|
22
|
+
}
|
|
23
|
+
function allKeyRows(times) {
|
|
6
24
|
return [
|
|
7
25
|
['Enter', 'Submit (idle) / queue steering (running); re-pins to the newest output'],
|
|
8
26
|
['Alt+Enter / Shift+Enter', 'Newline when supported; the host may intercept either binding'],
|
|
@@ -133,13 +151,13 @@ const FAST_ROWS = [
|
|
|
133
151
|
['<fast_review>', 'An automated second opinion from the fast model - advice, not the user'],
|
|
134
152
|
];
|
|
135
153
|
/** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
|
|
136
|
-
export function helpRows(theme, caps) {
|
|
154
|
+
export function helpRows(theme, caps, native = false) {
|
|
137
155
|
const glyphs = pickGlyphs(caps);
|
|
138
156
|
const rows = [];
|
|
139
157
|
const section = (title) => (_jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: title }, `s-${title}`));
|
|
140
158
|
const pair = (k, d) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.primary, children: k.padEnd(KEY_PAD) }), _jsx(Text, { color: theme.muted, children: d })] }, `r-${k}`));
|
|
141
159
|
rows.push(section('Keybindings'));
|
|
142
|
-
for (const [k, d] of keyRows(glyphs.times))
|
|
160
|
+
for (const [k, d] of keyRows(glyphs.times, native))
|
|
143
161
|
rows.push(pair(k, d));
|
|
144
162
|
rows.push(section('Slash commands'));
|
|
145
163
|
for (const [c, d] of COMMANDS)
|
package/dist/ui/screen.d.ts
CHANGED
|
@@ -71,6 +71,22 @@ export interface AltScreenOptions {
|
|
|
71
71
|
* CI) — writing screen-control sequences into a redirected file corrupts it.
|
|
72
72
|
*/
|
|
73
73
|
export declare function enterAltScreen(stdout: NodeJS.WriteStream | undefined, options?: AltScreenOptions): ScreenHandle;
|
|
74
|
+
export interface NativeScreenOptions {
|
|
75
|
+
/** Same discipline as `AltScreenOptions.bracketedPaste`: a filter that consumes it exists. */
|
|
76
|
+
readonly bracketedPaste?: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Native-scroll mode's counterpart of `enterAltScreen` (tui-native-terminal-scroll
|
|
80
|
+
* §4.8 / D6): it deliberately does almost nothing. The main buffer stays in use so
|
|
81
|
+
* the conversation lands in the terminal's own scrollback; there is no alternate
|
|
82
|
+
* screen, no mouse reporting and no DEC 1007 save/restore.
|
|
83
|
+
*
|
|
84
|
+
* The only mode it touches is DEC 2004 (bracketed paste), which changes how input
|
|
85
|
+
* is encoded and has nothing to do with scrolling. Returns the SAME `ScreenHandle`
|
|
86
|
+
* shape, so the four idempotent restore paths in `cli.tsx` need not know which mode
|
|
87
|
+
* is running. A non-TTY stdout is a hard no-op, like `enterAltScreen`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function enterNativeScreen(stdout: NodeJS.WriteStream | undefined, options?: NativeScreenOptions): ScreenHandle;
|
|
74
90
|
/**
|
|
75
91
|
* Replay the session summary into the normal buffer after `restore()`, so the
|
|
76
92
|
* conversation does not evaporate along with the alternate screen.
|
package/dist/ui/screen.js
CHANGED
|
@@ -181,6 +181,38 @@ export function enterAltScreen(stdout, options = { mouse: false }) {
|
|
|
181
181
|
setMouseCapture,
|
|
182
182
|
};
|
|
183
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Native-scroll mode's counterpart of `enterAltScreen` (tui-native-terminal-scroll
|
|
186
|
+
* §4.8 / D6): it deliberately does almost nothing. The main buffer stays in use so
|
|
187
|
+
* the conversation lands in the terminal's own scrollback; there is no alternate
|
|
188
|
+
* screen, no mouse reporting and no DEC 1007 save/restore.
|
|
189
|
+
*
|
|
190
|
+
* The only mode it touches is DEC 2004 (bracketed paste), which changes how input
|
|
191
|
+
* is encoded and has nothing to do with scrolling. Returns the SAME `ScreenHandle`
|
|
192
|
+
* shape, so the four idempotent restore paths in `cli.tsx` need not know which mode
|
|
193
|
+
* is running. A non-TTY stdout is a hard no-op, like `enterAltScreen`.
|
|
194
|
+
*/
|
|
195
|
+
export function enterNativeScreen(stdout, options = {}) {
|
|
196
|
+
if (!stdout || !stdout.isTTY || typeof stdout.write !== 'function')
|
|
197
|
+
return NOOP_HANDLE;
|
|
198
|
+
const bracketedPaste = options.bracketedPaste === true;
|
|
199
|
+
if (bracketedPaste)
|
|
200
|
+
safeWrite(stdout, ENABLE_BRACKETED_PASTE);
|
|
201
|
+
let restored = false;
|
|
202
|
+
return {
|
|
203
|
+
restore() {
|
|
204
|
+
if (restored)
|
|
205
|
+
return;
|
|
206
|
+
restored = true;
|
|
207
|
+
// 2004 is FORCED off rather than saved/restored, for the reason recorded at
|
|
208
|
+
// `DISABLE_BRACKETED_PASTE`. The cursor is shown again because Ink's
|
|
209
|
+
// log-update hides it and a signal kill never gives it back.
|
|
210
|
+
safeWrite(stdout, (bracketedPaste ? DISABLE_BRACKETED_PASTE : '') + SHOW_CURSOR);
|
|
211
|
+
},
|
|
212
|
+
// No mouse reporting in this mode: nothing to hand back or take again.
|
|
213
|
+
setMouseCapture() { },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
184
216
|
/**
|
|
185
217
|
* Replay the session summary into the normal buffer after `restore()`, so the
|
|
186
218
|
* conversation does not evaporate along with the alternate screen.
|