@aragon-agent/cli 0.6.7 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +107 -77
  3. package/dist/agent/controller.d.ts +11 -7
  4. package/dist/agent/controller.js +35 -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 +7 -113
  9. package/dist/agent/reducer.js +0 -62
  10. package/dist/cli.js +111 -182
  11. package/dist/commands/builtins.js +3 -1
  12. package/dist/commands/perf.d.ts +32 -2
  13. package/dist/commands/perf.js +32 -2
  14. package/dist/commands/registry.d.ts +4 -5
  15. package/dist/config/env.js +0 -130
  16. package/dist/config/load.d.ts +0 -2
  17. package/dist/config/load.js +2 -31
  18. package/dist/config/schema.d.ts +1 -14
  19. package/dist/config/schema.js +0 -1
  20. package/dist/config/store.js +2 -0
  21. package/dist/input/interrupt-gesture.d.ts +18 -0
  22. package/dist/input/interrupt-gesture.js +19 -0
  23. package/dist/input/pointer-router.d.ts +11 -0
  24. package/dist/input/pointer-router.js +14 -0
  25. package/dist/logging/install.d.ts +2 -2
  26. package/dist/logging/install.js +2 -2
  27. package/dist/todo/follow-through.d.ts +0 -4
  28. package/dist/todo/follow-through.js +0 -4
  29. package/dist/todo/limits.d.ts +0 -10
  30. package/dist/todo/limits.js +0 -10
  31. package/dist/todo/prompt.d.ts +0 -15
  32. package/dist/todo/store.d.ts +8 -13
  33. package/dist/todo/store.js +13 -14
  34. package/dist/todo/types.d.ts +2 -1
  35. package/dist/ui/ActivityLine.d.ts +26 -34
  36. package/dist/ui/ActivityLine.js +30 -19
  37. package/dist/ui/App.d.ts +3 -41
  38. package/dist/ui/App.js +223 -247
  39. package/dist/ui/BottomStatusRow.d.ts +1 -3
  40. package/dist/ui/BottomStatusRow.js +4 -8
  41. package/dist/ui/Composer.d.ts +38 -5
  42. package/dist/ui/Composer.js +45 -23
  43. package/dist/ui/Header.d.ts +2 -16
  44. package/dist/ui/Header.js +3 -18
  45. package/dist/ui/Logo.d.ts +1 -17
  46. package/dist/ui/Logo.js +0 -16
  47. package/dist/ui/ModeChip.d.ts +7 -18
  48. package/dist/ui/ModeChip.js +9 -0
  49. package/dist/ui/PromptCaret.d.ts +11 -0
  50. package/dist/ui/PromptCaret.js +31 -0
  51. package/dist/ui/PromptInput.d.ts +10 -33
  52. package/dist/ui/PromptInput.js +37 -20
  53. package/dist/ui/StatusBar.d.ts +1 -11
  54. package/dist/ui/StatusBar.js +14 -8
  55. package/dist/ui/ToastStack.d.ts +2 -18
  56. package/dist/ui/ToastStack.js +4 -11
  57. package/dist/ui/Transcript.d.ts +5 -71
  58. package/dist/ui/Transcript.js +10 -199
  59. package/dist/ui/composer-rows.js +50 -9
  60. package/dist/ui/entries/AssistantEntry.d.ts +0 -23
  61. package/dist/ui/entries/AssistantEntry.js +3 -4
  62. package/dist/ui/entries/DiffView.d.ts +1 -7
  63. package/dist/ui/entries/DiffView.js +2 -2
  64. package/dist/ui/entries/ToolCard.d.ts +0 -6
  65. package/dist/ui/entries/ToolCard.js +4 -13
  66. package/dist/ui/frame-differ.d.ts +13 -0
  67. package/dist/ui/frame-differ.js +37 -24
  68. package/dist/ui/frame-observer.d.ts +17 -0
  69. package/dist/ui/frame-observer.js +81 -0
  70. package/dist/ui/frame-parser.d.ts +7 -0
  71. package/dist/ui/frame-parser.js +18 -0
  72. package/dist/ui/ink-runtime.d.ts +1 -0
  73. package/dist/ui/ink-runtime.js +21 -0
  74. package/dist/ui/layout/AppShell.d.ts +4 -58
  75. package/dist/ui/layout/AppShell.js +3 -6
  76. package/dist/ui/layout/OverlayFrame.d.ts +1 -25
  77. package/dist/ui/layout/OverlayFrame.js +0 -24
  78. package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
  79. package/dist/ui/layout/ScrollIndicator.js +4 -4
  80. package/dist/ui/layout/ScrollViewport.d.ts +16 -66
  81. package/dist/ui/layout/ScrollViewport.js +195 -185
  82. package/dist/ui/layout/budget.d.ts +3 -51
  83. package/dist/ui/layout/budget.js +4 -53
  84. package/dist/ui/layout/document-layout.d.ts +13 -0
  85. package/dist/ui/layout/document-layout.js +11 -0
  86. package/dist/ui/layout/follow-state.d.ts +1 -0
  87. package/dist/ui/layout/follow-state.js +1 -3
  88. package/dist/ui/layout/frame.d.ts +0 -22
  89. package/dist/ui/layout/frame.js +0 -36
  90. package/dist/ui/layout/scroll-indicator.d.ts +1 -1
  91. package/dist/ui/layout/scroll-indicator.js +3 -2
  92. package/dist/ui/layout/scrollbar-geometry.d.ts +17 -0
  93. package/dist/ui/layout/scrollbar-geometry.js +13 -0
  94. package/dist/ui/layout/todo-layout.d.ts +2 -3
  95. package/dist/ui/layout/todo-layout.js +9 -9
  96. package/dist/ui/layout/viewport-geometry.d.ts +2 -1
  97. package/dist/ui/layout/viewport-geometry.js +2 -1
  98. package/dist/ui/layout/virtual-window.d.ts +1 -0
  99. package/dist/ui/layout/virtual-window.js +1 -1
  100. package/dist/ui/live-clamp.d.ts +1 -29
  101. package/dist/ui/live-clamp.js +1 -29
  102. package/dist/ui/overlays/HelpOverlay.js +6 -2
  103. package/dist/ui/run-status-row.d.ts +61 -0
  104. package/dist/ui/run-status-row.js +70 -0
  105. package/dist/ui/screen.d.ts +1 -1
  106. package/dist/ui/screen.js +4 -6
  107. package/dist/ui/scrollbar-controller.d.ts +30 -0
  108. package/dist/ui/scrollbar-controller.js +124 -0
  109. package/dist/ui/transcript-text.d.ts +0 -8
  110. package/dist/ui/transcript-text.js +0 -8
  111. package/dist/ui/use-startup-notices.d.ts +1 -57
  112. package/dist/ui/use-startup-notices.js +3 -54
  113. package/dist/ui/use-wheel-routing.d.ts +3 -1
  114. package/dist/ui/use-wheel-routing.js +6 -0
  115. package/package.json +2 -2
  116. package/dist/ui/TodoStrip.d.ts +0 -36
  117. package/dist/ui/TodoStrip.js +0 -32
