@aragon-agent/cli 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/README.md +241 -18
  3. package/dist/agent/agent-mode.d.ts +39 -0
  4. package/dist/agent/agent-mode.js +36 -0
  5. package/dist/agent/controller.d.ts +74 -1
  6. package/dist/agent/controller.js +200 -1
  7. package/dist/agent/headless.d.ts +8 -0
  8. package/dist/agent/headless.js +47 -0
  9. package/dist/agent/reducer.d.ts +120 -0
  10. package/dist/agent/reducer.js +201 -2
  11. package/dist/cli.js +297 -6
  12. package/dist/commands/builtins.js +73 -23
  13. package/dist/commands/registry.d.ts +27 -0
  14. package/dist/compaction/archive.d.ts +175 -0
  15. package/dist/compaction/archive.js +267 -0
  16. package/dist/compaction/child.d.ts +97 -0
  17. package/dist/compaction/child.js +113 -0
  18. package/dist/compaction/command.d.ts +41 -0
  19. package/dist/compaction/command.js +342 -0
  20. package/dist/compaction/compactor.d.ts +268 -0
  21. package/dist/compaction/compactor.js +689 -0
  22. package/dist/compaction/digest.d.ts +62 -0
  23. package/dist/compaction/digest.js +164 -0
  24. package/dist/compaction/limits.d.ts +165 -0
  25. package/dist/compaction/limits.js +172 -0
  26. package/dist/compaction/pressure.d.ts +135 -0
  27. package/dist/compaction/pressure.js +167 -0
  28. package/dist/compaction/summarize-call.d.ts +70 -0
  29. package/dist/compaction/summarize-call.js +86 -0
  30. package/dist/compaction/summary-prompt.d.ts +85 -0
  31. package/dist/compaction/summary-prompt.js +197 -0
  32. package/dist/compaction/tail-budget.d.ts +52 -0
  33. package/dist/compaction/tail-budget.js +67 -0
  34. package/dist/compaction/types.d.ts +179 -0
  35. package/dist/compaction/types.js +22 -0
  36. package/dist/compaction/wiring.d.ts +255 -0
  37. package/dist/compaction/wiring.js +556 -0
  38. package/dist/config/app-paths.d.ts +14 -0
  39. package/dist/config/app-paths.js +16 -0
  40. package/dist/config/cli-commands.d.ts +41 -0
  41. package/dist/config/cli-commands.js +77 -1
  42. package/dist/config/env.d.ts +4 -1
  43. package/dist/config/env.js +92 -2
  44. package/dist/config/load.d.ts +31 -0
  45. package/dist/config/load.js +63 -1
  46. package/dist/config/schema.d.ts +159 -0
  47. package/dist/config/schema.js +137 -0
  48. package/dist/config/store.js +17 -1
  49. package/dist/config/ui-state.d.ts +44 -6
  50. package/dist/config/ui-state.js +21 -12
  51. package/dist/diagnostics/doctor.js +41 -1
  52. package/dist/exec/events.d.ts +50 -1
  53. package/dist/exec/index.js +8 -4
  54. package/dist/exec/runner.js +44 -2
  55. package/dist/input/mouse-events.d.ts +31 -5
  56. package/dist/input/mouse-events.js +86 -27
  57. package/dist/input/stdin-mouse-filter.d.ts +8 -3
  58. package/dist/logging/install.d.ts +51 -1
  59. package/dist/logging/install.js +28 -0
  60. package/dist/logging/logger.d.ts +1 -1
  61. package/dist/session/cli-commands.js +23 -1
  62. package/dist/session/persist.js +19 -0
  63. package/dist/session/store.d.ts +18 -0
  64. package/dist/session/store.js +59 -2
  65. package/dist/team/report.js +7 -1
  66. package/dist/team/runtime.d.ts +10 -0
  67. package/dist/team/runtime.js +7 -0
  68. package/dist/team/subagent.d.ts +31 -1
  69. package/dist/team/subagent.js +42 -15
  70. package/dist/team/types.d.ts +8 -0
  71. package/dist/ui/ActivityLine.d.ts +19 -1
  72. package/dist/ui/ActivityLine.js +10 -2
  73. package/dist/ui/App.d.ts +48 -1
  74. package/dist/ui/App.js +317 -17
  75. package/dist/ui/Composer.d.ts +26 -1
  76. package/dist/ui/Composer.js +6 -6
  77. package/dist/ui/PromptInput.d.ts +34 -1
  78. package/dist/ui/PromptInput.js +54 -3
  79. package/dist/ui/StatusBar.d.ts +40 -0
  80. package/dist/ui/StatusBar.js +13 -4
  81. package/dist/ui/TeamPanel.js +7 -1
  82. package/dist/ui/Transcript.d.ts +21 -1
  83. package/dist/ui/Transcript.js +32 -2
  84. package/dist/ui/clipboard.d.ts +52 -0
  85. package/dist/ui/clipboard.js +91 -0
  86. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  87. package/dist/ui/entries/CompactionCard.js +158 -0
  88. package/dist/ui/frame-differ.d.ts +46 -0
  89. package/dist/ui/frame-differ.js +94 -10
  90. package/dist/ui/gauge.d.ts +16 -1
  91. package/dist/ui/gauge.js +25 -6
  92. package/dist/ui/glyphs.d.ts +11 -0
  93. package/dist/ui/glyphs.js +2 -0
  94. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  95. package/dist/ui/layout/ScrollViewport.js +128 -21
  96. package/dist/ui/layout/budget.d.ts +10 -3
  97. package/dist/ui/layout/budget.js +10 -3
  98. package/dist/ui/layout/follow-state.d.ts +137 -0
  99. package/dist/ui/layout/follow-state.js +150 -0
  100. package/dist/ui/layout/virtual-window.d.ts +11 -0
  101. package/dist/ui/layout/virtual-window.js +34 -0
  102. package/dist/ui/overlays/HelpOverlay.js +18 -2
  103. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  104. package/dist/ui/overlays/SettingsScreen.js +60 -1
  105. package/dist/ui/palettes.d.ts +16 -0
  106. package/dist/ui/palettes.js +6 -0
  107. package/dist/ui/screen.d.ts +22 -0
  108. package/dist/ui/screen.js +70 -8
  109. package/dist/ui/selection/highlight.d.ts +39 -0
  110. package/dist/ui/selection/highlight.js +64 -0
  111. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  112. package/dist/ui/selection/screen-mirror.js +35 -0
  113. package/dist/ui/selection/selection-controller.d.ts +88 -0
  114. package/dist/ui/selection/selection-controller.js +282 -0
  115. package/dist/ui/selection/selection.d.ts +90 -0
  116. package/dist/ui/selection/selection.js +223 -0
  117. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  118. package/dist/ui/stdout-frame-writer.js +15 -0
  119. package/dist/ui/theme.d.ts +11 -0
  120. package/dist/ui/theme.js +2 -0
  121. package/dist/ui/transcript-text.js +23 -0
  122. package/dist/ui/use-startup-notices.d.ts +62 -5
  123. package/dist/ui/use-startup-notices.js +81 -17
  124. package/dist/ui/use-wheel-routing.js +8 -0
  125. package/dist/ui/win-vt-force.d.ts +136 -0
  126. package/dist/ui/win-vt-force.js +237 -0
  127. package/package.json +4 -2
