@aragon-agent/cli 0.6.4 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +163 -37
  3. package/dist/agent/controller.d.ts +49 -8
  4. package/dist/agent/controller.js +160 -48
  5. package/dist/agent/headless.d.ts +2 -1
  6. package/dist/agent/prompt-options.d.ts +15 -0
  7. package/dist/agent/prompt-options.js +1 -0
  8. package/dist/agent/reducer.d.ts +33 -19
  9. package/dist/agent/reducer.js +62 -71
  10. package/dist/agent/usage.d.ts +34 -0
  11. package/dist/agent/usage.js +43 -0
  12. package/dist/cli.js +131 -54
  13. package/dist/commands/builtins.js +12 -1
  14. package/dist/commands/registry.d.ts +2 -3
  15. package/dist/compaction/compactor.d.ts +60 -24
  16. package/dist/compaction/compactor.js +89 -61
  17. package/dist/compaction/context-command.d.ts +33 -0
  18. package/dist/compaction/context-command.js +166 -0
  19. package/dist/compaction/meter.d.ts +251 -0
  20. package/dist/compaction/meter.js +428 -0
  21. package/dist/compaction/pressure.d.ts +6 -0
  22. package/dist/compaction/pressure.js +4 -0
  23. package/dist/compaction/types.d.ts +41 -0
  24. package/dist/compaction/wiring.d.ts +47 -0
  25. package/dist/compaction/wiring.js +88 -27
  26. package/dist/config/cli-commands.js +5 -0
  27. package/dist/config/env.js +15 -1
  28. package/dist/config/load.d.ts +17 -0
  29. package/dist/config/load.js +45 -1
  30. package/dist/config/schema.d.ts +44 -0
  31. package/dist/config/schema.js +36 -0
  32. package/dist/config/store.js +27 -1
  33. package/dist/exec/events.d.ts +138 -1
  34. package/dist/exec/events.js +42 -0
  35. package/dist/exec/index.js +43 -6
  36. package/dist/exec/progress-reporter.d.ts +23 -0
  37. package/dist/exec/progress-reporter.js +88 -0
  38. package/dist/exec/runner.d.ts +34 -0
  39. package/dist/exec/runner.js +137 -25
  40. package/dist/fast/reviewer.d.ts +19 -1
  41. package/dist/fast/reviewer.js +22 -0
  42. package/dist/fast/types.d.ts +15 -0
  43. package/dist/fast/wiring.d.ts +10 -1
  44. package/dist/fast/wiring.js +18 -1
  45. package/dist/input/interrupt-gesture.d.ts +18 -0
  46. package/dist/input/interrupt-gesture.js +19 -0
  47. package/dist/input/pointer-router.d.ts +11 -0
  48. package/dist/input/pointer-router.js +14 -0
  49. package/dist/todo/limits.d.ts +4 -17
  50. package/dist/todo/limits.js +4 -17
  51. package/dist/todo/panel-rows.d.ts +8 -6
  52. package/dist/todo/panel-rows.js +19 -6
  53. package/dist/todo/store.d.ts +8 -13
  54. package/dist/todo/store.js +13 -14
  55. package/dist/todo/types.d.ts +2 -1
  56. package/dist/ui/App.d.ts +2 -0
  57. package/dist/ui/App.js +233 -183
  58. package/dist/ui/AutocompletePopup.d.ts +3 -1
  59. package/dist/ui/AutocompletePopup.js +9 -11
  60. package/dist/ui/BottomStatusRow.d.ts +1 -1
  61. package/dist/ui/Composer.d.ts +13 -5
  62. package/dist/ui/Composer.js +7 -11
  63. package/dist/ui/PromptCaret.d.ts +11 -0
  64. package/dist/ui/PromptCaret.js +31 -0
  65. package/dist/ui/PromptInput.d.ts +12 -6
  66. package/dist/ui/PromptInput.js +50 -16
  67. package/dist/ui/StatusBar.d.ts +22 -15
  68. package/dist/ui/StatusBar.js +61 -16
  69. package/dist/ui/TeamPanel.d.ts +3 -1
  70. package/dist/ui/TeamPanel.js +13 -9
  71. package/dist/ui/TodoPanel.d.ts +5 -5
  72. package/dist/ui/TodoPanel.js +56 -24
  73. package/dist/ui/Transcript.js +1 -0
  74. package/dist/ui/UpdateLine.d.ts +1 -1
  75. package/dist/ui/capabilities.js +5 -5
  76. package/dist/ui/composer-rows.js +50 -9
  77. package/dist/ui/exit-snapshot.d.ts +9 -0
  78. package/dist/ui/frame-differ.js +2 -19
  79. package/dist/ui/frame-observer.d.ts +17 -0
  80. package/dist/ui/frame-observer.js +81 -0
  81. package/dist/ui/frame-parser.d.ts +7 -0
  82. package/dist/ui/frame-parser.js +18 -0
  83. package/dist/ui/layout/AppShell.d.ts +7 -52
  84. package/dist/ui/layout/AppShell.js +2 -2
  85. package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
  86. package/dist/ui/layout/ScrollIndicator.js +4 -4
  87. package/dist/ui/layout/ScrollViewport.d.ts +13 -66
  88. package/dist/ui/layout/ScrollViewport.js +183 -185
  89. package/dist/ui/layout/autocomplete.d.ts +18 -0
  90. package/dist/ui/layout/autocomplete.js +26 -0
  91. package/dist/ui/layout/budget.d.ts +3 -51
  92. package/dist/ui/layout/budget.js +4 -53
  93. package/dist/ui/layout/document-layout.d.ts +13 -0
  94. package/dist/ui/layout/document-layout.js +11 -0
  95. package/dist/ui/layout/follow-state.d.ts +1 -0
  96. package/dist/ui/layout/follow-state.js +1 -3
  97. package/dist/ui/layout/rail.d.ts +4 -53
  98. package/dist/ui/layout/rail.js +10 -62
  99. package/dist/ui/layout/scroll-indicator.d.ts +1 -1
  100. package/dist/ui/layout/scroll-indicator.js +3 -2
  101. package/dist/ui/layout/scrollbar-geometry.d.ts +17 -0
  102. package/dist/ui/layout/scrollbar-geometry.js +13 -0
  103. package/dist/ui/layout/team-panel.d.ts +18 -0
  104. package/dist/ui/layout/team-panel.js +22 -0
  105. package/dist/ui/layout/todo-layout.d.ts +25 -0
  106. package/dist/ui/layout/todo-layout.js +28 -0
  107. package/dist/ui/layout/viewport-geometry.d.ts +1 -0
  108. package/dist/ui/layout/viewport-geometry.js +1 -0
  109. package/dist/ui/layout/virtual-window.d.ts +1 -0
  110. package/dist/ui/layout/virtual-window.js +1 -1
  111. package/dist/ui/overlays/HelpOverlay.js +5 -2
  112. package/dist/ui/screen.d.ts +1 -1
  113. package/dist/ui/screen.js +4 -6
  114. package/dist/ui/scrollbar-controller.d.ts +30 -0
  115. package/dist/ui/scrollbar-controller.js +124 -0
  116. package/dist/ui/transcript-text.js +6 -2
  117. package/dist/ui/use-wheel-routing.d.ts +2 -0
  118. package/dist/ui/use-wheel-routing.js +6 -0
  119. package/package.json +2 -2
