@aragon-agent/cli 0.5.8 → 0.5.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 (123) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/README.md +1429 -1163
  3. package/dist/agent/coalesce.d.ts +13 -0
  4. package/dist/agent/coalesce.js +50 -11
  5. package/dist/agent/controller.d.ts +141 -2
  6. package/dist/agent/controller.js +396 -4
  7. package/dist/agent/headless.d.ts +8 -0
  8. package/dist/agent/headless.js +40 -0
  9. package/dist/agent/reducer.d.ts +245 -3
  10. package/dist/agent/reducer.js +490 -17
  11. package/dist/agent/retry-view.d.ts +68 -0
  12. package/dist/agent/retry-view.js +95 -0
  13. package/dist/agent/system-prompt.d.ts +11 -0
  14. package/dist/agent/system-prompt.js +2 -0
  15. package/dist/cli.js +86 -2
  16. package/dist/commands/builtins.d.ts +1 -1
  17. package/dist/commands/builtins.js +308 -2
  18. package/dist/config/cli-commands.d.ts +53 -2
  19. package/dist/config/cli-commands.js +136 -3
  20. package/dist/config/env.d.ts +6 -3
  21. package/dist/config/env.js +103 -3
  22. package/dist/config/load.d.ts +49 -0
  23. package/dist/config/load.js +83 -1
  24. package/dist/config/schema.d.ts +212 -2
  25. package/dist/config/schema.js +207 -1
  26. package/dist/config/store.js +20 -1
  27. package/dist/fast/critique.d.ts +43 -0
  28. package/dist/fast/critique.js +62 -0
  29. package/dist/fast/digest.d.ts +42 -0
  30. package/dist/fast/digest.js +85 -0
  31. package/dist/fast/frames.d.ts +99 -0
  32. package/dist/fast/frames.js +180 -0
  33. package/dist/fast/limits.d.ts +98 -0
  34. package/dist/fast/limits.js +98 -0
  35. package/dist/fast/prompt.d.ts +72 -0
  36. package/dist/fast/prompt.js +94 -0
  37. package/dist/fast/resolve.d.ts +43 -0
  38. package/dist/fast/resolve.js +105 -0
  39. package/dist/fast/review-call.d.ts +99 -0
  40. package/dist/fast/review-call.js +130 -0
  41. package/dist/fast/reviewer.d.ts +248 -0
  42. package/dist/fast/reviewer.js +664 -0
  43. package/dist/fast/types.d.ts +128 -0
  44. package/dist/fast/types.js +14 -0
  45. package/dist/fast/wiring.d.ts +170 -0
  46. package/dist/fast/wiring.js +316 -0
  47. package/dist/logging/install.d.ts +15 -1
  48. package/dist/logging/install.js +37 -4
  49. package/dist/logging/logger.d.ts +1 -1
  50. package/dist/session/persist.js +57 -1
  51. package/dist/team/normalize.d.ts +26 -1
  52. package/dist/team/normalize.js +23 -3
  53. package/dist/team/prompt.d.ts +11 -0
  54. package/dist/team/prompt.js +10 -0
  55. package/dist/team/report.d.ts +18 -0
  56. package/dist/team/report.js +27 -1
  57. package/dist/team/runtime.d.ts +13 -2
  58. package/dist/team/runtime.js +33 -6
  59. package/dist/team/subagent.d.ts +19 -1
  60. package/dist/team/subagent.js +72 -10
  61. package/dist/team/task-tool.d.ts +26 -0
  62. package/dist/team/task-tool.js +48 -4
  63. package/dist/team/types.d.ts +80 -1
  64. package/dist/tools/bash-tool.js +19 -3
  65. package/dist/tools/diff.d.ts +25 -5
  66. package/dist/tools/diff.js +72 -41
  67. package/dist/tools/file-change-store.d.ts +34 -0
  68. package/dist/tools/file-change-store.js +57 -0
  69. package/dist/tools/fs-tools.d.ts +26 -0
  70. package/dist/tools/fs-tools.js +62 -5
  71. package/dist/tools/index.d.ts +31 -0
  72. package/dist/tools/index.js +7 -1
  73. package/dist/tools/patch.d.ts +115 -0
  74. package/dist/tools/patch.js +453 -0
  75. package/dist/tools/terminal-output.d.ts +107 -0
  76. package/dist/tools/terminal-output.js +198 -0
  77. package/dist/tools/tool-output-store.d.ts +68 -0
  78. package/dist/tools/tool-output-store.js +102 -0
  79. package/dist/ui/ActivityLine.d.ts +48 -0
  80. package/dist/ui/ActivityLine.js +22 -0
  81. package/dist/ui/App.d.ts +1 -4
  82. package/dist/ui/App.js +273 -37
  83. package/dist/ui/BottomStatusRow.d.ts +43 -0
  84. package/dist/ui/BottomStatusRow.js +14 -0
  85. package/dist/ui/Composer.d.ts +1 -6
  86. package/dist/ui/Composer.js +2 -2
  87. package/dist/ui/PromptInput.d.ts +1 -15
  88. package/dist/ui/PromptInput.js +1 -38
  89. package/dist/ui/StatusBar.d.ts +37 -0
  90. package/dist/ui/StatusBar.js +5 -3
  91. package/dist/ui/TeamPanel.d.ts +5 -4
  92. package/dist/ui/TeamPanel.js +1 -1
  93. package/dist/ui/TodoPanel.d.ts +5 -4
  94. package/dist/ui/Transcript.d.ts +26 -3
  95. package/dist/ui/Transcript.js +76 -10
  96. package/dist/ui/activity-phrases.d.ts +33 -0
  97. package/dist/ui/activity-phrases.js +50 -0
  98. package/dist/ui/entries/AssistantEntry.d.ts +20 -0
  99. package/dist/ui/entries/AssistantEntry.js +3 -2
  100. package/dist/ui/entries/DiffView.d.ts +67 -0
  101. package/dist/ui/entries/DiffView.js +103 -0
  102. package/dist/ui/entries/FastCard.d.ts +53 -0
  103. package/dist/ui/entries/FastCard.js +87 -0
  104. package/dist/ui/entries/RetryCard.d.ts +56 -0
  105. package/dist/ui/entries/RetryCard.js +60 -0
  106. package/dist/ui/entries/TeamCard.js +6 -1
  107. package/dist/ui/entries/ToolCard.d.ts +38 -1
  108. package/dist/ui/entries/ToolCard.js +68 -5
  109. package/dist/ui/glyphs.d.ts +9 -0
  110. package/dist/ui/glyphs.js +2 -0
  111. package/dist/ui/layout/AppShell.d.ts +13 -22
  112. package/dist/ui/layout/AppShell.js +3 -38
  113. package/dist/ui/layout/virtual-window.d.ts +1 -1
  114. package/dist/ui/layout/virtual-window.js +109 -5
  115. package/dist/ui/overlays/HelpOverlay.js +16 -2
  116. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  117. package/dist/ui/overlays/SettingsScreen.js +147 -1
  118. package/dist/ui/transcript-text.js +27 -0
  119. package/dist/ui/use-wheel-routing.d.ts +16 -32
  120. package/dist/ui/use-wheel-routing.js +21 -38
  121. package/package.json +2 -2
  122. package/dist/ui/layout/regions.d.ts +0 -55
  123. package/dist/ui/layout/regions.js +0 -52