@@ -21,6 +21,7 @@
21
21
  import React from 'react';
22
22
  import type { Theme } from './theme.js';
23
23
  import type { TermCapabilities } from './capabilities.js';
24
+ import { pickGlyphs } from './glyphs.js';
24
25
  import { type EditIntent } from '../input/keymap.js';
25
26
  import { type Suggestion } from './AutocompletePopup.js';
26
27
  import type { AgentMode } from '../agent/agent-mode.js';
@@ -65,7 +66,39 @@ interface PromptInputProps {
65
66
  };
66
67
  /** Fires only when the draft flips empty ⇄ non-empty, for the border color. */
67
68
  onDraftChange?: (hasDraft: boolean) => void;
69
+ /**
70
+ * Rows of newer output hidden BELOW the viewport — `ScrollViewport`'s offset
71
+ * (tui-selection-and-scroll-follow §4.2 / G7).
72
+ *
73
+ * Rendered as a right-aligned chip ON THE INPUT ROW, INSIDE THE BORDER, which
74
+ * is what the requirement asks for (输入框内部) and what makes the hint cost
75
+ * zero rows. It used to take the viewport's own last row, which spent a line of
76
+ * the user's content exactly when the transcript was longest AND fed the number
77
+ * back into its own derivation (§3.3).
78
+ *
79
+ * IT IS STATE, NOT A HINT. It deliberately ignores `showHint` / `hintsEnabled`
80
+ * — those govern the teaching row BELOW the box, which fades with experience
81
+ * and is dropped on a short terminal. "You are 12 rows behind the newest
82
+ * output" must survive both (T-23).
83
+ *
84
+ * Only meaningful in full-screen mode, so it renders only when `bordered` is
85
+ * set: inline mode has no self-drawn viewport and this is always 0 there.
86
+ */
87
+ scrolledLines?: number;
68
88
  }
89
+ export interface ScrollChip {
90
+ text: string;
91
+ /** FIXED width, so the number gaining a digit cannot re-wrap the draft (P2-4). */
92
+ cells: number;
93
+ }
94
+ /**
95
+ * Build the chip, or `null` when the viewport is pinned to the newest output.
96
+ *
97
+ * Every glyph comes from `pickGlyphs` and none from a literal (I-5): on a legacy
98
+ * console this renders `v12` rather than mojibake, and `glyphs.test.ts`'s static
99
+ * scan fails the build if that rule is broken here.
100
+ */
101
+ export declare function scrollChip(scrolledLines: number, cols: number, glyphs: ReturnType<typeof pickGlyphs>): ScrollChip | null;
69
102
  /** Apply an edit intent as a pure buffer/cursor transform. */
70
103
  export declare function applyEdit(buffer: string, cursor: number, intent: EditIntent): {
71
104
  buffer: string;
@@ -94,5 +127,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
94
127
  end: number;
95
128
  query: string;
96
129
  } | null;
97
- export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }: PromptInputProps): React.ReactElement;
130
+ export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, scrolledLines, }: PromptInputProps): React.ReactElement;
98
131
  export {};
@@ -20,13 +20,49 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
20
20
  * rather than from a key.
21
21
  */
22
22
  import { useEffect, useReducer, useRef, useState } from 'react';
