@aragon-agent/cli 0.6.14 → 0.6.15

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 (113) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +250 -233
  3. package/dist/agent/controller.d.ts +27 -1
  4. package/dist/agent/controller.js +182 -45
  5. package/dist/agent/plan-prompt.d.ts +1 -1
  6. package/dist/agent/plan-prompt.js +4 -3
  7. package/dist/agent/project-guidance-prompt.d.ts +4 -0
  8. package/dist/agent/project-guidance-prompt.js +104 -0
  9. package/dist/agent/reducer.d.ts +2 -0
  10. package/dist/agent/reducer.js +4 -0
  11. package/dist/agent/system-prompt.js +4 -0
  12. package/dist/commands/builtins.js +2 -2
  13. package/dist/compaction/child.d.ts +9 -1
  14. package/dist/compaction/child.js +39 -13
  15. package/dist/compaction/command.js +20 -10
  16. package/dist/compaction/compactor.d.ts +33 -204
  17. package/dist/compaction/compactor.js +311 -726
  18. package/dist/compaction/digest.d.ts +15 -51
  19. package/dist/compaction/digest.js +113 -148
  20. package/dist/compaction/limits.d.ts +19 -142
  21. package/dist/compaction/limits.js +20 -147
  22. package/dist/compaction/memory-identity.d.ts +16 -0
  23. package/dist/compaction/memory-identity.js +63 -0
  24. package/dist/compaction/memory-input.d.ts +25 -0
  25. package/dist/compaction/memory-input.js +106 -0
  26. package/dist/compaction/memory-render.d.ts +4 -0
  27. package/dist/compaction/memory-render.js +47 -0
  28. package/dist/compaction/memory-validation.d.ts +21 -0
  29. package/dist/compaction/memory-validation.js +268 -0
  30. package/dist/compaction/memory.d.ts +79 -0
  31. package/dist/compaction/memory.js +118 -0
  32. package/dist/compaction/meter.d.ts +14 -10
  33. package/dist/compaction/meter.js +53 -19
  34. package/dist/compaction/operation.d.ts +18 -0
  35. package/dist/compaction/operation.js +68 -0
  36. package/dist/compaction/pressure.d.ts +5 -20
  37. package/dist/compaction/pressure.js +13 -23
  38. package/dist/compaction/summarize-call.d.ts +3 -16
  39. package/dist/compaction/summarize-call.js +20 -20
  40. package/dist/compaction/summary-prompt.d.ts +29 -66
  41. package/dist/compaction/summary-prompt.js +132 -178
  42. package/dist/compaction/types.d.ts +21 -3
  43. package/dist/compaction/wiring.d.ts +10 -25
  44. package/dist/compaction/wiring.js +56 -66
  45. package/dist/config/schema.d.ts +1 -1
  46. package/dist/config/schema.js +1 -1
  47. package/dist/exec/events.d.ts +8 -0
  48. package/dist/exec/index.js +13 -6
  49. package/dist/exec/runner.d.ts +8 -0
  50. package/dist/exec/runner.js +26 -2
  51. package/dist/session/persist.d.ts +3 -0
  52. package/dist/session/persist.js +9 -0
  53. package/dist/session/store.js +2 -0
  54. package/dist/session/validate-session.js +34 -0
  55. package/dist/team/subagent.js +8 -0
  56. package/dist/ui/App.js +218 -116
  57. package/dist/ui/BottomStatusRow.d.ts +5 -10
  58. package/dist/ui/BottomStatusRow.js +7 -11
  59. package/dist/ui/Composer.d.ts +4 -2
  60. package/dist/ui/Composer.js +2 -2
  61. package/dist/ui/Header.d.ts +6 -2
  62. package/dist/ui/Header.js +7 -6
  63. package/dist/ui/PromptInput.d.ts +3 -1
  64. package/dist/ui/PromptInput.js +32 -41
  65. package/dist/ui/StatusBar.d.ts +6 -0
  66. package/dist/ui/StatusBar.js +8 -7
  67. package/dist/ui/Transcript.js +1 -1
  68. package/dist/ui/entries/CompactionCard.d.ts +3 -1
  69. package/dist/ui/entries/CompactionCard.js +7 -5
  70. package/dist/ui/frame-differ.js +27 -0
  71. package/dist/ui/frame-observer.js +18 -5
  72. package/dist/ui/interaction-copy.d.ts +73 -73
  73. package/dist/ui/interaction-copy.js +30 -38
  74. package/dist/ui/interaction-hints.d.ts +8 -1
  75. package/dist/ui/interaction-hints.js +56 -69
  76. package/dist/ui/layout/AppShell.d.ts +6 -2
  77. package/dist/ui/layout/AppShell.js +2 -2
  78. package/dist/ui/layout/OverlayFrame.d.ts +2 -0
  79. package/dist/ui/layout/OverlayFrame.js +23 -14
  80. package/dist/ui/layout/ScrollViewport.d.ts +3 -1
  81. package/dist/ui/layout/ScrollViewport.js +10 -2
  82. package/dist/ui/layout/budget.d.ts +25 -26
  83. package/dist/ui/layout/budget.js +29 -22
  84. package/dist/ui/layout/document-layout.d.ts +2 -2
  85. package/dist/ui/layout/document-layout.js +2 -3
  86. package/dist/ui/layout/queue-layout.js +4 -4
  87. package/dist/ui/layout/status-detail-layout.d.ts +9 -0
  88. package/dist/ui/layout/status-detail-layout.js +56 -0
  89. package/dist/ui/layout/status-layout.d.ts +19 -2
  90. package/dist/ui/layout/status-layout.js +95 -84
  91. package/dist/ui/layout/wrap-rows.d.ts +2 -0
  92. package/dist/ui/layout/wrap-rows.js +21 -7
  93. package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
  94. package/dist/ui/overlays/ConfirmDialog.js +22 -3
  95. package/dist/ui/overlays/HelpOverlay.js +9 -2
  96. package/dist/ui/overlays/ModelPicker.d.ts +5 -8
  97. package/dist/ui/overlays/ModelPicker.js +44 -10
  98. package/dist/ui/overlays/ModelProfileEditor.js +2 -0
  99. package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
  100. package/dist/ui/overlays/ModelProfilePicker.js +4 -2
  101. package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
  102. package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
  103. package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
  104. package/dist/ui/overlays/QuestionOverlay.js +45 -32
  105. package/dist/ui/overlays/QueueOverlay.js +2 -2
  106. package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
  107. package/dist/ui/overlays/SettingsScreen.js +10 -3
  108. package/dist/ui/run-metrics.d.ts +23 -0
  109. package/dist/ui/run-metrics.js +27 -0
  110. package/dist/ui/status-feedback.d.ts +33 -0
  111. package/dist/ui/status-feedback.js +36 -0
  112. package/dist/ui/stdout-frame-writer.js +28 -0
  113. package/package.json +2 -2
