@aragon-agent/cli 0.5.1 → 0.5.3

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 (106) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +155 -5
  3. package/dist/agent/controller.d.ts +51 -1
  4. package/dist/agent/controller.js +177 -2
  5. package/dist/agent/headless.d.ts +12 -0
  6. package/dist/agent/headless.js +41 -1
  7. package/dist/agent/reducer.d.ts +48 -0
  8. package/dist/agent/reducer.js +91 -2
  9. package/dist/agent/system-prompt.d.ts +17 -0
  10. package/dist/agent/system-prompt.js +4 -0
  11. package/dist/cli.js +131 -6
  12. package/dist/commands/builtins.js +82 -0
  13. package/dist/config/app-paths.d.ts +17 -0
  14. package/dist/config/app-paths.js +21 -0
  15. package/dist/config/cli-commands.d.ts +26 -2
  16. package/dist/config/cli-commands.js +63 -4
  17. package/dist/config/env.d.ts +2 -2
  18. package/dist/config/env.js +36 -2
  19. package/dist/config/history-commands.d.ts +20 -0
  20. package/dist/config/history-commands.js +71 -0
  21. package/dist/config/load.d.ts +15 -0
  22. package/dist/config/load.js +88 -5
  23. package/dist/config/migrate-state-out-of-config.d.ts +32 -0
  24. package/dist/config/migrate-state-out-of-config.js +85 -0
  25. package/dist/config/prompt-history.d.ts +86 -0
  26. package/dist/config/prompt-history.js +332 -0
  27. package/dist/config/schema.d.ts +125 -9
  28. package/dist/config/schema.js +90 -5
  29. package/dist/config/store.js +13 -4
  30. package/dist/config/ui-state.d.ts +52 -0
  31. package/dist/config/ui-state.js +149 -0
  32. package/dist/input/mouse-events.d.ts +61 -0
  33. package/dist/input/mouse-events.js +153 -0
  34. package/dist/input/stdin-mouse-filter.d.ts +42 -0
  35. package/dist/input/stdin-mouse-filter.js +153 -0
  36. package/dist/logging/install.d.ts +85 -0
  37. package/dist/logging/install.js +94 -0
  38. package/dist/logging/logger.d.ts +1 -1
  39. package/dist/session/persist.d.ts +16 -0
  40. package/dist/session/persist.js +19 -1
  41. package/dist/team/activity.d.ts +67 -0
  42. package/dist/team/activity.js +130 -0
  43. package/dist/team/bus.d.ts +190 -0
  44. package/dist/team/bus.js +366 -0
  45. package/dist/team/comm-tools.d.ts +61 -0
  46. package/dist/team/comm-tools.js +221 -0
  47. package/dist/team/human-queue.d.ts +55 -0
  48. package/dist/team/human-queue.js +72 -0
  49. package/dist/team/limits.d.ts +123 -0
  50. package/dist/team/limits.js +128 -0
  51. package/dist/team/normalize.d.ts +39 -0
  52. package/dist/team/normalize.js +106 -0
  53. package/dist/team/panel-rows.d.ts +48 -0
  54. package/dist/team/panel-rows.js +75 -0
  55. package/dist/team/prompt.d.ts +49 -0
  56. package/dist/team/prompt.js +87 -0
  57. package/dist/team/report.d.ts +58 -0
  58. package/dist/team/report.js +205 -0
  59. package/dist/team/retry.d.ts +48 -0
  60. package/dist/team/retry.js +56 -0
  61. package/dist/team/runtime.d.ts +122 -0
  62. package/dist/team/runtime.js +406 -0
  63. package/dist/team/subagent.d.ts +90 -0
  64. package/dist/team/subagent.js +294 -0
  65. package/dist/team/task-tool.d.ts +47 -0
  66. package/dist/team/task-tool.js +118 -0
  67. package/dist/team/types.d.ts +172 -0
  68. package/dist/team/types.js +13 -0
  69. package/dist/tools/index.d.ts +48 -2
  70. package/dist/tools/index.js +26 -1
  71. package/dist/ui/App.d.ts +9 -1
  72. package/dist/ui/App.js +140 -21
  73. package/dist/ui/Composer.d.ts +6 -1
  74. package/dist/ui/Composer.js +2 -2
  75. package/dist/ui/PromptInput.d.ts +15 -1
  76. package/dist/ui/PromptInput.js +38 -1
  77. package/dist/ui/StatusBar.d.ts +21 -0
  78. package/dist/ui/StatusBar.js +5 -2
  79. package/dist/ui/TeamPanel.d.ts +57 -0
  80. package/dist/ui/TeamPanel.js +134 -0
  81. package/dist/ui/Transcript.js +10 -0
  82. package/dist/ui/entries/TeamCard.d.ts +31 -0
  83. package/dist/ui/entries/TeamCard.js +39 -0
  84. package/dist/ui/glyphs.d.ts +22 -0
  85. package/dist/ui/glyphs.js +10 -0
  86. package/dist/ui/layout/AppShell.d.ts +20 -1
  87. package/dist/ui/layout/AppShell.js +39 -4
  88. package/dist/ui/layout/ScrollIndicator.d.ts +31 -0
  89. package/dist/ui/layout/ScrollIndicator.js +15 -0
  90. package/dist/ui/layout/ScrollViewport.d.ts +8 -2
  91. package/dist/ui/layout/ScrollViewport.js +24 -4
  92. package/dist/ui/layout/regions.d.ts +55 -0
  93. package/dist/ui/layout/regions.js +52 -0
  94. package/dist/ui/layout/scroll-indicator.d.ts +27 -0
  95. package/dist/ui/layout/scroll-indicator.js +35 -0
  96. package/dist/ui/layout/scroll.d.ts +9 -0
  97. package/dist/ui/layout/scroll.js +23 -0
  98. package/dist/ui/overlays/HelpOverlay.js +14 -0
  99. package/dist/ui/screen.d.ts +14 -1
  100. package/dist/ui/screen.js +37 -3
  101. package/dist/ui/transcript-text.js +20 -0
  102. package/dist/ui/use-startup-notices.d.ts +11 -1
  103. package/dist/ui/use-startup-notices.js +29 -2
  104. package/dist/ui/use-wheel-routing.d.ts +68 -0
  105. package/dist/ui/use-wheel-routing.js +139 -0
  106. package/package.json +2 -2
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Wheel band hit-testing (mouse-wheel-region-routing §4.5) — pure, React-free.
3
+ *
4
+ * The requirement is stated in terms of REGIONS ("scrolling over the agent area
5
+ * scrolls the transcript; scrolling over the input box steps prompt history"),
6
+ * so the router needs to turn a reported terminal row into one of two bands.
7
+ *
8
+ * The boundary is MEASURED, never computed from `budget.ts`. That arithmetic
9
+ * describes the PLANNED chrome, but the real bottom chrome grows and shrinks at
10
+ * run time: the toast row comes and goes, the hint row is dropped below
11
+ * `HINT_MIN_ROWS`, and the autocomplete popup adds up to nine rows. Routing off
12
+ * the plan would put the boundary in the wrong place exactly when the popup is
13
+ * open — the moment precision matters most (D-5 / D-16).
14
+ */
15
+ /**
16
+ * The "I do not know where the frame is" value.
17
+ *
18
+ * `App` writes it into the geometry ref on every render path that does NOT
19
+ * render the full-screen `AppShell` — the terminal-too-small placeholder and
20
+ * inline mode — so a stale measurement can never outlive the frame it
21
+ * described. There is deliberately no arithmetic fallback: an unmeasured frame
22
+ * routes to `'content'`, which is the band that cannot destroy anything, and
23
+ * one invisible frame after a resize is a better failure than a wrong band
24
+ * (§4.5, P1-5 / D-16).
25
+ */
26
+ export const UNMEASURED_GEOMETRY = { top: 1, height: 0, bottom: 0 };
27
+ /**
28
+ * Which band does terminal row `y` belong to?
29
+ *
30
+ * Fails toward `'content'`: the whole point of this feature is that a wheel
31
+ * notch must never edit the user's draft (I-3), so every degenerate input —
32
+ * nothing measured yet, a row outside the frame, a chrome measurement that
33
+ * swallowed the screen — resolves to the harmless direction.
34
+ */
35
+ export function hitTestWheel(y, geometry) {
36
+ const { top, height } = geometry;
37
+ if (!Number.isFinite(height) || height <= 0)
38
+ return 'content';
39
+ if (!Number.isFinite(y))
40
+ return 'content';
41
+ // Clamp into the frame so the one undrawn bottom row — and any off-by-one
42
+ // from a terminal that reports 0-based rows — lands on a real band instead of
43
+ // being dropped.
44
+ const row = Math.min(Math.max(Math.floor(y), top), top + height - 1);
45
+ // I-6: at least one content row always survives. This clamp is a backstop
46
+ // that should never fire, and that is checkable rather than hopeful: the
47
+ // completion popup is capped at `viewportRows(rows) - 4`, so the measured
48
+ // chrome tops out at `rows - 6` against a frame of `rows - 1` — five content
49
+ // rows survive the worst case the layout can produce.
50
+ const bottom = Math.min(Math.max(Math.floor(geometry.bottom) || 0, 0), height - 1);
51
+ return row >= top + height - bottom ? 'composer' : 'content';
52
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Scroll-thumb geometry (mouse-wheel-region-routing §4.8) — pure, React-free.
3
+ *
4
+ * Wheel scrolling without a position indicator is a half-finished affordance:
5
+ * the user learns THAT they moved but not HOW FAR or how much is left. The
6
+ * status bar's `↑N` answers neither.
7
+ *
8
+ * OFFSET COUNTS FROM THE BOTTOM, exactly as `scroll.ts` documents — `0` is
9
+ * pinned to the newest output and `overflow` is parked at the very top. The
10
+ * rows hidden ABOVE are therefore `overflow - offset`, and that inversion is
11
+ * precisely what gets done by accident, so it is computed here once and named.
12
+ */
13
+ export interface ThumbRange {
14
+ /** 0-based row of the thumb's first row, within the viewport. */
15
+ start: number;
16
+ /** Thumb height in rows; always >= 1 when a range is returned. */
17
+ size: number;
18
+ }
19
+ /**
20
+ * Where the thumb sits, or `null` when the content fits and there is nothing to
21
+ * indicate.
22
+ *
23
+ * `size` never rounds to zero: a 10-row window over a 5000-row transcript still
24
+ * gets a one-row thumb, because an indicator that vanishes at exactly the
25
+ * moment the transcript is longest is worse than no indicator at all.
26
+ */
27
+ export declare function thumbRange(viewportRows: number, contentRows: number, offsetFromBottom: number): ThumbRange | null;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Scroll-thumb geometry (mouse-wheel-region-routing §4.8) — pure, React-free.
3
+ *
4
+ * Wheel scrolling without a position indicator is a half-finished affordance:
5
+ * the user learns THAT they moved but not HOW FAR or how much is left. The
6
+ * status bar's `↑N` answers neither.
7
+ *
8
+ * OFFSET COUNTS FROM THE BOTTOM, exactly as `scroll.ts` documents — `0` is
9
+ * pinned to the newest output and `overflow` is parked at the very top. The
10
+ * rows hidden ABOVE are therefore `overflow - offset`, and that inversion is
11
+ * precisely what gets done by accident, so it is computed here once and named.
12
+ */
13
+ /**
14
+ * Where the thumb sits, or `null` when the content fits and there is nothing to
15
+ * indicate.
16
+ *
17
+ * `size` never rounds to zero: a 10-row window over a 5000-row transcript still
18
+ * gets a one-row thumb, because an indicator that vanishes at exactly the
19
+ * moment the transcript is longest is worse than no indicator at all.
20
+ */
21
+ export function thumbRange(viewportRows, contentRows, offsetFromBottom) {
22
+ const viewport = Math.floor(viewportRows);
23
+ const content = Math.floor(contentRows);
24
+ if (!Number.isFinite(viewport) || !Number.isFinite(content))
25
+ return null;
26
+ if (viewport <= 0 || content <= viewport)
27
+ return null;
28
+ const overflow = content - viewport;
29
+ const offset = Math.min(Math.max(Math.floor(offsetFromBottom) || 0, 0), overflow);
30
+ const size = Math.min(viewport, Math.max(1, Math.round((viewport * viewport) / content)));
31
+ const travel = viewport - size;
32
+ const hiddenAbove = overflow - offset;
33
+ const start = travel <= 0 ? 0 : Math.round((hiddenAbove / overflow) * travel);
34
+ return { start: Math.min(Math.max(start, 0), travel), size };
35
+ }
@@ -26,3 +26,12 @@ export declare function clampScroll(offset: number, overflowLines: number): numb
26
26
  export declare function pageSize(viewportHeight: number): number;
27
27
  export declare function isPinnedToBottom(state: ScrollState): boolean;
28
28
  export declare function applyScroll(state: ScrollState, intent: ScrollIntent, metrics: ViewportMetrics): ScrollState;
29
+ /**
30
+ * Fold `applyScroll` `times` times — one `setState`, one render, however fast
31
+ * the wheel spins (mouse-wheel-region-routing §5.5).
32
+ *
33
+ * Folding rather than multiplying is deliberate: `applyScroll` owns the
34
+ * downward bottom-snap and the clamp, and reimplementing either here would give
35
+ * the wheel a different notion of "pinned to the bottom" than the keyboard has.
36
+ */
37
+ export declare function applyScrollTimes(state: ScrollState, intent: ScrollIntent, metrics: ViewportMetrics, times: number): ScrollState;
@@ -74,3 +74,26 @@ export function applyScroll(state, intent, metrics) {
74
74
  }
75
75
  return { offset: clamped };
76
76
  }
77
+ /**
78
+ * Upper bound on one folded burst. `repeat` arrives from the wheel router's
79
+ * coalescing window, and an unbounded fold is a free CPU-burn primitive for a
80
+ * high-resolution / free-spinning wheel (R-10).
81
+ */
82
+ const MAX_SCROLL_REPEAT = 100;
83
+ /**
84
+ * Fold `applyScroll` `times` times — one `setState`, one render, however fast
85
+ * the wheel spins (mouse-wheel-region-routing §5.5).
86
+ *
87
+ * Folding rather than multiplying is deliberate: `applyScroll` owns the
88
+ * downward bottom-snap and the clamp, and reimplementing either here would give
89
+ * the wheel a different notion of "pinned to the bottom" than the keyboard has.
90
+ */
91
+ export function applyScrollTimes(state, intent, metrics, times) {
92
+ const n = Number.isFinite(times)
93
+ ? Math.min(MAX_SCROLL_REPEAT, Math.max(1, Math.floor(times)))
94
+ : 1;
95
+ let next = state;
96
+ for (let i = 0; i < n; i += 1)
97
+ next = applyScroll(next, intent, metrics);
98
+ return next;
99
+ }
@@ -13,6 +13,8 @@ function keyRows(times) {
13
13
  ['Ctrl+T', 'Toggle thinking blocks'],
14
14
  ['PgUp / PgDn', 'Scroll a page (transcript, or this overlay)'],
15
15
  ['Shift+Up / Shift+Down', 'Scroll the transcript a line (full-screen mode)'],
16
+ ['Wheel', 'Scroll the transcript (over the input box: prompt history)'],
17
+ ['Shift+Wheel', 'Scroll the transcript a page'],
16
18
  ['Up / Down', 'Prompt history (empty input)'],
17
19
  ];
18
20
  }
