@aragon-agent/cli 0.6.8 → 0.6.9

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 (78) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +38 -37
  3. package/dist/agent/controller.d.ts +1 -1
  4. package/dist/agent/reducer.d.ts +7 -113
  5. package/dist/agent/reducer.js +0 -62
  6. package/dist/cli.js +48 -149
  7. package/dist/commands/perf.d.ts +32 -2
  8. package/dist/commands/perf.js +32 -2
  9. package/dist/commands/registry.d.ts +2 -2
  10. package/dist/config/env.js +0 -130
  11. package/dist/config/load.d.ts +0 -2
  12. package/dist/config/load.js +2 -31
  13. package/dist/config/schema.d.ts +1 -14
  14. package/dist/config/schema.js +0 -1
  15. package/dist/config/store.js +2 -0
  16. package/dist/logging/install.d.ts +2 -2
  17. package/dist/logging/install.js +2 -2
  18. package/dist/todo/follow-through.d.ts +0 -4
  19. package/dist/todo/follow-through.js +0 -4
  20. package/dist/todo/limits.d.ts +0 -10
  21. package/dist/todo/limits.js +0 -10
  22. package/dist/todo/prompt.d.ts +0 -15
  23. package/dist/ui/ActivityLine.d.ts +26 -34
  24. package/dist/ui/ActivityLine.js +30 -19
  25. package/dist/ui/App.d.ts +1 -41
  26. package/dist/ui/App.js +66 -128
  27. package/dist/ui/BottomStatusRow.d.ts +1 -3
  28. package/dist/ui/BottomStatusRow.js +4 -8
  29. package/dist/ui/Composer.d.ts +28 -1
  30. package/dist/ui/Composer.js +43 -17
  31. package/dist/ui/Header.d.ts +2 -16
  32. package/dist/ui/Header.js +3 -18
  33. package/dist/ui/Logo.d.ts +1 -17
  34. package/dist/ui/Logo.js +0 -16
  35. package/dist/ui/ModeChip.d.ts +7 -18
  36. package/dist/ui/ModeChip.js +9 -0
  37. package/dist/ui/PromptInput.d.ts +2 -28
  38. package/dist/ui/PromptInput.js +4 -7
  39. package/dist/ui/StatusBar.d.ts +0 -11
  40. package/dist/ui/ToastStack.d.ts +2 -18
  41. package/dist/ui/ToastStack.js +4 -11
  42. package/dist/ui/Transcript.d.ts +5 -71
  43. package/dist/ui/Transcript.js +9 -199
  44. package/dist/ui/entries/AssistantEntry.d.ts +0 -23
  45. package/dist/ui/entries/AssistantEntry.js +3 -4
  46. package/dist/ui/entries/DiffView.d.ts +1 -7
  47. package/dist/ui/entries/DiffView.js +2 -2
  48. package/dist/ui/entries/ToolCard.d.ts +0 -6
  49. package/dist/ui/entries/ToolCard.js +4 -13
  50. package/dist/ui/frame-differ.d.ts +13 -0
  51. package/dist/ui/frame-differ.js +36 -6
  52. package/dist/ui/ink-runtime.d.ts +1 -0
  53. package/dist/ui/ink-runtime.js +21 -0
  54. package/dist/ui/layout/AppShell.d.ts +1 -10
  55. package/dist/ui/layout/AppShell.js +2 -5
  56. package/dist/ui/layout/OverlayFrame.d.ts +1 -25
  57. package/dist/ui/layout/OverlayFrame.js +0 -24
  58. package/dist/ui/layout/ScrollViewport.d.ts +3 -0
  59. package/dist/ui/layout/ScrollViewport.js +17 -5
  60. package/dist/ui/layout/frame.d.ts +0 -22
  61. package/dist/ui/layout/frame.js +0 -36
  62. package/dist/ui/layout/todo-layout.d.ts +1 -3
  63. package/dist/ui/layout/todo-layout.js +7 -8
  64. package/dist/ui/layout/viewport-geometry.d.ts +1 -1
  65. package/dist/ui/layout/viewport-geometry.js +1 -1
  66. package/dist/ui/live-clamp.d.ts +1 -29
  67. package/dist/ui/live-clamp.js +1 -29
  68. package/dist/ui/overlays/HelpOverlay.js +1 -0
  69. package/dist/ui/run-status-row.d.ts +61 -0
  70. package/dist/ui/run-status-row.js +70 -0
  71. package/dist/ui/transcript-text.d.ts +0 -8
  72. package/dist/ui/transcript-text.js +0 -8
  73. package/dist/ui/use-startup-notices.d.ts +1 -57
  74. package/dist/ui/use-startup-notices.js +3 -54
  75. package/dist/ui/use-wheel-routing.d.ts +1 -1
  76. package/package.json +2 -2
  77. package/dist/ui/TodoStrip.d.ts +0 -36
  78. package/dist/ui/TodoStrip.js +0 -32