@@ -1,15 +1,10 @@
1
- /** The permanent action/feedback row. StatusBar owns all activity animation. */
1
+ /** Renders an already allocated detail row; it never chooses visibility or animation. */
2
2
  import React from 'react';
3
3
  import type { Theme } from './theme.js';
4
- import type { Toast } from '../agent/reducer.js';
5
- import { type ActionHintInput } from './interaction-hints.js';
4
+ import type { StatusLinePlan } from './layout/status-layout.js';
6
5
  export interface BottomStatusRowProps {
7
- toasts: Toast[];
8
- hints?: ActionHintInput;
9
- /** Legacy assembly props are accepted but never mount an activity spinner. */
10
- activity?: React.ReactNode;
11
- activityGlyph?: React.ReactElement | null;
12
- update?: React.ReactNode;
6
+ plan: StatusLinePlan;
7
+ columns: number;
13
8
  theme: Theme;
14
9
  }
15
- export declare function BottomStatusRow(props: BottomStatusRowProps): React.ReactElement;
10
+ export declare function BottomStatusRow({ plan, columns, theme }: BottomStatusRowProps): React.ReactElement;
@@ -1,12 +1,8 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { Box, Text, useStdout } from 'ink';
3
- import { buildActionClauses } from './interaction-hints.js';
4
- export function BottomStatusRow(props) {
5
- const { stdout } = useStdout();
6
- const toast = props.toasts[props.toasts.length - 1];
7
- const hints = props.hints ?? { cols: stdout?.columns ?? 80,
8
- interactionPhase: props.activity ? 'running' : 'idle' };
9
- const clauses = buildActionClauses({ ...hints, toast: hints.toast ?? toast?.text,
10
- updateAvailable: hints.updateAvailable ?? Boolean(props.update) });
11
- return _jsx(Box, { height: 1, width: hints.cols, flexShrink: 0, children: _jsxs(Text, { color: toast?.level === 'error' ? props.theme.noticeError : props.theme.muted, children: [' ', clauses.join(' | ')] }) });
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Renders an already allocated detail row; it never chooses visibility or animation. */
3
+ import React from 'react';
4
+ import { Box, Text } from 'ink';
5
+ export function BottomStatusRow({ plan, columns, theme }) {
6
+ const tones = { normal: theme.primary, muted: theme.muted, warning: theme.noticeWarn, error: theme.noticeError };
7
+ return _jsxs(Box, { height: 1, width: columns, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: " " }) }), plan.fields.map((field, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width: plan.separatorCells, flexShrink: 0, children: _jsx(Text, { color: theme.muted, children: plan.separator }) }), _jsx(Box, { width: field.cells, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", color: tones[field.tone], children: field.text }) })] }, field.id))] });
12
8
  }
@@ -20,6 +20,8 @@ export interface ComposerProps {
20
20
  /** Compatibility prop; activity is now rendered by the global StatusBar. */
21
21
  runRow?: RunRowProps | null;
22
22
  cols?: number;
23
+ terminalRows?: number;
24
+ statusExpanded?: boolean;
23
25
  deleteDisambiguated?: boolean;
24
26
  onCompletionContextChange?: (context: 'none' | 'slash' | 'file') => void;
25
27
  cursorVisible?: boolean;
@@ -47,7 +49,7 @@ export interface ComposerProps {
47
49
  popupMaxRows?: number;
48
50
  popupMaxHeight?: number;
49
51
  onPopupRowsChange?: (rows: number) => void;
50
- /** Rows of newer output below the viewport, forwarded to the input's scroll chip. */
52
+ /** Compatibility prop; history position is displayed by Header. */
51
53
  scrolledLines?: number;
52
54
  /** Compatibility prop; the global status and action rows show live services. */
53
55
  services?: number;
@@ -86,5 +88,5 @@ declare function hintText(opts: {
86
88
  }): string;
87
89
  /** Compatibility export for existing hint contract tests. */
88
90
  export declare const hintTextForTest: typeof hintText;
89
- export declare function Composer({ deleteDisambiguated, onCompletionContextChange, cols, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
91
+ export declare function Composer({ deleteDisambiguated, onCompletionContextChange, cols, terminalRows, statusExpanded, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
90
92
  export {};
@@ -36,7 +36,7 @@ function hintText(opts) {
36
36
  }
37
37
  /** Compatibility export for existing hint contract tests. */
38
38
  export const hintTextForTest = hintText;
39
- export function Composer({ deleteDisambiguated, onCompletionContextChange, cols, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines = 0, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
39
+ export function Composer({ deleteDisambiguated, onCompletionContextChange, cols, terminalRows, statusExpanded, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, agentMode, popupMaxRows, popupMaxHeight, onPopupRowsChange, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
40
40
  const [hasDraft, setHasDraft] = useState(false);
41
41
  const onDraftChange = useCallback((next) => {
42
42
  setHasDraft(next.hasDraft);
@@ -54,5 +54,5 @@ export function Composer({ deleteDisambiguated, onCompletionContextChange, cols,
54
54
  : hasDraft
55
55
  ? theme.focusBorder ?? theme.primary
56
56
  : theme.idleBorder ?? theme.border;
57
- return (_jsx(Box, { ref: measureRef, flexDirection: "column", flexShrink: 0, children: _jsx(PromptInput, { deleteDisambiguated: deleteDisambiguated, onCompletionContextChange: onCompletionContextChange, 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 }) }));
57
+ return (_jsx(Box, { ref: measureRef, width: cols, flexDirection: "column", flexShrink: 0, children: _jsx(PromptInput, { deleteDisambiguated: deleteDisambiguated, onCompletionContextChange: onCompletionContextChange, cols: cols, terminalRows: terminalRows, statusExpanded: statusExpanded, 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 }) }));
58
58
  }
@@ -1,4 +1,4 @@
1
- /** One-row brand bar, with a compact variant for narrow terminals. */
1
+ /** One-row brand bar with a fixed-width history and detail affordance. */
2
2
  import React from 'react';
3
3
  import type { Theme } from './theme.js';
4
4
  import type { TermCapabilities } from './capabilities.js';
@@ -11,6 +11,10 @@ interface HeaderProps {
11
11
  variant: HeaderVariant;
12
12
  theme: Theme;
13
13
  caps: TermCapabilities;
14
+ columns?: number;
15
+ statusExpanded?: boolean;
16
+ scrolledLines?: number;
17
+ overlayOpen?: boolean;
14
18
  }
15
- export declare function Header({ cwd, provider, model, hasKey, variant, theme, caps, }: HeaderProps): React.ReactElement;
19
+ export declare function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns, statusExpanded, scrolledLines, overlayOpen }: HeaderProps): React.ReactElement;
16
20
  export {};
package/dist/ui/Header.js CHANGED
@@ -1,10 +1,11 @@
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
  import { basename } from 'node:path';
3
3
  import { Box, Text } from 'ink';
4
4
  import { Logo } from './Logo.js';
5
- export function Header({ cwd, provider, model, hasKey, variant, theme, caps, }) {
6
- if (variant === 'mini') {
7
- return (_jsx(Box, { flexDirection: "column", flexShrink: 0, children: _jsx(Text, { wrap: "truncate", children: _jsx(Logo, { variant: "mini", theme: theme, caps: caps }) }) }));
8
- }
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] })] }) }));
5
+ export function Header({ cwd, provider, model, hasKey, variant, theme, caps, columns = 80, statusExpanded = false, scrolledLines = 0, overlayOpen = false }) {
6
+ const scroll = Number.isFinite(scrolledLines) && scrolledLines > 0
7
+ ? (scrolledLines > 9999 ? '^9999+' : '^' + Math.floor(scrolledLines)) + (overlayOpen ? ' ' : ' ') : '';
8
+ const detail = overlayOpen ? (statusExpanded ? 'Details on' : 'Details off')
9
+ : '^G ' + (statusExpanded ? 'less' : 'more');
10
+ return _jsxs(Box, { flexDirection: "row", height: 1, width: columns, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: Math.max(0, columns - 18), flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Logo, { variant: variant === 'mini' ? 'mini' : 'bar', theme: theme, caps: caps }), variant !== 'mini' && _jsxs(_Fragment, { children: [_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] })] })] }) }), _jsx(Box, { width: 18, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { color: theme.muted, wrap: "truncate", children: scroll + detail }) })] });
10
11
  }
