@aragon-agent/cli 0.5.4 → 0.5.8
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 +747 -577
- package/README.md +1163 -995
- package/dist/agent/controller.d.ts +71 -1
- package/dist/agent/controller.js +135 -1
- package/dist/agent/entry-limits.d.ts +65 -0
- package/dist/agent/entry-limits.js +133 -0
- package/dist/agent/headless.d.ts +29 -0
- package/dist/agent/headless.js +86 -1
- package/dist/agent/reducer.d.ts +53 -0
- package/dist/agent/reducer.js +145 -14
- package/dist/agent/system-prompt.d.ts +8 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +76 -3
- package/dist/commands/builtins.js +171 -0
- package/dist/commands/perf.d.ts +45 -0
- package/dist/commands/perf.js +78 -0
- package/dist/commands/registry.d.ts +27 -1
- package/dist/config/env.d.ts +3 -1
- package/dist/config/env.js +74 -1
- package/dist/config/load.d.ts +41 -0
- package/dist/config/load.js +67 -2
- package/dist/config/schema.d.ts +108 -8
- package/dist/config/schema.js +81 -6
- package/dist/config/store.js +10 -1
- package/dist/session/persist.d.ts +19 -1
- package/dist/session/persist.js +21 -2
- package/dist/team/subagent.js +11 -0
- package/dist/todo/follow-through.d.ts +117 -0
- package/dist/todo/follow-through.js +186 -0
- package/dist/todo/limits.d.ts +114 -0
- package/dist/todo/limits.js +114 -0
- package/dist/todo/normalize.d.ts +33 -0
- package/dist/todo/normalize.js +117 -0
- package/dist/todo/panel-rows.d.ts +53 -0
- package/dist/todo/panel-rows.js +103 -0
- package/dist/todo/prompt.d.ts +54 -0
- package/dist/todo/prompt.js +76 -0
- package/dist/todo/store.d.ts +86 -0
- package/dist/todo/store.js +183 -0
- package/dist/todo/todo-tool.d.ts +38 -0
- package/dist/todo/todo-tool.js +134 -0
- package/dist/todo/types.d.ts +62 -0
- package/dist/todo/types.js +12 -0
- package/dist/tools/index.d.ts +16 -2
- package/dist/tools/index.js +13 -0
- package/dist/ui/App.js +328 -14
- package/dist/ui/Markdown.d.ts +7 -1
- package/dist/ui/Markdown.js +15 -48
- package/dist/ui/StatusBar.d.ts +31 -0
- package/dist/ui/StatusBar.js +7 -2
- package/dist/ui/TodoPanel.d.ts +48 -0
- package/dist/ui/TodoPanel.js +74 -0
- package/dist/ui/TodoStrip.d.ts +36 -0
- package/dist/ui/TodoStrip.js +32 -0
- package/dist/ui/Transcript.d.ts +56 -4
- package/dist/ui/Transcript.js +123 -11
- package/dist/ui/entries/AssistantEntry.d.ts +18 -1
- package/dist/ui/entries/AssistantEntry.js +33 -3
- package/dist/ui/entries/TeamCard.d.ts +9 -1
- package/dist/ui/entries/TeamCard.js +22 -1
- package/dist/ui/entries/TodoCard.d.ts +39 -0
- package/dist/ui/entries/TodoCard.js +57 -0
- package/dist/ui/entries/ToolCard.d.ts +13 -1
- package/dist/ui/entries/ToolCard.js +39 -5
- package/dist/ui/entries/ToolPreview.d.ts +2 -2
- package/dist/ui/entries/UserEntry.d.ts +9 -1
- package/dist/ui/entries/UserEntry.js +17 -1
- package/dist/ui/exit-snapshot.d.ts +7 -0
- package/dist/ui/glyphs.d.ts +12 -0
- package/dist/ui/glyphs.js +8 -0
- package/dist/ui/layout/AppShell.d.ts +34 -1
- package/dist/ui/layout/AppShell.js +3 -3
- package/dist/ui/layout/MeasuredEntry.d.ts +21 -0
- package/dist/ui/layout/MeasuredEntry.js +30 -0
- package/dist/ui/layout/ScrollViewport.d.ts +13 -1
- package/dist/ui/layout/ScrollViewport.js +10 -4
- package/dist/ui/layout/rail.d.ts +56 -0
- package/dist/ui/layout/rail.js +68 -0
- package/dist/ui/layout/viewport-geometry.d.ts +28 -0
- package/dist/ui/layout/viewport-geometry.js +27 -0
- package/dist/ui/layout/virtual-window.d.ts +100 -0
- package/dist/ui/layout/virtual-window.js +204 -0
- package/dist/ui/live-clamp.d.ts +42 -0
- package/dist/ui/live-clamp.js +74 -0
- package/dist/ui/markdown-blocks.d.ts +25 -1
- package/dist/ui/markdown-blocks.js +43 -0
- package/dist/ui/overlays/HelpOverlay.js +4 -0
- package/dist/ui/render-cache.d.ts +81 -0
- package/dist/ui/render-cache.js +195 -0
- package/dist/ui/render-governor.d.ts +54 -0
- package/dist/ui/render-governor.js +78 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +5 -0
- package/dist/ui/use-height-store.d.ts +31 -0
- package/dist/ui/use-height-store.js +76 -0
- package/dist/ui/use-render-governor.d.ts +39 -0
- package/dist/ui/use-render-governor.js +57 -0
- package/dist/ui/use-startup-notices.d.ts +9 -1
- package/dist/ui/use-startup-notices.js +35 -1
- package/package.json +3 -2
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* TeamCard — the settled-dispatch transcript entry (team-subagents §6.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: every glyph comes from `pickGlyphs`.
|
|
6
|
+
*
|
|
7
|
+
* The panel is EPHEMERAL and this card is the HISTORY. It renders like a
|
|
8
|
+
* `ToolCard` — same `EntryFrame` supplied by `EntryView`, same left rail for the
|
|
9
|
+
* expanded body — so a dispatch reads as one more step in the transcript rather
|
|
10
|
+
* than a second kind of object.
|
|
11
|
+
*
|
|
12
|
+
* `Ctrl+O` expands it to the per-agent summaries, which is why `App.tsx`'s
|
|
13
|
+
* Ctrl+O finder had to widen from `kind === 'tool'` to `tool || team`.
|
|
14
|
+
*/
|
|
15
|
+
import React from 'react';
|
|
2
16
|
import { Box, Text } from 'ink';
|
|
3
17
|
import Spinner from 'ink-spinner';
|
|
4
18
|
import { pickGlyphs } from '../glyphs.js';
|
|
@@ -15,7 +29,7 @@ export function teamCardColor(runs, active, aborted, theme) {
|
|
|
15
29
|
return theme.toolError;
|
|
16
30
|
return theme.toolDone;
|
|
17
31
|
}
|
|
18
|
-
|
|
32
|
+
function TeamCardImpl(props) {
|
|
19
33
|
const { requested, runs, aborted, durationMs, active, expanded, reducedMotion, theme, caps } = props;
|
|
20
34
|
const glyphs = pickGlyphs(caps);
|
|
21
35
|
const color = teamCardColor(runs, active, aborted, theme);
|
|
@@ -37,3 +51,10 @@ export function TeamCard(props) {
|
|
|
37
51
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "team" }), _jsxs(Text, { color: theme.muted, children: [' ', count, dropped, ' '] }), active && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " ", headline] })) : (_jsxs(Text, { color: color, children: [active ? `${glyphs.spinnerStill} ` : '', headline] }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [runs.map((run) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: isOk(run) ? theme.toolDone : theme.toolError, children: isOk(run) ? glyphs.toolDone : glyphs.toolError }), _jsxs(Text, { color: theme.primary, children: [" ", run.label.padEnd(8).slice(0, 8)] }), _jsxs(Text, { color: theme.muted, children: [' ', run.description, ' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? 0) - run.startedAt)) : '', ` ${run.turns} turns`, run.filesTouched.length > 0 ? ` ${run.filesTouched.length} files` : '', run.error ? ` ${run.error}` : ''] })] }, run.label))), expanded &&
|
|
38
52
|
runs.map((run) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.accent, children: [run.label, " ", glyphs.midDot, " ", run.description] }), _jsx(Text, { color: theme.muted, children: run.summary ?? '(no summary)' })] }, `s-${run.label}`))), !expanded && !active && runs.length > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", runs.length, " ", runs.length === 1 ? 'summary' : 'summaries', " (Ctrl+O)"] })), expanded && _jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" })] })] }));
|
|
39
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
56
|
+
* `mapEntry` already preserves object identity for untouched entries, so the
|
|
57
|
+
* array props below are stable references on a settled card, and `theme` /
|
|
58
|
+
* `caps` are `useMemo`d in `App.tsx` (I-L2-1).
|
|
59
|
+
*/
|
|
60
|
+
export const TeamCard = React.memo(TeamCardImpl);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TodoCard — the transcript checklist entry (todo-plan-execution §6.2).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: every glyph comes from `pickGlyphs`.
|
|
5
|
+
*
|
|
6
|
+
* ONE CARD PER USER TURN, REWRITTEN IN PLACE. The rail is EPHEMERAL and this is
|
|
7
|
+
* the HISTORY, exactly as `TeamPanel` and `TeamCard` divide the same work. It
|
|
8
|
+
* renders through the shared `EntryFrame` supplied by `EntryView`, so a plan
|
|
9
|
+
* reads as one more step in the transcript rather than a second kind of object.
|
|
10
|
+
*
|
|
11
|
+
* THE FULL LIST, NO WINDOWING (the transcript scrolls; the rail does not), and
|
|
12
|
+
* NEVER ANIMATED even while `live` — a resumed session would otherwise spin
|
|
13
|
+
* forever on a card whose run ended days ago.
|
|
14
|
+
*/
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import type { Theme } from '../theme.js';
|
|
17
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
18
|
+
import type { TodoItem } from '../../todo/types.js';
|
|
19
|
+
export interface TodoCardProps {
|
|
20
|
+
items: TodoItem[];
|
|
21
|
+
doneCount: number;
|
|
22
|
+
total: number;
|
|
23
|
+
live: boolean;
|
|
24
|
+
/** Saved mid-run and resumed: the run behind this card died with the process. */
|
|
25
|
+
interrupted?: boolean;
|
|
26
|
+
theme: Theme;
|
|
27
|
+
caps: TermCapabilities;
|
|
28
|
+
}
|
|
29
|
+
/** The card's aggregate colour: complete is done, anything else is in flight. */
|
|
30
|
+
export declare function todoCardColor(doneCount: number, total: number, live: boolean, theme: Theme): string | undefined;
|
|
31
|
+
declare function TodoCardImpl(props: TodoCardProps): React.ReactElement;
|
|
32
|
+
/**
|
|
33
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
34
|
+
* `mapEntry` already preserves object identity for untouched entries, so the
|
|
35
|
+
* array props below are stable references on a settled card, and `theme` /
|
|
36
|
+
* `caps` are `useMemo`d in `App.tsx` (I-L2-1).
|
|
37
|
+
*/
|
|
38
|
+
export declare const TodoCard: React.MemoExoticComponent<typeof TodoCardImpl>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* TodoCard — the transcript checklist entry (todo-plan-execution §6.2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: every glyph comes from `pickGlyphs`.
|
|
6
|
+
*
|
|
7
|
+
* ONE CARD PER USER TURN, REWRITTEN IN PLACE. The rail is EPHEMERAL and this is
|
|
8
|
+
* the HISTORY, exactly as `TeamPanel` and `TeamCard` divide the same work. It
|
|
9
|
+
* renders through the shared `EntryFrame` supplied by `EntryView`, so a plan
|
|
10
|
+
* reads as one more step in the transcript rather than a second kind of object.
|
|
11
|
+
*
|
|
12
|
+
* THE FULL LIST, NO WINDOWING (the transcript scrolls; the rail does not), and
|
|
13
|
+
* NEVER ANIMATED even while `live` — a resumed session would otherwise spin
|
|
14
|
+
* forever on a card whose run ended days ago.
|
|
15
|
+
*/
|
|
16
|
+
import React from 'react';
|
|
17
|
+
import { Box, Text } from 'ink';
|
|
18
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
19
|
+
import { railBorderProps } from '../layout/Gutter.js';
|
|
20
|
+
/** The card's aggregate colour: complete is done, anything else is in flight. */
|
|
21
|
+
export function todoCardColor(doneCount, total, live, theme) {
|
|
22
|
+
if (live)
|
|
23
|
+
return theme.toolRunning;
|
|
24
|
+
if (total > 0 && doneCount === total)
|
|
25
|
+
return theme.toolDone;
|
|
26
|
+
return theme.muted;
|
|
27
|
+
}
|
|
28
|
+
function marker(item, glyphs) {
|
|
29
|
+
if (item.status === 'completed')
|
|
30
|
+
return glyphs.todoDone;
|
|
31
|
+
if (item.status === 'in_progress')
|
|
32
|
+
return glyphs.todoActive;
|
|
33
|
+
return glyphs.todoPending;
|
|
34
|
+
}
|
|
35
|
+
function rowColor(item, theme) {
|
|
36
|
+
if (item.status === 'completed')
|
|
37
|
+
return theme.muted;
|
|
38
|
+
if (item.status === 'in_progress')
|
|
39
|
+
return theme.primary;
|
|
40
|
+
return theme.hintFg ?? theme.muted;
|
|
41
|
+
}
|
|
42
|
+
function TodoCardImpl(props) {
|
|
43
|
+
const { items, doneCount, total, live, interrupted, theme, caps } = props;
|
|
44
|
+
const glyphs = pickGlyphs(caps);
|
|
45
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "Todos" }), _jsxs(Text, { color: todoCardColor(doneCount, total, live, theme), children: [' ', doneCount, "/", total] }), interrupted && (
|
|
46
|
+
// A resumed `2/7` would otherwise read as a run still in flight
|
|
47
|
+
// (P2-6). Saying so plainly is the same fix `TeamCard` makes for a
|
|
48
|
+
// dispatch that never got a duration.
|
|
49
|
+
_jsxs(Text, { color: theme.muted, children: [' ', "interrupted (session resumed)"] }))] }), _jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: items.map((item, index) => (_jsxs(Text, { wrap: "truncate", color: rowColor(item, theme), children: [marker(item, glyphs).padEnd(3), item.status === 'in_progress' ? item.activeForm : item.content] }, `${index}-${item.content}`))) })] }));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
53
|
+
* `mapEntry` already preserves object identity for untouched entries, so the
|
|
54
|
+
* array props below are stable references on a settled card, and `theme` /
|
|
55
|
+
* `caps` are `useMemo`d in `App.tsx` (I-L2-1).
|
|
56
|
+
*/
|
|
57
|
+
export const TodoCard = React.memo(TodoCardImpl);
|
|
@@ -28,11 +28,23 @@ interface ToolCardProps {
|
|
|
28
28
|
isError?: boolean;
|
|
29
29
|
expanded?: boolean;
|
|
30
30
|
reducedMotion?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Inline mode only (L5): ceiling on the rows a LIVE card contributes to the
|
|
33
|
+
* non-`<Static>` region. `undefined` — what the full-screen branch passes —
|
|
34
|
+
* means no clamp and byte-identical output.
|
|
35
|
+
*/
|
|
36
|
+
liveClampRows?: number;
|
|
31
37
|
theme: Theme;
|
|
32
38
|
caps: TermCapabilities;
|
|
33
39
|
}
|
|
34
40
|
export declare function statusColor(status: ToolStatus, theme: Theme): string | undefined;
|
|
35
41
|
/** The tool's own glyph, used by `EntryView` as the entry's rail marker. */
|
|
36
42
|
export declare function toolEntryGlyph(name: string, caps: TermCapabilities): string;
|
|
37
|
-
|
|
43
|
+
declare function ToolCardImpl(props: ToolCardProps): React.ReactElement;
|
|
44
|
+
/**
|
|
45
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
46
|
+
* Every prop is a scalar except `args` (a stable reference on a settled entry),
|
|
47
|
+
* `theme` and `caps` (both `useMemo`d in `App.tsx` — I-L2-1).
|
|
48
|
+
*/
|
|
49
|
+
export declare const ToolCard: React.MemoExoticComponent<typeof ToolCardImpl>;
|
|
38
50
|
export {};
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Tool-call card (spec §3.5): a per-tool glyph, the tool name, a compact args
|
|
4
|
+
* summary, a colored status badge, and a duration, over a collapsible rich
|
|
5
|
+
* preview delegated to `<ToolPreview>`. Collapsed to the first 8 lines with a
|
|
6
|
+
* `+M lines (Ctrl+O)` footer; `Ctrl+O` / `/expand` toggle the full preview.
|
|
7
|
+
*
|
|
8
|
+
* Two v0.4.0 changes (§4.1 / §4.3):
|
|
9
|
+
* - capabilities arrive as a prop. This used to infer Unicode support by
|
|
10
|
+
* comparing `theme.symbols.gaugeFull` against a literal block character, so
|
|
11
|
+
* editing an unrelated gauge glyph would have silently sent every tool icon
|
|
12
|
+
* back to ASCII;
|
|
13
|
+
* - the preview's round border is gone. It cost 2 rows and 4 columns per card,
|
|
14
|
+
* and nested visibly inside markdown code blocks. A left rail carries the
|
|
15
|
+
* same grouping in 1 column and 0 rows.
|
|
16
|
+
*/
|
|
17
|
+
import React from 'react';
|
|
2
18
|
import { Box, Text } from 'ink';
|
|
3
19
|
import Spinner from 'ink-spinner';
|
|
4
20
|
import { pickGlyphs, toolGlyph } from '../glyphs.js';
|
|
5
21
|
import { railBorderProps } from '../layout/Gutter.js';
|
|
22
|
+
import { splitLinesCached } from '../render-cache.js';
|
|
6
23
|
import { formatDuration } from '../../agent/usage.js';
|
|
7
24
|
import { ToolPreview, bashBadge } from './ToolPreview.js';
|
|
8
25
|
/** Number of preview lines shown before collapse. */
|
|
9
26
|
const COLLAPSED_LINES = 8;
|
|
27
|
+
/** Shared empty slice, so an argument-less card allocates nothing per frame. */
|
|
28
|
+
const EMPTY_LINES = [];
|
|
10
29
|
export function statusColor(status, theme) {
|
|
11
30
|
switch (status) {
|
|
12
31
|
case 'pending':
|
|
@@ -58,15 +77,30 @@ function badgeLabel(name, status, preview) {
|
|
|
58
77
|
export function toolEntryGlyph(name, caps) {
|
|
59
78
|
return toolGlyph(pickGlyphs(caps), name);
|
|
60
79
|
}
|
|
61
|
-
|
|
62
|
-
const { name, argsRaw, args, status, preview, durationMs, isError, expanded, reducedMotion, theme, caps, } = props;
|
|
80
|
+
function ToolCardImpl(props) {
|
|
81
|
+
const { name, argsRaw, args, status, preview, durationMs, isError, expanded, reducedMotion, liveClampRows, theme, caps, } = props;
|
|
63
82
|
const glyphs = pickGlyphs(caps);
|
|
64
83
|
const color = statusColor(status, theme);
|
|
65
84
|
const previewText = preview ?? '';
|
|
66
|
-
|
|
67
|
-
|
|
85
|
+
// `splitLinesCached` rather than `previewText.split('\n')`: a stored preview
|
|
86
|
+
// is immutable once written, and this used to reallocate up to 200 strings per
|
|
87
|
+
// card per frame (R3).
|
|
88
|
+
const previewLines = previewText.length > 0 ? splitLinesCached(previewText) : EMPTY_LINES;
|
|
89
|
+
// Head-first, exactly as before: the collapsed card shows the FIRST 8 lines.
|
|
90
|
+
// The inline clamp only lowers the ceiling — it never changes which end is
|
|
91
|
+
// kept, because `Ctrl+O` on a 5 000-line bash result is precisely how an
|
|
92
|
+
// inline session reaches `ink.js:121` (I-L5-1) and the fix must not also
|
|
93
|
+
// reshuffle what a full-screen user has been reading for four releases.
|
|
94
|
+
const ceiling = Math.min(expanded ? previewLines.length : COLLAPSED_LINES, liveClampRows ?? Number.POSITIVE_INFINITY);
|
|
95
|
+
const visibleLines = ceiling >= previewLines.length ? previewLines : previewLines.slice(0, ceiling);
|
|
68
96
|
const hidden = previewLines.length - visibleLines.length;
|
|
69
97
|
const showBody = previewLines.length > 0 && (status === 'done' || status === 'error');
|
|
70
98
|
const badge = status === 'running' && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " running"] })) : (_jsxs(Text, { color: color, children: [status === 'running' ? `${glyphs.spinnerStill} ` : '', badgeLabel(name, status, previewText)] }));
|
|
71
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: name }), _jsxs(Text, { color: theme.muted, children: [" ", summarizeArgs(name, glyphs.ellipsis, args, argsRaw), " "] }), badge, durationMs !== undefined && (_jsxs(Text, { color: theme.muted, children: [" (", formatDuration(durationMs), ")"] }))] }), showBody && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, isError ? theme.toolError : theme.border), children: [_jsx(ToolPreview, { name: name, lines: visibleLines, theme: theme, isError: isError }), !expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " (Ctrl+O)"] })), expanded && previewLines.length > COLLAPSED_LINES && (_jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" }))] }))] }));
|
|
99
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: name }), _jsxs(Text, { color: theme.muted, children: [" ", summarizeArgs(name, glyphs.ellipsis, args, argsRaw), " "] }), badge, durationMs !== undefined && (_jsxs(Text, { color: theme.muted, children: [" (", formatDuration(durationMs), ")"] }))] }), showBody && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, isError ? theme.toolError : theme.border), children: [_jsx(ToolPreview, { name: name, lines: visibleLines, theme: theme, isError: isError }), !expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " (Ctrl+O)"] })), expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " ", glyphs.midDot, " shown in full when this entry finishes"] })), expanded && previewLines.length > COLLAPSED_LINES && (_jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" }))] }))] }));
|
|
72
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
103
|
+
* Every prop is a scalar except `args` (a stable reference on a settled entry),
|
|
104
|
+
* `theme` and `caps` (both `useMemo`d in `App.tsx` — I-L2-1).
|
|
105
|
+
*/
|
|
106
|
+
export const ToolCard = React.memo(ToolCardImpl);
|
|
@@ -35,7 +35,7 @@ export declare function parseWriteFile(text: string): {
|
|
|
35
35
|
path: string;
|
|
36
36
|
} | null;
|
|
37
37
|
/** `3 questions · Postgres · REST` — or `dismissed`. */
|
|
38
|
-
export declare function summarizeAskUser(lines: string[]): string | null;
|
|
38
|
+
export declare function summarizeAskUser(lines: readonly string[]): string | null;
|
|
39
39
|
export interface ListRow {
|
|
40
40
|
kind: 'dir' | 'file';
|
|
41
41
|
name: string;
|
|
@@ -51,7 +51,7 @@ export declare function parseListRow(line: string): ListRow | null;
|
|
|
51
51
|
*/
|
|
52
52
|
interface ToolPreviewProps {
|
|
53
53
|
name: string;
|
|
54
|
-
lines: string[];
|
|
54
|
+
lines: readonly string[];
|
|
55
55
|
theme: Theme;
|
|
56
56
|
isError?: boolean;
|
|
57
57
|
}
|
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import type { Theme } from '../theme.js';
|
|
11
|
-
|
|
11
|
+
declare function UserEntryImpl({ text, theme }: {
|
|
12
12
|
text: string;
|
|
13
13
|
theme: Theme;
|
|
14
14
|
}): React.ReactElement;
|
|
15
|
+
/**
|
|
16
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
17
|
+
* `mapEntry` already preserves object identity for untouched entries, so the
|
|
18
|
+
* array props below are stable references on a settled card, and `theme` /
|
|
19
|
+
* `caps` are `useMemo`d in `App.tsx` (I-L2-1).
|
|
20
|
+
*/
|
|
21
|
+
export declare const UserEntry: React.MemoExoticComponent<typeof UserEntryImpl>;
|
|
22
|
+
export {};
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Render a user message entry.
|
|
4
|
+
*
|
|
5
|
+
* The role glyph, the gutter rail and the inter-entry spacing all belong to
|
|
6
|
+
* `EntryFrame` now (§4.3), so this component renders content and nothing else.
|
|
7
|
+
* That is also why it takes no `caps`: it has no glyph of its own left to
|
|
8
|
+
* degrade.
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
2
11
|
import { Box, Text } from 'ink';
|
|
3
|
-
|
|
12
|
+
function UserEntryImpl({ text, theme }) {
|
|
4
13
|
return (_jsx(Box, { flexDirection: "column", children: text.split('\n').map((line, i) => (_jsx(Text, { color: theme.user, bold: i === 0, children: line }, i))) }));
|
|
5
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
17
|
+
* `mapEntry` already preserves object identity for untouched entries, so the
|
|
18
|
+
* array props below are stable references on a settled card, and `theme` /
|
|
19
|
+
* `caps` are `useMemo`d in `App.tsx` (I-L2-1).
|
|
20
|
+
*/
|
|
21
|
+
export const UserEntry = React.memo(UserEntryImpl);
|
|
@@ -18,6 +18,13 @@ export interface ExitSnapshot {
|
|
|
18
18
|
provider: string;
|
|
19
19
|
model: string;
|
|
20
20
|
startedAt: number;
|
|
21
|
+
/**
|
|
22
|
+
* Entries the retain ring removed (tui-render-performance L1).
|
|
23
|
+
*
|
|
24
|
+
* OPTIONAL so every existing caller and test is unchanged; absent means the
|
|
25
|
+
* replay says nothing, which is correct for a session that dropped nothing.
|
|
26
|
+
*/
|
|
27
|
+
droppedEntries?: number;
|
|
21
28
|
}
|
|
22
29
|
/** Called by `App` after every view-state change. Synchronous, side-effect free. */
|
|
23
30
|
export declare function publishExitSnapshot(next: ExitSnapshot): void;
|
package/dist/ui/glyphs.d.ts
CHANGED
|
@@ -96,6 +96,18 @@ export interface Glyphs {
|
|
|
96
96
|
*/
|
|
97
97
|
teamAgent: string;
|
|
98
98
|
teamMail: string;
|
|
99
|
+
/**
|
|
100
|
+
* Todo planning (todo-plan-execution §6.1). One marker per item state, plus
|
|
101
|
+
* the rail marker for the transcript card via `tool.todo_write`.
|
|
102
|
+
*
|
|
103
|
+
* THE ASCII TIER USES BRACKETED FORMS FOR THE TWO SETTLED STATES, because a
|
|
104
|
+
* bare single letter cannot distinguish "done" from "failed" on a monochrome
|
|
105
|
+
* terminal. The panel pads the marker cell to 3 columns, which is what makes
|
|
106
|
+
* the mixed widths line up without a per-tier branch.
|
|
107
|
+
*/
|
|
108
|
+
todoPending: string;
|
|
109
|
+
todoActive: string;
|
|
110
|
+
todoDone: string;
|
|
99
111
|
/**
|
|
100
112
|
* Ink `borderStyle` name for every boxed region (composer, completion popup,
|
|
101
113
|
* overlay frame).
|
package/dist/ui/glyphs.js
CHANGED
|
@@ -61,6 +61,9 @@ const UNICODE_GLYPHS = {
|
|
|
61
61
|
boxStyle: 'round',
|
|
62
62
|
teamAgent: '◆',
|
|
63
63
|
teamMail: '✉',
|
|
64
|
+
todoPending: '○',
|
|
65
|
+
todoActive: '▸',
|
|
66
|
+
todoDone: '✔',
|
|
64
67
|
tool: {
|
|
65
68
|
read_file: '▤',
|
|
66
69
|
write_file: '✎',
|
|
@@ -72,6 +75,7 @@ const UNICODE_GLYPHS = {
|
|
|
72
75
|
ask_user: '?',
|
|
73
76
|
submit_plan: '◈',
|
|
74
77
|
task: '◆',
|
|
78
|
+
todo_write: '☰',
|
|
75
79
|
},
|
|
76
80
|
toolDefault: '•',
|
|
77
81
|
};
|
|
@@ -118,6 +122,9 @@ const ASCII_GLYPHS = {
|
|
|
118
122
|
boxStyle: 'classic',
|
|
119
123
|
teamAgent: '*',
|
|
120
124
|
teamMail: '@',
|
|
125
|
+
todoPending: '[ ]',
|
|
126
|
+
todoActive: '>',
|
|
127
|
+
todoDone: '[x]',
|
|
121
128
|
tool: {
|
|
122
129
|
read_file: '=',
|
|
123
130
|
write_file: '+',
|
|
@@ -129,6 +136,7 @@ const ASCII_GLYPHS = {
|
|
|
129
136
|
ask_user: '?',
|
|
130
137
|
submit_plan: '#',
|
|
131
138
|
task: '*',
|
|
139
|
+
todo_write: '=',
|
|
132
140
|
},
|
|
133
141
|
toolDefault: '*',
|
|
134
142
|
};
|
|
@@ -28,6 +28,39 @@ export interface AppShellProps {
|
|
|
28
28
|
* composer band boundary stale the instant a dispatch started.
|
|
29
29
|
*/
|
|
30
30
|
team?: React.ReactNode;
|
|
31
|
+
/**
|
|
32
|
+
* The right-hand region of the MIDDLE band (todo-plan-execution §3.9), or
|
|
33
|
+
* nothing.
|
|
34
|
+
*
|
|
35
|
+
* NAMED FOR ITS POSITION, NOT ITS CONTENT (D-18): this file is a layout
|
|
36
|
+
* primitive and has no business knowing what a todo is. `team` above already
|
|
37
|
+
* leaks content vocabulary in here; there is no reason to repeat it.
|
|
38
|
+
*
|
|
39
|
+
* IT MUST NOT BE RENDERED INSIDE `bottomRef` (C-6 / I-4). That box is MEASURED
|
|
40
|
+
* and the wheel router reads the composer band boundary from it; a column
|
|
41
|
+
* added there would inflate the measurement and start routing transcript
|
|
42
|
+
* scrolls into prompt history.
|
|
43
|
+
*
|
|
44
|
+
* The INLINE branch does not include it at all — a structural guarantee rather
|
|
45
|
+
* than a promise that the caller passes `null` (non-goal 4).
|
|
46
|
+
*/
|
|
47
|
+
rail?: React.ReactNode;
|
|
48
|
+
/**
|
|
49
|
+
* The one-row plan strip for INLINE mode (todo-plan-followthrough §3.7), or
|
|
50
|
+
* nothing.
|
|
51
|
+
*
|
|
52
|
+
* THE STRUCTURAL MIRROR OF `rail` ABOVE, in the opposite direction: the
|
|
53
|
+
* FULL-SCREEN branch does not reference it at all. The reason is the same one
|
|
54
|
+
* `rail`'s inline omission records — a prop that is merely SUPPOSED to be null
|
|
55
|
+
* in one mode becomes non-null eventually, and then the rail and the strip are
|
|
56
|
+
* both on screen showing the same list (I-6). Fullscreen has the rail; that is
|
|
57
|
+
* the whole design.
|
|
58
|
+
*
|
|
59
|
+
* It sits between `viewport` and `team`, i.e. OUTSIDE the measured box in the
|
|
60
|
+
* other branch — but inline has no measured box at all (`geometryRef` is set
|
|
61
|
+
* to `UNMEASURED_GEOMETRY` there), so there is nothing here to invalidate.
|
|
62
|
+
*/
|
|
63
|
+
strip?: React.ReactNode;
|
|
31
64
|
toast: React.ReactNode;
|
|
32
65
|
composer: React.ReactNode;
|
|
33
66
|
status: React.ReactNode;
|
|
@@ -40,4 +73,4 @@ export interface AppShellProps {
|
|
|
40
73
|
*/
|
|
41
74
|
geometryRef?: React.MutableRefObject<FrameGeometry>;
|
|
42
75
|
}
|
|
43
|
-
export declare function AppShell({ mode, rows, cols, header, viewport, team, toast, composer, status, geometryRef, }: AppShellProps): React.ReactElement;
|
|
76
|
+
export declare function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, geometryRef, }: AppShellProps): React.ReactElement;
|
|
@@ -14,7 +14,7 @@ import { useLayoutEffect, useRef } from 'react';
|
|
|
14
14
|
import { Box, measureElement } from 'ink';
|
|
15
15
|
import { frameHeight } from './frame.js';
|
|
16
16
|
import { UNMEASURED_GEOMETRY } from './regions.js';
|
|
17
|
-
export function AppShell({ mode, rows, cols, header, viewport, team, toast, composer, status, geometryRef, }) {
|
|
17
|
+
export function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, geometryRef, }) {
|
|
18
18
|
const bottomRef = useRef(null);
|
|
19
19
|
// DECLARED ABOVE THE INLINE EARLY RETURN, and it has to stay there: React
|
|
20
20
|
// keys hooks by call order, and a hook that only runs in one mode changes
|
|
@@ -38,7 +38,7 @@ export function AppShell({ mode, rows, cols, header, viewport, team, toast, comp
|
|
|
38
38
|
};
|
|
39
39
|
});
|
|
40
40
|
if (mode === 'inline') {
|
|
41
|
-
return (_jsxs(Box, { flexDirection: "column", children: [header, viewport, team, toast, composer, status] }));
|
|
41
|
+
return (_jsxs(Box, { flexDirection: "column", children: [header, viewport, strip, team, toast, composer, status] }));
|
|
42
42
|
}
|
|
43
|
-
return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, children: header }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), _jsxs(Box, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [team, toast, composer, status] })] }));
|
|
43
|
+
return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, children: header }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), rail] }), _jsxs(Box, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [team, toast, composer, status] })] }));
|
|
44
44
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One transcript entry, wrapped so its rendered height reaches the height store
|
|
3
|
+
* (tui-render-performance L3).
|
|
4
|
+
*
|
|
5
|
+
* IT MUST NOT CONSTRAIN HEIGHT. The natural height IS the value being measured;
|
|
6
|
+
* a `height` prop here would make every entry report back whatever it was told,
|
|
7
|
+
* and the spacers would encode a fiction.
|
|
8
|
+
*
|
|
9
|
+
* Ink computes yoga layout in `onRender`, which React runs BEFORE layout
|
|
10
|
+
* effects, so the measurement below reflects the commit that just happened —
|
|
11
|
+
* the same mechanism `ScrollViewport.tsx:87-88` and `AppShell.tsx:100-102`
|
|
12
|
+
* already document and rely on.
|
|
13
|
+
*/
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import type { HeightKey } from './virtual-window.js';
|
|
16
|
+
export interface MeasuredEntryProps {
|
|
17
|
+
heightKey: HeightKey;
|
|
18
|
+
onMeasure: (key: HeightKey, rows: number) => void;
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
export declare function MeasuredEntry({ heightKey, onMeasure, children, }: MeasuredEntryProps): React.ReactElement;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* One transcript entry, wrapped so its rendered height reaches the height store
|
|
4
|
+
* (tui-render-performance L3).
|
|
5
|
+
*
|
|
6
|
+
* IT MUST NOT CONSTRAIN HEIGHT. The natural height IS the value being measured;
|
|
7
|
+
* a `height` prop here would make every entry report back whatever it was told,
|
|
8
|
+
* and the spacers would encode a fiction.
|
|
9
|
+
*
|
|
10
|
+
* Ink computes yoga layout in `onRender`, which React runs BEFORE layout
|
|
11
|
+
* effects, so the measurement below reflects the commit that just happened —
|
|
12
|
+
* the same mechanism `ScrollViewport.tsx:87-88` and `AppShell.tsx:100-102`
|
|
13
|
+
* already document and rely on.
|
|
14
|
+
*/
|
|
15
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
16
|
+
import { Box, measureElement } from 'ink';
|
|
17
|
+
export function MeasuredEntry({ heightKey, onMeasure, children, }) {
|
|
18
|
+
const ref = useRef(null);
|
|
19
|
+
// No dependency array on purpose: the entry's content can change without the
|
|
20
|
+
// key changing identity in a way this component can see, and `report` is a
|
|
21
|
+
// no-op when the measurement is unchanged — so an unconditional effect costs
|
|
22
|
+
// one `getComputedHeight()` read and cannot miss a resize.
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
const node = ref.current;
|
|
25
|
+
if (!node)
|
|
26
|
+
return;
|
|
27
|
+
onMeasure(heightKey, measureElement(node).height);
|
|
28
|
+
});
|
|
29
|
+
return (_jsx(Box, { ref: ref, flexDirection: "column", flexShrink: 0, children: children }));
|
|
30
|
+
}
|
|
@@ -32,8 +32,20 @@ export interface ScrollViewportProps {
|
|
|
32
32
|
onScrolledLinesChange?: (lines: number) => void;
|
|
33
33
|
/** Reserve the right-edge scroll-position rail (mouse-wheel §4.8). */
|
|
34
34
|
showScrollIndicator?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* The viewport's own width, when it is narrower than the terminal
|
|
37
|
+
* (todo-plan-execution §3.9 / C-7).
|
|
38
|
+
*
|
|
39
|
+
* This component is the ONLY one inside the viewport subtree that reads
|
|
40
|
+
* `stdout.columns` directly, and both places it uses the number — the terse
|
|
41
|
+
* hint threshold and the `MIN_INDICATOR_COLS` gate — are about how much room
|
|
42
|
+
* THIS BOX has, not how wide the terminal is. Once the todo rail takes a fifth
|
|
43
|
+
* of the frame those two stop being the same number. Optional so every
|
|
44
|
+
* existing caller (and every test) is unchanged.
|
|
45
|
+
*/
|
|
46
|
+
cols?: number;
|
|
35
47
|
theme: Theme;
|
|
36
48
|
caps: TermCapabilities;
|
|
37
49
|
children: React.ReactNode;
|
|
38
50
|
}
|
|
39
|
-
export declare function ScrollViewport({ intent, pinToBottomNonce, onScrolledLinesChange, showScrollIndicator, theme, caps, children, }: ScrollViewportProps): React.ReactElement;
|
|
51
|
+
export declare function ScrollViewport({ intent, pinToBottomNonce, onScrolledLinesChange, showScrollIndicator, cols: colsProp, theme, caps, children, }: ScrollViewportProps): React.ReactElement;
|
|
@@ -13,17 +13,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
13
13
|
* measure → setState → re-render → measure loop whose convergence is provable
|
|
14
14
|
* but whose frame count is not — expensive inside a 33 ms streaming window.
|
|
15
15
|
*/
|
|
16
|
-
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
16
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
17
17
|
import { Box, Text, measureElement, useStdout } from 'ink';
|
|
18
18
|
import { pickGlyphs } from '../glyphs.js';
|
|
19
|
+
import { ViewportGeometryContext } from './viewport-geometry.js';
|
|
19
20
|
import { applyScrollTimes, clampScroll, } from './scroll.js';
|
|
20
21
|
import { thumbRange } from './scroll-indicator.js';
|
|
21
22
|
import { MIN_INDICATOR_COLS, ScrollIndicator } from './ScrollIndicator.js';
|
|
22
23
|
/** Below this the full hint sentence wraps and eats a second viewport row. */
|
|
23
24
|
const TERSE_HINT_COLS = 42;
|
|
24
|
-
export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesChange, showScrollIndicator = false, theme, caps, children, }) {
|
|
25
|
+
export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesChange, showScrollIndicator = false, cols: colsProp, theme, caps, children, }) {
|
|
25
26
|
const { stdout } = useStdout();
|
|
26
|
-
const cols = stdout?.columns ?? 80;
|
|
27
|
+
const cols = colsProp ?? stdout?.columns ?? 80;
|
|
27
28
|
const glyphs = pickGlyphs(caps);
|
|
28
29
|
const clipRef = useRef(null);
|
|
29
30
|
const innerRef = useRef(null);
|
|
@@ -81,7 +82,12 @@ export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesCh
|
|
|
81
82
|
// more than the affordance. The routing never reads this flag, which is what
|
|
82
83
|
// keeps the indicator independently revertible (§13-Q3).
|
|
83
84
|
const indicatorVisible = showScrollIndicator && cols >= MIN_INDICATOR_COLS;
|
|
84
|
-
|
|
85
|
+
// PUBLISHED, NOT LIFTED (tui-render-performance L3 / §3.3.2). The three
|
|
86
|
+
// numbers virtualisation needs are already owned here; a `useMemo`d context
|
|
87
|
+
// value hands them down without adding a second owner or a second measure
|
|
88
|
+
// pass, which is exactly what this component's header (P1-7) forbids.
|
|
89
|
+
const geometry = useMemo(() => ({ viewportRows: metrics.viewport, offset, contentRows: metrics.content }), [metrics.viewport, metrics.content, offset]);
|
|
90
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { ref: clipRef, flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Box, { ref: innerRef, flexDirection: "column", flexShrink: 0, marginTop: -shiftUp, children: _jsx(ViewportGeometryContext.Provider, { value: geometry, children: children }) }) }), indicatorVisible && (_jsx(ScrollIndicator, { rows: metrics.viewport, thumb: thumbRange(metrics.viewport, metrics.content, offset), theme: theme, caps: caps }))] }), showHint && (
|
|
85
91
|
// Not an overlay — Ink has no absolute positioning or z-index. This takes
|
|
86
92
|
// the viewport's last row, trading one line of content for a hint that
|
|
87
93
|
// cannot drift. Cheaper than stacking with negative margins.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rail geometry (todo-plan-execution §3.9) — the ONE place that says how wide
|
|
3
|
+
* and how tall the todo rail is allowed to be.
|
|
4
|
+
*
|
|
5
|
+
* Pure and React-free, mirroring `budget.ts`, so `todo-rail.test.ts` can pin the
|
|
6
|
+
* arithmetic without mounting anything. Deliberately NOT part of `budget.ts`:
|
|
7
|
+
* that module answers "how many ROWS does the viewport get" and stays a function
|
|
8
|
+
* of `rows` alone; the rail spends COLUMNS.
|
|
9
|
+
*
|
|
10
|
+
* NAMED FOR ITS POSITION, NOT ITS CONTENT, like `AppShellProps.rail` (D-18):
|
|
11
|
+
* this is a layout primitive and `viewportRows` is its neighbour, not
|
|
12
|
+
* `TodoPanel`.
|
|
13
|
+
*/
|
|
14
|
+
/** Below this the marker + a two-word title do not fit and the column is noise. */
|
|
15
|
+
export declare const TODO_RAIL_MIN_COLS = 18;
|
|
16
|
+
/**
|
|
17
|
+
* Above this, 20% is being spent on whitespace: at 36 columns a step title
|
|
18
|
+
* already fits on one line, and the transcript is the surface that benefits from
|
|
19
|
+
* a 200-column terminal. A deliberate, revertible deviation from a literal
|
|
20
|
+
* reading of R-e; it only binds above 180 columns.
|
|
21
|
+
*/
|
|
22
|
+
export declare const TODO_RAIL_MAX_COLS = 36;
|
|
23
|
+
/** Under 80 columns the transcript needs every column; no rail at all. */
|
|
24
|
+
export declare const TODO_RAIL_MIN_TOTAL_COLS = 80;
|
|
25
|
+
/** Below this rail width the 2-column index prefix is dropped for content. */
|
|
26
|
+
export declare const TODO_RAIL_INDEX_MIN_COLS = 22;
|
|
27
|
+
/**
|
|
28
|
+
* Two properties are pinned by tests, for the reason `budget.ts` records about
|
|
29
|
+
* the viewport being non-monotonic before anyone measured it:
|
|
30
|
+
*
|
|
31
|
+
* - NON-DECREASING in `cols` across [40, 300]. A user dragging a window wider
|
|
32
|
+
* must never see the rail get narrower.
|
|
33
|
+
* - `cols - todoRailWidth(cols) >= 62` for every `cols >= 80`. Every existing
|
|
34
|
+
* width heuristic inside the viewport (`TERSE_HINT_COLS` 42,
|
|
35
|
+
* `MIN_INDICATOR_COLS` 50, the status bar's 60/72 steps) sits below that
|
|
36
|
+
* floor, so no existing behaviour changes because a rail appeared (R-3).
|
|
37
|
+
*/
|
|
38
|
+
export declare function todoRailWidth(cols: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* How many rows the rail may actually draw.
|
|
41
|
+
*
|
|
42
|
+
* IT IS NOT `viewportBudget`, AND THE DIFFERENCE IS NOT COSMETIC (P1-3 / D-23).
|
|
43
|
+
* `viewportRows()` subtracts the STATIC chrome only, and says so; the live team
|
|
44
|
+
* roster is mounted inside `AppShell`'s measured bottom box, where it costs a
|
|
45
|
+
* header row, up to `TEAM_LIMITS.panelMaxRows` child rows, a `+N more` row and a
|
|
46
|
+
* mail row that `viewportBudget` knows nothing about. `ScrollViewport` is immune
|
|
47
|
+
* because it MEASURES itself; the rail does not measure - a second
|
|
48
|
+
* measure -> setState -> measure loop is exactly what §1.2 refuses - so it has
|
|
49
|
+
* to subtract.
|
|
50
|
+
*
|
|
51
|
+
* Ink clips overflow from the BOTTOM, so an over-request does not error: it
|
|
52
|
+
* silently eats the last row, which is the `+N below` marker - the one row whose
|
|
53
|
+
* absence is indistinguishable from "the list is short". That is why this is
|
|
54
|
+
* arithmetic rather than a shrug at `overflow: hidden`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function todoRailRows(viewportBudget: number, teamActive: boolean): number;
|