@@ -1,28 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- /**
3
- * OverlayFrame (spec §4.4) — the shared shell for all four overlays.
4
- *
5
- * TWO CONTENT MODES, and the distinction is load-bearing:
6
- *
7
- * - mode A (`rows`): the caller hands over an array of elements that are each
8
- * exactly one row and `wrap="truncate"`. The frame slices them and OWNS the
9
- * scroll keys. Used by Help and Settings.
10
- *
11
- * - mode B (`children`): the content manages itself. The frame draws the title,
12
- * the border and the footer, and touches NOTHING else — no slicing, no
13
- * arrow-key handling, no position indicator.
14
- *
15
- * Mode B exists because of `ink-select-input`: it already windows itself via
16
- * `limit` and registers its own `useInput` with `isFocused` defaulting to true.
17
- * Ink dispatches a key to EVERY mounted input handler, so a frame that also
18
- * claimed the arrow keys would move the model picker's selection twice per press
19
- * — breaking a feature that already works. Slicing it by element would be worse
20
- * still, since it is a single component rather than a list of rows.
21
- *
22
- * `maxRows` may be `Infinity` (the inline render path, which has no fixed frame
23
- * and therefore no height to fit): the frame then renders everything and shows
24
- * no position indicator.
25
- */
26
2
  import { useEffect } from 'react';
27
3
  import { Box, Text } from 'ink';
28
4
  import { pickGlyphs } from '../glyphs.js';