@@ -7,6 +7,7 @@
7
7
  * simply print everything twice.
8
8
  */
9
9
  import { formatCost, formatDuration, formatTokens } from '../agent/usage.js';
10
+ import { formatRetryLine } from '../agent/retry-view.js';
10
11
  const DEFAULT_MAX_ENTRIES = 200;
11
12
  const CONTINUATION_INDENT = ' ';
12
13
  const TOOL_INDENT = ' ';
@@ -84,6 +85,32 @@ function renderEntry(entry, glyphs) {
84
85
  ];
85
86
  return lines;
86
87
  }
88
+ case 'retry': {
89
+ // A turn that spent three minutes retrying is exactly the kind of cost an
90
+ // exported transcript is supposed to account for — the same argument the
91
+ // `team` case above makes in its own words. This file's `default: return []`
92
+ // means the addition is a CHOICE rather than a compile requirement, and it is
93
+ // worth making: without it a replay of a session that lost two minutes to a
94
+ // 529 shows a gap and no reason for it.
95
+ const glyph = entry.phase === 'exhausted' ? glyphs.error : glyphs.retry;
96
+ return [
97
+ `${TOOL_INDENT}${glyph} ${formatRetryLine(entry, glyphs, Date.now())}`,
98
+ ];
99
+ }
100
+ case 'fast': {
101
+ // Reviews are real spend and real advice, and an exported transcript that
102
+ // omitted them would be unaccountable for both — the argument the `team`
103
+ // and `retry` cases above each make in their own words.
104
+ const glyph = entry.status === 'failed' ? glyphs.toolError : glyphs.toolDone;
105
+ const duration = entry.durationMs === undefined ? '' : ` (${entry.durationMs}ms)`;
106
+ const head = `${TOOL_INDENT}${glyph} fast review #${entry.reviewIndex} ${entry.model} ` +
107
+ `turn ${entry.turn}${duration}`;
108
+ if (entry.status === 'advice' && entry.text) {
109
+ return [head, ...entry.text.split('\n').map((l) => `${TOOL_INDENT}${CONTINUATION_INDENT}${l}`)];
110
+ }
111
+ const detail = entry.detail ? `: ${entry.detail}` : '';
112
+ return [`${head} ${entry.status}${detail}`];
113
+ }
87
114
  case 'notice': {
88
115
  const glyph = entry.level === 'error'
89
116
  ? glyphs.error
@@ -1,31 +1,26 @@
1
1
  /**
2
- * Wheel routing (mouse-wheel-region-routing §4.6 / §4.7).
2
+ * Wheel routing (wheel-scrolls-transcript-only §4.1).
3
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).
4
+ * Subscribes to the mouse source and dispatches every notch to THE ONE
5
+ * SCROLLABLE REGION ON SCREEN — the transcript, or the controlled overlay
6
+ * covering it. Lives outside `App.tsx` because the routing table is the most
7
+ * testable part of this feature and a table wants to be readable on its own,
8
+ * away from a large component (D-13).
10
9
  *
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:
10
+ * THE POINTER ROW IS NOT CONSULTED, and that is the requirement rather than a
11
+ * simplification (§1). A wheel notch is a VIEWPORT gesture: the pointer in a
12
+ * TUI rests wherever the user last left it — in a chat-shaped app, next to the
13
+ * thing they type into — so routing by row meant the ordinary "scroll back to
14
+ * see what the agent just did" gesture landed on the composer and replaced the
15
+ * draft with an old prompt. Prompt history belongs to `↑` / `↓` alone (G1 / G3),
16
+ * which is the contract Claude Code, Codex and every full-screen terminal app
17
+ * the user brings muscle memory from already follow.
14
18
  *
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).
19
+ * Every notch is coalesced into one intent carrying `repeat`, no matter how fast
20
+ * the wheel spins: one gesture, one `setState`, one render.
24
21
  */
25
- import { type MutableRefObject } from 'react';
26
22
  import type { Overlay } from '../agent/reducer.js';
27
23
  import type { MouseSource } from '../input/stdin-mouse-filter.js';
28
- import { type FrameGeometry } from './layout/regions.js';
29
24
  import type { ScrollIntent } from './layout/scroll.js';
30
25
  /**
31
26
  * Rows a PgUp/PgDn — and a Shift+wheel notch — moves inside a controlled
@@ -37,22 +32,11 @@ export declare const OVERLAY_PAGE = 8;
37
32
  export interface WheelRoutingOptions {
38
33
  /** Absent when mouse support is off, in inline mode, or on a non-TTY. */
39
34
  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
35
  /** Read at EVENT time for the routing branch, and again at FLUSH time (P1-3). */
51
36
  getOverlay: () => Overlay | null;
52
37
  onScroll: (kind: ScrollIntent, repeat: number) => void;
53
38
  /** Signed row delta; the caller clamps at 0 the way the keyboard branch does. */
54
39
  onOverlayScroll: (delta: number) => void;
55
- onHistoryStep: (dir: 'up' | 'down') => void;
56
40
  }
