@aragon-agent/cli 0.6.4 → 0.6.7

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +108 -11
  3. package/dist/agent/controller.d.ts +39 -2
  4. package/dist/agent/controller.js +125 -0
  5. package/dist/agent/reducer.d.ts +33 -19
  6. package/dist/agent/reducer.js +62 -71
  7. package/dist/agent/usage.d.ts +34 -0
  8. package/dist/agent/usage.js +43 -0
  9. package/dist/cli.js +48 -1
  10. package/dist/commands/builtins.js +9 -0
  11. package/dist/compaction/compactor.d.ts +60 -24
  12. package/dist/compaction/compactor.js +89 -61
  13. package/dist/compaction/context-command.d.ts +33 -0
  14. package/dist/compaction/context-command.js +166 -0
  15. package/dist/compaction/meter.d.ts +251 -0
  16. package/dist/compaction/meter.js +428 -0
  17. package/dist/compaction/pressure.d.ts +6 -0
  18. package/dist/compaction/pressure.js +4 -0
  19. package/dist/compaction/types.d.ts +41 -0
  20. package/dist/compaction/wiring.d.ts +47 -0
  21. package/dist/compaction/wiring.js +88 -27
  22. package/dist/config/cli-commands.js +5 -0
  23. package/dist/config/env.js +15 -1
  24. package/dist/config/load.d.ts +17 -0
  25. package/dist/config/load.js +45 -1
  26. package/dist/config/schema.d.ts +44 -0
  27. package/dist/config/schema.js +36 -0
  28. package/dist/config/store.js +27 -1
  29. package/dist/exec/events.d.ts +138 -1
  30. package/dist/exec/events.js +42 -0
  31. package/dist/exec/index.js +43 -6
  32. package/dist/exec/progress-reporter.d.ts +23 -0
  33. package/dist/exec/progress-reporter.js +88 -0
  34. package/dist/exec/runner.d.ts +34 -0
  35. package/dist/exec/runner.js +137 -25
  36. package/dist/fast/reviewer.d.ts +19 -1
  37. package/dist/fast/reviewer.js +22 -0
  38. package/dist/fast/types.d.ts +15 -0
  39. package/dist/fast/wiring.d.ts +10 -1
  40. package/dist/fast/wiring.js +18 -1
  41. package/dist/todo/limits.d.ts +4 -17
  42. package/dist/todo/limits.js +4 -17
  43. package/dist/todo/panel-rows.d.ts +8 -6
  44. package/dist/todo/panel-rows.js +19 -6
  45. package/dist/ui/App.js +75 -63
  46. package/dist/ui/AutocompletePopup.d.ts +3 -1
  47. package/dist/ui/AutocompletePopup.js +9 -11
  48. package/dist/ui/BottomStatusRow.d.ts +1 -1
  49. package/dist/ui/Composer.d.ts +3 -1
  50. package/dist/ui/Composer.js +2 -2
  51. package/dist/ui/PromptInput.d.ts +4 -1
  52. package/dist/ui/PromptInput.js +19 -5
  53. package/dist/ui/StatusBar.d.ts +21 -15
  54. package/dist/ui/StatusBar.js +48 -9
  55. package/dist/ui/TeamPanel.d.ts +3 -1
  56. package/dist/ui/TeamPanel.js +13 -9
  57. package/dist/ui/TodoPanel.d.ts +5 -5
  58. package/dist/ui/TodoPanel.js +56 -24
  59. package/dist/ui/UpdateLine.d.ts +1 -1
  60. package/dist/ui/capabilities.js +5 -5
  61. package/dist/ui/exit-snapshot.d.ts +9 -0
  62. package/dist/ui/layout/ScrollViewport.d.ts +1 -1
  63. package/dist/ui/layout/autocomplete.d.ts +18 -0
  64. package/dist/ui/layout/autocomplete.js +26 -0
  65. package/dist/ui/layout/rail.d.ts +4 -53
  66. package/dist/ui/layout/rail.js +10 -62
  67. package/dist/ui/layout/team-panel.d.ts +18 -0
  68. package/dist/ui/layout/team-panel.js +22 -0
  69. package/dist/ui/layout/todo-layout.d.ts +24 -0
  70. package/dist/ui/layout/todo-layout.js +27 -0
  71. package/dist/ui/transcript-text.js +6 -2
  72. package/package.json +2 -2
@@ -5,7 +5,7 @@ import { pickGlyphs } from './glyphs.js';
5
5
  import { formatDuration } from '../agent/usage.js';
6
6
  import { describeToolActivity, sanitizeActivity } from '../team/activity.js';
7
7
  import { TEAM_LIMITS } from '../team/limits.js';