@@ -23,6 +25,7 @@ const COMMANDS = [
23
25
  ['/plan [on|off|status]', 'Toggle plan mode (same as Shift+Tab)'],
24
26
  ['/theme <name>', 'auto | warm | cool | light'],
25
27
  ['/thinking <level>', 'Set thinking level'],
28
+ ['/team [on|off|max n]', 'Team subagents: status, switch, fan-out width'],
26
29
  ['/tools', 'List active tools'],
27
30
  ['/clear', 'Clear the visible transcript'],
28
31
  ['/reset', 'New conversation'],
@@ -51,6 +54,14 @@ const PLAN_ROWS = [
51
54
  ['submit_plan', 'The agent submits a plan: a approve, r revise, Esc dismiss'],
52
55
  ['--plan / --no-plan', 'Start a session in a given mode (also ARAGON_PLAN=1)'],
53
56
  ];
57
+ const TEAM_ROWS = [
58
+ ['task', 'The tool the agent uses to run 2-5 subagents in parallel'],
59
+ ['/team', 'Status: on/off, max subagents, and the live roster'],
60
+ ['/team on | off', 'Switch team mode for this session (and save it)'],
61
+ ['/team max <n>', 'Fan-out width, 1-10 (5 by default)'],
62
+ ['--team / --no-team', 'Start a session with team mode on or off (also ARAGON_TEAM=0)'],
63
+ ['team_send / team_wait', 'How subagents message each other; 6 each, one every 15s'],
64
+ ];
54
65
  /** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
55
66
  export function helpRows(theme, caps) {
56
67
  const glyphs = pickGlyphs(caps);
@@ -66,6 +77,9 @@ export function helpRows(theme, caps) {
66
77
  rows.push(section('Plan mode'));
67
78
  for (const [c, d] of PLAN_ROWS)
68
79
  rows.push(pair(c, d));
80
+ rows.push(section('Team subagents'));
81
+ for (const [c, d] of TEAM_ROWS)
82
+ rows.push(pair(c, d));
69
83
  rows.push(section('Skills'));
70
84
  for (const [c, d] of SKILL_ROWS)
71
85
  rows.push(pair(c, d));
@@ -15,11 +15,24 @@ export interface ScreenHandle {
15
15
  /** Leave the alternate screen. Idempotent — safe from every exit path. */
16
16
  restore(): void;
17
17
  }