@@ -6,6 +6,7 @@
6
6
  import React from 'react';
7
7
  import type { Theme } from './theme.js';
8
8
  import type { TermCapabilities } from './capabilities.js';
9
+ import { type AutocompleteLayout } from './layout/autocomplete.js';
9
10
  export interface Suggestion {
10
11
  label: string;
11
12
  hint?: string;
@@ -16,6 +17,7 @@ interface AutocompletePopupProps {
16
17
  theme: Theme;
17
18
  caps: TermCapabilities;
18
19
  maxRows?: number;
20
+ layout?: AutocompleteLayout;
19
21
  }
20
- export declare function AutocompletePopup({ items, selected, theme, caps, maxRows, }: AutocompletePopupProps): React.ReactElement | null;
22
+ export declare function AutocompletePopup({ items, selected, theme, caps, maxRows, layout, }: AutocompletePopupProps): React.ReactElement | null;
21
23
  export {};
@@ -1,19 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { pickGlyphs } from './glyphs.js';
4
- const DEFAULT_MAX_ROWS = 6;
5
- export function AutocompletePopup({ items, selected, theme, caps, maxRows = DEFAULT_MAX_ROWS, }) {
6
- if (items.length === 0)
4
+ import { buildAutocompleteLayout } from './layout/autocomplete.js';
5
+ export function AutocompletePopup({ items, selected, theme, caps, maxRows, layout, }) {
6
+ const projection = layout ?? buildAutocompleteLayout({ itemCount: items.length, selected, maxRows });
7
+ if (projection.rowCount === 0)
7
8
  return null;
8
9
  const glyphs = pickGlyphs(caps);
9
- // Keep the highlighted row within a window of `maxRows`.
10
- const start = Math.max(0, Math.min(selected - maxRows + 1, items.length - maxRows));
11
- const windowStart = Math.max(0, start);
12
- const visible = items.slice(windowStart, windowStart + maxRows);
13
- const moreBelow = items.length - (windowStart + visible.length);
14
- return (_jsxs(Box, { flexDirection: "column", borderStyle: glyphs.boxStyle, borderColor: theme.border, paddingX: 1, children: [visible.map((item, i) => {
10
+ const { start: windowStart, count, moreBelow, showMore } = projection;
11
+ const visible = items.slice(windowStart, windowStart + count);
12
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: theme.border, paddingX: 1, children: [visible.map((item, i) => {
15
13
  const idx = windowStart + i;
16
14
  const active = idx === selected;
17
- return (_jsxs(Text, { inverse: active, children: [_jsx(Text, { color: active ? undefined : theme.primary, children: item.label }), item.hint ? _jsxs(Text, { color: theme.muted, children: [" ", item.hint] }) : null] }, item.label));
18
- }), moreBelow > 0 && _jsxs(Text, { color: theme.muted, children: [" +", moreBelow, " more"] })] }));
15
+ return (_jsxs(Text, { inverse: active, wrap: "truncate", children: [_jsx(Text, { color: active ? undefined : theme.primary, children: item.label.replace(/\s+/g, ' ') }), item.hint ? _jsxs(Text, { color: theme.muted, children: [" ", item.hint.replace(/\s+/g, ' ')] }) : null] }, idx));
16
+ }), showMore && _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [" +", moreBelow, " more"] })] }));
19
17
  }