8
- import { selectPanelRows } from '../team/panel-rows.js';
8
+ import { buildTeamPanelLayout } from './layout/team-panel.js';
9
9
  function isRunning(run) {
10
10
  return run.phase === 'starting' || run.phase === 'thinking' || run.phase === 'tool' ||
11
11
  run.phase === 'waiting';
@@ -101,24 +101,28 @@ export function activityLine(run, cols) {
101
101
  return 'aborted';
102
102
  }
103
103
  }
104
- export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, }) {
104
+ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, layout, }) {
105
+ const projection = layout ?? buildTeamPanelLayout({
106
+ snapshot, terminalRows: rows, availableRows: Infinity,
107
+ });
108
+ if (projection.rowCount === 0)
109
+ return null;
105
110
  const glyphs = pickGlyphs(caps);
106
111
  const at = now ?? Date.now();
107
112
  const running = snapshot.runs.filter(isRunning).length;
108
113
  const done = snapshot.runs.filter(isSettled).length;
109
114
  const elapsed = formatDuration(Math.max(0, at - snapshot.startedAt));
110
115
  const header = (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate", color: theme.accent, bold: true, children: [glyphs.teamAgent, " team", ' ', _jsxs(Text, { color: theme.muted, children: [running, " running ", glyphs.midDot, " ", done, " done ", glyphs.midDot, " ", elapsed] })] }), snapshot.messageCount > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.teamMail, " ", snapshot.messageCount] }))] }));
111
- // Below 20 rows the roster is more expensive than the transcript it displaces,
112
- // so the panel degrades to the one line that still answers "is something
113
- // running, and for how long" (§6.4).
114
- if (rows < TEAM_LIMITS.panelCollapseRows) {
116
+ // Short terminals or a long draft leave only the shared header projection,
117
+ // which still answers whether anything is running and for how long.
118
+ if (projection.collapsed) {
115
119
  return _jsx(Box, { flexDirection: "column", flexShrink: 0, children: header });
116
120
  }
117
121
  // RANKED, not sliced by array index (F-2). The slot pool hands out indices in
118
122
  // order, so `slice(0, 5)` showed the five children that finished FIRST and hid
119
123
  // everything still working behind `+N more` - at exactly the fan-out widths
120
124
  // the requirement permits.
121
- const { visible, hiddenTotal, hiddenRunning } = selectPanelRows(snapshot.runs, TEAM_LIMITS.panelMaxRows);
125
+ const { visible, hiddenTotal, hiddenRunning } = projection;
122
126
  return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [header, visible.map((run) => {
123
127
  const color = statusColor(run, theme);
124
128
  // One spinner per RUNNING row, and the row cap is 5 precisely because
@@ -126,8 +130,8 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
126
130
  // `reducedMotion` and a non-Unicode terminal fall back to the same
127
131
  // static marker, exactly as `AssistantEntry` and `ToolCard` already do.
128
132
  const animate = isRunning(run) && !reducedMotion && caps.unicode;
129
- return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: `${run.label}${run.tier === 'fast' ? '~' : ''}`.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
130
- }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject] }))] }));
133
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: `${run.label}${run.tier === 'fast' ? '~' : ''}`.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description.replace(/\s+/g, ' ') }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
134
+ }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject.replace(/\s+/g, ' ')] }))] }));
131
135
  }