18
+ export interface AltScreenOptions {
19
+ /**
20
+ * TRUE MEANS "A MOUSE FILTER IS ALREADY INSTALLED", NOT "THE USER WANTS MOUSE
21
+ * SUPPORT" (invariant I-8).
22
+ *
23
+ * This module must never derive the value from config, env or platform.
24
+ * Enabling reporting without a filter in front of Ink does not degrade the
25
+ * feature — it types `[<0;12;5M` into the user's message on every click and
26
+ * every notch, which is strictly worse than the bug the feature fixes. The
27
+ * only caller is `cli.tsx::runInteractive`, and it passes `filter !== null`.
28
+ */
29
+ readonly mouse: boolean;
30
+ }
18
31
  /**
19
32
  * Enter the alternate screen. A non-TTY stdout is a hard no-op (tests, pipes,
20
33
  * CI) — writing screen-control sequences into a redirected file corrupts it.
21
34
  */
22
- export declare function enterAltScreen(stdout: NodeJS.WriteStream | undefined): ScreenHandle;
35
+ export declare function enterAltScreen(stdout: NodeJS.WriteStream | undefined, options?: AltScreenOptions): ScreenHandle;
23
36
  /**
24
37
  * Replay the session summary into the normal buffer after `restore()`, so the
25
38
  * conversation does not evaporate along with the alternate screen.
package/dist/ui/screen.js CHANGED
@@ -17,6 +17,32 @@ const CURSOR_HOME = '\x1b[H';
17
17
  const CLEAR_VISIBLE = '\x1b[2J';
18
18
  /** Ink's log-update hides the cursor; a signal kill never gives it back. */