@@ -11,7 +11,7 @@
11
11
  * box is its only `flexShrink={1}` child — so an extra chrome row does not make
12
12
  * the frame taller, Yoga takes the row out of the TRANSCRIPT. Meanwhile
13
13
  * `viewportRows()` keeps returning the old number to `ScrollViewport`,
14
- * `selectWindow`'s spacers, `overlayMaxRows`, `popupMaxRows` and `todoRailRows`.
14
+ * `selectWindow`'s spacers, `overlayMaxRows`, `popupMaxRows` and `buildTodoRailLayout`.
15
15
  * The transcript would draw one row shorter than every consumer believes, ONLY
16
16
  * WHILE A RUN IS IN FLIGHT, so the layout would shift on submit and shift back on
17
17
  * `agent_end`.
@@ -9,20 +9,26 @@
9
9
  * "the input is no longer empty", and routing that through App would re-render
10
10
  * the transcript on the first keystroke of every message.
11
11
  *
12
- * As of v0.4.0 this is the ONLY place keybinding hints are shown — the status
13
- * bar's duplicate cluster is gone (§4.6). Two hints for the same keys, one of
14
- * them 31 columns wide, were both redundant and the reason the status bar had a
15
- * `cols >= 110` breakpoint.
12
+ * The fixed status row retains emergency and history-navigation hints while
13
+ * this footer scrolls outside the viewport. Editor state stays mounted.
16
14
  */
17
15
  import React from 'react';
16
+ import { type DOMElement } from 'ink';
18
17
  import type { Theme } from './theme.js';
19
18
  import type { TermCapabilities } from './capabilities.js';
20
19
  import { pickGlyphs } from './glyphs.js';
21
20
  import { type CommandOption } from './PromptInput.js';
22
21
  import { type AgentMode } from '../agent/agent-mode.js';