@@ -54,9 +54,7 @@
54
54
  import React from 'react';
55
55
  import type { Theme } from './theme.js';
56
56
  import type { Toast } from '../agent/reducer.js';
57
- import type { RenderMode } from './layout/frame.js';
58
57
  export interface BottomStatusRowProps {
59
- mode: RenderMode;
60
58
  toasts: Toast[];
61
59
  /** `<ActivityLine>` while a run owns the screen, else `null`. */
62
60
  activity: React.ReactNode;
@@ -94,4 +92,4 @@ export interface BottomStatusRowProps {
94
92
  update?: React.ReactNode;
95
93
  theme: Theme;
96
94
  }
97
- export declare function BottomStatusRow({ mode, toasts, activity, activityGlyph, update, theme, }: BottomStatusRowProps): React.ReactElement | null;
95
+ export declare function BottomStatusRow({ toasts, activity, activityGlyph, update, theme, }: BottomStatusRowProps): React.ReactElement | null;
@@ -1,15 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { ToastStack } from './ToastStack.js';
4
- export function BottomStatusRow({ mode, toasts, activity, activityGlyph, update, theme, }) {
4
+ export function BottomStatusRow({ toasts, activity, activityGlyph, update, theme, }) {
5
5
  // A toast wins the row outright — but a run in flight keeps its spinner on it.
6
6
  if (toasts.length > 0) {
7
7
  const spinner = activity ? activityGlyph : null;
8
8
  // Byte-identical to the old branch whenever there is no live spinner to
9
9
  // carry: no run, an ASCII terminal, or `reducedMotion`.
10
10
  if (!spinner)
11
- return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
12
- const fullscreen = mode === 'fullscreen';
11
+ return _jsx(ToastStack, { toasts: toasts, theme: theme });
13
12
  return (
14
13
  // `flexDirection` SPELLED OUT even though `row` is Yoga's default: it is
15
14
  // what keeps two children on ONE row, and the header above turns on that
@@ -17,15 +16,12 @@ export function BottomStatusRow({ mode, toasts, activity, activityGlyph, update,
17
16
  // below that declares `column` — and "make them match" is a one-word edit
18
17
  // that doubles the row, takes it from the transcript, and desynchronises
19
18
  // every `viewportRows()` consumer without failing a type or a lint.
20
- _jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, marginTop: fullscreen ? 0 : 1, children: _jsxs(Text, { color: theme.thinking, children: [fullscreen ? ' ' : '', spinner, fullscreen ? '' : ' '] }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode }) })] }));
19
+ _jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, children: _jsxs(Text, { color: theme.thinking, children: [' ', spinner] }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(ToastStack, { toasts: toasts, theme: theme }) })] }));
21
20
  }