23
- import { Box, Text, useInput } from 'ink';
23
+ import { Box, Text, useInput, useStdout } from 'ink';
24
24
  import { glob as tinyGlob } from 'tinyglobby';
25
25
  import { pickGlyphs } from './glyphs.js';
26
26
  import { recognize } from '../input/keymap.js';
27
27
  import { AutocompletePopup } from './AutocompletePopup.js';
28
28
  import { editorReducer, INITIAL_EDITOR_STATE } from './editor-reducer.js';
29
29
  // ---------------------------------------------------------------------------
30
+ // The scroll chip (§4.2)
31
+ // ---------------------------------------------------------------------------
32
+ /** Below this the full sentence does not fit beside a draft; the chip goes terse. */
33
+ const CHIP_FULL_MIN_COLS = 64;
34
+ /**
35
+ * Ceiling on the number the chip prints, so `chipCells` is a CONSTANT per branch
36
+ * rather than a function of how far behind the user is (P2-4). Past it the chip
37
+ * reads `999+`.
38
+ */
39
+ const CHIP_MAX_N = 999;
40
+ /** `↓ 999+ new lines · PgDn` — the widest form the full branch can produce. */
41
+ const CHIP_FULL_CELLS = 24;
42
+ /** `↓999+` — the widest form the terse branch can produce. */
43
+ const CHIP_TERSE_CELLS = 6;
44
+ /**
45
+ * Build the chip, or `null` when the viewport is pinned to the newest output.
46
+ *
47
+ * Every glyph comes from `pickGlyphs` and none from a literal (I-5): on a legacy
48
+ * console this renders `v12` rather than mojibake, and `glyphs.test.ts`'s static
49
+ * scan fails the build if that rule is broken here.
50
+ */
51
+ export function scrollChip(scrolledLines, cols, glyphs) {
52
+ if (!Number.isFinite(scrolledLines) || scrolledLines <= 0)
53
+ return null;
54
+ const n = Math.floor(scrolledLines);
55
+ const shown = n > CHIP_MAX_N ? `${CHIP_MAX_N}+` : String(n);
56
+ if (cols < CHIP_FULL_MIN_COLS) {
57
+ return { text: `${glyphs.arrowDown}${shown}`, cells: CHIP_TERSE_CELLS };
58
+ }
59
+ const noun = n === 1 ? 'line' : 'lines';
60
+ return {
61
+ text: `${glyphs.arrowDown} ${shown} new ${noun} ${glyphs.midDot} PgDn`,
62
+ cells: CHIP_FULL_CELLS,
63
+ };
64
+ }
65
+ // ---------------------------------------------------------------------------
30
66
  // Pure editing helpers (unit-tested in input.test.ts)
31
67
  // ---------------------------------------------------------------------------
32
68
  const isSpace = (ch) => !!ch && /\s/.test(ch);
@@ -138,7 +174,8 @@ function isControlSeq(input) {
138
174
  // ---------------------------------------------------------------------------
139
175
  // Component
140
176
  // ---------------------------------------------------------------------------
141
- export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }) {
177
+ export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, scrolledLines = 0, }) {
178
+ const { stdout } = useStdout();
142
179
  const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
143
180
  const { buffer, cursor, historyIndex, dismissed, sel } = editor;
144
181
  const [fileMatches, setFileMatches] = useState([]);
@@ -358,7 +395,21 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
358
395
  ? `Describe what you want to build; I'll research and plan it first${glyphs.ellipsis}`
359
396
  : `Send a message (/ for commands, @ for files)${glyphs.ellipsis}`;
360
397
  const lines = renderWithCursor(buffer, cursor, isActive);
361
- const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", children: buffer.length === 0 ? (_jsx(Text, { color: theme.muted, children: placeholder })) : (lines.map((line, i) => _jsx(Text, { children: line }, i))) })] }));
398
+ // Full-screen only: inline mode has no self-drawn viewport, so `scrolledLines`
399
+ // is always 0 there and the branch is unreachable rather than merely unused.
400
+ // The guard is explicit so it stays that way.
401
+ const chip = bordered ? scrollChip(scrolledLines, stdout?.columns ?? 80, glyphs) : null;
402
+ 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 })) : (lines.map((line, i) => _jsx(Text, { children: line }, i))) }), chip && (
403
+ // `alignSelf="flex-start"` pins the chip to the FIRST row of a multi-line
404
+ // draft. Yoga's default `stretch` sizes this box to the full height of the
405
+ // editor; it renders on the top row either way today, and relying on that
406
+ // is how a later `justifyContent` edit moves it without failing anything.
407
+ //
408
+ // `width={chip.cells}` is a FIXED cell, not the chip's natural width
409
+ // (P2-4). The chip is the only thing on this row whose width changes while
410
+ // the user is typing — `↓ 9` becomes `↓ 10` becomes `↓ 100` — and every
411
+ // change would otherwise re-wrap a draft that fills the line.
412
+ _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 }) }))] }));
362
413
  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)] }));
363
414
  }
364
415
  /** Split the buffer into lines and render an inverse cursor at `cursor`. */