@@ -33,6 +33,8 @@ export interface CommandOption {
33
33
  }
34
34
  interface PromptInputProps {
35
35
  cols?: number;
36
+ terminalRows?: number;
37
+ statusExpanded?: boolean;
36
38
  cursorVisible?: boolean;
37
39
  onInteraction?: () => void;
38
40
  isActive: boolean;
@@ -157,5 +159,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
157
159
  end: number;
158
160
  query: string;
159
161
  } | null;
160
- export declare function PromptInput({ cols: colsProp, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, borderColor, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
162
+ export declare function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, statusExpanded, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, borderColor, onDraftChange, onNotice, }: PromptInputProps): React.ReactElement;
161
163
  export {};
@@ -21,6 +21,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
21
21
  */
22
22
  import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
23
23
  import { Box, Text, useInput, useStdout } from 'ink';
24
+ import { popupCapacity } from './layout/budget.js';
24
25
  import { glob as tinyGlob } from 'tinyglobby';
25
26
  import { pickGlyphs } from './glyphs.js';
26
27
  import { PromptCaret } from './PromptCaret.js';
@@ -243,7 +244,7 @@ function isControlSeq(input) {
243
244
  // ---------------------------------------------------------------------------
244
245
  // Component
245
246
  // ---------------------------------------------------------------------------
246
- export function PromptInput({ cols: colsProp, cursorVisible = true, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated = false, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion = false, borderColor, onDraftChange, onNotice, scrolledLines = 0, }) {
247
+ export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, statusExpanded = false, cursorVisible = true, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated = false, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion = false, borderColor, onDraftChange, onNotice, }) {
247
248
  const { stdout } = useStdout();
248
249
  const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
249
250
  const { buffer, cursor, historyIndex, dismissed, sel } = editor;
@@ -260,30 +261,6 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
260
261
  ? fileMatches.map((p) => ({ label: p }))
261
262
  : [];
262
263
  const clampedSel = popupItems.length > 0 ? Math.min(sel, popupItems.length - 1) : 0;
263
- const popupLayout = buildAutocompleteLayout({
264
- itemCount: isActive ? popupItems.length : 0,
265
- selected: clampedSel,
266
- maxRows: popupMaxRows,
267
- maxHeight: popupMaxHeight,
268
- });
269
- const popupVisible = isActive && popupLayout.rowCount > 0;
270
- // Report on every mount, including zero. Cleanup belongs to a separate
271
- // effect so changing height does not publish an intermediate zero budget.
272
- useLayoutEffect(() => {
273
- onPopupRowsChange?.(popupLayout.rowCount);
274
- }, [onPopupRowsChange, popupLayout.rowCount]);
275
- useLayoutEffect(() => () => onPopupRowsChange?.(0), [onPopupRowsChange]);
276
- const completion = popupVisible ? popupKind ?? 'none' : 'none';
277
- const completionCallback = useRef(onCompletionContextChange);
278
- completionCallback.current = onCompletionContextChange;
279
- const lastCompletion = useRef();
280
- useLayoutEffect(() => {
281
- if (!onCompletionContextChange || lastCompletion.current === completion)
282
- return;
283
- lastCompletion.current = completion;
284
- onCompletionContextChange?.(completion);
285
- }, [onCompletionContextChange, completion]);
286
- useLayoutEffect(() => () => completionCallback.current?.('none'), []);
287
264
  // --- Debounced `@file` scan. ---------------------------------------------
288
265
  useEffect(() => {
289
266
  if (fileQuery === null || fileQuery.length < 1) {
@@ -322,13 +299,12 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
322
299
  // the trap `BottomStatusRow.tsx:5-27` documents.
323
300
  const glyphs = pickGlyphs(caps);
324
301
  const cols = colsProp ?? stdout?.columns ?? 80;
325
- const terminalRows = stdout?.rows ?? 24;
326
- const chip = scrollChip(scrolledLines, cols, glyphs);
302
+ const terminalRows = terminalRowsProp ?? stdout?.rows ?? 24;
327
303
  // Err NARROW. Wrapping wider than the box Ink gives us would make Ink wrap a
328
304
  // second time, and then `layout.rows.length` is a lie — which is the one thing
329
305
  // this module may not be.
330
306
  const chromeCols = 2 /* border */ + 2 /* paddingX */ + 2 /* marker */;
331
- const baseCols = Math.max(8, cols - chromeCols - (chip ? chip.cells + 1 : 0));
307
+ const baseCols = Math.max(8, cols - chromeCols);
332
308
  const measure = (wrapCols) => layoutComposer({
333
309
  buffer,
334
310
  cursor,
@@ -356,6 +332,31 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
356
332
  const preferredColumnWidth = useRef();
357
333
  useLayoutEffect(() => { preferredColumnWidth.current = undefined; }, [usableCols]);
358
334
  const draftRowCount = Math.max(1, layout.rows.length);
335
+ const menuCapacity = Math.min(popupMaxHeight ?? Infinity, popupCapacity(terminalRows, draftRowCount, statusExpanded));
336
+ const popupLayout = buildAutocompleteLayout({
337
+ itemCount: isActive ? popupItems.length : 0,
338
+ selected: clampedSel,
339
+ maxRows: popupMaxRows,
340
+ maxHeight: menuCapacity,
341
+ });
342
+ const popupVisible = isActive && popupLayout.rowCount > 0;
343
+ // Report on every mount, including zero. Cleanup belongs to a separate
344
+ // effect so changing height does not publish an intermediate zero budget.
345
+ useLayoutEffect(() => {
346
+ onPopupRowsChange?.(popupLayout.rowCount);
347
+ }, [onPopupRowsChange, popupLayout.rowCount]);
348
+ useLayoutEffect(() => () => onPopupRowsChange?.(0), [onPopupRowsChange]);
349
+ const completion = popupVisible ? popupKind ?? 'none' : 'none';
350
+ const completionCallback = useRef(onCompletionContextChange);
351
+ completionCallback.current = onCompletionContextChange;
352
+ const lastCompletion = useRef();
353
+ useLayoutEffect(() => {
354
+ if (!onCompletionContextChange || lastCompletion.current === completion)
355
+ return;
356
+ lastCompletion.current = completion;
357
+ onCompletionContextChange?.(completion);
358
+ }, [onCompletionContextChange, completion]);
359
+ useLayoutEffect(() => () => completionCallback.current?.('none'), []);
359
360
  const caretResetKey = useMemo(() => ({}), [buffer, cursor, isActive, usableCols]);
360
361
  // --- Draft presence and height, reported only on a transition. -----------
361
362
  // Notifying on every keystroke would re-render the whole App tree per key. The
@@ -363,7 +364,7 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
363
364
  // (R-6), so widening this callback does not widen how often it fires.
364
365
  const hasDraft = buffer.length > 0;
365
366
  const lastReportedDraft = useRef(null);
366
- useEffect(() => {
367
+ useLayoutEffect(() => {
367
368
  if (!onDraftChange)
368
369
  return;
369
370
  const last = lastReportedDraft.current;
@@ -385,7 +386,7 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
385
386
  const selectedIndex = Math.min(state.sel, (suggestions?.length ?? 0) - 1);
386
387
  const suggestionLayout = buildAutocompleteLayout({
387
388
  itemCount: isActive ? suggestions?.length ?? 0 : 0,
388
- selected: selectedIndex, maxRows: popupMaxRows, maxHeight: popupMaxHeight,
389
+ selected: selectedIndex, maxRows: popupMaxRows, maxHeight: menuCapacity,
389
390
  });
390
391
  const selected = suggestionLayout.rowCount > 0 ? suggestions?.[selectedIndex] : undefined;
391
392
  const text = selected?.label ?? expandPastes(state.buffer, state.pastes);
@@ -605,16 +606,6 @@ export function PromptInput({ cols: colsProp, cursorVisible = true, onInteractio
605
606
  // The overflow indicator rides the LAST rendered row rather than
606
607
  // taking one of its own: an extra row would put the composer's
607
608
  // real height one above the `draftRows` it just reported (I-8).
608
- _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { children: node }) }), _jsx(Box, { flexShrink: 0, width: overflow.cells, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: overflow.text }) })] }, i)) : (_jsx(Text, { children: node }, i)))) }), chip && (
609
- // `alignSelf="flex-start"` pins the chip to the FIRST row of a multi-line
610
- // draft. Yoga's default `stretch` sizes this box to the full height of the
611
- // editor; it renders on the top row either way today, and relying on that
612
- // is how a later `justifyContent` edit moves it without failing anything.
613
- //
614
- // `width={chip.cells}` is a FIXED cell, not the chip's natural width
615
- // (P2-4). The chip is the only thing on this row whose width changes while
616
- // the user is typing — `↓ 9` becomes `↓ 10` becomes `↓ 100` — and every
617
- // change would otherwise re-wrap a draft that fills the line.
618
- _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 }) }))] }));
609
+ _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { children: node }) }), _jsx(Box, { flexShrink: 0, width: overflow.cells, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: overflow.text }) })] }, i)) : (_jsx(Text, { children: node }, i)))) })] }));
619
610
  return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [popupVisible && (_jsx(AutocompletePopup, { items: popupItems, selected: clampedSel, theme: theme, caps: caps, maxRows: popupMaxRows, layout: popupLayout })), (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: borderColor, paddingX: 1, children: inputRow }))] }));