57
41
  export interface WheelRouting {
58
42
  /**
@@ -1,29 +1,25 @@
1
1
  /**
2
- * Wheel routing (mouse-wheel-region-routing §4.6 / §4.7).
2
+ * Wheel routing (wheel-scrolls-transcript-only §4.1).
3
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).
4
+ * Subscribes to the mouse source and dispatches every notch to THE ONE
5
+ * SCROLLABLE REGION ON SCREEN — the transcript, or the controlled overlay
6
+ * covering it. Lives outside `App.tsx` because the routing table is the most
7
+ * testable part of this feature and a table wants to be readable on its own,
8
+ * away from a large component (D-13).
10
9
  *
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:
10
+ * THE POINTER ROW IS NOT CONSULTED, and that is the requirement rather than a
11
+ * simplification (§1). A wheel notch is a VIEWPORT gesture: the pointer in a
12
+ * TUI rests wherever the user last left it — in a chat-shaped app, next to the
13
+ * thing they type into — so routing by row meant the ordinary "scroll back to
14
+ * see what the agent just did" gesture landed on the composer and replaced the
15
+ * draft with an old prompt. Prompt history belongs to `↑` / `↓` alone (G1 / G3),
16
+ * which is the contract Claude Code, Codex and every full-screen terminal app
17
+ * the user brings muscle memory from already follow.
14
18
  *
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).
19
+ * Every notch is coalesced into one intent carrying `repeat`, no matter how fast
20
+ * the wheel spins: one gesture, one `setState`, one render.
24
21
  */
25
22
  import { useCallback, useEffect, useRef } from 'react';
26
- import { hitTestWheel } from './layout/regions.js';
27
23
  /**
28
24
  * Rows a PgUp/PgDn — and a Shift+wheel notch — moves inside a controlled
29
25
  * overlay. Declared here rather than in `App.tsx` because both the keyboard
@@ -89,42 +85,29 @@ export function useWheelRouting(options) {
89
85
  }, [flush]);
90
86
  const route = useCallback((event) => {
91
87
  const current = optionsRef.current;
92
- const geometry = current.geometryOverride ?? current.geometryRef.current;
93
- const band = hitTestWheel(event.y, geometry);
94
88
  const overlay = current.getOverlay();
95
- // THE OVERLAY BRANCH IS EVALUATED FIRST AND RETURNS FOR EVERY BAND IT
89
+ // THE OVERLAY BRANCH IS EVALUATED FIRST AND RETURNS FOR EVERY OVERLAY IT
96
90
  // RECOGNIZES — the same two ordering rules the keyboard handler follows,
97
91
  // and for the same reason (R-P1-7).
98
92
  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
93
  // Modes A only. `model` / `confirm` / `question` own their own keys and
104
94
  // scrolling them would leave an inconsistent cursor (R-11).
105
95
  const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan';
106
96
  if (!controlled)
107
97
  return;
98
+ // D-3: the row is not consulted here either. Behind an overlay the
99
+ // composer is inactive and the overlay is the only scrollable thing on
100
+ // screen, so a notch anywhere scrolls it.
108
101
  const step = event.shift ? OVERLAY_PAGE : OVERLAY_LINES_PER_NOTCH;
109
102
  current.onOverlayScroll(event.dir === 'up' ? -step : step);
110
103
  return;
111
104
  }
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
105
  if (event.shift) {
123
106
  accumulate(event.dir === 'up' ? 'pageUp' : 'pageDown', 1);
124
107
  return;
125
108
  }
126
109
  accumulate(event.dir === 'up' ? 'lineUp' : 'lineDown', CONTENT_LINES_PER_NOTCH);
127
- }, [accumulate, flush]);
110
+ }, [accumulate]);
128
111
  const { mouseSource } = options;
129
112
  useEffect(() => {
130
113
  if (!mouseSource)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.5.8",
3
+ "version": "0.5.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",
@@ -52,7 +52,7 @@
52
52
  "prepublishOnly": "npm run build"
53
53
  },
54
54
  "dependencies": {
55
- "@aragon-agent/core": "^0.2.8",
55
+ "@aragon-agent/core": "^0.2.9",
56
56
  "chalk": "^5.6.2",
57
57
  "cli-highlight": "^2.0.0",
58
58
  "commander": "^12.1.0",
@@ -1,55 +0,0 @@
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
- export type WheelBand = 'content' | 'composer';
16
- export interface FrameGeometry {
17
- /**
18
- * 1-based row of the frame's first row. 1 in the alternate screen.
19
- *
20
- * This is DERIVED, not assumed, and it has a named dependency: it holds
21
- * because the full-screen transcript renders no `<Static>`
22
- * (`Transcript.tsx`), so Ink's `log-update` writes `frameHeight(rows) = rows-1`
23
- * lines plus a newline into a `rows`-row buffer — nothing scrolls, and the
24
- * frame sits at rows `1 … rows-1` with the cursor parked on the undrawn last
25
- * row. Reintroducing `<Static>` into the full-screen path pushes the frame
26
- * down the screen and silently biases every hit test; whoever does that has
27
- * to change `top` here at the same time.
28
- */
29
- top: number;
30
- /** Rows the frame occupies — `frameHeight(rows)`. */
31
- height: number;
32
- /** Measured rows of the bottom chrome (toast + composer + status). */
33
- bottom: number;
34
- }
35
- /**
36
- * The "I do not know where the frame is" value.
37
- *
38
- * `App` writes it into the geometry ref on every render path that does NOT
39
- * render the full-screen `AppShell` — the terminal-too-small placeholder and
40
- * inline mode — so a stale measurement can never outlive the frame it
41
- * described. There is deliberately no arithmetic fallback: an unmeasured frame
42
- * routes to `'content'`, which is the band that cannot destroy anything, and
43
- * one invisible frame after a resize is a better failure than a wrong band
44
- * (§4.5, P1-5 / D-16).
45
- */
46
- export declare const UNMEASURED_GEOMETRY: FrameGeometry;
47
- /**
48
- * Which band does terminal row `y` belong to?
49
- *
50
- * Fails toward `'content'`: the whole point of this feature is that a wheel
51
- * notch must never edit the user's draft (I-3), so every degenerate input —
52
- * nothing measured yet, a row outside the frame, a chrome measurement that
53
- * swallowed the screen — resolves to the harmless direction.
54
- */
55
- export declare function hitTestWheel(y: number, geometry: FrameGeometry): WheelBand;
@@ -1,52 +0,0 @@
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
- }