19
19
  const SHOW_CURSOR = '\x1b[?25h';
20
+ /**
21
+ * `?1000` is NORMAL TRACKING (press / release), the minimum mode that reports
22
+ * wheel buttons; `?1006` selects SGR encoding so coordinates arrive as decimal
23
+ * text rather than raw bytes above 0x7F that `setEncoding('utf8')` would
24
+ * mangle. Motion tracking (`?1002` / `?1003`) is deliberately NOT enabled: it
25
+ * floods stdin on every pointer move and buys nothing this feature uses.
26
+ */
27
+ const ENABLE_MOUSE = '\x1b[?1000h\x1b[?1006h';
28
+ const DISABLE_MOUSE = '\x1b[?1006l\x1b[?1000l';
29
+ /**
30
+ * Alternate scroll (DEC private mode 1007) is what turns a wheel notch into a
31
+ * burst of arrow keys, which `PromptInput` reads as prompt-history recall. With
32
+ * mouse reporting ON we do not touch it at all: 1007 is defined to translate
33
+ * the wheel only while no application is tracking the mouse, so `?1000h`
34
+ * already suppresses it and a second mutation of shared state buys nothing.
35
+ *
36
+ * With mouse reporting OFF we turn it off so the wheel is INERT rather than
37
+ * destructive — the fallback rung of the fail-safe ladder. But 1007 is GLOBAL
38
+ * TERMINAL STATE THIS APP DOES NOT OWN, so it is SAVED and RESTORED (XTSAVE /
39
+ * XTRESTORE) rather than force-set on the way out: a user who deliberately
40
+ * disabled alternate scroll in their terminal config must not have it switched
41
+ * back on by running `aragon --no-mouse` once. A terminal implementing neither
42
+ * sequence ignores both, so the fallback is no worse than writing nothing.
43
+ */
44
+ const SAVE_ALT_SCROLL_OFF = '\x1b[?1007s\x1b[?1007l';
45
+ const RESTORE_ALT_SCROLL = '\x1b[?1007r';
20
46
  const NOOP_HANDLE = { restore() { } };