620
611
  }
@@ -7,7 +7,13 @@ import type { ContextUsageSnapshot } from '../compaction/types.js';
7
7
  import type { AgentMode } from '../agent/agent-mode.js';
8
8
  import type { PendingSteering } from '../agent/queued-messages.js';
9
9
  import { type RunPhase } from './layout/status-layout.js';
10
+ import type { EscapeAction, StatusFeedback } from './status-feedback.js';
10
11
  export interface StatusBarProps {
12
+ columns: number;
13
+ speedKnown: boolean;
14
+ feedback?: StatusFeedback;
15
+ escapeAction?: EscapeAction;
16
+ interruptPhase?: 'ready' | 'armed' | 'stopping';
11
17
  runPhase?: RunPhase;
12
18
  activeTool?: {
13
19
  name: string;
@@ -1,24 +1,25 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** A single row of complete, measured status fields. */
3
3
  import React from 'react';
4
- import { Box, Text, useStdout } from 'ink';
4
+ import { Box, Text } from 'ink';
5
5
  import { pickGlyphs } from './glyphs.js';
6
6
  import { liveSpinner } from './ActivityLine.js';
7
- import { planStatusFields } from './layout/status-layout.js';
7
+ import { planPrimaryStatusFields } from './layout/status-layout.js';
8
8
  /** An invisible leading byte change forces Ink to repaint on Ctrl+L. */
9
9
  const redrawChar = (nonce) => nonce % 2 ? String.fromCharCode(0x00a0) : ' ';
10
10
  export function StatusBar(props) {
11
- const { stdout } = useStdout();
12
- const cols = stdout?.columns ?? 80;
11
+ const cols = props.columns;
13
12
  const phase = props.runPhase ?? (props.status === 'running' ? 'waiting' : 'idle');
14
13
  const active = phase !== 'idle' || !!props.compactionActive?.inFlight;
15
14
  const glyph = pickGlyphs(props.caps).spinnerStill;
16
15
  const spinner = active ? liveSpinner(props.reducedMotion ?? false, props.caps) : null;
17
- const fields = planStatusFields({
16
+ const plan = planPrimaryStatusFields({
18
17
  columns: cols, phase, pendingCount: props.pendingSteering?.length ?? 0,
19
18
  context: props.context, usageTotal: props.usageTotal,
20
19
  thinkingLevel: props.thinkingLevel, elapsedMs: props.elapsedMs, tokPerSec: props.tokPerSec,
21
- activeTool: props.activeTool, runOutcome: props.runOutcome, spinner: active ? glyph : '',
20
+ activeTool: props.activeTool, runOutcome: props.runOutcome, spinner: glyph,
21
+ speedKnown: props.speedKnown, feedback: props.feedback, escapeAction: props.escapeAction,
22
+ interruptPhase: props.interruptPhase,
22
23
  waitingForConfirmation: props.waitingForConfirmation, stopping: props.stopping,
23
24
  compacting: props.compactionActive?.inFlight, retryActive: props.retryActive,
24
25
  mode: props.agentMode, pendingMode: props.pendingAgentMode, servicesActive: props.servicesActive,
@@ -27,7 +28,7 @@ export function StatusBar(props) {
27
28
  });
28
29
  const tones = { normal: props.theme.primary, muted: props.theme.muted,
29
30
  warning: props.theme.noticeWarn, error: props.theme.noticeError };
30
- return _jsxs(Box, { width: cols, height: 1, flexShrink: 0, flexDirection: "row", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(props.redrawNonce ?? 0) }) }), fields.map((item, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width: 3, flexShrink: 0, children: _jsx(Text, { color: props.theme.muted, children: " | " }) }), _jsx(Box, { width: item.cells, flexShrink: 0, children: _jsx(Text, { color: tones[item.tone], children: item.id === 'phase' && active
31
+ return _jsxs(Box, { width: cols, height: 1, flexShrink: 0, flexDirection: "row", overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(props.redrawNonce ?? 0) }) }), plan.fields.map((item, index) => _jsxs(React.Fragment, { children: [index > 0 && _jsx(Box, { width: plan.separatorCells, flexShrink: 0, children: _jsx(Text, { color: props.theme.muted, children: plan.separator }) }), _jsx(Box, { width: item.cells, flexShrink: 0, children: _jsx(Text, { color: tones[item.tone], wrap: "truncate", children: item.id === 'phase' && active
31
32
  ? _jsxs(_Fragment, { children: [spinner ?? glyph, item.text.slice(glyph.length)] })
32
33
  : item.text }) })] }, item.id))] });
33
34
  }
@@ -98,7 +98,7 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
98
98
  case 'fast':
99
99
  return frame(entry.status === 'failed' ? glyphs.toolError : glyphs.teamAgent, fastCardColor(entry.status, theme), _jsx(FastCard, { reviewIndex: entry.reviewIndex, model: entry.model, status: entry.status, text: entry.text, detail: entry.detail, turn: entry.turn, durationMs: entry.durationMs, live: entry.live, reducedMotion: reducedMotion, theme: theme, caps: caps }));
100
100
  case 'compaction':
101
- return frame(entry.applied || entry.live ? glyphs.compaction : glyphs.toolError, compactionCardColor(entry, theme), _jsx(CompactionCard, { index: entry.index, trigger: entry.trigger, mode: entry.mode, applied: entry.applied, reason: entry.reason, messagesBefore: entry.messagesBefore, messagesAfter: entry.messagesAfter, tokensBefore: entry.tokensBefore, tokensAfter: entry.tokensAfter, summary: entry.summary, model: entry.model, durationMs: entry.durationMs, tailRelief: entry.tailRelief,
101
+ return frame(entry.applied || entry.live ? glyphs.compaction : glyphs.toolError, compactionCardColor(entry, theme), _jsx(CompactionCard, { index: entry.index, decision: entry.decision, memoryVersion: entry.memoryVersion, trigger: entry.trigger, mode: entry.mode, applied: entry.applied, reason: entry.reason, messagesBefore: entry.messagesBefore, messagesAfter: entry.messagesAfter, tokensBefore: entry.tokensBefore, tokensAfter: entry.tokensAfter, summary: entry.summary, model: entry.model, durationMs: entry.durationMs, tailRelief: entry.tailRelief,
102
102
  // ONLY WHILE LIVE, and only when the clock has ticked at least once:
103
103
  // `nowSec` is `undefined` until then, and a settled card must keep
104
104
  // rendering `durationMs` rather than a figure that would drift.
@@ -22,8 +22,10 @@
22
22
  import React from 'react';
23
23
  import type { Theme } from '../theme.js';
24
24
  import type { TermCapabilities } from '../capabilities.js';
25
- import type { CompactionMode, CompactionUiTrigger } from '../../compaction/types.js';
25
+ import type { CompactionDecision, CompactionMode, CompactionUiTrigger } from '../../compaction/types.js';
26
26
  export interface CompactionCardProps {
27
+ decision?: CompactionDecision;
28
+ memoryVersion?: 2;
27
29
  index: number;
28
30
  trigger: CompactionUiTrigger;
29
31
  mode: CompactionMode;
@@ -59,7 +59,7 @@ function headline(props, arrow, dot) {
59
59
  ? `compacting context #${index}${dot}${props.model}${elapsed}`
60
60
  : `compacting context #${index}${elapsed}`;
61
61
  }
62
- if (!props.applied) {
62
+ if (!props.applied || props.tokensAfter >= props.tokensBefore) {
63
63
  const why = props.reason ? `: ${props.reason}` : '';
64
64
  return `context not compacted #${index}${why}`;
65
65
  }
@@ -90,8 +90,8 @@ function detailLine(props) {
90
90
  return '';
91
91
  return `summarizing with ${props.model} - esc to cancel`;
92
92
  }
93
- if (!props.applied)
94
- return props.mode === 'none' && !props.reason ? 'nothing was changed' : '';
93
+ if (!props.applied || props.tokensAfter >= props.tokensBefore)
94
+ return 'History preserved.';
95
95
  const dropped = Math.max(0, props.messagesBefore - props.messagesAfter);
96
96
  if (props.mode === 'relieved') {
97
97
  const n = props.tailRelief?.messages ?? 0;
@@ -138,7 +138,8 @@ function CompactionCardImpl(props) {
138
138
  const head = headline(props, glyphs.arrowRight, dot);
139
139
  const detail = detailLine(props);
140
140
  const relief = reliefLine(props);
141
- const rows = summary ? summaryRows(summary, expanded === true) : [];
141
+ const showMemory = props.memoryVersion !== 2 || expanded === true;
142
+ const rows = summary && showMemory ? summaryRows(summary, expanded === true) : [];
142
143
  const totalRows = summary ? summary.split('\n').filter((l) => l.trim().length > 0).length : 0;
143
144
  const hidden = Math.max(0, totalRows - rows.length);
144
145
  // COLOUR TRACKS `live`, MOTION TRACKS `animate` — one headline, two separate
@@ -147,7 +148,8 @@ function CompactionCardImpl(props) {
147
148
  // every run, and folding the two together would grey out every live card for
148
149
  // the whole run, losing the status colour along with the motion.
149
150
  const animate = live && reducedMotion !== true && caps.unicode;
150
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "compact" }), _jsxs(Text, { color: live ? color : theme.muted, wrap: "truncate", children: [' ', animate ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), ' '] })) : live ? (`${glyphs.spinnerStill} `) : (''), head] })] }), detail.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: color, wrap: "truncate", children: detail }) })), relief.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: theme.noticeWarn, children: relief }) })), rows.length > 0 && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [rows.map((line, index) => (_jsx(Text, { color: theme.primary, children: line }, index))), hidden > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " +", hidden, " more lines (ctrl+o to expand)"] })), hidden === 0 && expanded === true && totalRows > COMPACTION_LIMITS.cardTextRows && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " ctrl+o to collapse"] }))] }))] }));
151
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.accent, bold: true, children: props.trigger === 'manual' ? 'Manual compaction' : props.trigger === 'overflow'
152
+ ? 'Overflow: threshold reached' : 'Automatic: threshold reached' }), _jsxs(Text, { color: live ? color : theme.muted, wrap: caps.unicode ? 'truncate' : 'wrap', children: [animate ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), ' '] })) : live ? (`${glyphs.spinnerStill} `) : (''), head] })] }), props.memoryVersion === 2 && (_jsxs(Text, { color: theme.muted, children: ["Task memory v2", expanded ? '' : ' (ctrl+o to expand)'] })), detail.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: color, wrap: caps.unicode ? 'truncate' : 'wrap', children: detail }) })), relief.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: theme.noticeWarn, children: relief }) })), rows.length > 0 && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [rows.map((line, index) => (_jsx(Text, { color: theme.primary, children: line }, index))), hidden > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " +", hidden, " more lines (ctrl+o to expand)"] })), hidden === 0 && expanded === true && totalRows > COMPACTION_LIMITS.cardTextRows && (_jsxs(Text, { color: theme.muted, children: [glyphs.railEnd, " ctrl+o to collapse"] }))] }))] }));
151
153
  }