132
136
  function marker(run, glyphs) {
133
137
  if (run.phase === 'done')
@@ -14,13 +14,13 @@
14
14
  * stop being a rail and start eating viewport height, growing and shrinking the
15
15
  * transcript every time the list changed length.
16
16
  *
17
- * WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 18-36
17
+ * WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 14-36
18
18
  * columns wide and the system prompt's last line is "Respond in the language the
19
19
  * user writes in", so CJK item text in an 18-column rail is the NORMAL case
20
20
  * rather than the edge case. Every text cell is `wrap="truncate"` and Ink's
21
21
  * truncation is display-width aware, so nothing can overflow the box — and NO
22
- * ARITHMETIC HERE MAY BUDGET USER TEXT BY `.length`. The `padEnd(3)` /
23
- * `padStart(2)` cells below are ASCII markers and indices, which is why they are
22
+ * ARITHMETIC HERE MAY BUDGET USER TEXT BY `.length`. The count string and
23
+ * `padStart(2)` cells below are ASCII counts and indices, which is why they are
24
24
  * safe; a "characters that fit" calculation over `content` would not be. That is
25
25
  * the whole of the lesson `TeamPanel`'s `WIDE_CHAR_ALLOWANCE` records at length,
26
26
  * and the mitigation here is to have no such budget at all.
@@ -34,7 +34,7 @@ export interface TodoPanelProps {
34
34
  /** From `todoRailWidth(cols)`. The rail's whole column budget, separator included. */
35
35
  width: number;
36
36
  /**
37
- * From `todoRailRows(viewportBudget, teamActive)` — NOT `viewportBudget`
37
+ * From `buildTodoRailLayout` after actual team/menu rows are subtracted.
38
38
  * (I-10 / P1-3). The gate that mounts this component and the number it lays
39
39
  * out against are ONE value computed once in `App`: a panel that mounts on one
40
40
  * number and lays out against another renders nothing and reports nothing.
@@ -46,4 +46,4 @@ export interface TodoPanelProps {
46
46
  theme: Theme;
47
47
  caps: TermCapabilities;
48
48
  }
49
- export declare function TodoPanel({ snapshot, width, rows, running, reducedMotion, theme, caps, }: TodoPanelProps): React.ReactElement;
49
+ export declare function TodoPanel({ snapshot, width, rows, running, reducedMotion, theme, caps, }: TodoPanelProps): React.ReactElement | null;
@@ -1,4 +1,32 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * TodoPanel — the right rail (todo-plan-execution §6.1).
4
+ *
5
+ * ASCII ONLY: every glyph comes from `pickGlyphs`; a literal here bypasses the
6
+ * capability probe and shows mojibake on a legacy console (`glyphs.ts` header).
7
+ *
8
+ * MOUNTED ONLY WHEN A LIST EXISTS. `App` passes `null` the rest of the time and
9
+ * this component is not rendered at all, so a session that never plans has zero
10
+ * columns and zero layout shift (R-g's "no furniture when idle" / AC-24).
11
+ *
12
+ * IT MUST BE MOUNTED IN `AppShell`'s MIDDLE BAND, NEVER IN THE BOTTOM CHROME
13
+ * (C-6 / I-4). This is a COLUMN BESIDE the viewport, and the middle band is the
14
+ * only row-wrapped region there is; moved down into the bottom chrome it would
15
+ * stop being a rail and start eating viewport height, growing and shrinking the
16
+ * transcript every time the list changed length.
17
+ *
18
+ * WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 14-36
19
+ * columns wide and the system prompt's last line is "Respond in the language the
20
+ * user writes in", so CJK item text in an 18-column rail is the NORMAL case
21
+ * rather than the edge case. Every text cell is `wrap="truncate"` and Ink's
22
+ * truncation is display-width aware, so nothing can overflow the box — and NO
23
+ * ARITHMETIC HERE MAY BUDGET USER TEXT BY `.length`. The count string and
24
+ * `padStart(2)` cells below are ASCII counts and indices, which is why they are
25
+ * safe; a "characters that fit" calculation over `content` would not be. That is
26
+ * the whole of the lesson `TeamPanel`'s `WIDE_CHAR_ALLOWANCE` records at length,
27
+ * and the mitigation here is to have no such budget at all.
28
+ */
29
+ import { useMemo } from 'react';
2
30
  import { Box, Text } from 'ink';
3
31
  import Spinner from 'ink-spinner';
4
32
  import { pickGlyphs } from './glyphs.js';
@@ -6,7 +34,7 @@ import { buildGauge } from './gauge.js';
6
34
  import { railBorderProps } from './layout/Gutter.js';
7
35
  import { TODO_RAIL_INDEX_MIN_COLS } from './layout/rail.js';
8
36
  import { TODO_LIMITS } from '../todo/limits.js';
9
- import { selectTodoRows } from '../todo/panel-rows.js';
37
+ import { selectCompactTodoRows, selectTodoRows } from '../todo/panel-rows.js';
10
38
  /** Header row + the blank row under the gauge. */
11
39
  const FIXED_ROWS = 2;
12
40
  /** The gauge is the one purely decorative element, so it is the first to go. */
@@ -19,8 +47,11 @@ const MARKER_COLS = 3;
19
47
  /** Index cell: two digits plus the space that separates it from the text. */
20
48
  const INDEX_COLS = 3;
21
49
  function marker(item, glyphs) {
22
- if (item.status === 'completed')
23
- return glyphs.todoDone;
50
+ // Ink's tokenizer otherwise treats the bare emoji-capable check as one cell,
51
+ // while string-width counts two, adding an extra visible space to each row.
52
+ if (item.status === 'completed') {
53
+ return glyphs.todoDone.length === 1 ? `${glyphs.todoDone}\uFE0F` : glyphs.todoDone;
54
+ }
24
55
  if (item.status === 'in_progress')
25
56
  return glyphs.todoActive;
26
57
  return glyphs.todoPending;
@@ -34,41 +65,42 @@ function rowColor(item, theme) {
34
65
  }
35
66
  export function TodoPanel({ snapshot, width, rows, running, reducedMotion, theme, caps, }) {
36
67
  const glyphs = pickGlyphs(caps);
68
+ // Ink reapplies all four Yoga borders when the borderStyle object changes,
69
+ // but unchanged borderTop/Bottom=false props are not reapplied on that update.
70
+ const border = useMemo(() => railBorderProps(glyphs.railVertical), [glyphs.railVertical]);
71
+ if (snapshot.items.length === 0 || rows < TODO_LIMITS.panelMinRows)
72
+ return null;
73
+ const compact = rows < TODO_LIMITS.panelFullRows;
37
74
  const showGauge = width >= GAUGE_MIN_COLS && rows >= GAUGE_MIN_ROWS;
38
- const showIndex = width >= TODO_RAIL_INDEX_MIN_COLS;
39
- const itemRows = Math.max(1, rows - FIXED_ROWS - (showGauge ? 1 : 0));
40
- const { visible, hiddenAbove, hiddenBelow } = selectTodoRows(snapshot.items, itemRows);
75
+ const showIndex = !compact && width >= TODO_RAIL_INDEX_MIN_COLS;
76
+ const compactFooter = compact && snapshot.items.length > rows - 1;
77
+ const itemRows = compact ? rows - 1 - Number(compactFooter)
78
+ : rows - FIXED_ROWS - Number(showGauge);
79
+ const { visible, hiddenAbove, hiddenBelow } = compact
80
+ ? selectCompactTodoRows(snapshot.items, itemRows)
81
+ : selectTodoRows(snapshot.items, itemRows);
41
82
  const pct = snapshot.total > 0 ? Math.round((snapshot.doneCount / snapshot.total) * 100) : 0;
42
83
  const gauge = buildGauge(pct, Math.max(4, width - CHROME_COLS - 2), theme, caps);
43
84
  const complete = snapshot.doneCount === snapshot.total;
85
+ const countText = `${snapshot.doneCount}/${snapshot.total}`;
86
+ const showDone = complete
87
+ && width - CHROME_COLS >= 'TODO '.length + countText.length + ' done'.length;
88
+ const count = `${countText}${showDone ? ' done' : ''}`;
44
89
  return (
45
90
  // `flexShrink={0}` IS LOAD-BEARING (P1-4). Ink's `Box.defaultProps` is
46
91
  // `{ flexWrap:'nowrap', flexDirection:'row', flexGrow:0, flexShrink:1 }`, so
47
92
  // a `width={n}` box inside a shrinking row is SQUEEZED under pressure — and
48
93
  // every piece of arithmetic in `rail.ts` silently becomes a suggestion at
49
94
  // exactly the narrow widths where the guarantee matters. `ScrollIndicator`
50
- // spells the same pair one level down. Cross-axis height needs no prop:
51
- // `alignItems` is unset in this tree and yoga's default is `stretch`.
52
- _jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, overflow: "hidden", paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: "TODO" }), _jsxs(Text, { wrap: "truncate", color: complete ? theme.toolDone : theme.muted, children: [snapshot.doneCount, "/", snapshot.total, complete ? ' done' : ''] })] }), showGauge && (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty })] })), _jsx(Text, { children: " " }), hiddenAbove > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenAbove, " above"] })), visible.map(({ item, index }) => {
95
+ // spells the same pair one level down. Height uses the same row budget as
96
+ // selection, so content cannot silently enlarge the middle band.
97
+ _jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, height: rows, overflow: "hidden", paddingLeft: 1, ...border, borderColor: theme.border, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: "TODO" }) }), _jsx(Box, { width: count.length, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", color: complete ? theme.toolDone : theme.muted, children: count }) })] }), showGauge && (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty })] })), !compact && _jsx(Text, { children: " " }), !compact && hiddenAbove > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenAbove, " above"] })), visible.map(({ item, index }) => {
53
98
  const active = item.status === 'in_progress';
54
99
  // The spinner is gated on `running`: a spinner while the agent is idle
55
100
  // is a lie about the state of the world, and this panel outlives the run
56
101
  // that filled it. Reduced motion and a non-Unicode terminal fall back to
57
102
  // the same static marker, exactly as `AssistantEntry` and `TeamPanel` do.
58
103
  const animate = active && running && !reducedMotion && caps.unicode;
59
- return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, width: MARKER_COLS, children: _jsx(Text, { color: rowColor(item, theme), bold: active, children: animate ? _jsx(Spinner, { type: "dots" }) : marker(item, glyphs) }) }), showIndex && (_jsx(Box, { flexShrink: 0, width: INDEX_COLS, children: _jsx(Text, { color: theme.muted, children: String(index + 1).padStart(2) }) })), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: active ? 'wrap' : 'truncate', color: rowColor(item, theme), bold: active, children: active ? clampWrapped(item.activeForm, width) : item.content }) })] }, `${index}-${item.content}`));
60
- }), hiddenBelow > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenBelow, " more"] }))] }));
61
- }
62
- /**
63
- * Keep the wrapping row inside `TODO_LIMITS.activeWrapRows`.
64
- *
65
- * A CHARACTER BUDGET IS NOT A COLUMN BUDGET (P2-3), so this is deliberately
66
- * generous rather than exact: it exists to stop a maximum-length `activeForm`
67
- * from claiming five rows of an 18-column rail, and Ink's display-width-aware
68
- * truncation is what actually keeps the row inside the box. Erring long costs
69
- * nothing — the surrounding `overflow="hidden"` clips it.
70
- */
71
- function clampWrapped(text, width) {
72
- const budget = Math.max(1, width - CHROME_COLS) * TODO_LIMITS.activeWrapRows;
73
- return text.length > budget ? text.slice(0, budget) : text;
104
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, width: MARKER_COLS, children: _jsx(Text, { color: rowColor(item, theme), bold: active, children: animate ? _jsx(Spinner, { type: "dots" }) : marker(item, glyphs) }) }), showIndex && (_jsx(Box, { flexShrink: 0, width: INDEX_COLS, children: _jsx(Text, { color: theme.muted, children: String(index + 1).padStart(2) }) })), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: rowColor(item, theme), bold: active, children: active ? item.activeForm : item.content }) })] }, `${index}-${item.content}`));
105
+ }), compactFooter && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: ["-", hiddenAbove, " +", hiddenBelow] })), !compact && hiddenBelow > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenBelow, " more"] }))] }, glyphs.railVertical));
74
106
  }