21
47
  function safeWrite(stdout, data) {
22
48
  try {
@@ -30,17 +56,25 @@ function safeWrite(stdout, data) {
30
56
  * Enter the alternate screen. A non-TTY stdout is a hard no-op (tests, pipes,
31
57
  * CI) — writing screen-control sequences into a redirected file corrupts it.
32
58
  */
33
- export function enterAltScreen(stdout) {
59
+ export function enterAltScreen(stdout, options = { mouse: false }) {
34
60
  if (!stdout || !stdout.isTTY || typeof stdout.write !== 'function')
35
61
  return NOOP_HANDLE;
36
- safeWrite(stdout, ENTER_ALT + CLEAR_VISIBLE + CURSOR_HOME);
62
+ const mouse = options.mouse === true;
63
+ safeWrite(stdout, ENTER_ALT + CLEAR_VISIBLE + CURSOR_HOME + (mouse ? ENABLE_MOUSE : SAVE_ALT_SCROLL_OFF));
37
64
  let restored = false;
38
65
  return {
39
66
  restore() {
40
67
  if (restored)
41
68
  return;
42
69
  restored = true;
43
- safeWrite(stdout, SHOW_CURSOR + LEAVE_ALT);
70
+ // Mouse reporting is disabled BEFORE leaving the alternate screen (I-2).
71
+ // This handle is what all four exit paths converge on — `process.on
72
+ // ('exit')`, the crash handler's `setScreenRestore`, the signal
73
+ // terminator, and `waitUntilExit()` — which is why the sequences live
74
+ // here and not in `App.tsx`, a component that is on none of them.
75
+ // Leaving reporting on prints `[<0;12;5M` in the user's shell on every
76
+ // click, forever, until they run `reset`.
77
+ safeWrite(stdout, (mouse ? DISABLE_MOUSE : RESTORE_ALT_SCROLL) + SHOW_CURSOR + LEAVE_ALT);
44
78
  },
45
79
  };
46
80
  }
@@ -64,6 +64,26 @@ function renderEntry(entry, glyphs) {
64
64
  const duration = entry.durationMs === undefined ? '' : ` (${entry.durationMs}ms)`;
65
65
  return [`${TOOL_INDENT}${glyph} ${entry.label || entry.name}${args ? ` ${args}` : ''}${duration}`];
66
66
  }
67
+ case 'team': {
68
+ // Same rows as `TeamCard`, without colour (team-subagents §6.3). A
69
+ // dispatch is often the most expensive thing in a session, so leaving it
70
+ // out of the plain-text replay would make the exported transcript
71
+ // unaccountable for most of its own cost.
72
+ const ok = entry.runs.filter((r) => r.phase === 'done' && !r.error).length;
73
+ const failed = entry.runs.length - ok;
74
+ const head = entry.active || (entry.aborted && entry.durationMs === undefined)
75
+ ? 'interrupted'
76
+ : `${entry.durationMs ?? 0}ms (${ok} ok, ${failed} failed)`;
77
+ const lines = [
78
+ `${TOOL_INDENT}team ${entry.runs.length} subagents ${head}`,
79
+ ...entry.runs.map((run) => {
80
+ const glyph = run.phase === 'done' && !run.error ? glyphs.toolDone : glyphs.toolError;
81
+ const detail = run.error ? ` ${run.error}` : '';
82
+ return `${TOOL_INDENT}${CONTINUATION_INDENT}${glyph} ${run.label} ${run.description}${detail}`;
83
+ }),
84
+ ];
85
+ return lines;
86
+ }
67
87
  case 'notice': {
68
88
  const glyph = entry.level === 'error'
69
89
  ? glyphs.error
@@ -12,4 +12,14 @@
12
12
  * not otherwise see — rather than belonging to the view.
13
13
  */
14
14
  import type { ViewAction } from '../agent/reducer.js';
15
- export declare function useStartupNotices(dispatch: (action: ViewAction) => void): void;
15
+ export interface MouseNoticeOptions {
16
+ /**
17
+ * Whether reporting is ACTUALLY in effect — i.e. a filter was installed, not
18
+ * merely that the config asked for one. A user on a terminal where R-1 fires
19
+ * must never be given advice about a mode that is not running.
20
+ */
21
+ enabled: boolean;
22
+ /** Record that it was shown (`state.json`). Called once, after dispatch. */
23
+ onSeen: () => void;
24
+ }
25
+ export declare function useStartupNotices(dispatch: (action: ViewAction) => void, mouseNotice?: MouseNoticeOptions): void;
@@ -11,10 +11,21 @@
11
11
  * because these two effects share one job — surfacing a condition the user could
12
12
  * not otherwise see — rather than belonging to the view.
13
13
  */
14
- import { useEffect } from 'react';
14
+ import { useEffect, useRef } from 'react';
15
15
  import { readConfigFile } from '../config/store.js';
16
+ import { getMouseNoticeSeen } from '../config/ui-state.js';
16
17
  import { getLogger } from '../logging/logger.js';
17
- export function useStartupNotices(dispatch) {
18
+ /**
19
+ * The §6.1 notice (mouse-wheel-region-routing). Shown at most once, ever.
20
+ *
21
+ * R-3 is the only part of wheel region routing that reaches users who never
22
+ * scroll, and it REMOVES a gesture — drag to select — that is used more often
23
+ * than the one being added. A README is consulted only after the belief
24
+ * "aragon broke copy/paste" has already formed.
25
+ */
26
+ const MOUSE_SELECTION_NOTICE = 'Mouse wheel scrolls the transcript. Hold Shift to select text, ' +
27
+ 'or turn it off with `aragon config set mouse false`.';
28
+ export function useStartupNotices(dispatch, mouseNotice) {
18
29
  // The sink gives up after repeated write failures (a read-only log directory,
19
30
  // a full disk). Silently logging nothing is exactly the state a user must not
20
31
  // discover later, from an empty file, while trying to explain a bug.
@@ -35,4 +46,20 @@ export function useStartupNotices(dispatch) {
35
46
  text: `Using default settings: could not parse ${parseError}`,
36
47
  });
37
48
  }, [dispatch]);
49
+ // A `notice`, not a toast: it lands in the transcript and scrolls away
50
+ // instead of stealing a row. §6's argument against toasts is about per-notch
51
+ // chatter and does not extend to a one-shot.
52
+ const noticeRef = useRef(mouseNotice);
53
+ noticeRef.current = mouseNotice;
54
+ const mouseEnabled = mouseNotice?.enabled ?? false;
55
+ const mouseNoticeShown = useRef(false);
56
+ useEffect(() => {
57
+ if (!mouseEnabled || mouseNoticeShown.current)
58
+ return;
59
+ if (getMouseNoticeSeen())
60
+ return;
61
+ mouseNoticeShown.current = true;
62
+ dispatch({ type: 'notice', level: 'info', text: MOUSE_SELECTION_NOTICE });
63
+ noticeRef.current?.onSeen();
64
+ }, [mouseEnabled, dispatch]);
38
65
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Wheel routing (mouse-wheel-region-routing §4.6 / §4.7).
3
+ *
4
+ * Subscribes to the mouse source, turns each wheel notch into a band via
5
+ * `hitTestWheel`, and dispatches it to the transcript, an open overlay, or the
6
+ * prompt-history stepper. Lives outside `App.tsx` because that file is exactly
7
+ * at the 1000-line ceiling `CLAUDE.md` sets, and because the routing table is
8
+ * the most testable part of this feature and does not want to be inside a
9
+ * 1000-line component (D-13).
10
+ *
11
+ * THE TWO BANDS HAVE DIFFERENT PHYSICS, and that is the requirement rather than
12
+ * a tuning choice (§4.7). A physical notch produces ONE SGR report, whereas
13
+ * alternate-scroll used to produce three arrow keys:
14
+ *
15
+ * - transcript: 3 rows per notch, matching the terminal's own convention and
16
+ * the muscle memory users bring from every other full-screen app;
17
+ * - composer: exactly ONE history step per notch. Multiplying here would make
18
+ * a single flick jump three prompts back — unusable, and destructive when a
19
+ * draft is in play.
20
+ *
21
+ * Bursts are handled differently for the same reason: transcript events are
22
+ * coalesced into one intent carrying `repeat`, composer events never are (they
23
+ * are rate-limited by the human, and each must produce its own visible step).
24
+ */
25
+ import { type MutableRefObject } from 'react';
26
+ import type { Overlay } from '../agent/reducer.js';
27
+ import type { MouseSource } from '../input/stdin-mouse-filter.js';
28
+ import { type FrameGeometry } from './layout/regions.js';
29
+ import type { ScrollIntent } from './layout/scroll.js';
30
+ /**
31
+ * Rows a PgUp/PgDn — and a Shift+wheel notch — moves inside a controlled
32
+ * overlay. Declared here rather than in `App.tsx` because both the keyboard
33
+ * branch and the wheel branch need it and `App` imports this module; the other
34
+ * direction would be a cycle.
35
+ */
36
+ export declare const OVERLAY_PAGE = 8;
37
+ export interface WheelRoutingOptions {
38
+ /** Absent when mouse support is off, in inline mode, or on a non-TTY. */
39
+ mouseSource?: MouseSource;
40
+ /** Live frame geometry, written by `AppShell`'s layout effect (§4.5). */
41
+ geometryRef: MutableRefObject<FrameGeometry>;
42
+ /**
43
+ * Test-only seam (§8.2): when set, the router reads this instead of the
44
+ * measured ref, so a routing test never depends on `measureElement` working
45
+ * under `ink-testing-library` — a dependency no other test in this package
46
+ * exercises, and one that would make a routing regression present as a
47
+ * measurement flake.
48
+ */
49
+ geometryOverride?: FrameGeometry;
50
+ /** Read at EVENT time for the routing branch, and again at FLUSH time (P1-3). */
51
+ getOverlay: () => Overlay | null;
52
+ onScroll: (kind: ScrollIntent, repeat: number) => void;
53
+ /** Signed row delta; the caller clamps at 0 the way the keyboard branch does. */
54
+ onOverlayScroll: (delta: number) => void;
55
+ onHistoryStep: (dir: 'up' | 'down') => void;
56
+ }
57
+ export interface WheelRouting {
58
+ /**
59
+ * Drop any accumulated-but-unflushed transcript scroll.
60
+ *
61
+ * Called by `App` from its existing `state.overlay` effect rather than from a
62
+ * second subscriber to the same transition. This is guard #2 of the pair that
63
+ * has to ship together (§14 condition 3); guard #1 is the overlay re-check
64
+ * inside the flush, and guard #3 is `ScrollViewport`'s I-9 mount-skip.
65
+ */
66
+ clearCoalescer: () => void;
67
+ }
68
+ export declare function useWheelRouting(options: WheelRoutingOptions): WheelRouting;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Wheel routing (mouse-wheel-region-routing §4.6 / §4.7).
3
+ *
4
+ * Subscribes to the mouse source, turns each wheel notch into a band via
5
+ * `hitTestWheel`, and dispatches it to the transcript, an open overlay, or the
6
+ * prompt-history stepper. Lives outside `App.tsx` because that file is exactly
7
+ * at the 1000-line ceiling `CLAUDE.md` sets, and because the routing table is
8
+ * the most testable part of this feature and does not want to be inside a
9
+ * 1000-line component (D-13).
10
+ *
11
+ * THE TWO BANDS HAVE DIFFERENT PHYSICS, and that is the requirement rather than
12
+ * a tuning choice (§4.7). A physical notch produces ONE SGR report, whereas
13
+ * alternate-scroll used to produce three arrow keys:
14
+ *
15
+ * - transcript: 3 rows per notch, matching the terminal's own convention and
16
+ * the muscle memory users bring from every other full-screen app;
17
+ * - composer: exactly ONE history step per notch. Multiplying here would make
18
+ * a single flick jump three prompts back — unusable, and destructive when a
19
+ * draft is in play.
20
+ *
21
+ * Bursts are handled differently for the same reason: transcript events are
22
+ * coalesced into one intent carrying `repeat`, composer events never are (they
23
+ * are rate-limited by the human, and each must produce its own visible step).
24
+ */
25
+ import { useCallback, useEffect, useRef } from 'react';
26
+ import { hitTestWheel } from './layout/regions.js';
27
+ /**
28
+ * Rows a PgUp/PgDn — and a Shift+wheel notch — moves inside a controlled
29
+ * overlay. Declared here rather than in `App.tsx` because both the keyboard
30
+ * branch and the wheel branch need it and `App` imports this module; the other
31
+ * direction would be a cycle.
32
+ */
33
+ export const OVERLAY_PAGE = 8;
34
+ /** Transcript rows per notch. The terminal's own convention. */
35
+ const CONTENT_LINES_PER_NOTCH = 3;
36
+ /** Overlay rows per notch. Overlay content is short; a page needs Shift. */
37
+ const OVERLAY_LINES_PER_NOTCH = 3;
38
+ /**
39
+ * Coalescing window for transcript scrolling — one `setState`, one render, no
40
+ * matter how fast the wheel spins (R-10). Deliberately tighter than the 33 ms
41
+ * streaming coalescer: this one is in the direct path of a hand gesture.
42
+ */
43
+ const WHEEL_COALESCE_MS = 16;
44
+ export function useWheelRouting(options) {
45
+ // Every mutable input reaches the subscription through this ref, so the
46
+ // effect below subscribes exactly once per `mouseSource` instead of tearing
47
+ // the listener down and rebuilding it on every parent render.
48
+ const optionsRef = useRef(options);
49
+ optionsRef.current = options;
50
+ const accumulator = useRef(null);
51
+ const timer = useRef(null);
52
+ const clearCoalescer = useCallback(() => {
53
+ if (timer.current) {
54
+ clearTimeout(timer.current);
55
+ timer.current = null;
56
+ }
57
+ accumulator.current = null;
58
+ }, []);
59
+ const flush = useCallback(() => {
60
+ if (timer.current) {
61
+ clearTimeout(timer.current);
62
+ timer.current = null;
63
+ }
64
+ const pending = accumulator.current;
65
+ accumulator.current = null;
66
+ if (!pending)
67
+ return;
68
+ // P1-3: THE DECISION BELONGS AT FLUSH TIME, not at event time, because the
69
+ // event-time decision is the one that goes stale. `App` swaps
70
+ // `ScrollViewport` out for an overlay, and 16 ms is more than wide enough
71
+ // for an agent-raised `confirm` or an `ask_user` to land inside the window
72
+ // — the intent would then be applied on a component that is not there, and
73
+ // the transcript would jump by itself when the overlay closed.
74
+ if (optionsRef.current.getOverlay())
75
+ return;
76
+ optionsRef.current.onScroll(pending.kind, pending.repeat);
77
+ }, []);
78
+ const accumulate = useCallback((kind, repeat) => {
79
+ // A fast up-then-down must never cancel itself into a no-op, so a change
80
+ // of direction (or of granularity) flushes what is already held.
81
+ if (accumulator.current && accumulator.current.kind !== kind)
82
+ flush();
83
+ const held = accumulator.current;
84
+ accumulator.current = { kind, repeat: (held?.repeat ?? 0) + repeat };
85
+ if (!timer.current) {
86
+ timer.current = setTimeout(flush, WHEEL_COALESCE_MS);
87
+ timer.current.unref?.();
88
+ }
89
+ }, [flush]);
90
+ const route = useCallback((event) => {
91
+ const current = optionsRef.current;
92
+ const geometry = current.geometryOverride ?? current.geometryRef.current;
93
+ const band = hitTestWheel(event.y, geometry);
94
+ const overlay = current.getOverlay();
95
+ // THE OVERLAY BRANCH IS EVALUATED FIRST AND RETURNS FOR EVERY BAND IT
96
+ // RECOGNIZES — the same two ordering rules the keyboard handler follows,
97
+ // and for the same reason (R-P1-7).
98
+ if (overlay) {
99
+ // The composer is inactive behind an overlay; stepping a hidden draft
100
+ // is worse than doing nothing.
101
+ if (band !== 'content')
102
+ return;
103
+ // Modes A only. `model` / `confirm` / `question` own their own keys and
104
+ // scrolling them would leave an inconsistent cursor (R-11).
105
+ const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan';
106
+ if (!controlled)
107
+ return;
108
+ const step = event.shift ? OVERLAY_PAGE : OVERLAY_LINES_PER_NOTCH;
109
+ current.onOverlayScroll(event.dir === 'up' ? -step : step);
110
+ return;
111
+ }
112
+ if (band === 'composer') {
113
+ // §4.7: the accumulator flushes when the BAND changes, not only when
114
+ // the direction does. The composer path is never coalesced, so without
115
+ // this the transcript scroll that preceded the pointer moving down
116
+ // lands ~16 ms AFTER the history step it happened before — the two
117
+ // halves of one gesture, applied out of order.
118
+ flush();
119
+ current.onHistoryStep(event.dir);
120
+ return;
121
+ }
122
+ if (event.shift) {
123
+ accumulate(event.dir === 'up' ? 'pageUp' : 'pageDown', 1);
124
+ return;
125
+ }
126
+ accumulate(event.dir === 'up' ? 'lineUp' : 'lineDown', CONTENT_LINES_PER_NOTCH);
127
+ }, [accumulate, flush]);
128
+ const { mouseSource } = options;
129
+ useEffect(() => {
130
+ if (!mouseSource)
131
+ return undefined;
132
+ const unsubscribe = mouseSource.subscribe(route);
133
+ return () => {
134
+ unsubscribe();
135
+ clearCoalescer();
136
+ };
137
+ }, [mouseSource, route, clearCoalescer]);
138
+ return { clearCoalescer };
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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",
@@ -51,7 +51,7 @@
51
51
  "prepublishOnly": "npm run build"
52
52
  },
53
53
  "dependencies": {
54
- "@aragon-agent/core": "^0.2.1",
54
+ "@aragon-agent/core": "^0.2.3",
55
55
  "chalk": "^5.6.2",
56
56
  "cli-highlight": "^2.0.0",
57
57
  "commander": "^12.1.0",