152
154
  /**
153
155
  * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
@@ -64,6 +64,18 @@ const ERASE_DOWN = `${CSI}J`;
64
64
  /** DEC private mode 2026 — begin/end synchronized update (F2, §3.3). */
65
65
  const SYNC_BEGIN = `${CSI}?2026h`;
66
66
  const SYNC_END = `${CSI}?2026l`;
67
+ /**
68
+ * `ansiEscapes.clearTerminal` — Ink's tall-frame write (`ink.js:121`).
69
+ *
70
+ * When `outputHeight >= stdout.rows` Ink abandons log-update and writes
71
+ * `clearTerminal + frame` in one chunk: no erase-lines prefix, no trailing
72
+ * `\n`. That happens on ANY height shrink, because Ink re-renders
73
+ * synchronously from its own `'resize'` handler while the React tree still
74
+ * holds the pre-resize height (`useTerminalSize` debounces 50 ms). Without
75
+ * recognition the chunk lands in the prefix-less branch and counts as a
76
+ * foreign write — the same §5.6 notice, one ordinary window-drag later.
77
+ */
78
+ const CLEAR_TERMINAL = `${CSI}2J${CSI}3J${CSI}H`;
67
79
  function cursorTo(row) {
68
80
  return `${CSI}${row};1H`;
69
81
  }