@@ -9,7 +9,7 @@
9
9
  * still truthy. Taking that branch would emit a box of ZERO rows - the frame
10
10
  * keeps its fixed height, Yoga hands the row to the transcript, and
11
11
  * `viewportRows()` goes on reporting the old number to `ScrollViewport`,
12
- * `selectWindow`, `overlayMaxRows`, `popupMaxRows` and `todoRailRows`. The
12
+ * `selectWindow`, `overlayMaxRows`, `popupMaxRows` and `buildTodoRailLayout`. The
13
13
  * transcript would gain a row while the updater is idle and lose it the moment
14
14
  * it has news. Presence is the CALLER's decision, through
15
15
  * `shouldRenderUpdateLine`, exactly as `App.tsx` already decides the activity
@@ -15,10 +15,6 @@ function detectColorLevel(env) {
15
15
  return 0;
16
16
  if (env.TERM === 'dumb')
17
17
  return 0;
18
- // A truecolor-capable terminal advertises itself.
19
- const colorterm = env.COLORTERM?.toLowerCase();
20
- if (colorterm === 'truecolor' || colorterm === '24bit')
21
- return 3;
22
18
  // FORCE_COLOR is an explicit override of the auto-detected depth.
23
19
  const force = env.FORCE_COLOR;
24
20
  if (force !== undefined) {
@@ -30,6 +26,10 @@ function detectColorLevel(env) {
30
26
  return 3;
31
27
  return 1; // '1' | 'true' | '' → at least basic color
32
28
  }
29
+ // A truecolor-capable terminal advertises itself.
30
+ const colorterm = env.COLORTERM?.toLowerCase();
31
+ if (colorterm === 'truecolor' || colorterm === '24bit')
32
+ return 3;
33
33
  // Fall back to chalk's own platform detection.
34
34
  const level = chalk.level;
35
35
  return level >= 0 && level <= 3 ? level : 0;
@@ -42,7 +42,7 @@ function detectUnicode(env) {
42
42
  if (env.WT_SESSION)
43
43
  return true; // Windows Terminal
44
44
  const program = env.TERM_PROGRAM;
45
- if (program === 'vscode' || program === 'iTerm.app' || program === 'Apple_Terminal')
45
+ if (program === 'aragonmesh' || program === 'vscode' || program === 'iTerm.app' || program === 'Apple_Terminal')
46
46
  return true;
47
47
  // Legacy cmd.exe (no TERM, no WT_SESSION) and unknown terminals fall back to ASCII.
48
48
  return false;
@@ -14,6 +14,15 @@
14
14
  import type { Entry, UsageTotal } from '../agent/reducer.js';
15
15
  export interface ExitSnapshot {
16
16
  entries: Entry[];
17
+ /**
18
+ * The session total, FORWARDED not copied.
19
+ *
20
+ * It gained `cacheReadTokens` / `cacheWriteTokens`
21
+ * (context-usage-gauge-accuracy §3.6) and this file needed no change for it,
22
+ * which is the point of referencing the reducer's type rather than restating
23
+ * its fields: `renderTranscriptText` reads the total through `promptTokensOf`,
24
+ * so the exit summary and the status bar cannot drift apart.
25
+ */
17
26
  usageTotal: UsageTotal;
18
27
  provider: string;
19
28
  model: string;
@@ -76,7 +76,7 @@ export interface ScrollViewportProps {
76
76
  * This component is the ONLY one inside the viewport subtree that reads
77
77
  * `stdout.columns` directly, and the place it uses the number — the
78
78
  * `MIN_INDICATOR_COLS` gate — is about how much room THIS BOX has, not how
79
- * wide the terminal is. Once the todo rail takes a fifth of the frame those two
79
+ * wide the terminal is. Once the todo rail takes its responsive share of the frame those two
80
80
  * stop being the same number. Optional so every existing caller (and every
81
81
  * test) is unchanged.
82
82
  */
@@ -0,0 +1,18 @@
1
+ export interface AutocompleteLayoutInput {
2
+ itemCount: number;
3
+ selected: number;
4
+ maxRows?: number;
5
+ maxHeight?: number;
6
+ }
7
+ export interface AutocompleteLayout {
8
+ start: number;
9
+ count: number;
10
+ moreBelow: number;
11
+ showMore: boolean;
12
+ rowCount: number;
13
+ }
14
+ /** Window suggestions within a total row budget including borders and footer.
15
+ * Only omitted maxHeight is unlimited. Explicit invalid numbers become zero.
16
+ * Selection is clamped to the available items; no input is mutated or rejected.
17
+ */
18
+ export declare function buildAutocompleteLayout(input: AutocompleteLayoutInput): AutocompleteLayout;
@@ -0,0 +1,26 @@
1
+ /** Window suggestions within a total row budget including borders and footer.
2
+ * Only omitted maxHeight is unlimited. Explicit invalid numbers become zero.
3
+ * Selection is clamped to the available items; no input is mutated or rejected.
4
+ */
5
+ export function buildAutocompleteLayout(input) {
6
+ const total = nonNegativeInt(input.itemCount);
7
+ const maxRows = input.maxRows === undefined ? 6 : nonNegativeInt(input.maxRows);
8
+ const height = input.maxHeight === undefined ? Infinity : nonNegativeInt(input.maxHeight);
9
+ if (total === 0 || maxRows === 0 || height < 3) {
10
+ return { start: 0, count: 0, moreBelow: 0, showMore: false, rowCount: 0 };
11
+ }
12
+ const selected = Math.min(total - 1, nonNegativeInt(input.selected));
13
+ let count = Math.min(total, maxRows, height - 2);
14
+ const windowStart = () => Math.max(0, Math.min(selected - count + 1, total - count));
15
+ let start = windowStart();
16
+ if (count >= 2 && start + count < total && count + 3 > height) {
17
+ count--;
18
+ start = windowStart();
19
+ }
20
+ const moreBelow = total - start - count;
21
+ const showMore = moreBelow > 0 && count + 3 <= height;
22
+ return { start, count, moreBelow, showMore, rowCount: 2 + count + Number(showMore) };
23
+ }
24
+ function nonNegativeInt(value) {
25
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
26
+ }
@@ -1,56 +1,7 @@
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
- */
1
+ export declare const TODO_RAIL_MIN_COLS = 14;
22
2
  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. */
3
+ export declare const TODO_TRANSCRIPT_MIN_COLS = 62;
4
+ export declare const TODO_RAIL_MIN_TOTAL_COLS: number;
26
5
  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
- */
6
+ /** Return a non-decreasing rail width; invalid or narrow terminals have no rail. */
38
7
  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;
@@ -1,68 +1,16 @@
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
- import { TODO_LIMITS } from '../../todo/limits.js';
15
- /** R-e, literally: the rail asks for a fifth of the terminal. */
16
- const RAIL_FRACTION = 0.2;
17
- /** Below this the marker + a two-word title do not fit and the column is noise. */
18
- export const TODO_RAIL_MIN_COLS = 18;
19
- /**
20
- * Above this, 20% is being spent on whitespace: at 36 columns a step title
21
- * already fits on one line, and the transcript is the surface that benefits from
22
- * a 200-column terminal. A deliberate, revertible deviation from a literal
23
- * reading of R-e; it only binds above 180 columns.
24
- */
1
+ /** Responsive TODO rail widths, measured in terminal columns including chrome. */
2
+ const RAIL_FRACTION = 0.15;
3
+ export const TODO_RAIL_MIN_COLS = 14;
25
4
  export const TODO_RAIL_MAX_COLS = 36;
26
- /** Under 80 columns the transcript needs every column; no rail at all. */
27
- export const TODO_RAIL_MIN_TOTAL_COLS = 80;
28
- /** Below this rail width the 2-column index prefix is dropped for content. */
5
+ export const TODO_TRANSCRIPT_MIN_COLS = 62;
6
+ export const TODO_RAIL_MIN_TOTAL_COLS = TODO_TRANSCRIPT_MIN_COLS + TODO_RAIL_MIN_COLS;
29
7
  export const TODO_RAIL_INDEX_MIN_COLS = 22;
30
- /**
31
- * Two properties are pinned by tests, for the reason `budget.ts` records about
32
- * the viewport being non-monotonic before anyone measured it:
33
- *
34
- * - NON-DECREASING in `cols` across [40, 300]. A user dragging a window wider
35
- * must never see the rail get narrower.
36
- * - `cols - todoRailWidth(cols) >= 62` for every `cols >= 80`. Every existing
37
- * width heuristic inside the viewport (`TERSE_HINT_COLS` 42,
38
- * `MIN_INDICATOR_COLS` 50, the status bar's 60/72 steps) sits below that
39
- * floor, so no existing behaviour changes because a rail appeared (R-3).
40
- */
8
+ /** Return a non-decreasing rail width; invalid or narrow terminals have no rail. */
41
9
  export function todoRailWidth(cols) {
42
- if (!Number.isFinite(cols) || cols < TODO_RAIL_MIN_TOTAL_COLS)
10
+ if (!Number.isFinite(cols))
43
11
  return 0;
44
- return Math.min(TODO_RAIL_MAX_COLS, Math.max(TODO_RAIL_MIN_COLS, Math.round(cols * RAIL_FRACTION)));
45
- }
46
- /**
47
- * How many rows the rail may actually draw.
48
- *
49
- * IT IS NOT `viewportBudget`, AND THE DIFFERENCE IS NOT COSMETIC (P1-3 / D-23).
50
- * `viewportRows()` subtracts the STATIC chrome only, and says so; the live team
51
- * roster is mounted inside `AppShell`'s measured bottom box, where it costs a
52
- * header row, up to `TEAM_LIMITS.panelMaxRows` child rows, a `+N more` row and a
53
- * mail row that `viewportBudget` knows nothing about. `ScrollViewport` is immune
54
- * because it MEASURES itself; the rail does not measure - a second
55
- * measure -> setState -> measure loop is exactly what §1.2 refuses - so it has
56
- * to subtract.
57
- *
58
- * Ink clips overflow from the BOTTOM, so an over-request does not error: it
59
- * silently eats the last row, which is the `+N below` marker - the one row whose
60
- * absence is indistinguishable from "the list is short". That is why this is
61
- * arithmetic rather than a shrug at `overflow: hidden`.
62
- */
63
- export function todoRailRows(viewportBudget, teamActive) {
64
- if (!Number.isFinite(viewportBudget))
12
+ const columns = Math.floor(cols);
13
+ if (columns < TODO_RAIL_MIN_TOTAL_COLS)
65
14
  return 0;
66
- const reserved = teamActive ? TODO_LIMITS.railReservedRows : 0;
67
- return Math.max(0, viewportBudget - reserved);
15
+ return Math.min(TODO_RAIL_MAX_COLS, Math.max(TODO_RAIL_MIN_COLS, Math.round(columns * RAIL_FRACTION)));
68
16
  }
@@ -0,0 +1,18 @@
1
+ import type { SubagentRun, TeamSnapshot } from '../../team/types.js';
2
+ export interface TeamPanelLayout {
3
+ collapsed: boolean;
4
+ rowCount: number;
5
+ visible: SubagentRun[];
6
+ hiddenTotal: number;
7
+ hiddenRunning: number;
8
+ }
9
+ export interface TeamPanelLayoutInput {
10
+ snapshot: TeamSnapshot | null;
11
+ terminalRows: number;
12
+ availableRows: number;
13
+ }
14
+ /** Project actual roster rows, preserving three middle rows when expanded.
15
+ * Invalid geometry becomes zero; legacy callers may pass positive Infinity
16
+ * for an unlimited available-row budget. Does not throw or mutate the snapshot.
17
+ */
18
+ export declare function buildTeamPanelLayout(input: TeamPanelLayoutInput): TeamPanelLayout;
@@ -0,0 +1,22 @@
1
+ import { TEAM_LIMITS } from '../../team/limits.js';
2
+ import { selectPanelRows } from '../../team/panel-rows.js';
3
+ import { TODO_LIMITS } from '../../todo/limits.js';
4
+ /** Project actual roster rows, preserving three middle rows when expanded.
5
+ * Invalid geometry becomes zero; legacy callers may pass positive Infinity
6
+ * for an unlimited available-row budget. Does not throw or mutate the snapshot.
7
+ */
8
+ export function buildTeamPanelLayout(input) {
9
+ const available = input.availableRows === Infinity ? Infinity : nonNegativeInt(input.availableRows);
10
+ if (!input.snapshot || available < 1) {
11
+ return { collapsed: true, rowCount: 0, visible: [], hiddenTotal: 0, hiddenRunning: 0 };
12
+ }
13
+ const selection = selectPanelRows(input.snapshot.runs, TEAM_LIMITS.panelMaxRows);
14
+ const natural = 1 + selection.visible.length + Number(selection.hiddenTotal > 0)
15
+ + Number(Boolean(input.snapshot.lastMessage));
16
+ const collapsed = nonNegativeInt(input.terminalRows) < TEAM_LIMITS.panelCollapseRows
17
+ || available - natural < TODO_LIMITS.panelMinRows;
18
+ return { ...selection, collapsed, rowCount: collapsed ? 1 : natural };
19
+ }
20
+ function nonNegativeInt(value) {
21
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
22
+ }
@@ -0,0 +1,24 @@
1
+ import type { RenderMode } from './frame.js';
2
+ export interface TodoRailLayoutInput {
3
+ mode: RenderMode;
4
+ cols: number;
5
+ panelEnabled: boolean;
6
+ overlayOpen: boolean;
7
+ itemCount: number;
8
+ viewportBudget: number;
9
+ teamRows: number;
10
+ popupRows: number;
11
+ }
12
+ export interface TodoRailLayout {
13
+ visible: boolean;
14
+ reason: 'visible' | 'inline' | 'disabled' | 'overlay' | 'empty' | 'narrow' | 'short';
15
+ width: number;
16
+ rows: number;
17
+ contentCols: number;
18
+ popupMaxHeight: number;
19
+ }
20
+ /** Allocate columns and rows from the same team/menu projections as the renderers.
21
+ * Invalid geometry becomes zero, excess team/menu rows are clamped. Menu capacity
22
+ * never depends on menu occupancy or rail visibility, preventing feedback loops.
23
+ */
24
+ export declare function buildTodoRailLayout(input: TodoRailLayoutInput): TodoRailLayout;
@@ -0,0 +1,27 @@
1
+ import { TODO_LIMITS } from '../../todo/limits.js';
2
+ import { todoRailWidth } from './rail.js';
3
+ /** Allocate columns and rows from the same team/menu projections as the renderers.
4
+ * Invalid geometry becomes zero, excess team/menu rows are clamped. Menu capacity
5
+ * never depends on menu occupancy or rail visibility, preventing feedback loops.
6
+ */
7
+ export function buildTodoRailLayout(input) {
8
+ const cols = nonNegativeInt(input.cols);
9
+ const budget = nonNegativeInt(input.viewportBudget);
10
+ const teamRows = Math.min(budget, nonNegativeInt(input.teamRows));
11
+ const popupMaxHeight = Math.max(0, budget - teamRows - TODO_LIMITS.panelMinRows);
12
+ const popupRows = popupMaxHeight < 3 ? 0 : Math.min(nonNegativeInt(input.popupRows), popupMaxHeight);
13
+ const rows = budget - teamRows - popupRows;
14
+ const width = todoRailWidth(cols);
15
+ const reason = input.mode !== 'fullscreen' ? 'inline'
16
+ : !input.panelEnabled ? 'disabled'
17
+ : input.overlayOpen ? 'overlay'
18
+ : nonNegativeInt(input.itemCount) === 0 ? 'empty'
19
+ : width === 0 ? 'narrow'
20
+ : rows < TODO_LIMITS.panelMinRows ? 'short'
21
+ : 'visible';
22
+ const visible = reason === 'visible';
23
+ return { visible, reason, width, rows, contentCols: cols - (visible ? width : 0), popupMaxHeight };
24
+ }
25
+ function nonNegativeInt(value) {
26
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
27
+ }
@@ -6,7 +6,7 @@
6
6
  * transcript in the terminal's native scrollback, and replaying there would
7
7
  * simply print everything twice.
8
8
  */
9
- import { formatCost, formatDuration, formatTokens } from '../agent/usage.js';
9
+ import { formatCost, formatDuration, formatTokens, promptTokensOf } from '../agent/usage.js';
10
10
  import { formatRetryLine } from '../agent/retry-view.js';
11
11
  const DEFAULT_MAX_ENTRIES = 200;
12
12
  const CONTINUATION_INDENT = ' ';
@@ -199,7 +199,11 @@ function renderEntry(entry, glyphs) {
199
199
  export function renderTranscriptText(entries, opts) {
200
200
  const turns = entries.filter((e) => e.kind === 'user').length;
201
201
  const g = opts.glyphs;
202
- const tokens = `${formatTokens(opts.usageTotal.inputTokens)}${g.arrowUp} ${formatTokens(opts.usageTotal.outputTokens)}${g.arrowDown}`;
202
+ // THE SAME PROMPT-SIDE TOTAL THE STATUS BAR SHOWS (context-usage-gauge-accuracy
203
+ // §3.6). This line is what a user compares against the bar they were watching a
204
+ // second earlier, so a different unit here would look like the exit summary
205
+ // losing tokens.
206
+ const tokens = `${formatTokens(promptTokensOf(opts.usageTotal))}${g.arrowUp} ${formatTokens(opts.usageTotal.outputTokens)}${g.arrowDown}`;
203
207
  const summary = [
204
208
  'aragon',
205
209
  `${opts.provider}:${opts.model}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.6.4",
3
+ "version": "0.6.7",
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.16",
59
+ "@aragon-agent/core": "^0.2.19",
60
60
  "chalk": "^5.6.2",
61
61
  "cli-highlight": "^2.0.0",
62
62
  "commander": "^12.1.0",