@@ -138,6 +138,46 @@ interface StatusBarProps {
138
138
  fastActive?: {
139
139
  inFlight: boolean;
140
140
  };
141
+ /**
142
+ * Live context compaction (context-auto-compaction §6.2), or absent when it is
143
+ * off for this session — in which case an ordinary session's bar is unchanged.
144
+ *
145
+ * RIGHT CLUSTER, next to `fast` and `eco`, and it DEGRADES below
146
+ * `COMPACTION_LIMITS.statusCompactCols`. The `teamActive` / `ecoRung` /
147
+ * `fastActive` treatment rather than the `agentMode` one, for the reason the
148
+ * note on `agentMode` gives: the mode must never be unreportable, whereas
149
+ * compaction has a fuller home in `/compact status` — the GUARANTEED reporting
150
+ * surface (§6.5) — and a chip on a 60-column terminal costs the context gauge
151
+ * more than it says.
152
+ *
153
+ * `inFlight` renders `compacting`, so a summarization that is taking a while is
154
+ * visible on this row without a spinner competing with the activity line's.
155
+ */
156
+ compactionActive?: {
157
+ inFlight: boolean;
158
+ };
159
+ /**
160
+ * Where the gauge turns amber and red (§6.2).
161
+ *
162
+ * ABSENT means today's hardcoded `{ warn: 60, high: 85 }`, so a session with
163
+ * compaction off renders a byte-identical bar. When present these are
164
+ * `warnThreshold * 100` and `threshold * 100`, which is what makes the colour
165
+ * the user sees agree with the number the trigger fires on.
166
+ */
167
+ gaugeMarks?: {
168
+ warn: number;
169
+ high: number;
170
+ };
171
+ /**
172
+ * Whether `contextTokens` is a DERIVED figure rather than a measured one.
173
+ *
174
+ * It WIDENS THE EXISTING `~`, it does not add a second marker. Today that
175
+ * prefix means "the window is a guess" (`contextWindowKnown`); with compaction
176
+ * it also has to mean "the occupancy is a guess", because the bar is written
177
+ * from an estimate for one turn after every compaction so it can fall
178
+ * immediately (§6.2 / AC-5).
179
+ */
180
+ contextEstimated?: boolean;
141
181
  }
142
182
  export declare function StatusBar(props: StatusBarProps): React.ReactElement;
143
183
  export {};
@@ -6,6 +6,7 @@ import { buildGauge } from './gauge.js';
6
6
  import { MODE_LABEL } from '../agent/agent-mode.js';
7
7
  import { formatRetryChip } from '../agent/retry-view.js';
8
8
  import { FAST_LIMITS } from '../fast/limits.js';
9
+ import { COMPACTION_LIMITS } from '../compaction/limits.js';
9
10
  import { TEAM_LIMITS } from '../team/limits.js';
10
11
  import { TODO_LIMITS } from '../todo/limits.js';
11
12
  /** Below this the `eco` chip costs the context gauge more than it is worth. */
@@ -29,14 +30,22 @@ function redrawChar(nonce) {
29
30
  return nonce % 2 === 0 ? ' ' : NBSP;
30
31
  }
31
32
  export function StatusBar(props) {
32
- const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, retryActive, ecoRung = 0, fastActive, } = props;
33
+ const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, retryActive, ecoRung = 0, fastActive, compactionActive, gaugeMarks, contextEstimated = false, } = props;
33
34
  const { stdout } = useStdout();
34
35
  const cols = stdout?.columns ?? 80;
35
36
  const running = status === 'running';
36
37
  const glyphs = pickGlyphs(caps);
37
38
  const pct = contextWindow > 0 ? Math.min(100, Math.round((contextTokens / contextWindow) * 100)) : 0;
38
- const gauge = buildGauge(pct, cols < 72 ? 8 : 12, theme, caps);
39
- const pctLabel = `${contextWindowKnown ? '' : '~'}${gauge.pct}%`;
39
+ // `gaugeMarks` is spread rather than passed as `undefined`, so a session with
40
+ // compaction off takes `buildGauge`'s own default and the bar is byte-identical
41
+ // to the pre-feature build.
42
+ const gauge = gaugeMarks
43
+ ? buildGauge(pct, cols < 72 ? 8 : 12, theme, caps, gaugeMarks)
44
+ : buildGauge(pct, cols < 72 ? 8 : 12, theme, caps);
45
+ // ONE `~` FOR TWO KINDS OF APPROXIMATION (§6.2). A second marker would be a
46
+ // second thing to explain for a distinction the user does not act on
47
+ // differently: either way the number is a guess.
48
+ const pctLabel = `${contextWindowKnown && !contextEstimated ? '' : '~'}${gauge.pct}%`;
40
49
  const tokens = `${formatTokens(usageTotal.inputTokens)}${glyphs.arrowUp} ${formatTokens(usageTotal.outputTokens)}${glyphs.arrowDown}`;
41
50
  const statusGlyph = running ? theme.symbols.toolRunning : theme.symbols.toolPending;
42
51
  // Nothing at all in build mode with nothing pending: the default posture is
@@ -57,5 +66,5 @@ export function StatusBar(props) {
57
66
  ? `agents ${teamActive.running}/${teamActive.total}`
58
67
  : `[${teamActive.running}]`] })), retryActive && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', formatRetryChip(retryActive, cols)] })), todoActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= TODO_LIMITS.statusCompactCols
59
68
  ? `todo ${todoActive.done}/${todoActive.total}`