22
- // Inline has no frame and therefore nothing to protect: `ToastStack`'s inline
23
- // branch is already a conditional 0-3 row stack, and these are simply one more
24
- // conditional row above it.
25
21
  if (activity)
26
22
  return _jsx(Box, { flexShrink: 0, children: activity });
27
23
  if (update)
28
24
  return _jsx(Box, { flexShrink: 0, children: update });
29
25
  // The blank full-screen row that IS the budget must keep being emitted.
30
- return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
26
+ return _jsx(ToastStack, { toasts: toasts, theme: theme });
31
27
  }
@@ -9,20 +9,42 @@
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';
21
+ import { type RunActivity } from './run-status-row.js';
22
22
  import { type AgentMode } from '../agent/agent-mode.js';
23
+ /**
24
+ * The run status row (tui-scrollbar-edge-and-run-row §3.3): the spinner, the phrase
25
+ * and the `steer / interrupt / exit` clauses on ONE row above the input box.
26
+ * `App` passes it only while a run is in flight AND the idle hint row would have
27
+ * been shown, so enabling it never changes the footer's height.
28
+ */
29
+ export interface RunRowProps {
30
+ activity: RunActivity;
31
+ /** The row is in the viewport with no overlay: draw the animated spinner. */
32
+ live: boolean;
33
+ /** Measured by `ScrollViewport` to learn whether the row has scrolled away. */
34
+ rowRef?: React.RefObject<DOMElement>;
35
+ }
23
36
  export interface ComposerProps {
37
+ /** Present only while the run status row replaces the hint row below the input. */
38
+ runRow?: RunRowProps | null;
39
+ cols?: number;
40
+ cursorVisible?: boolean;
41
+ onInteraction?: () => void;
42
+ measureRef?: React.RefObject<DOMElement>;
24
43
  /** False while an overlay owns the keyboard (the input is visually blurred). */
25
44
  isActive: boolean;
45
+ reducedMotion?: boolean;
46
+ onEscape?: () => void;
47
+ onEscapeDismiss?: () => void;
26
48
  running: boolean;
27
49
  history: string[];
28
50
  commands: CommandOption[];
@@ -90,6 +112,17 @@ export interface ComposerProps {
90
112
  onSubmit: (text: string) => void;
91
113
  onHelp?: () => void;
92
114
  }
115
+ /**
116
+ * The running-state hint as separate clauses, most urgent first.
117
+ *
118
+ * `hintText` joins them (its output is pinned by AC-43), and the run status row
119
+ * consumes the SAME array so it can drop whole clauses from the tail on a narrow
120
+ * terminal instead of cutting `interrupt` in half. One source, two consumers.
121
+ */
122
+ export declare function runningHintClauses(opts: {
123
+ glyphs: ReturnType<typeof pickGlyphs>;
124
+ services: number;
125
+ }): string[];
93
126
  /**
94
127
  * Build the hint row.
95
128
  *
@@ -119,5 +152,5 @@ declare function hintText(opts: {
119
152
  * than fishing the string out of a rendered frame.
120
153
  */
121
154
  export declare const hintTextForTest: typeof hintText;
122
- export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services, modeToggleKey, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
155
+ export declare function Composer({ runRow, 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;
123
156
  export {};
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
3
  * Composer (spec §4.7) — the thin shell around the existing `PromptInput`.
4
4
  *
@@ -10,18 +10,35 @@ 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';
18
+ import stringWidth from 'string-width';
20
19
  import { pickGlyphs } from './glyphs.js';
21
20
  import { HINT_FADE_AFTER } from '../config/schema.js';
22
21
  import { PromptInput } from './PromptInput.js';
23
- import { ModeChip } from './ModeChip.js';
22
+ import { ModeChip, modeChipCols } from './ModeChip.js';
23
+ import { ActivityLabel, resolveActivityLabel } from './ActivityLine.js';
24
+ import { RUN_ROW_LEAD, planRunRow } from './run-status-row.js';
24
25
  import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
26
+ /**
27
+ * The running-state hint as separate clauses, most urgent first.
28
+ *
29
+ * `hintText` joins them (its output is pinned by AC-43), and the run status row
30
+ * consumes the SAME array so it can drop whole clauses from the tail on a narrow
31
+ * terminal instead of cutting `interrupt` in half. One source, two consumers.
32
+ */
33
+ export function runningHintClauses(opts) {
34
+ const { glyphs, services } = opts;
35
+ const clauses = [`${glyphs.enterKey} steer`, `esc${glyphs.times}2 interrupt`];
36
+ // Confirmation is named even when service controls also need space.
37
+ if (services > 0)
38
+ clauses.push(`ctrl+c stop ${services}`);
39
+ clauses.push(`ctrl+c${glyphs.times}2 exit`);
40
+ return clauses;
41
+ }
25
42
  /**
26
43
  * Build the hint row.
27
44
  *
@@ -42,20 +59,8 @@ function hintText(opts) {
42
59
  // clauses are shown; the row is `wrap="truncate"` and they are ordered
43
60
  // most-urgent-first, so a terminal too narrow for all of them drops the exit
44
61
  // clause last rather than the abort key.
45
- if (running) {
46
- if (services > 0) {
47
- return [
48
- `${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`,
53
- `ctrl+c stop ${services}`,
54
- `ctrl+c${glyphs.times}2 exit`,
55
- ].join(dot);
56
- }
57
- return [`${glyphs.enterKey} steer`, 'esc abort', `ctrl+c${glyphs.times}2 exit`].join(dot);
58
- }
62
+ if (running)
63
+ return runningHintClauses({ glyphs, services }).join(dot);
59
64
  // The hint names the DESTINATION, not the current state — the correct label
60
65
  // for a toggle affordance, and the reason the mode word here is the OPPOSITE
61
66
  // of the one on the chip beside it.
@@ -90,7 +95,21 @@ function hintText(opts) {
90
95
  * than fishing the string out of a rendered frame.
91
96
  */
92
97
  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, popupMaxHeight, onPopupRowsChange, scrolledLines = 0, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
98
+ function RunRow({ runRow, cols, services, agentMode, theme, caps, }) {
99
+ const glyphs = pickGlyphs(caps);
100
+ const separator = ` ${glyphs.midDot} `;
101
+ const clauses = runningHintClauses({ glyphs, services });
102
+ const plan = planRunRow({
103
+ cols,
104
+ // Spinner glyph + the space after it, then the label.
105
+ labelCols: 2 + stringWidth(resolveActivityLabel(runRow.activity, glyphs)),
106
+ hintClauseCols: clauses.map((clause) => stringWidth(clause)),
107
+ separatorCols: stringWidth(separator),
108
+ chipCols: modeChipCols(agentMode),
109
+ });
110
+ return (_jsxs(Box, { ref: runRow.rowRef, flexDirection: "row", flexShrink: 0, children: [_jsx(Box, { width: RUN_ROW_LEAD, flexShrink: 0, children: _jsx(Text, { children: ' ' }) }), _jsx(Box, { width: plan.labelCols, flexShrink: 0, children: _jsx(ActivityLabel, { ...runRow.activity, spinnerLive: runRow.live, theme: theme, caps: caps }) }), plan.hintClauses > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: [separator, clauses.slice(0, plan.hintClauses).join(separator)] })), plan.chip && (_jsxs(_Fragment, { children: [_jsx(Box, { flexGrow: 1 }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps })] }))] }));
111
+ }
112
+ export function Composer({ runRow, 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
113
  const [hasDraft, setHasDraft] = useState(false);
95
114
  const onDraftChange = useCallback((next) => {
96
115
  setHasDraft(next.hasDraft);
@@ -109,8 +128,11 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
109
128
  ? theme.focusBorder ?? theme.primary
110
129
  : theme.idleBorder ?? theme.border;
111
130
  const glyphs = pickGlyphs(caps);
112
- 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, popupMaxHeight: popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }), visible && (
131
+ // While the run row is up it REPLACES the hint row below the input (same row
132
+ // count, so starting a run never moves the layout).
133
+ const runRowActive = running && !!runRow;
134
+ const visible = showHint && hintsEnabled && !runRowActive;
135
+ return (_jsxs(Box, { ref: measureRef, flexDirection: "column", flexShrink: 0, children: [runRowActive && (_jsx(RunRow, { runRow: runRow, cols: cols ?? process.stdout.columns ?? 80, services: services, agentMode: agentMode, theme: theme, caps: caps })), _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, borderColor: borderColor, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }), visible && (
114
136
  // The chip rides the EXISTING hint row rather than adding one, so it
115
137
  // costs zero rows on a 24-row terminal.
116
138
  _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({
@@ -1,23 +1,9 @@
1
- /**
2
- * Header (spec §4.2) — the always-present brand bar.
3
- *
4
- * In full-screen this is EXACTLY ONE ROW at every size, which is what makes the
5
- * viewport height monotonic in `rows` and the first submit jump-free. The 6-row
6
- * wordmark it used to host now lives in `SessionOpener`, inside the viewport.
7
- *
8
- * `banner` survives for the inline path only (`App.tsx` picks it with a local
9
- * ternary and never calls `pickHeaderVariant`), so inline keeps its v0.3.0
10
- * geometry. `art` is no longer reachable from here.
11
- *
12
- * Every line is `wrap="truncate"`: a long cwd silently costing a second row
13
- * would push the bottom chrome off-frame and break R2.
14
- */
1
+ /** One-row brand bar, with a compact variant for narrow terminals. */
15
2
  import React from 'react';
16
3
  import type { Theme } from './theme.js';
17
4
  import type { TermCapabilities } from './capabilities.js';
18
5
  import { type HeaderVariant } from './Logo.js';
19
6
  interface HeaderProps {
20
- version: string;
21
7
  cwd: string;
22
8
  provider: string;
23
9
  model: string;
@@ -26,5 +12,5 @@ interface HeaderProps {
26
12
  theme: Theme;
27
13
  caps: TermCapabilities;
28
14
  }
29
- export declare function Header({ version, cwd, provider, model, hasKey, variant, theme, caps, }: HeaderProps): React.ReactElement;
15
+ export declare function Header({ cwd, provider, model, hasKey, variant, theme, caps, }: HeaderProps): React.ReactElement;
30
16
  export {};
package/dist/ui/Header.js CHANGED
@@ -1,25 +1,10 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { homedir } from 'node:os';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
2
  import { basename } from 'node:path';
4
3
  import { Box, Text } from 'ink';
5
4
  import { Logo } from './Logo.js';
6
- /** Collapse the home directory to `~` for a shorter path display. */
7
- function tildeCwd(cwd) {
8
- const home = homedir();
9
- if (home && cwd.startsWith(home))
10
- return `~${cwd.slice(home.length)}`;
11
- return cwd;
12
- }
13
- function KeyDot({ hasKey, theme }) {
14
- return (_jsxs(Text, { color: hasKey ? theme.toolDone : theme.toolRunning, children: [hasKey ? theme.symbols.keyOn : theme.symbols.keyOff, " ", hasKey ? 'key set' : 'no key'] }));
15
- }
16
- export function Header({ version, cwd, provider, model, hasKey, variant, theme, caps, }) {
5
+ export function Header({ cwd, provider, model, hasKey, variant, theme, caps, }) {
17
6
  if (variant === 'mini') {
18
7
  return (_jsx(Box, { flexDirection: "column", flexShrink: 0, children: _jsx(Text, { wrap: "truncate", children: _jsx(Logo, { variant: "mini", theme: theme, caps: caps }) }) }));
19
8
  }
20
- if (variant === 'bar') {
21
- return (_jsx(Box, { flexDirection: "column", flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Logo, { variant: "bar", theme: theme, caps: caps }), _jsxs(Text, { color: theme.accent, children: [' ', provider, ":", model] }), _jsxs(Text, { color: theme.muted, children: [' ', basename(cwd) || cwd] }), _jsxs(Text, { color: hasKey ? theme.toolDone : theme.toolRunning, children: [' ', hasKey ? theme.symbols.keyOn : theme.symbols.keyOff] })] }) }));
22
- }
23
- // `banner` (inline only): wordmark row + meta row.
24
- return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Logo, { variant: variant, theme: theme, caps: caps }), _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: theme.muted, children: ["v", version] }), _jsxs(Text, { color: theme.accent, children: [' ', provider, ":", model] }), _jsx(Text, { children: ' ' }), _jsx(KeyDot, { hasKey: hasKey, theme: theme }), _jsxs(Text, { color: theme.muted, children: [' ', tildeCwd(cwd)] })] })] }));
9
+ return (_jsx(Box, { flexDirection: "column", flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Logo, { variant: "bar", theme: theme, caps: caps }), _jsxs(Text, { color: theme.accent, children: [' ', provider, ":", model] }), _jsxs(Text, { color: theme.muted, children: [' ', basename(cwd) || cwd] }), _jsxs(Text, { color: hasKey ? theme.toolDone : theme.toolRunning, children: [' ', hasKey ? theme.symbols.keyOn : theme.symbols.keyOff] })] }) }));
25
10
  }