23
22
  export interface ComposerProps {
23
+ cols?: number;
24
+ cursorVisible?: boolean;
25
+ onInteraction?: () => void;
26
+ measureRef?: React.RefObject<DOMElement>;
24
27
  /** False while an overlay owns the keyboard (the input is visually blurred). */
25
28
  isActive: boolean;
29
+ reducedMotion?: boolean;
30
+ onEscape?: () => void;
31
+ onEscapeDismiss?: () => void;
26
32
  running: boolean;
27
33
  history: string[];
28
34
  commands: CommandOption[];
@@ -49,6 +55,8 @@ export interface ComposerProps {
49
55
  */
50
56
  modeToggleKey?: string;
51
57
  popupMaxRows?: number;
58
+ popupMaxHeight?: number;
59
+ onPopupRowsChange?: (rows: number) => void;
52
60
  /**
53
61
  * Rows of newer output below the viewport, forwarded to the input row's chip
54
62
  * (tui-selection-and-scroll-follow §4.2 / G7).
@@ -117,5 +125,5 @@ declare function hintText(opts: {
117
125
  * than fishing the string out of a rendered frame.
118
126
  */
119
127
  export declare const hintTextForTest: typeof hintText;
120
- export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services, modeToggleKey, popupMaxRows, scrolledLines, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
128
+ export declare function Composer({ cols, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services, modeToggleKey, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
121
129
  export {};
@@ -10,10 +10,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  * "the input is no longer empty", and routing that through App would re-render
11
11
  * the transcript on the first keystroke of every message.
12
12
  *
13
- * As of v0.4.0 this is the ONLY place keybinding hints are shown — the status
14
- * bar's duplicate cluster is gone (§4.6). Two hints for the same keys, one of
15
- * them 31 columns wide, were both redundant and the reason the status bar had a
16
- * `cols >= 110` breakpoint.
13
+ * The fixed status row retains emergency and history-navigation hints while
14
+ * this footer scrolls outside the viewport. Editor state stays mounted.
17
15
  */
18
16
  import { useCallback, useState } from 'react';
19
17
  import { Box, Text } from 'ink';
@@ -46,15 +44,13 @@ function hintText(opts) {
46
44
  if (services > 0) {
47
45
  return [
48
46
  `${glyphs.enterKey} steer`,
49
- // Rung two is named because the whole point of the ladder is that the
50
- // first press can fail; a user who does not know there is a second press
51
- // is left exactly where the reported screenshot left them.
52
- `esc abort${glyphs.times}2 force`,
47
+ // Confirmation is named even when service controls also need space.
48
+ `esc${glyphs.times}2 interrupt`,
53
49
  `ctrl+c stop ${services}`,
54
50
  `ctrl+c${glyphs.times}2 exit`,
55
51
  ].join(dot);
56
52
  }
57
- return [`${glyphs.enterKey} steer`, 'esc abort', `ctrl+c${glyphs.times}2 exit`].join(dot);
53
+ return [`${glyphs.enterKey} steer`, `esc${glyphs.times}2 interrupt`, `ctrl+c${glyphs.times}2 exit`].join(dot);
58
54
  }
59
55
  // The hint names the DESTINATION, not the current state — the correct label
60
56
  // for a toggle affordance, and the reason the mode word here is the OPPOSITE
@@ -90,7 +86,7 @@ function hintText(opts) {
90
86
  * than fishing the string out of a rendered frame.
91
87
  */
92
88
  export const hintTextForTest = hintText;
93
- export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services = 0, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, scrolledLines = 0, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
89
+ export function Composer({ cols, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services = 0, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines = 0, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
94
90
  const [hasDraft, setHasDraft] = useState(false);
95
91
  const onDraftChange = useCallback((next) => {
96
92
  setHasDraft(next.hasDraft);
@@ -110,7 +106,7 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
110
106
  : theme.idleBorder ?? theme.border;
111
107
  const glyphs = pickGlyphs(caps);
112
108
  const visible = showHint && hintsEnabled;
113
- return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }), visible && (
109
+ return (_jsxs(Box, { ref: measureRef, flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { cols: cols, cursorVisible: cursorVisible, onInteraction: onInteraction, isActive: isActive, reducedMotion: reducedMotion, onEscape: onEscape, onEscapeDismiss: onEscapeDismiss, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, popupMaxHeight: popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }), visible && (
114
110
  // The chip rides the EXISTING hint row rather than adding one, so it
115
111
  // costs zero rows on a 24-row terminal.
116
112
  _jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText({
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ export interface PromptCaretProps {
3
+ text: string;
4
+ active: boolean;
5
+ reducedMotion: boolean;
6
+ colorLevel: 0 | 1 | 2 | 3;
7
+ color?: string;
8
+ resetKey: object;
9
+ }
10
+ /** The only blinking state belongs to this leaf, never to the editor. */
11
+ export declare function PromptCaret({ text, active, reducedMotion, colorLevel, color, resetKey, }: PromptCaretProps): React.ReactElement;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useState } from 'react';
3
+ import { Text } from 'ink';
4
+ import { displayWidth } from './composer-rows.js';
5
+ function monochromeMarker(text) {
6
+ // Ink's output grid advances for combining marks, even though they have no
7
+ // terminal width. Retain them in both phases so its cell count stays stable.
8
+ const replace = (marker) => Array.from(text, (point) => {
9
+ const width = displayWidth(point);
10
+ return width > 0 ? marker.repeat(width) : point;
11
+ }).join('');
12
+ const underline = replace('_');
13
+ return underline === text ? replace('^') : underline;
14
+ }
15
+ /** The only blinking state belongs to this leaf, never to the editor. */
16
+ export function PromptCaret({ text, active, reducedMotion, colorLevel, color, resetKey, }) {
17
+ const [visible, setVisible] = useState(true);
18
+ useLayoutEffect(() => {
19
+ setVisible(true);
20
+ if (!active || reducedMotion)
21
+ return;
22
+ const timer = setInterval(() => setVisible((previous) => !previous), 500);
23
+ return () => clearInterval(timer);
24
+ }, [resetKey, active, reducedMotion]);
25
+ const bright = active && (reducedMotion || visible);
26
+ let content = text;
27
+ if (bright && colorLevel === 0) {
28
+ content = monochromeMarker(text);
29
+ }
30
+ return _jsx(Text, { color: color, inverse: bright && colorLevel > 0, children: content });
31
+ }
@@ -31,6 +31,9 @@ export interface CommandOption {
31
31
  description: string;
32
32
  }
33
33
  interface PromptInputProps {
34
+ cols?: number;
35
+ cursorVisible?: boolean;
36
+ onInteraction?: () => void;
34
37
  isActive: boolean;
35
38
  running: boolean;
36
39
  history: string[];
@@ -41,15 +44,18 @@ interface PromptInputProps {
41
44
  /**
42
45
  * Row budget for the completion popup (§5.2 / R-14).
43
46
  *
44
- * The popup renders INSIDE this component, inside the shell's `flexShrink={0}`
45
- * bottom chrome, and can add up to 9 rows. On a short terminal that pushes the
46
- * status bar past the frame height, where the root box's `overflow: hidden`
47
- * clips it away — the one piece of chrome R2 promises is always visible.
48
- * `App` derives this from `budget.ts` so the popup shrinks instead.
47
+ * The popup shares the scrolling footer with the editor. Its height budget
48
+ * reserves visible message rows without changing the viewport or TODO rail.
49
49
  */
50
50
  popupMaxRows?: number;
51
+ /** Total menu rows, including borders and the overflow hint. */
52
+ popupMaxHeight?: number;
53
+ onPopupRowsChange?: (rows: number) => void;
51
54
  onSubmit: (text: string) => void;
52
55
  onHelp?: () => void;
56
+ onEscape?: () => void;
57
+ onEscapeDismiss?: () => void;
58
+ reducedMotion?: boolean;
53
59
  /**
54
60
  * Mirror of the session mode. Read for the placeholder only — the toggle
55
61
  * itself belongs to `App`, and this component's job is to get out of its way
@@ -173,5 +179,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
173
179
  end: number;
174
180
  query: string;
175
181
  } | null;
176
- export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
182
+ export declare function PromptInput({ cols: colsProp, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, bordered, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
177
183
  export {};
@@ -19,12 +19,14 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
19
19
  * stays a plain `useState` because it is written from the debounced glob effect
20
20
  * rather than from a key.
21
21
  */
22
- import { useEffect, useReducer, useRef, useState } from 'react';
22
+ import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
23
23
  import { Box, Text, useInput, useStdout } from 'ink';
24
24
  import { glob as tinyGlob } from 'tinyglobby';
25
25
  import { pickGlyphs } from './glyphs.js';
26
+ import { PromptCaret } from './PromptCaret.js';
26
27
  import { recognize } from '../input/keymap.js';
27
28
  import { AutocompletePopup } from './AutocompletePopup.js';
29
+ import { buildAutocompleteLayout } from './layout/autocomplete.js';
28
30
  import { editorReducer, INITIAL_EDITOR_STATE, } from './editor-reducer.js';
29
31
  import { hasPasteFrame, splitPasteFrames } from './paste-frames.js';
30
32
  import { allocatePasteId, expandPastes, expandRangeOverTokens } from './paste-tokens.js';
@@ -42,8 +44,8 @@ const CHIP_FULL_MIN_COLS = 64;
42
44
  * reads `999+`.
43
45
  */
44
46
  const CHIP_MAX_N = 999;
45
- /** `↓ 999+ new lines · PgDn` — the widest form the full branch can produce. */
46
- const CHIP_FULL_CELLS = 24;
47
+ /** `↓ 999+ lines below · PgDn` — the widest form the full branch can produce. */
48
+ const CHIP_FULL_CELLS = 27;
47
49
  /** `↓999+` — the widest form the terse branch can produce. */
48
50
  const CHIP_TERSE_CELLS = 6;
49
51
  /**
@@ -63,7 +65,7 @@ export function scrollChip(scrolledLines, cols, glyphs) {
63
65
  }
64
66
  const noun = n === 1 ? 'line' : 'lines';
65
67
  return {
66
- text: `${glyphs.arrowDown} ${shown} new ${noun} ${glyphs.midDot} PgDn`,
68
+ text: `${glyphs.arrowDown} ${shown} ${noun} below ${glyphs.midDot} PgDn`,
67
69
  cells: CHIP_FULL_CELLS,
68
70
  };
69
71
  }
@@ -246,7 +248,7 @@ function isControlSeq(input) {
246
248
  // ---------------------------------------------------------------------------
247
249
  // Component
248
250
  // ---------------------------------------------------------------------------
249
- export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines = 0, }) {
251
+ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion = false, bordered, onDraftChange, onNotice, scrolledLines = 0, }) {
250
252
  const { stdout } = useStdout();
251
253
  const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
252
254
  const { buffer, cursor, historyIndex, dismissed, sel } = editor;
@@ -263,6 +265,19 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
263
265
  ? fileMatches.map((p) => ({ label: p }))
264
266
  : [];
265
267
  const clampedSel = popupItems.length > 0 ? Math.min(sel, popupItems.length - 1) : 0;
268
+ const popupLayout = buildAutocompleteLayout({
269
+ itemCount: isActive ? popupItems.length : 0,
270
+ selected: clampedSel,
271
+ maxRows: popupMaxRows,
272
+ maxHeight: popupMaxHeight,
273
+ });
274
+ const popupVisible = isActive && popupLayout.rowCount > 0;
275
+ // Report on every mount, including zero. Cleanup belongs to a separate
276
+ // effect so changing height does not publish an intermediate zero budget.
277
+ useLayoutEffect(() => {
278
+ onPopupRowsChange?.(popupLayout.rowCount);
279
+ }, [onPopupRowsChange, popupLayout.rowCount]);
280
+ useLayoutEffect(() => () => onPopupRowsChange?.(0), [onPopupRowsChange]);
266
281
  // --- Debounced `@file` scan. ---------------------------------------------
267
282
  useEffect(() => {
268
283
  if (fileQuery === null || fileQuery.length < 1) {
@@ -300,7 +315,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
300
315
  // Deriving it twice — once for the frame and once for the budget — is exactly
301
316
  // the trap `BottomStatusRow.tsx:5-27` documents.
302
317
  const glyphs = pickGlyphs(caps);
303
- const cols = stdout?.columns ?? 80;
318
+ const cols = colsProp ?? stdout?.columns ?? 80;
304
319
  const terminalRows = stdout?.rows ?? 24;
305
320
  // Full-screen only: inline mode has no self-drawn viewport, so `scrolledLines`
306
321
  // is always 0 there and the branch is unreachable rather than merely unused.
@@ -316,7 +331,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
316
331
  cursor,
317
332
  cols: wrapCols,
318
333
  maxRows: draftMaxRows(terminalRows),
319
- active: isActive,
334
+ active: isActive && cursorVisible,
320
335
  });
321
336
  // TWO PASSES, AND THE SECOND ONE IS LOAD-BEARING (I-8). The overflow indicator
322
337
  // rides the last rendered row INSIDE the box the draft wraps in, so its cells
@@ -334,6 +349,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
334
349
  const usableCols = overflows ? Math.max(8, baseCols - OVERFLOW_CELLS - 1) : baseCols;
335
350
  const layout = overflows ? measure(usableCols) : wide;
336
351
  const draftRowCount = Math.max(1, layout.rows.length);
352
+ const caretResetKey = useMemo(() => ({}), [buffer, cursor, isActive, usableCols]);
337
353
  // --- Draft presence and height, reported only on a transition. -----------
338
354
  // Notifying on every keystroke would re-render the whole App tree per key. The
339
355
  // row count changes on a line break or a wrap boundary, not per character
@@ -354,6 +370,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
354
370
  // by line. Two dispatches from one key would put the legacy root back to two
355
371
  // commits and undo the whole of F3 for that branch.
356
372
  const insert = (text) => {
373
+ onInteraction?.();
357
374
  dispatch({ type: 'insert', text });
358
375
  };
359
376
  /**
@@ -365,6 +382,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
365
382
  * needs. `clear` releases every payload in the same dispatch.
366
383
  */
367
384
  const submit = (text) => {
385
+ onInteraction?.();
368
386
  onSubmit(expandPastes(text, editor.pastes));
369
387
  dispatch({ type: 'clear' });
370
388
  };
@@ -390,6 +408,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
390
408
  dispatch({ type: 'recall', buffer: value, cursor: value.length, historyIndex: idx });
391
409
  };
392
410
  const completeSelection = () => {
411
+ onInteraction?.();
393
412
  const chosen = popupItems[clampedSel];
394
413
  if (!chosen)
395
414
  return;
@@ -403,6 +422,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
403
422
  }
404
423
  };
405
424
  const verticalOrHistory = (dir) => {
425
+ onInteraction?.();
406
426
  const moved = moveVertical(buffer, cursor, dir);
407
427
  if (moved) {
408
428
  dispatch({ type: 'moveCursor', cursor: moved.cursor });
@@ -427,13 +447,17 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
427
447
  // appears with a popup open and is therefore easy to ship (AC-P3 / R-P2).
428
448
  if (key.tab && key.shift)
429
449
  return;
450
+ if (key.pageUp || key.pageDown || (key.shift && (key.upArrow || key.downArrow)))
451
+ return;
430
452
  // Popup navigation owns Up/Down/Tab/→/Enter/Esc while it is open.
431
- if (popupItems.length > 0) {
453
+ if (popupVisible) {
432
454
  if (key.upArrow) {
455
+ onInteraction?.();
433
456
  dispatch({ type: 'select', sel: Math.max(0, clampedSel - 1) });
434
457
  return;
435
458
  }
436
459
  if (key.downArrow) {
460
+ onInteraction?.();
437
461
  dispatch({ type: 'select', sel: Math.min(popupItems.length - 1, clampedSel + 1) });
438
462
  return;
439
463
  }
@@ -449,15 +473,19 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
449
473
  }
450
474
  if (key.escape) {
451
475
  dispatch({ type: 'dismiss' });
476
+ onEscapeDismiss?.();
452
477
  return;
453
478
  }
454
479
  // Any other key falls through so typing keeps filtering the popup.
455
480
  }
456
- if (key.escape)
457
- return; // App owns abort / close-overlay.
481
+ if (key.escape) {
482
+ onEscape?.();
483
+ return;
484
+ }
458
485
  // Line-editing intents (Home/End/word/kill) recognized from raw keys.
459
486
  const intent = recognize(input, key);
460
487
  if (intent) {
488
+ onInteraction?.();
461
489
  const next = applyEdit(buffer, cursor, intent);
462
490
  dispatch({ type: 'replace', buffer: next.buffer, cursor: next.cursor });
463
491
  return;
@@ -475,6 +503,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
475
503
  return;
476
504
  }
477
505
  if (key.backspace || key.delete) {
506
+ onInteraction?.();
478
507
  // The `cursor > 0` guard lives in the reducer now, so this branch is one
479
508
  // unconditional dispatch and a no-op at the left edge returns the same
480
509
  // state object — which React bails out on, exactly as the old
@@ -483,11 +512,15 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
483
512
  return;
484
513
  }
485
514
  if (key.leftArrow) {
486
- dispatch({ type: 'moveCursor', cursor: Math.max(0, cursor - 1) });
515
+ onInteraction?.();
516
+ const step = cursor >= 2 && (buffer.codePointAt(cursor - 2) ?? 0) > 0xffff ? 2 : 1;
517
+ dispatch({ type: 'moveCursor', cursor: Math.max(0, cursor - step) });
487
518
  return;
488
519
  }
489
520
  if (key.rightArrow) {
490
- dispatch({ type: 'moveCursor', cursor: Math.min(buffer.length, cursor + 1) });
521
+ onInteraction?.();
522
+ const step = (buffer.codePointAt(cursor) ?? 0) > 0xffff ? 2 : 1;
523
+ dispatch({ type: 'moveCursor', cursor: Math.min(buffer.length, cursor + step) });
491
524
  return;
492
525
  }
493
526
  // `!key.shift` is load-bearing: Shift+↑/↓ is the viewport's line-scroll
@@ -525,6 +558,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
525
558
  onNotice?.('warn', refusal);
526
559
  return;
527
560
  }
561
+ onInteraction?.();
528
562
  dispatch({ type: 'input', segments });
529
563
  return;
530
564
  }
@@ -535,7 +569,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
535
569
  const marker = running ? glyphs.steer : glyphs.caret;
536
570
  const markerColor = running ? theme.toolRunning : theme.primary;
537
571
  const placeholder = running
538
- ? `Type to steer the run, Esc to abort${glyphs.ellipsis}`
572
+ ? `Type to steer the run, Esc twice to interrupt${glyphs.ellipsis}`
539
573
  : agentMode === 'plan'
540
574
  ? `Describe what you want to build; I'll research and plan it first${glyphs.ellipsis}`
541
575
  : `Send a message (/ for commands, @ for files)${glyphs.ellipsis}`;
@@ -548,9 +582,9 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
548
582
  // The caret is drawn as one inverse cell, split at the column
549
583
  // `layoutComposer` reported — the same width table it wrapped with (I-9).
550
584
  const split = splitRowAtColumn(row, layout.cursorCol);
551
- return (_jsxs(_Fragment, { children: [renderSegments(split.before, `b${i}.`), _jsx(Text, { inverse: true, children: split.at ? split.at.text : ' ' }), renderSegments(split.after, `a${i}.`)] }));
585
+ return (_jsxs(_Fragment, { children: [renderSegments(split.before, `b${i}.`), _jsx(PromptCaret, { text: split.at ? split.at.text : ' ', active: isActive && cursorVisible, reducedMotion: reducedMotion, colorLevel: caps.colorLevel, resetKey: caretResetKey, color: split.at?.kind === 'token' ? tokenColor : undefined }), renderSegments(split.after, `a${i}.`)] }));
552
586
  });
553
- const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: buffer.length === 0 ? (_jsx(Text, { color: theme.muted, children: placeholder })) : (rowNodes.map((node, i) => overflow && i === rowNodes.length - 1 ? (
587
+ const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: buffer.length === 0 ? (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [_jsx(PromptCaret, { text: String.fromCodePoint(placeholder.codePointAt(0)), active: isActive && cursorVisible, reducedMotion: reducedMotion, colorLevel: caps.colorLevel, color: theme.muted, resetKey: caretResetKey }), placeholder.slice(String.fromCodePoint(placeholder.codePointAt(0)).length)] })) : (rowNodes.map((node, i) => overflow && i === rowNodes.length - 1 ? (
554
588
  // The overflow indicator rides the LAST rendered row rather than
555
589
  // taking one of its own: an extra row would put the composer's
556
590
  // real height one above the `draftRows` it just reported (I-8).
@@ -565,5 +599,5 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
565
599
  // the user is typing — `↓ 9` becomes `↓ 10` becomes `↓ 100` — and every
566
600
  // change would otherwise re-wrap a draft that fills the line.
567
601
  _jsx(Box, { flexShrink: 0, alignSelf: "flex-start", marginLeft: 1, width: chip.cells, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: chip.text }) }))] }));
568
- return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [popupItems.length > 0 && (_jsx(AutocompletePopup, { items: popupItems, selected: clampedSel, theme: theme, caps: caps, maxRows: popupMaxRows })), bordered ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: bordered.color, paddingX: 1, children: inputRow })) : (inputRow)] }));
602
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [popupVisible && (_jsx(AutocompletePopup, { items: popupItems, selected: clampedSel, theme: theme, caps: caps, maxRows: popupMaxRows, layout: popupLayout })), bordered ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: bordered.color, paddingX: 1, children: inputRow })) : (inputRow)] }));
569
603
  }