60
- : `[${todoActive.done}/${todoActive.total}]`] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [fastActive && cols >= FAST_LIMITS.statusCompactCols && (_jsxs(Text, { color: theme.muted, children: ["fast", fastActive.inFlight ? '*' : '', ' '] })), ecoRung > 0 && cols >= ECO_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: ["eco", ' '] })), scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= 72 && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
69
+ : `[${todoActive.done}/${todoActive.total}]`] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [compactionActive && cols >= COMPACTION_LIMITS.statusCompactCols && (_jsxs(Text, { color: compactionActive.inFlight ? theme.accent : theme.muted, children: [compactionActive.inFlight ? 'compacting' : 'compact', ' '] })), fastActive && cols >= FAST_LIMITS.statusCompactCols && (_jsxs(Text, { color: theme.muted, children: ["fast", fastActive.inFlight ? '*' : '', ' '] })), ecoRung > 0 && cols >= ECO_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: ["eco", ' '] })), scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= 72 && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
61
70
  }
@@ -88,7 +88,13 @@ export function activityLine(run, cols) {
88
88
  ? 'waiting for mail'
89
89
  : 'tool';
90
90
  case 'done':
91
- return `done ${run.turns} turns`;
91
+ // `compacted N` ON THE SETTLED ROW ONLY (hardening §3.4.4 / W3). A running
92
+ // row is already competing for `activityBudget(cols)` with the child's own
93
+ // prose, and a marker that displaces what the child is DOING right now
94
+ // buys nothing - the dispatch report carries the same fact for good.
95
+ return run.compactions
96
+ ? `done ${run.turns} turns compacted ${run.compactions}`
97
+ : `done ${run.turns} turns`;
92
98
  case 'failed':
93
99
  return 'failed';
94
100
  case 'aborted':
@@ -31,6 +31,7 @@ import type { TermCapabilities } from './capabilities.js';
31
31
  import { type DensityMode } from './density.js';
32
32
  import type { Entry } from '../agent/reducer.js';
33
33
  import type { RenderMode } from './layout/frame.js';
34
+ import { type TailSink } from './layout/follow-state.js';
34
35
  import type { HeightStore } from './use-height-store.js';
35
36
  /**
36
37
  * Largest prefix length P such that `entries[0..P)` are all terminal, none is
@@ -119,7 +120,7 @@ export declare const EntryView: React.MemoExoticComponent<typeof EntryViewImpl>;
119
120
  * `selectWindow`, and the two spacers are what keep the scroll maths honest
120
121
  * while they are absent.
121
122
  */
122
- export declare function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, }: TranscriptProps & {
123
+ export declare function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, tailSink, }: TranscriptProps & {
123
124
  windowSize: number;
124
125
  /** The viewport's own width — narrower than the terminal when the rail is up. */
125
126
  cols: number;
@@ -132,6 +133,25 @@ export declare function TranscriptList({ entries, expandedToolIds, thinkingVisib
132
133
  mountedSink?: {
133
134
  current: number;
134
135
  };
136
+ /**
137
+ * Rows appended AT THE TAIL, published for `ScrollViewport`'s Rule A
138
+ * (tui-selection-and-scroll-follow §4.3.1a). THIS IS P0-1's FIX.
139
+ *
140
+ * It lives here because this is the only place that holds the per-entry height
141
+ * table, and therefore the only place that can tell "the agent produced output"
142
+ * apart from "an entry above changed height" or "the horizon dropped an entry
143
+ * off the front". Reacting to `measureElement(inner).height` instead conflates
144
+ * all three, breaks `virtual-window.ts`'s invariant V-4, and closes an
145
+ * offset → mounted-set → measured-height → offset loop.
146
+ *
147
+ * A REF WRITTEN DURING RENDER, exactly as `mountedSink` above is and for the
148
+ * same reason: a `setState` here would make observing the tail cost a render.
149
+ * If this sink ever needs more than that — a `setState`, or the geometry
150
+ * context — STOP AND RE-REVIEW: the loop above is open again, and the correct
151
+ * response is full entry anchoring (D-13's rejected alternative), not a larger
152
+ * sink.
153
+ */
154
+ tailSink?: TailSink;
135
155
  }): React.ReactElement;
136
156
  export declare function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, }: TranscriptProps): React.ReactElement;
137
157
  export {};
@@ -39,9 +39,11 @@ import { TeamCard, teamCardColor } from './entries/TeamCard.js';
39
39
  import { TodoCard, todoCardColor } from './entries/TodoCard.js';
40
40
  import { RetryCard, retryCardColor } from './entries/RetryCard.js';
41
41
  import { FastCard, fastCardColor } from './entries/FastCard.js';
42
+ import { CompactionCard, compactionCardColor } from './entries/CompactionCard.js';
42
43
  import { MeasuredEntry } from './layout/MeasuredEntry.js';
43
44
  import { useViewportGeometry } from './layout/viewport-geometry.js';
44
45
  import { estimateEntryRows, heightKey, selectWindow, VIRTUAL_LIMITS, } from './layout/virtual-window.js';
46
+ import { advanceTailRows } from './layout/follow-state.js';
45
47
  import { useTerminalSize } from './layout/useTerminalSize.js';
46
48
  /** Keep the last K entries in the live region so a mutating tail stays hot. */
47
49
  const LIVE_TAIL = 1;