package/dist/ui/Logo.d.ts CHANGED
@@ -8,25 +8,9 @@ import type { TermCapabilities } from './capabilities.js';
8
8
  /** 6 rows × 52 columns, every row equal width (pinned by logo.test.ts). */
9
9
  export declare const LOGO_ART: readonly string[];
10
10
  export declare const LOGO_ART_WIDTH = 52;
11
- export type HeaderVariant = 'art' | 'banner' | 'bar' | 'mini';
11
+ export type HeaderVariant = 'bar' | 'mini';
12
12
  /** How the one-off opening wordmark renders inside the viewport (§4.2). */
13
13
  export type OpenerVariant = 'art' | 'banner' | 'none';
14
- /**
15
- * Pick the header tier (spec §4.2).
16
- *
17
- * `cols` is the only input, and that is the point. The old signature also took
18
- * `empty` and `rows` and could return `art`, so the brand region was 8 rows tall
19
- * on an empty session and 1 row afterwards: sending the first message made the
20
- * viewport jump by 7 rows, and widening a 27-row terminal to 28 rows made the
21
- * visible content SHRINK. A constant-height brand bar removes both, which is
22
- * what lets `budget.ts::viewportRows` be monotonic (A-3/A-4/A-9).
23
- *
24
- * The wordmark did not disappear — it moved into the viewport as
25
- * `SessionOpener`, where it scrolls away like any other content.
26
- *
27
- * NOTE: `Header` still renders `banner`; the inline path picks it directly at
28
- * `App.tsx` and does not call this function.
29
- */
30
14
  export declare function pickHeaderVariant(cols: number): HeaderVariant;