@@ -1,8 +1,15 @@
1
1
  /**
2
2
  * Status bar (spec §4.8). Width-aware, borderless, EXACTLY one row: a left
3
3
  * cluster (run status, model, thinking level) and a right cluster (context
4
- * gauge, tokens, cost, tokens/sec + elapsed while running, off-bottom `↑N`).
5
- * Clusters drop out gracefully on narrow terminals.
4
+ * gauge, occupancy pair, session total, cost, tokens/sec + elapsed while
5
+ * running, off-bottom `↑N`). Clusters drop out gracefully on narrow terminals.
6
+ *
7
+ * THE RIGHT CLUSTER'S WIDTH LADDER (context-usage-gauge-accuracy §3.6):
8
+ *
9
+ * >= 96 [####....] 43% 86.0k/200.0k total 1.2M^ 48.0k v $3.21 12 tok/s 1m02s
10
+ * 72-95 [####....] 43% 86.0k/200.0k $3.21
11
+ * 60-71 [####....] 43% $3.21
12
+ * < 60 43% $3.21
6
13
  *
7
14
  * Dropping the round border here buys back 3 rows of a 24-row terminal — an
8
15
  * eighth of the screen was being spent framing a single line of text.
@@ -11,14 +18,24 @@ import React from 'react';
11
18
  import type { Theme } from './theme.js';
12
19
  import type { TermCapabilities } from './capabilities.js';
13
20
  import type { UsageTotal } from '../agent/reducer.js';
21
+ import type { ContextUsageSnapshot } from '../compaction/types.js';
14
22
  import { type AgentMode } from '../agent/agent-mode.js';
15
23
  interface StatusBarProps {
24
+ interruptHint?: string;
16
25
  model: string;
17
26
  provider: string;
18
27
  usageTotal: UsageTotal;
19
- contextTokens: number;
20
- contextWindow: number;
21
- contextWindowKnown: boolean;
28
+ /**
29
+ * Context occupancy, as one object (context-usage-gauge-accuracy §4.3).
30
+ *
31
+ * IT REPLACED FOUR PARALLEL PROPS - `contextTokens`, `contextWindow`,
32
+ * `contextWindowKnown`, `contextEstimated`. Four props describing one reading
33
+ * are four chances for a caller to update three of them, which is the props-
34
+ * layer form of the "many writers" defect this feature removed from the
35
+ * reducer. `usageTotal` stays separate because it genuinely IS another
36
+ * quantity: session spend, not occupancy.
37
+ */
38
+ context: ContextUsageSnapshot;
22
39
  status: 'idle' | 'running';
23
40
  elapsedMs: number;
24
41
  thinkingLevel: string;
@@ -187,16 +204,6 @@ interface StatusBarProps {
187
204
  warn: number;
188
205
  high: number;
189
206
  };
190
- /**
191
- * Whether `contextTokens` is a DERIVED figure rather than a measured one.
192
- *
193
- * It WIDENS THE EXISTING `~`, it does not add a second marker. Today that
194
- * prefix means "the window is a guess" (`contextWindowKnown`); with compaction
195
- * it also has to mean "the occupancy is a guess", because the bar is written
196
- * from an estimate for one turn after every compaction so it can fall
197
- * immediately (§6.2 / AC-5).
198
- */
199
- contextEstimated?: boolean;
200
207
  }
201
208
  export declare function StatusBar(props: StatusBarProps): React.ReactElement;
202
209
  export {};