@@ -27,6 +27,9 @@ export interface ScrollViewportProps {
27
27
  scrollbar?: ScrollbarBridge;
28
28
  composerRef?: React.RefObject<DOMElement>;
29
29
  onComposerVisibilityChange?: (visible: boolean) => void;
30
+ /** The run status row above the input box (tui-scrollbar-edge-and-run-row). */
31
+ activityRef?: React.RefObject<DOMElement>;
32
+ onActivityVisibilityChange?: (visible: boolean) => void;
30
33
  theme: Theme;
31
34
  caps: TermCapabilities;
32
35
  children: React.ReactNode;
@@ -8,17 +8,23 @@ import { reduceFollow, shouldArmResume } from './follow-state.js';
8
8
  import { thumbRange } from './scroll-indicator.js';
9
9
  import { ScrollIndicator } from './ScrollIndicator.js';
10
10
  import { buildDocumentLayout } from './document-layout.js';
11
- function composerVisible(composer, clip) {
12
- if (!composer || !clip)
11
+ /**
12
+ * Whether `element` lies fully inside `clip`. A missing element (not mounted) or
13
+ * clip reports `true`, so a caller that consumes the answer must first know the
14
+ * element is actually mounted. Rows are one line tall, so "partially visible"
15
+ * cannot occur for the run status row.
16
+ */
17
+ function elementVisible(element, clip) {
18
+ if (!element || !clip)
13
19
  return true;
14
20
  let top = 0;
15
- let current = composer;
21
+ let current = element;
16
22
  while (current && current !== clip) {
17
23
  top += current.yogaNode?.getComputedTop() ?? 0;
18
24
  current = current.parentNode;
19
25
  }
20
26
  return current === clip && top >= 0 &&
21
- top + measureElement(composer).height <= measureElement(clip).height;
27
+ top + measureElement(element).height <= measureElement(clip).height;
22
28
  }
23
29
  /** One offset for messages, padding and editor; hidden trees keep their local state. */
24
30
  export function ScrollViewport(props) {
@@ -45,6 +51,7 @@ export function ScrollViewport(props) {
45
51
  const lastPin = useRef(props.pinToBottomNonce);
46
52
  const lastReset = useRef(props.resetKey);
47
53
  const lastVisible = useRef();
54
+ const lastActivityVisible = useRef();
48
55
  const previousActive = useRef(active);
49
56
  const geometryRevision = useRef(0);
50
57
  const geometryBounds = useRef('');
@@ -186,11 +193,16 @@ export function ScrollViewport(props) {
186
193
  thumb: thumbRange(viewport, content, next, true), revision: geometryRevision.current };
187
194
  scrollbar.onGeometry?.();
188
195
  }
189
- const visible = composerVisible(props.composerRef?.current ?? null, clipRef.current);
196
+ const visible = elementVisible(props.composerRef?.current ?? null, clipRef.current);
190
197
  if (lastVisible.current !== visible) {
191
198
  lastVisible.current = visible;
192
199
  props.onComposerVisibilityChange?.(visible);
193
200
  }
201
+ const activityVisible = elementVisible(props.activityRef?.current ?? null, clipRef.current);
202
+ if (lastActivityVisible.current !== activityVisible) {
203
+ lastActivityVisible.current = activityVisible;
204
+ props.onActivityVisibilityChange?.(activityVisible);
205
+ }
194
206
  });
195
207
  const shiftUp = Math.max(0, metrics.content - metrics.viewport - offset);
196
208
  useEffect(() => { props.onScrolledLinesChange?.(offset); }, [offset, props.onScrolledLinesChange]);
@@ -4,7 +4,6 @@
4
4
  * Two pure functions with zero React dependency so the load-bearing invariant
5
5
  * I-1 (`frameHeight(r) < r`) can be pinned by a unit test rather than by hope.
6
6
  */
7
- export type RenderMode = 'fullscreen' | 'inline';
8
7
  /** Below this many rows a full-screen frame has no usable viewport left. */
9
8
  export declare const MIN_FULLSCREEN_ROWS = 12;
10
9
  /** Below this many columns the chrome (composer + status bar) cannot line-wrap sanely. */
@@ -35,24 +34,3 @@ export declare const FALLBACK_COLS = 80;
35
34
  * terminal is strictly better than a per-frame full-screen repaint.
36
35
  */
37
36
  export declare function frameHeight(rows: number | undefined): number;
38
- interface StdoutLike {
39
- isTTY?: boolean;
40
- rows?: number;
41
- columns?: number;
42
- }
43
- /**
44
- * Decide the render mode once, at startup (never re-decided at run time —
45
- * entering/leaving the alt-screen mid-session is a state machine we do not want).
46
- *
47
- * Order is semantics, not style:
48
- * 1. an explicit opt-out always wins (users can always get v0.2.0 back);
49
- * 2. `isTTY` is the one gate `--fullscreen` cannot override — writing ANSI into
50
- * a pipe or a redirected file is a contract violation, whereas a heuristic
51
- * misfire only costs polish;
52
- * 3. the explicit force comes next so it can beat the heuristics below;
53
- * 4. heuristics (TERM/CI/size), all overridable by `--fullscreen`.
54
- */
55
- export declare function decideRenderMode(flags: {
56
- fullscreen?: boolean;
57
- }, env: Record<string, string | undefined>, stdout: StdoutLike | undefined): RenderMode;
58
- export {};
@@ -37,39 +37,3 @@ export function frameHeight(rows) {
37
37
  const r = rows && rows > 0 ? rows : FALLBACK_ROWS;
38
38
  return Math.max(0, r - 1);
39
39
  }
40
- /** `CI=` / `CI=0` / `CI=false` are not CI; anything else non-empty is. */
41
- function isTruthyEnv(value) {
42
- if (value === undefined)
43
- return false;
44
- const v = value.trim().toLowerCase();
45
- return v.length > 0 && v !== '0' && v !== 'false';
46
- }
47
- /**
48
- * Decide the render mode once, at startup (never re-decided at run time —
49
- * entering/leaving the alt-screen mid-session is a state machine we do not want).
50
- *
51
- * Order is semantics, not style:
52
- * 1. an explicit opt-out always wins (users can always get v0.2.0 back);
53
- * 2. `isTTY` is the one gate `--fullscreen` cannot override — writing ANSI into
54
- * a pipe or a redirected file is a contract violation, whereas a heuristic
55
- * misfire only costs polish;
56
- * 3. the explicit force comes next so it can beat the heuristics below;
57
- * 4. heuristics (TERM/CI/size), all overridable by `--fullscreen`.
58
- */
59
- export function decideRenderMode(flags, env, stdout) {
60
- if (flags.fullscreen === false)
61
- return 'inline';
62
- if (!stdout?.isTTY)
63
- return 'inline';
64
- if (flags.fullscreen === true)
65
- return 'fullscreen';
66
- if (env.TERM === 'dumb')
67
- return 'inline';
68
- if (isTruthyEnv(env.CI))
69
- return 'inline';
70
- if ((stdout.rows ?? 0) < MIN_FULLSCREEN_ROWS)
71
- return 'inline';
72
- if ((stdout.columns ?? 0) < MIN_FULLSCREEN_COLS)
73
- return 'inline';
74
- return 'fullscreen';
75
- }
@@ -1,6 +1,4 @@
1
- import type { RenderMode } from './frame.js';
2
1
  export interface TodoRailLayoutInput {
3
- mode: RenderMode;
4
2
  cols: number;
5
3
  panelEnabled: boolean;
6
4
  overlayOpen: boolean;
@@ -12,7 +10,7 @@ export interface TodoRailLayoutInput {
12
10
  }
13
11
  export interface TodoRailLayout {
14
12
  visible: boolean;
15
- reason: 'visible' | 'inline' | 'disabled' | 'overlay' | 'empty' | 'narrow' | 'short';
13
+ reason: 'visible' | 'disabled' | 'overlay' | 'empty' | 'narrow' | 'short';
16
14
  width: number;
17
15
  rows: number;
18
16
  contentCols: number;
@@ -11,15 +11,14 @@ export function buildTodoRailLayout(input) {
11
11
  const popupMaxHeight = Math.max(0, budget - teamRows - TODO_LIMITS.panelMinRows
12
12
  - nonNegativeInt(input.composerBaseRows ?? 0));
13
13
  const popupRows = popupMaxHeight < 3 ? 0 : Math.min(nonNegativeInt(input.popupRows), popupMaxHeight);
14
- const rows = input.mode === 'fullscreen' ? budget : budget - teamRows - popupRows;
14
+ const rows = budget;
15
15
  const width = todoRailWidth(cols);
16
- const reason = input.mode !== 'fullscreen' ? 'inline'
17
- : !input.panelEnabled ? 'disabled'
18
- : input.overlayOpen ? 'overlay'
19
- : nonNegativeInt(input.itemCount) === 0 ? 'empty'
20
- : width === 0 ? 'narrow'
21
- : rows < TODO_LIMITS.panelMinRows ? 'short'
22
- : 'visible';
16
+ const reason = !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';
23
22
  const visible = reason === 'visible';
24
23
  return { visible, reason, width, rows, contentCols: cols - (visible ? width : 0), popupMaxHeight };
25
24
  }
@@ -21,7 +21,7 @@ export interface ViewportGeometry {
21
21
  }
22
22
  /**
23
23
  * The default is the UNMEASURED geometry, and consumers must treat it as such:
24
- * a `TranscriptList` rendered outside a `ScrollViewport` (inline mode, and every
24
+ * a `TranscriptList` rendered outside a `ScrollViewport` (including
25
25
  * test that mounts it bare) sees `viewportRows: 0`, which `selectWindow` reads
26
26
  * as "first frame" and answers by pinning the live tail.
27
27
  */
@@ -13,7 +13,7 @@
13
13
  import { createContext, useContext } from 'react';
14
14
  /**
15
15
  * The default is the UNMEASURED geometry, and consumers must treat it as such:
16
- * a `TranscriptList` rendered outside a `ScrollViewport` (inline mode, and every
16
+ * a `TranscriptList` rendered outside a `ScrollViewport` (including
17
17
  * test that mounts it bare) sees `viewportRows: 0`, which `selectWindow` reads
18
18
  * as "first frame" and answers by pinning the live tail.
19
19
  */
@@ -1,32 +1,4 @@
1
- /**
2
- * Live-region clamp for INLINE render mode (tui-render-performance L5 / R6).
3
- *
4
- * Inline mode has no fixed frame, so nothing kept its live (non-`<Static>`)
5
- * region shorter than the terminal. When one live entry grew taller than
6
- * `stdout.rows`, Ink took the branch at `ink.js:118-123`:
7
- *
8
- * if (outputHeight >= this.options.stdout.rows) {
9
- * stdout.write(clearTerminal + this.fullStaticOutput + output);
10
- * }
11
- *
12
- * `fullStaticOutput` accumulates EVERY `<Static>` frame of the whole session and
13
- * is never trimmed, so once that branch is taken it is taken on every subsequent
14
- * frame — bypassing both the `output !== lastOutput` dedupe and `throttledLog` —
15
- * and the terminal receives the entire session history thirty times a second.
16
- * That is a genuine hard freeze, and it is reachable today by any inline user
17
- * (`--no-fullscreen`, `TERM=dumb`, CI, a short or narrow terminal) whose model
18
- * writes a long answer.
19
- *
20
- * I-L5-1 — keeping the live region strictly below `stdout.rows` makes that
21
- * branch UNREACHABLE, which is the inline analogue of `frame.ts`'s
22
- * `frameHeight(r) < r` and deserves the same standing. We cannot bound Ink's
23
- * `fullStaticOutput` growth (it is private and append-only); preventing the
24
- * branch that WRITES it is the entire mitigation, and nothing about the memory
25
- * it holds is fixed by this module.
26
- *
27
- * Nothing is lost, only deferred: when the entry settles it moves into
28
- * `<Static>` and is printed in full.
29
- */
1
+ /** Keep the newest thinking lines visible while an assistant entry streams. */
30
2
  export interface LiveClamp {
31
3
  text: string;
32
4
  /** Rows dropped off the FRONT. `0` means the text was returned untouched. */
@@ -1,32 +1,4 @@
1
- /**
2
- * Live-region clamp for INLINE render mode (tui-render-performance L5 / R6).
3
- *
4
- * Inline mode has no fixed frame, so nothing kept its live (non-`<Static>`)
5
- * region shorter than the terminal. When one live entry grew taller than
6
- * `stdout.rows`, Ink took the branch at `ink.js:118-123`:
7
- *
8
- * if (outputHeight >= this.options.stdout.rows) {
9
- * stdout.write(clearTerminal + this.fullStaticOutput + output);
10
- * }
11
- *
12
- * `fullStaticOutput` accumulates EVERY `<Static>` frame of the whole session and
13
- * is never trimmed, so once that branch is taken it is taken on every subsequent
14
- * frame — bypassing both the `output !== lastOutput` dedupe and `throttledLog` —
15
- * and the terminal receives the entire session history thirty times a second.
16
- * That is a genuine hard freeze, and it is reachable today by any inline user
17
- * (`--no-fullscreen`, `TERM=dumb`, CI, a short or narrow terminal) whose model
18
- * writes a long answer.
19
- *
20
- * I-L5-1 — keeping the live region strictly below `stdout.rows` makes that
21
- * branch UNREACHABLE, which is the inline analogue of `frame.ts`'s
22
- * `frameHeight(r) < r` and deserves the same standing. We cannot bound Ink's
23
- * `fullStaticOutput` growth (it is private and append-only); preventing the
24
- * branch that WRITES it is the entire mitigation, and nothing about the memory
25
- * it holds is fixed by this module.
26
- *
27
- * Nothing is lost, only deferred: when the entry settles it moves into
28
- * `<Static>` and is printed in full.
29
- */
1
+ /** Keep the newest thinking lines visible while an assistant entry streams. */
30
2
  /**
31
3
  * Keep the LAST `maxRows` rows of `text`; report how many were dropped.
32
4
  *
@@ -27,6 +27,7 @@ function keyRows(times) {
27
27
  // a key, so it has to be named somewhere they can look it up. The one-shot
28
28
  // startup notice is the other place, and it scrolls away.
29
29
  ['Drag right edge', 'Scroll messages and input together; type to return'],
30
+ ['Run status row', 'Spinner and steer / interrupt keys, above the input during a run'],
30
31
  ['Drag text (left button)', 'Select text on screen; releasing copies it'],
31
32
  ['Up / Down', 'Prompt history (empty input)'],
32
33
  // Pasting is the one affordance here with no key of its own, so the only
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The run status row's width plan (tui-scrollbar-edge-and-run-row §3.3.3).
3
+ *
4
+ * While a run is in flight, ONE row above the input box carries the spinner, the
5
+ * phrase and the `steer / interrupt / exit` hint clauses, and `planRunRow` decides
6
+ * what survives when the line is too narrow. It is a pure function with no React
7
+ * and no I/O so the whole ladder can be tested as arithmetic.
8
+ *
9
+ * THE ALLOCATION IS PER CLAUSE, NOT PER STRING, and that is the whole point.
10
+ * The idle hint row this one replaces owned the full line width and ended in
11
+ * `wrap="truncate"`; handing the label half the line and truncating the joined
12
+ * hint would cut `interrupt` to `inter...` at 38 columns - a regression of the
13
+ * "running hint is NEVER abbreviated" rule `Composer.hintText` documents. So:
14
+ *
15
+ * - clauses are dropped WHOLE, from the tail (`exit`, then `stop N`), and
16
+ * - `steer` and `interrupt` (REQUIRED_CLAUSES) are never dropped or cut mid-word
17
+ * while the row is enabled, and
18
+ * - the only thing allowed to end in an ellipsis is the label.
19
+ */
20
+ /** Everything the label needs, kept in one shape so there is one place that names it. */
21
+ export interface RunActivity {
22
+ /** Epoch ms the current run began - the phrase sequence's seed. */
23
+ startedAt: number;
24
+ /** `Date.now() - startedAt`, already computed by App's 200 ms ticker. */
25
+ elapsedMs: number;
26
+ /** The RAW config flag, never the widened one (single-spinner D-5). */
27
+ reducedMotion: boolean;
28
+ /** The tool currently in flight, if any. */
29
+ runningTool?: string;
30
+ /** A context compaction is in flight; outranks the tool name and the phrase. */
31
+ compacting?: boolean;
32
+ }
33
+ export interface RunRowPlanInput {
34
+ /** Width of the row's container (the composer's content columns). */
35
+ cols: number;
36
+ /** Spinner + space + label, in display columns (the label's natural width). */
37
+ labelCols: number;
38
+ /** Display width of each hint clause, most urgent first. */
39
+ hintClauseCols: readonly number[];
40
+ /** Display width of the separator between clauses (` · ` or ` - `). */
41
+ separatorCols: number;
42
+ /** Display width of the right-aligned mode chip; 0 = no chip. */
43
+ chipCols: number;
44
+ }
45
+ export interface RunRowPlan {
46
+ /** Columns the label may occupy; below its natural width it is ellipsized. */
47
+ labelCols: number;
48
+ /** How many leading clauses to show; 0 = none (defensive tier only). */
49
+ hintClauses: number;
50
+ /** Whether the chip fits after everything else. */
51
+ chip: boolean;
52
+ }
53
+ /** One leading space, matching the idle hint row's `' '` indent. */
54
+ export declare const RUN_ROW_LEAD = 1;
55
+ /** Narrowest the label may be squeezed before a clause is dropped instead. */
56
+ export declare const RUN_ROW_MIN_LABEL_COLS = 10;
57
+ /** `steer` and `interrupt`: always whole while the row is enabled. */
58
+ export declare const RUN_ROW_REQUIRED_CLAUSES = 2;
59
+ /** Space kept between the last clause and the chip. */
60
+ export declare const RUN_ROW_CHIP_GAP = 2;
61
+ export declare function planRunRow(input: RunRowPlanInput): RunRowPlan;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The run status row's width plan (tui-scrollbar-edge-and-run-row §3.3.3).
3
+ *
4
+ * While a run is in flight, ONE row above the input box carries the spinner, the
5
+ * phrase and the `steer / interrupt / exit` hint clauses, and `planRunRow` decides
6
+ * what survives when the line is too narrow. It is a pure function with no React
7
+ * and no I/O so the whole ladder can be tested as arithmetic.
8
+ *
9
+ * THE ALLOCATION IS PER CLAUSE, NOT PER STRING, and that is the whole point.
10
+ * The idle hint row this one replaces owned the full line width and ended in
11
+ * `wrap="truncate"`; handing the label half the line and truncating the joined
12
+ * hint would cut `interrupt` to `inter...` at 38 columns - a regression of the
13
+ * "running hint is NEVER abbreviated" rule `Composer.hintText` documents. So:
14
+ *
15
+ * - clauses are dropped WHOLE, from the tail (`exit`, then `stop N`), and
16
+ * - `steer` and `interrupt` (REQUIRED_CLAUSES) are never dropped or cut mid-word
17
+ * while the row is enabled, and
18
+ * - the only thing allowed to end in an ellipsis is the label.
19
+ */
20
+ /** One leading space, matching the idle hint row's `' '` indent. */
21
+ export const RUN_ROW_LEAD = 1;
22
+ /** Narrowest the label may be squeezed before a clause is dropped instead. */
23
+ export const RUN_ROW_MIN_LABEL_COLS = 10;
24
+ /** `steer` and `interrupt`: always whole while the row is enabled. */
25
+ export const RUN_ROW_REQUIRED_CLAUSES = 2;
26
+ /** Space kept between the last clause and the chip. */
27
+ export const RUN_ROW_CHIP_GAP = 2;
28
+ /** Non-finite or negative widths collapse to 0 so no input can make the plan throw. */
29
+ function width(value) {
30
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
31
+ }
32
+ /** Width of the first `count` clauses joined by `separator`. */
33
+ function clausesWidth(clauses, count, separator) {
34
+ if (count <= 0)
35
+ return 0;
36
+ let total = separator * (count - 1);
37
+ for (let i = 0; i < count; i += 1)
38
+ total += clauses[i] ?? 0;
39
+ return total;
40
+ }
41
+ export function planRunRow(input) {
42
+ const inner = Math.max(0, width(input.cols) - RUN_ROW_LEAD);
43
+ const natural = width(input.labelCols);
44
+ const separator = width(input.separatorCols);
45
+ const chipCols = width(input.chipCols);
46
+ const clauses = input.hintClauseCols.map(width);
47
+ const total = clauses.length;
48
+ const labelOnly = {
49
+ labelCols: Math.min(natural, inner), hintClauses: 0, chip: false,
50
+ };
51
+ if (total < RUN_ROW_REQUIRED_CLAUSES)
52
+ return labelOnly;
53
+ // Tier 1: everything fits; the chip is the first thing to go.
54
+ const everything = natural + separator + clausesWidth(clauses, total, separator);
55
+ if (everything <= inner) {
56
+ const chip = chipCols > 0 && everything + RUN_ROW_CHIP_GAP + chipCols <= inner;
57
+ return { labelCols: natural, hintClauses: total, chip };
58
+ }
59
+ // Tier 2: drop clauses from the tail, but never below the required two; only the
60
+ // label may be squeezed (down to its minimum) to make the remaining clauses fit.
61
+ for (let kept = total - 1; kept >= RUN_ROW_REQUIRED_CLAUSES; kept -= 1) {
62
+ const available = inner - separator - clausesWidth(clauses, kept, separator);
63
+ if (available >= RUN_ROW_MIN_LABEL_COLS) {
64
+ return { labelCols: Math.min(natural, available), hintClauses: kept, chip: false };
65
+ }
66
+ }
67
+ // Tier 3 (defensive, below ~40 columns): the label alone. The status bar's own
68
+ // `Esc x2 interrupt` carries the urgent hint at this width.
69
+ return labelOnly;
70
+ }
@@ -1,11 +1,3 @@
1
- /**
2
- * Plain-text session rendering (spec §4.4). Pure, colorless, ANSI-free, so the
3
- * exit replay can be piped, diffed, or pasted into an issue.
4
- *
5
- * Used only by the full-screen exit replay: inline mode already left the real
6
- * transcript in the terminal's native scrollback, and replaying there would
7
- * simply print everything twice.
8
- */
9
1
  import type { Entry, UsageTotal } from '../agent/reducer.js';
10
2
  import type { Glyphs } from './glyphs.js';
11
3
  export interface TranscriptTextOptions {
@@ -1,11 +1,3 @@
1
- /**
2
- * Plain-text session rendering (spec §4.4). Pure, colorless, ANSI-free, so the
3
- * exit replay can be piped, diffed, or pasted into an issue.
4
- *
5
- * Used only by the full-screen exit replay: inline mode already left the real
6
- * transcript in the terminal's native scrollback, and replaying there would
7
- * simply print everything twice.
8
- */
9
1
  import { formatCost, formatDuration, formatTokens, promptTokensOf } from '../agent/usage.js';
10
2
  import { formatRetryLine } from '../agent/retry-view.js';
11
3
  const DEFAULT_MAX_ENTRIES = 200;
@@ -40,53 +40,7 @@ export declare const MOUSE_NOTICE_VERSION = 1;
40
40
  * that offered only `/plan`, recorded back when this was a boolean.
41
41
  */
42
42
  export declare const VT_INPUT_NOTICE_VERSION = 1;
43
- /**
44
- * The `shift-tab-and-mouse-wheel-dead-on-some-terminals` notice (F1'). Shown at
45
- * most once per `VT_INPUT_NOTICE_VERSION`, and only on a Windows console that
46
- * cannot deliver either input — see `ui/win-vt-input.ts` for why that is a
47
- * property of the Node version rather than of the terminal.
48
- *
49
- * IT NAMES WHAT THE KEY DOES NOW, not merely that it is unavailable, and that
50
- * is the load-bearing half of the text. `Shift+Tab` on these machines is not
51
- * inert: the modifier is lost inside libuv, so Ink sees an ordinary `Tab` and
52
- * the `/` palette or an `@` popup completes its selection over the user's
53
- * draft. A user told only "this key does not work" keeps pressing it and keeps
54
- * losing text without ever connecting the two.
55
- *
56
- * THE FALLBACK KEY COMES BEFORE `/plan` IN BOTH BRANCHES, and it is named from
57
- * `MODE_TOGGLE_KEYS` rather than spelled out, so the four consumers of that
58
- * constant cannot drift apart. This notice is the last one of them to be wired
59
- * up, and it is also the only channel that reaches the affected user without
60
- * being asked to: the composer hint does not exist at all in inline mode and is
61
- * switched off by a short terminal or `hints: false` in full-screen, and
62
- * `/help` has to be typed by someone whose current belief is "the key does
63
- * nothing" rather than "I should read the help". Ordering follows cost — one
64
- * keystroke before four and a return.
65
- *
66
- * `Shift+Up` / `Shift+Down` is the scroll workaround named here because it is
67
- * the only one PROVEN to survive: both Node versions emit `\x1b[1;2A` for it,
68
- * byte for byte, in the probe data. `PgUp` / `PgDn` is expected to work too but
69
- * was never injected, so it is left out rather than promised.
70
- *
71
- * THE WHEEL HALF IS FULL-SCREEN ONLY, and dropping it inline is not a detail.
72
- * `Shift+Tab` is broken in both modes — the modifier dies inside libuv, well
73
- * below anything this app chooses — but the wheel is only ours to lose in
74
- * full-screen: inline never asks the console to report it (`mode` is a conjunct
75
- * of `cli.tsx::wantMouse`), so the wheel goes on scrolling the terminal's own
76
- * scrollback exactly as it always has. And `Shift+Up` / `Shift+Down` are handled
77
- * inside `App`'s `if (fullscreen)` branch, so recommending them inline names a
78
- * key that does nothing. Telling an inline user their working wheel is dead and
79
- * then handing them an inert workaround is the same failure this notice exists
80
- * to end — advice about a mode that is not running — merely pointed the other
81
- * way.
82
- *
83
- * EXPORTED FOR ITS TEST, unlike the two notices above it. Asserting this text
84
- * through a rendered frame does not work: the transcript wraps a notice at two
85
- * columns wider than it displays it, so any phrase landing on a wrap boundary
86
- * loses a character or two on the way to `lastFrame()` and the assertion fails
87
- * for a reason that has nothing to do with the text.
88
- */
89
- export declare function vtInputDeadNotice(nodeVersion: string, fullscreen: boolean): string;
43
+ export declare function vtInputDeadNotice(nodeVersion: string): string;
90
44
  /**
91
45
  * `{ requested, resolved }` when the raise happened, `undefined` otherwise —
92
46
  * i.e. `cfg.transcriptRetainRequested` and `cfg.transcriptRetain`.
@@ -132,16 +86,6 @@ export interface MouseNoticeOptions {
132
86
  export interface VtInputNoticeOptions {
133
87
  /** `process.versions.node`, quoted back so the user can act on it. */
134
88
  nodeVersion: string;
135
- /**
136
- * Whether this run owns the alternate screen, i.e. `App`'s own `fullscreen`.
137
- *
138
- * Selects which half of the text applies: inline keeps the wheel and loses the
139
- * full-screen scroll keys, so it is told about `Shift+Tab` and nothing else
140
- * (see `vtInputDeadNotice`). Taken from the caller rather than re-derived here
141
- * for the same reason the version is — one source, no chance of the notice and
142
- * the renderer disagreeing about which mode is running.
143
- */
144
- fullscreen: boolean;
145
89
  /**
146
90
  * Record that `VT_INPUT_NOTICE_VERSION` was shown (`state.json`). Called once,
147
91
  * after dispatch — and it must write that constant, not `true`: the gate
@@ -80,61 +80,11 @@ function retainRaisedNotice(requested, resolved) {
80
80
  * that offered only `/plan`, recorded back when this was a boolean.
81
81
  */
82
82
  export const VT_INPUT_NOTICE_VERSION = 1;
83
- /**
84
- * The `shift-tab-and-mouse-wheel-dead-on-some-terminals` notice (F1'). Shown at
85
- * most once per `VT_INPUT_NOTICE_VERSION`, and only on a Windows console that
86
- * cannot deliver either input — see `ui/win-vt-input.ts` for why that is a
87
- * property of the Node version rather than of the terminal.
88
- *
89
- * IT NAMES WHAT THE KEY DOES NOW, not merely that it is unavailable, and that
90
- * is the load-bearing half of the text. `Shift+Tab` on these machines is not
91
- * inert: the modifier is lost inside libuv, so Ink sees an ordinary `Tab` and
92
- * the `/` palette or an `@` popup completes its selection over the user's
93
- * draft. A user told only "this key does not work" keeps pressing it and keeps
94
- * losing text without ever connecting the two.
95
- *
96
- * THE FALLBACK KEY COMES BEFORE `/plan` IN BOTH BRANCHES, and it is named from
97
- * `MODE_TOGGLE_KEYS` rather than spelled out, so the four consumers of that
98
- * constant cannot drift apart. This notice is the last one of them to be wired
99
- * up, and it is also the only channel that reaches the affected user without
100
- * being asked to: the composer hint does not exist at all in inline mode and is
101
- * switched off by a short terminal or `hints: false` in full-screen, and
102
- * `/help` has to be typed by someone whose current belief is "the key does
103
- * nothing" rather than "I should read the help". Ordering follows cost — one
104
- * keystroke before four and a return.
105
- *
106
- * `Shift+Up` / `Shift+Down` is the scroll workaround named here because it is
107
- * the only one PROVEN to survive: both Node versions emit `\x1b[1;2A` for it,
108
- * byte for byte, in the probe data. `PgUp` / `PgDn` is expected to work too but
109
- * was never injected, so it is left out rather than promised.
110
- *
111
- * THE WHEEL HALF IS FULL-SCREEN ONLY, and dropping it inline is not a detail.
112
- * `Shift+Tab` is broken in both modes — the modifier dies inside libuv, well
113
- * below anything this app chooses — but the wheel is only ours to lose in
114
- * full-screen: inline never asks the console to report it (`mode` is a conjunct
115
- * of `cli.tsx::wantMouse`), so the wheel goes on scrolling the terminal's own
116
- * scrollback exactly as it always has. And `Shift+Up` / `Shift+Down` are handled
117
- * inside `App`'s `if (fullscreen)` branch, so recommending them inline names a
118
- * key that does nothing. Telling an inline user their working wheel is dead and
119
- * then handing them an inert workaround is the same failure this notice exists
120
- * to end — advice about a mode that is not running — merely pointed the other
121
- * way.
122
- *
123
- * EXPORTED FOR ITS TEST, unlike the two notices above it. Asserting this text
124
- * through a rendered frame does not work: the transcript wraps a notice at two
125
- * columns wider than it displays it, so any phrase landing on a wrap boundary
126
- * loses a character or two on the way to `lastFrame()` and the assertion fails
127
- * for a reason that has nothing to do with the text.
128
- */
129
- export function vtInputDeadNotice(nodeVersion, fullscreen) {
83
+ export function vtInputDeadNotice(nodeVersion) {
130
84
  const cause = `Node ${nodeVersion} on Windows does not turn on the console's VT input mode, so `;
131
85
  const cost = 'it will not switch mode, and with the / palette or an @ popup open it completes that ' +
132
86
  'instead, overwriting your draft. ';
133
87
  const toggle = `Press ${MODE_TOGGLE_KEYS.fallback} to switch mode instead (or /plan)`;
134
- if (!fullscreen) {
135
- return (`${cause}Shift+Tab never arrives: it comes through as a PLAIN TAB - ${cost}` +
136
- `${toggle}. Node 22.17.0+ (or 24.2.0+) restores it.`);
137
- }
138
88
  return (`${cause}two keys never arrive: the wheel sends nothing at all, and Shift+Tab arrives ` +
139
89
  `as a PLAIN TAB - ${cost}${toggle}, and Shift+Up / Shift+Down to scroll. ` +
140
90
  'Node 22.17.0+ (or 24.2.0+) restores both.');
@@ -199,7 +149,6 @@ export function useStartupNotices(dispatch, mouseNotice, retainNotice, vtInputNo
199
149
  const vtNoticeRef = useRef(vtInputNotice);
200
150
  vtNoticeRef.current = vtInputNotice;
201
151
  const vtNodeVersion = vtInputNotice?.nodeVersion;
202
- const vtFullscreen = vtInputNotice?.fullscreen ?? false;
203
152
  const vtNoticeShown = useRef(false);
204
153
  useEffect(() => {
205
154
  if (vtNodeVersion === undefined || vtNoticeShown.current)
@@ -210,10 +159,10 @@ export function useStartupNotices(dispatch, mouseNotice, retainNotice, vtInputNo
210
159
  dispatch({
211
160
  type: 'notice',
212
161
  level: 'warn',
213
- text: vtInputDeadNotice(vtNodeVersion, vtFullscreen),
162
+ text: vtInputDeadNotice(vtNodeVersion),
214
163
  });
215
164
  vtNoticeRef.current?.onSeen();
216
- }, [vtNodeVersion, vtFullscreen, dispatch]);
165
+ }, [vtNodeVersion, dispatch]);
217
166
  // A `warn` notice rather than a toast: it is about a value the user typed, so
218
167
  // it belongs in the transcript where it can be scrolled back to, not in a row
219
168
  // that expires. DEPENDS ON THE TWO SCALARS, never on the options object —
@@ -32,7 +32,7 @@ export declare const OVERLAY_PAGE = 8;
32
32
  export interface WheelRoutingOptions {
33
33
  isPointerCaptured?: () => boolean;
34
34
  isActive?: () => boolean;
35
- /** Absent when mouse support is off, in inline mode, or on a non-TTY. */
35
+ /** Absent when mouse support is off, or on a non-TTY. */
36
36
  mouseSource?: MouseSource;
37
37
  /** Read at EVENT time for the routing branch, and again at FLUSH time (P1-3). */
38
38
  getOverlay: () => Overlay | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
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.20",
59
+ "@aragon-agent/core": "^0.2.21",
60
60
  "chalk": "^5.6.2",
61
61
  "cli-highlight": "^2.0.0",
62
62
  "commander": "^12.1.0",