31
15
  /**
32
16
  * Pick the opening wordmark tier.
package/dist/ui/Logo.js CHANGED
@@ -22,22 +22,6 @@ const ART_MIN_VIEWPORT_ROWS = 14;
22
22
  const ART_MIN_COLS = LOGO_ART_WIDTH + 4;
23
23
  const BANNER_MIN_COLS = 48;
24
24
  const BAR_MIN_COLS = 40;
25
- /**
26
- * Pick the header tier (spec §4.2).
27
- *
28
- * `cols` is the only input, and that is the point. The old signature also took
29
- * `empty` and `rows` and could return `art`, so the brand region was 8 rows tall
30
- * on an empty session and 1 row afterwards: sending the first message made the
31
- * viewport jump by 7 rows, and widening a 27-row terminal to 28 rows made the
32
- * visible content SHRINK. A constant-height brand bar removes both, which is
33
- * what lets `budget.ts::viewportRows` be monotonic (A-3/A-4/A-9).
34
- *
35
- * The wordmark did not disappear — it moved into the viewport as
36
- * `SessionOpener`, where it scrolls away like any other content.
37
- *
38
- * NOTE: `Header` still renders `banner`; the inline path picks it directly at
39
- * `App.tsx` and does not call this function.
40
- */
41
25
  export function pickHeaderVariant(cols) {
42
26
  return cols >= BAR_MIN_COLS ? 'bar' : 'mini';
43
27
  }