@@ -294,6 +306,21 @@ export function createFrameDiffer(options) {
294
306
  return wrapSync(out.join(''));
295
307
  };
296
308
  const transform = (chunk) => {
309
+ // A+ · Ink's tall-frame write (see CLEAR_TERMINAL above): a shrink makes
310
+ // `outputHeight >= stdout.rows` while the tree still holds the old height,
311
+ // so Ink bypasses log-update and writes clearTerminal + frame in one chunk.
312
+ // The chunk paints the whole frame by itself, so it needs no parsing: pass
313
+ // it through, drop the cache (the screen it described is gone), and do NOT
314
+ // count it — this is Ink's own write at a moment absolute addressing cannot
315
+ // cover, not something behind Ink's back. The next log-update frame lands
316
+ // on an empty cache and is a full repaint, exactly what a cleared screen
317
+ // needs. Checked BEFORE the seed bookkeeping — and it must be, because the
318
+ // tall-frame write bypasses `this.log`: log-update's own
319
+ // `previousLineCount` stays 0, so the first log-update frame after it still
320
+ // carries no erase prefix. THAT write is the session's genuine seed frame
321
+ // (P1-1), and a control write arriving first must not spend its exemption.
322
+ if (chunk.startsWith(CLEAR_TERMINAL))
323
+ return passThrough(false);
297
324
  const firstChunk = !seenAnyChunk;
298
325
  seenAnyChunk = true;
299
326
  const rows = usableRows();
@@ -9,16 +9,25 @@ function summarizeFrame(chunk, rows, cols) {
9
9
  const unicode = pickGlyphs({ unicode: true, colorLevel: 0 });
10
10
  const cells = new Set(['|', '#', unicode.scrollTrack, unicode.scrollThumb]);
11
11
  const plain = lines.map((line) => stripAnsi(line));
12
- const track = plain.slice(1, rows - 3);
13
- if (plain.some((line) => stringWidth(line) > cols) ||
14
- !track.every((line) => stringWidth(line) === cols && cells.has(line.slice(-1))))
12
+ if (plain.some((line) => stringWidth(line) > cols))
15
13
  return null;
16
- return { columns: cols, rows, trackTop: 2, trackRows: rows - 4 };
14
+ let trackRows = 0;
15
+ // Fixed composer/status slots have no last-column rail. Discover the actual
16
+ // message track, then acknowledge it only against the published Yoga geometry.
17
+ for (const line of plain.slice(1)) {
18
+ if (stringWidth(line) !== cols || !cells.has(line.slice(-1)))
19
+ break;
20
+ trackRows++;
21
+ }
22
+ if (trackRows < 1)
23
+ return null;
24
+ return { columns: cols, rows, trackTop: 2, trackRows };
17
25
  }
18
26
  /** Observe before differ transformation, but acknowledge only after downstream write returns. */
19
27
  export function createFrameObserver(options) {
20
28
  const { stdout, terminal, scrollbar } = options;
21
29
  let summary = null;
30
+ let summaryRevision;
22
31
  let disposed = false;
23
32
  let resizeTimer = null;
24
33
  const invalidate = () => { summary = null; scrollbar.invalidate(); };
@@ -37,12 +46,16 @@ export function createFrameObserver(options) {
37
46
  };
38
47
  const match = () => {
39
48
  const g = scrollbar.geometry;
49
+ if (summary && g && summaryRevision === undefined)
50
+ summaryRevision = g.revision;
40
51
  scrollbar.frameReady = !!summary && !!g && summary.columns === terminal.columns &&
41
52
  summary.rows === terminal.rows && g.trackCol === summary.columns &&
42
- g.trackTop === summary.trackTop && g.trackRows === summary.trackRows;
53
+ g.trackTop === summary.trackTop && g.trackRows === summary.trackRows &&
54
+ g.revision === summaryRevision;
43
55
  };
44
56
  const observe = (chunk) => {
45
57
  summary = summarizeFrame(chunk, terminal.rows, terminal.columns);
58
+ summaryRevision = scrollbar.geometry?.revision;
46
59
  if (!summary) {
47
60
  invalidate();
48
61
  return;