@@ -99,6 +101,12 @@ export function computeSettledCount(entries, expandedToolIds) {
99
101
  // other half.
100
102
  if (e.kind === 'fast' && e.live)
101
103
  break;
104
+ // Nor is a compaction card whose call is still open: it is rewritten once,
105
+ // when the engine reports its verdict, and `<Static>` cannot un-print what it
106
+ // has already drawn (C-8). `session/persist.ts` forces `live: false` on load,
107
+ // which is the other half.
108
+ if (e.kind === 'compaction' && e.live)
109
+ break;
102
110
  count = i + 1;
103
111
  }
104
112
  return count;
@@ -111,7 +119,14 @@ export function computeSettledCount(entries, expandedToolIds) {
111
119
  * comparator, on every frame, forever.
112
120
  */
113
121
  function nowSecFor(entry, nowSec) {
114
- return entry.kind === 'tool' && entry.status === 'running' ? nowSec : undefined;
122
+ if (entry.kind === 'tool' && entry.status === 'running')
123
+ return nowSec;
124
+ // THE LIVE COMPACTION CARD IS THE SECOND CONSUMER (hardening W5). It is at most
125
+ // ONE entry in the whole transcript, and it settles - so the memo boundary this
126
+ // function protects is unchanged for every other card, forever.
127
+ if (entry.kind === 'compaction' && entry.live)
128
+ return nowSec;
129
+ return undefined;
115
130
  }
116
131
  function noticeColor(level, theme) {
117
132
  if (level === 'error')
@@ -185,6 +200,14 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
185
200
  reducedMotion: reducedMotion, theme: theme, caps: caps }));
186
201
  case 'fast':
187
202
  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 }));
203
+ case 'compaction':
204
+ 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,
205
+ // ONLY WHILE LIVE, and only when the clock has ticked at least once:
206
+ // `nowSec` is `undefined` until then, and a settled card must keep
207
+ // rendering `durationMs` rather than a figure that would drift.
208
+ elapsedMs: entry.live && nowSec !== undefined && entry.startedAt !== undefined
209
+ ? Math.max(0, nowSec * 1000 - entry.startedAt)
210
+ : undefined, live: entry.live, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
188
211
  case 'notice':
189
212
  return frame(noticeSymbol(entry.level, theme), noticeColor(entry.level, theme), _jsx(Text, { color: noticeColor(entry.level, theme), children: entry.text }));
190
213
  default:
@@ -248,7 +271,7 @@ export const EntryView = React.memo(EntryViewImpl, (a, b) => {
248
271
  * `selectWindow`, and the two spacers are what keep the scroll maths honest
249
272
  * while they are absent.
250
273
  */
251
- export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, }) {
274
+ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, tailSink, }) {
252
275
  const size = Math.max(1, Math.floor(windowSize));
253
276
  const visible = entries.length > size ? entries.slice(-size) : entries;
254
277
  const collapsed = entries.length - visible.length;
@@ -291,6 +314,13 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
291
314
  const slice = visible.slice(selection.startIndex, selection.endIndex);
292
315
  if (mountedSink)
293
316
  mountedSink.current = slice.length;
317
+ // One more pass over the array `selectWindow` has already walked, reading the
318
+ // SAME height table and never the geometry context (§4.3.1a). `visible` is
319
+ // what is drawn, so its last entry is the tail; entries the horizon dropped are
320
+ // not "removed rows" and must not register as one.
321
+ if (tailSink) {
322
+ tailSink.current = advanceTailRows(tailSink.current, visible.map((entry) => entry.id), heightOf);
323
+ }
294
324
  return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [collapsed > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: [glyphs.ellipsis, " ", collapsed, " earlier ", collapsed === 1 ? 'entry' : 'entries', " collapsed (/save exports the full session)"] })), selection.leadingRows > 0 && (_jsx(Box, { flexShrink: 0, height: selection.leadingRows })), slice.map((entry, i) => {
295
325
  const index = selection.startIndex + i;
296
326
  return (_jsx(MeasuredEntry, { heightKey: keyOf(entry), onMeasure: report, children: _jsx(EntryView, { entry: entry, prev: index > 0 ? visible[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, nowSec: nowSecFor(entry, nowSec), theme: theme, caps: caps }) }, entry.id));
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Clipboard (tui-selection-and-scroll-follow §4.4.5) — the SINGLE clipboard path
3
+ * for the package. `/copy` and the selection release both come through here.
4
+ *
5
+ * Two mechanisms, both attempted, and the return value names the better one so
6
+ * the toast can say what was SENT rather than claiming a success neither one can
7
+ * confirm:
8
+ *
9
+ * 1. **OSC 52.** It reaches the clipboard of the machine the USER is sitting
10
+ * at, which is the only correct target over SSH, and it is the only mechanism
11
+ * that works with no helper binary installed. `/copy` gained SSH support the
12
+ * moment it moved onto this module.
13
+ * 2. **The platform binary** (`clip` / `pbcopy` / `xclip`), moved here unchanged
14
+ * from `commands/builtins.ts`, for the local case and for terminals that
15
+ * ignore OSC 52 (tmux without `set -g set-clipboard on`, R-7).
16
+ *
17
+ * IT TAKES A WRITE *DOOR*, NOT A STREAM, and that is P1-6 rather than taste. In
18
+ * full-screen mode stdout is a `Proxy` in front of the frame differ; an
19
+ * unrecognised chunk there takes `passThrough(true)`, which increments
20
+ * `fallbacks` and — on the first one — raises `onFirstFallback`, wired to
21
+ * `console.warn(FRAME_FALLBACK_NOTICE)`. Sending OSC 52 through the proxy would
22
+ * therefore print a diagnostic at a user who did nothing wrong the first time
23
+ * they copied anything, and would permanently corrupt a counter documented to
24
+ * mean "something wrote to stdout behind Ink's back". The caller supplies
25
+ * `writeForeign` instead, and this module cannot pick the wrong door.
26
+ */
27
+ export type CopyVia = 'osc52' | 'native' | 'none';
28
+ /**
29
+ * Ceiling on an OSC 52 payload, in bytes of the ORIGINAL text.
30
+ *
31
+ * xterm's default limit is around 100 000 BASE64 characters and tmux / screen
32
+ * are stricter, so 56 000 source bytes (~74 700 base64) stays inside all three
33
+ * with room to spare. Past it the sequence is skipped rather than truncated: a
34
+ * terminal that drops an over-long OSC 52 leaves the clipboard holding whatever
35
+ * was there before, and half a selection is worse than none.
36
+ */
37
+ export declare const MAX_OSC52_BYTES = 56000;
38
+ /** `\x1b]52;c;<base64>\x07` — `c` is the CLIPBOARD selection, not PRIMARY. */
39
+ export declare function osc52(text: string): string;
40
+ /**
41
+ * Put `text` on the clipboard and report the best mechanism that was reached.
42
+ *
43
+ * `write` is the foreign-write door (see the module header). Omit it and OSC 52
44
+ * is skipped entirely, which is the correct behaviour for a caller that has no
45
+ * terminal to write to.
46
+ *
47
+ * NEITHER MECHANISM IS DETECTABLE. `spawn` returning without throwing does not
48
+ * mean `xclip` exists, and an OSC 52 sequence a terminal ignores is
49
+ * indistinguishable from one it honours. The return value therefore names what
50
+ * was ATTEMPTED, and every caller's user-facing text is worded accordingly.
51
+ */
52
+ export declare function copyText(text: string, write?: (chunk: string) => void): CopyVia;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Clipboard (tui-selection-and-scroll-follow §4.4.5) — the SINGLE clipboard path
3
+ * for the package. `/copy` and the selection release both come through here.
4
+ *
5
+ * Two mechanisms, both attempted, and the return value names the better one so
6
+ * the toast can say what was SENT rather than claiming a success neither one can
7
+ * confirm:
8
+ *
9
+ * 1. **OSC 52.** It reaches the clipboard of the machine the USER is sitting
10
+ * at, which is the only correct target over SSH, and it is the only mechanism
11
+ * that works with no helper binary installed. `/copy` gained SSH support the
12
+ * moment it moved onto this module.
13
+ * 2. **The platform binary** (`clip` / `pbcopy` / `xclip`), moved here unchanged
14
+ * from `commands/builtins.ts`, for the local case and for terminals that
15
+ * ignore OSC 52 (tmux without `set -g set-clipboard on`, R-7).
16
+ *
17
+ * IT TAKES A WRITE *DOOR*, NOT A STREAM, and that is P1-6 rather than taste. In
18
+ * full-screen mode stdout is a `Proxy` in front of the frame differ; an
19
+ * unrecognised chunk there takes `passThrough(true)`, which increments
20
+ * `fallbacks` and — on the first one — raises `onFirstFallback`, wired to
21
+ * `console.warn(FRAME_FALLBACK_NOTICE)`. Sending OSC 52 through the proxy would
22
+ * therefore print a diagnostic at a user who did nothing wrong the first time
23
+ * they copied anything, and would permanently corrupt a counter documented to
24
+ * mean "something wrote to stdout behind Ink's back". The caller supplies
25
+ * `writeForeign` instead, and this module cannot pick the wrong door.
26
+ */
27
+ import { spawn } from 'node:child_process';
28
+ import process from 'node:process';
29
+ /**
30
+ * Ceiling on an OSC 52 payload, in bytes of the ORIGINAL text.
31
+ *
32
+ * xterm's default limit is around 100 000 BASE64 characters and tmux / screen
33
+ * are stricter, so 56 000 source bytes (~74 700 base64) stays inside all three
34
+ * with room to spare. Past it the sequence is skipped rather than truncated: a
35
+ * terminal that drops an over-long OSC 52 leaves the clipboard holding whatever
36
+ * was there before, and half a selection is worse than none.
37
+ */
38
+ export const MAX_OSC52_BYTES = 56_000;
39
+ /** `\x1b]52;c;<base64>\x07` — `c` is the CLIPBOARD selection, not PRIMARY. */
40
+ export function osc52(text) {
41
+ return `\x1b]52;c;${Buffer.from(text, 'utf-8').toString('base64')}\x07`;
42
+ }
43
+ /** Best-effort clipboard copy via the platform's clipboard CLI. */
44
+ function copyNative(text) {
45
+ const cmd = process.platform === 'win32'
46
+ ? { file: 'clip', args: [] }
47
+ : process.platform === 'darwin'
48
+ ? { file: 'pbcopy', args: [] }
49
+ : { file: 'xclip', args: ['-selection', 'clipboard'] };
50
+ try {
51
+ const child = spawn(cmd.file, cmd.args, { stdio: ['pipe', 'ignore', 'ignore'] });
52
+ child.on('error', () => {
53
+ /* swallow — clipboard is best-effort, and a missing xclip is ordinary */
54
+ });
55
+ child.stdin?.end(text);
56
+ return true;
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ /**
63
+ * Put `text` on the clipboard and report the best mechanism that was reached.
64
+ *
65
+ * `write` is the foreign-write door (see the module header). Omit it and OSC 52
66
+ * is skipped entirely, which is the correct behaviour for a caller that has no
67
+ * terminal to write to.
68
+ *
69
+ * NEITHER MECHANISM IS DETECTABLE. `spawn` returning without throwing does not
70
+ * mean `xclip` exists, and an OSC 52 sequence a terminal ignores is
71
+ * indistinguishable from one it honours. The return value therefore names what
72
+ * was ATTEMPTED, and every caller's user-facing text is worded accordingly.
73
+ */
74
+ export function copyText(text, write) {
75
+ if (text.length === 0)
76
+ return 'none';
77
+ let sentOsc52 = false;
78
+ if (write && Buffer.byteLength(text, 'utf-8') <= MAX_OSC52_BYTES) {
79
+ try {
80
+ write(osc52(text));
81
+ sentOsc52 = true;
82
+ }
83
+ catch {
84
+ // A closed or broken stdout must never turn a copy into a crash.
85
+ }
86
+ }
87
+ const sentNative = copyNative(text);
88
+ if (sentOsc52)
89
+ return 'osc52';
90
+ return sentNative ? 'native' : 'none';
91
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * CompactionCard — one context compaction in the transcript
3
+ * (context-auto-compaction §6.3).
4
+ *
5
+ * ASCII ONLY: every glyph comes from `pickGlyphs`, INCLUDING THE ARROW. The mock
6
+ * in the design shows `112 -> 9 messages`; that arrow is `glyphs.arrowRight`, the
7
+ * one `StatusBar` already uses, not a literal — `ui/` is inside
8
+ * `glyphs.test.ts::inScope`, so a literal here fails the scan (P2-7).
9
+ *
10
+ * EXPANDABLE, UNLIKE `FastCard`. A critique is already clamped to
11
+ * `fast.reviewMaxChars` before it reaches its card, so there is nothing hidden
12
+ * for Ctrl+O to reveal; a summary is up to `summaryMaxChars` and there genuinely
13
+ * is more to see. That is why `App`'s Ctrl+O predicate had to learn about this
14
+ * kind — without it the card renders a hint that does nothing and expands an
15
+ * older tool card instead (C-15 / P1-8 / AC-21).
16
+ *
17
+ * EVERY STRING DESCRIBES THE HISTORY, NEVER THE SCREEN (§6.4 / P2-9). `/clear`
18
+ * empties the transcript view but not `messages`, so this card can legitimately
19
+ * report "112 -> 9 messages" with nine visible rows above it. Nothing here says
20
+ * "above" or "earlier in this transcript".
21
+ */
22
+ import React from 'react';
23
+ import type { Theme } from '../theme.js';
24
+ import type { TermCapabilities } from '../capabilities.js';
25
+ import type { CompactionMode, CompactionUiTrigger } from '../../compaction/types.js';
26
+ export interface CompactionCardProps {
27
+ index: number;
28
+ trigger: CompactionUiTrigger;
29
+ mode: CompactionMode;
30
+ applied: boolean;
31
+ reason?: string;
32
+ messagesBefore: number;
33
+ messagesAfter: number;
34
+ tokensBefore: number;
35
+ tokensAfter: number;
36
+ summary?: string;
37
+ model: string;
38
+ durationMs?: number;
39
+ /**
40
+ * Wall-clock since the card opened, LIVE ONLY
41
+ * (context-auto-compaction-hardening §3.6 / W5).
42
+ *
43
+ * Rendered in the position `durationMs` occupies once settled, so the number
44
+ * does not move when the card settles. Only the single live card receives it,
45
+ * so exactly one memoized card re-renders per second.
46
+ */
47
+ elapsedMs?: number;
48
+ /** Set when tail relief clipped the retained turns (W2). */
49
+ tailRelief?: {
50
+ messages: number;
51
+ charsRemoved: number;
52
+ };
53
+ live: boolean;
54
+ expanded?: boolean;
55
+ reducedMotion?: boolean;
56
+ theme: Theme;
57
+ caps: TermCapabilities;
58
+ }
59
+ /**
60
+ * The card's colour.
61
+ *
62
+ * `truncated` is a WARNING rather than the accent, because it is a real and
63
+ * bounded data loss the user is entitled to notice; `applied: false` is an error
64
+ * because the run is still at the occupancy that triggered it.
65
+ */
66
+ export declare function compactionCardColor(props: Pick<CompactionCardProps, 'live' | 'applied' | 'mode'>, theme: Theme): string | undefined;
67
+ declare function CompactionCardImpl(props: CompactionCardProps): React.ReactElement;
68
+ /**
69
+ * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
70
+ * `mapEntry` already preserves object identity for untouched entries, so the
71
+ * props below are stable references on a settled card, and `theme` / `caps` are
72
+ * `useMemo`d in `App.tsx` (I-L2-1).
73
+ */
74
+ export declare const CompactionCard: React.MemoExoticComponent<typeof CompactionCardImpl>;
75
+ export {};