@@ -1,21 +1,3 @@
1
- /**
2
- * ModeChip — the `PLAN` badge on the composer's hint row (plan-mode §6.1).
3
- *
4
- * `theme.chip = { fg, bg }` exists in every palette and was, until now, unused.
5
- * A mode badge is what it was for.
6
- *
7
- * SHOWN ONLY IN PLAN MODE. `BUILD` is the default, and a badge that is always on
8
- * is furniture rather than signal; it also means the Build-mode composer renders
9
- * exactly as it did before this feature existed.
10
- *
11
- * THE CHIP IS AN ENHANCEMENT, NOT THE GUARANTEE (P1-6). It rides the hint row,
12
- * which disappears on a short terminal (`showHint`), under `hints: false`, and
13
- * in inline render mode (which mounts a bare `PromptInput` and never mounts
14
- * `Composer` at all). The GUARANTEED carrier is the status-bar word, which
15
- * renders in both `AppShell` branches and has no opt-out. If you ever move the
16
- * mode indicator, that is the constraint to preserve: the status bar must always
17
- * name a non-default mode; the chip may.
18
- */
19
1
  import React from 'react';
20
2
  import type { Theme } from './theme.js';
21
3
  import type { TermCapabilities } from './capabilities.js';
@@ -25,5 +7,12 @@ interface ModeChipProps {
25
7
  theme: Theme;
26
8
  caps: TermCapabilities;
27
9
  }
10
+ /**
11
+ * Columns `ModeChip` occupies, including its trailing gap (plan => label + 3, any
12
+ * other mode 0). Both render branches (padded background chip, bracketed text)
13
+ * come to the same width, which is what lets the run status row budget for it
14
+ * without measuring a rendered element.
15
+ */
16
+ export declare function modeChipCols(mode: AgentMode): number;
28
17
  export declare function ModeChip({ mode, theme, caps }: ModeChipProps): React.ReactElement | null;
29
18
  export {};
@@ -1,6 +1,15 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { MODE_LABEL } from '../agent/agent-mode.js';
4
+ /**
5
+ * Columns `ModeChip` occupies, including its trailing gap (plan => label + 3, any
6
+ * other mode 0). Both render branches (padded background chip, bracketed text)
7
+ * come to the same width, which is what lets the run status row budget for it
8
+ * without measuring a rendered element.
9
+ */
10
+ export function modeChipCols(mode) {
11
+ return mode === 'plan' ? MODE_LABEL[mode].length + 3 : 0;
12
+ }
4
13
  export function ModeChip({ mode, theme, caps }) {
5
14
  if (mode !== 'plan')
6
15
  return null;
@@ -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,11 +44,8 @@ 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
51
  /** Total menu rows, including borders and the overflow hint. */
@@ -53,21 +53,16 @@ interface PromptInputProps {
53
53
  onPopupRowsChange?: (rows: number) => void;
54
54
  onSubmit: (text: string) => void;
55
55
  onHelp?: () => void;
56
+ onEscape?: () => void;
57
+ onEscapeDismiss?: () => void;
58
+ reducedMotion?: boolean;
56
59
  /**
57
60
  * Mirror of the session mode. Read for the placeholder only — the toggle
58
61
  * itself belongs to `App`, and this component's job is to get out of its way
59
62
  * (see the first statement of the input handler).
60
63
  */
61
64
  agentMode?: AgentMode;
62
- /**
63
- * Set by `Composer` in full-screen mode: draw the rounded frame around the
64
- * input row only, so the autocomplete popup lands OUTSIDE it (spec §4.7)
65
- * without lifting the editor's buffer/cursor state out of this component.
66
- * Unset ⇒ the v0.2.0 borderless inline rendering.
67
- */
68
- bordered?: {
69
- color?: string;
70
- };
65
+ borderColor?: string;
71
66
  /**
72
67
  * Fires when the draft flips empty ⇄ non-empty (for the border color) OR when
73
68
  * its ROW COUNT changes (for `layout/budget.ts`) — never per keystroke.
@@ -92,24 +87,6 @@ interface PromptInputProps {
92
87
  * component is the only place that holds both facts.
93
88
  */
94
89
  onNotice?: (level: 'warn' | 'error', text: string) => void;
95
- /**
96
- * Rows of newer output hidden BELOW the viewport — `ScrollViewport`'s offset
97
- * (tui-selection-and-scroll-follow §4.2 / G7).
98
- *
99
- * Rendered as a right-aligned chip ON THE INPUT ROW, INSIDE THE BORDER, which
100
- * is what the requirement asks for (输入框内部) and what makes the hint cost
101
- * zero rows. It used to take the viewport's own last row, which spent a line of
102
- * the user's content exactly when the transcript was longest AND fed the number
103
- * back into its own derivation (§3.3).
104
- *
105
- * IT IS STATE, NOT A HINT. It deliberately ignores `showHint` / `hintsEnabled`
106
- * — those govern the teaching row BELOW the box, which fades with experience
107
- * and is dropped on a short terminal. "You are 12 rows behind the newest
108
- * output" must survive both (T-23).
109
- *
110
- * Only meaningful in full-screen mode, so it renders only when `bordered` is
111
- * set: inline mode has no self-drawn viewport and this is always 0 there.
112
- */
113
90
  scrolledLines?: number;
114
91
  }
115
92
  export interface ScrollChip {
@@ -176,5 +153,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
176
153
  end: number;
177
154
  query: string;
178
155
  } | null;
179
- export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
156
+ export declare function PromptInput({ cols: colsProp, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, borderColor, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
180
157
  export {};