@aragon-agent/cli 0.6.2 → 0.6.4

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 (167) hide show
  1. package/CHANGELOG.md +321 -0
  2. package/README.md +297 -2
  3. package/dist/agent/controller.d.ts +193 -1
  4. package/dist/agent/controller.js +475 -2
  5. package/dist/agent/headless.d.ts +8 -0
  6. package/dist/agent/headless.js +47 -0
  7. package/dist/agent/reducer.d.ts +189 -0
  8. package/dist/agent/reducer.js +301 -2
  9. package/dist/agent/system-prompt.d.ts +14 -0
  10. package/dist/agent/system-prompt.js +2 -0
  11. package/dist/cli.js +297 -16
  12. package/dist/commands/builtins.js +165 -24
  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 +118 -0
  18. package/dist/compaction/command.d.ts +41 -0
  19. package/dist/compaction/command.js +355 -0
  20. package/dist/compaction/compactor.d.ts +302 -0
  21. package/dist/compaction/compactor.js +810 -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 +215 -0
  35. package/dist/compaction/types.js +22 -0
  36. package/dist/compaction/wiring.d.ts +264 -0
  37. package/dist/compaction/wiring.js +577 -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 +5 -2
  43. package/dist/config/env.js +140 -3
  44. package/dist/config/load.d.ts +42 -0
  45. package/dist/config/load.js +101 -1
  46. package/dist/config/schema.d.ts +244 -0
  47. package/dist/config/schema.js +170 -0
  48. package/dist/config/store.js +37 -1
  49. package/dist/config/ui-state.d.ts +28 -1
  50. package/dist/config/ui-state.js +9 -0
  51. package/dist/exec/events.d.ts +50 -1
  52. package/dist/exec/runner.js +42 -0
  53. package/dist/input/limits.d.ts +72 -0
  54. package/dist/input/limits.js +63 -0
  55. package/dist/input/mouse-events.d.ts +31 -5
  56. package/dist/input/mouse-events.js +86 -27
  57. package/dist/input/paste-parse.d.ts +71 -0
  58. package/dist/input/paste-parse.js +95 -0
  59. package/dist/input/stdin-filter.d.ts +81 -0
  60. package/dist/input/stdin-filter.js +379 -0
  61. package/dist/logging/install.d.ts +53 -1
  62. package/dist/logging/install.js +81 -1
  63. package/dist/logging/logger.d.ts +1 -1
  64. package/dist/proc/classify.d.ts +52 -0
  65. package/dist/proc/classify.js +143 -0
  66. package/dist/proc/kill-tree.d.ts +42 -0
  67. package/dist/proc/kill-tree.js +119 -0
  68. package/dist/proc/limits.d.ts +84 -0
  69. package/dist/proc/limits.js +84 -0
  70. package/dist/proc/log-ring.d.ts +68 -0
  71. package/dist/proc/log-ring.js +102 -0
  72. package/dist/proc/prompt.d.ts +24 -0
  73. package/dist/proc/prompt.js +49 -0
  74. package/dist/proc/readiness.d.ts +102 -0
  75. package/dist/proc/readiness.js +219 -0
  76. package/dist/proc/supervisor.d.ts +103 -0
  77. package/dist/proc/supervisor.js +465 -0
  78. package/dist/proc/types.d.ts +175 -0
  79. package/dist/proc/types.js +15 -0
  80. package/dist/session/persist.js +55 -1
  81. package/dist/team/report.js +7 -1
  82. package/dist/team/runtime.d.ts +10 -0
  83. package/dist/team/runtime.js +7 -0
  84. package/dist/team/subagent.d.ts +31 -1
  85. package/dist/team/subagent.js +42 -15
  86. package/dist/team/types.d.ts +8 -0
  87. package/dist/tools/bash-tool.d.ts +51 -2
  88. package/dist/tools/bash-tool.js +321 -51
  89. package/dist/tools/fs-tools.d.ts +17 -0
  90. package/dist/tools/index.d.ts +55 -2
  91. package/dist/tools/index.js +39 -1
  92. package/dist/tools/proc-tools.d.ts +25 -0
  93. package/dist/tools/proc-tools.js +102 -0
  94. package/dist/ui/ActivityLine.d.ts +19 -1
  95. package/dist/ui/ActivityLine.js +10 -2
  96. package/dist/ui/App.d.ts +62 -2
  97. package/dist/ui/App.js +546 -16
  98. package/dist/ui/Composer.d.ts +66 -1
  99. package/dist/ui/Composer.js +50 -6
  100. package/dist/ui/PromptInput.d.ts +82 -3
  101. package/dist/ui/PromptInput.js +232 -49
  102. package/dist/ui/StatusBar.d.ts +59 -0
  103. package/dist/ui/StatusBar.js +16 -4
  104. package/dist/ui/TeamPanel.js +7 -1
  105. package/dist/ui/Transcript.d.ts +21 -1
  106. package/dist/ui/Transcript.js +61 -3
  107. package/dist/ui/clipboard.d.ts +52 -0
  108. package/dist/ui/clipboard.js +91 -0
  109. package/dist/ui/composer-limits.d.ts +31 -0
  110. package/dist/ui/composer-limits.js +50 -0
  111. package/dist/ui/composer-rows.d.ts +73 -0
  112. package/dist/ui/composer-rows.js +201 -0
  113. package/dist/ui/editor-reducer.d.ts +37 -0
  114. package/dist/ui/editor-reducer.js +88 -12
  115. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  116. package/dist/ui/entries/CompactionCard.js +158 -0
  117. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  118. package/dist/ui/entries/ServiceCard.js +116 -0
  119. package/dist/ui/entries/UserEntry.d.ts +17 -3
  120. package/dist/ui/entries/UserEntry.js +34 -5
  121. package/dist/ui/frame-differ.d.ts +46 -0
  122. package/dist/ui/frame-differ.js +94 -10
  123. package/dist/ui/gauge.d.ts +16 -1
  124. package/dist/ui/gauge.js +25 -6
  125. package/dist/ui/glyphs.d.ts +11 -0
  126. package/dist/ui/glyphs.js +2 -0
  127. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  128. package/dist/ui/layout/ScrollViewport.js +128 -21
  129. package/dist/ui/layout/budget.d.ts +44 -9
  130. package/dist/ui/layout/budget.js +42 -10
  131. package/dist/ui/layout/follow-state.d.ts +137 -0
  132. package/dist/ui/layout/follow-state.js +150 -0
  133. package/dist/ui/layout/virtual-window.d.ts +11 -0
  134. package/dist/ui/layout/virtual-window.js +85 -1
  135. package/dist/ui/overlays/HelpOverlay.js +9 -1
  136. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  137. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  138. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  139. package/dist/ui/overlays/SettingsScreen.js +74 -2
  140. package/dist/ui/palettes.d.ts +16 -0
  141. package/dist/ui/palettes.js +6 -0
  142. package/dist/ui/paste-frames.d.ts +87 -0
  143. package/dist/ui/paste-frames.js +112 -0
  144. package/dist/ui/paste-tokens.d.ts +90 -0
  145. package/dist/ui/paste-tokens.js +147 -0
  146. package/dist/ui/screen.d.ts +38 -0
  147. package/dist/ui/screen.js +113 -8
  148. package/dist/ui/selection/highlight.d.ts +39 -0
  149. package/dist/ui/selection/highlight.js +64 -0
  150. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  151. package/dist/ui/selection/screen-mirror.js +35 -0
  152. package/dist/ui/selection/selection-controller.d.ts +88 -0
  153. package/dist/ui/selection/selection-controller.js +282 -0
  154. package/dist/ui/selection/selection.d.ts +90 -0
  155. package/dist/ui/selection/selection.js +223 -0
  156. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  157. package/dist/ui/stdout-frame-writer.js +15 -0
  158. package/dist/ui/theme.d.ts +11 -0
  159. package/dist/ui/theme.js +2 -0
  160. package/dist/ui/transcript-text.js +72 -0
  161. package/dist/ui/use-startup-notices.d.ts +27 -1
  162. package/dist/ui/use-startup-notices.js +43 -11
  163. package/dist/ui/use-wheel-routing.d.ts +1 -1
  164. package/dist/ui/use-wheel-routing.js +8 -0
  165. package/package.json +4 -2
  166. package/dist/input/stdin-mouse-filter.d.ts +0 -42
  167. package/dist/input/stdin-mouse-filter.js +0 -153
@@ -88,6 +88,25 @@ interface StatusBarProps {
88
88
  done: number;
89
89
  total: number;
90
90
  };
91
+ /**
92
+ * Live background services (background-service-supervision §3.8), or absent
93
+ * when none are running — an ordinary session's bar is then unchanged.
94
+ *
95
+ * LEFT CLUSTER, after the todo chip, and it DEGRADES rather than hiding below
96
+ * `PROC_LIMITS.statusCompactCols`. That is the `todoActive` / `teamActive`
97
+ * ladder rather than `compactionActive`'s hide-below-threshold one, and the
98
+ * choice is deliberate: `compact` and `fast` describe something the CLI is
99
+ * doing to itself, whereas this is a PROCESS ON THE USER'S MACHINE HOLDING A
100
+ * PORT. A narrow terminal is a reason to shorten it, never a reason to stop
101
+ * saying it.
102
+ *
103
+ * `live` counts `starting | ready | running`. The transcript records events
104
+ * about a service (D-11); this is where "what is running right now" lives,
105
+ * alongside `/bg` and `bash_output`.
106
+ */
107
+ servicesActive?: {
108
+ live: number;
109
+ };
91
110
  /**
92
111
  * Live retry state (llm-api-retry-backoff §6.6), or absent when nothing is
93
112
  * retrying — an ordinary session's bar is then unchanged.
@@ -138,6 +157,46 @@ interface StatusBarProps {
138
157
  fastActive?: {
139
158
  inFlight: boolean;
140
159
  };
160
+ /**
161
+ * Live context compaction (context-auto-compaction §6.2), or absent when it is
162
+ * off for this session — in which case an ordinary session's bar is unchanged.
163
+ *
164
+ * RIGHT CLUSTER, next to `fast` and `eco`, and it DEGRADES below
165
+ * `COMPACTION_LIMITS.statusCompactCols`. The `teamActive` / `ecoRung` /
166
+ * `fastActive` treatment rather than the `agentMode` one, for the reason the
167
+ * note on `agentMode` gives: the mode must never be unreportable, whereas
168
+ * compaction has a fuller home in `/compact status` — the GUARANTEED reporting
169
+ * surface (§6.5) — and a chip on a 60-column terminal costs the context gauge
170
+ * more than it says.
171
+ *
172
+ * `inFlight` renders `compacting`, so a summarization that is taking a while is
173
+ * visible on this row without a spinner competing with the activity line's.
174
+ */
175
+ compactionActive?: {
176
+ inFlight: boolean;
177
+ };
178
+ /**
179
+ * Where the gauge turns amber and red (§6.2).
180
+ *
181
+ * ABSENT means today's hardcoded `{ warn: 60, high: 85 }`, so a session with
182
+ * compaction off renders a byte-identical bar. When present these are
183
+ * `warnThreshold * 100` and `threshold * 100`, which is what makes the colour
184
+ * the user sees agree with the number the trigger fires on.
185
+ */
186
+ gaugeMarks?: {
187
+ warn: number;
188
+ high: number;
189
+ };
190
+ /**
191
+ * Whether `contextTokens` is a DERIVED figure rather than a measured one.
192
+ *
193
+ * It WIDENS THE EXISTING `~`, it does not add a second marker. Today that
194
+ * prefix means "the window is a guess" (`contextWindowKnown`); with compaction
195
+ * it also has to mean "the occupancy is a guess", because the bar is written
196
+ * from an estimate for one turn after every compaction so it can fall
197
+ * immediately (§6.2 / AC-5).
198
+ */
199
+ contextEstimated?: boolean;
141
200
  }
142
201
  export declare function StatusBar(props: StatusBarProps): React.ReactElement;
143
202
  export {};
@@ -6,8 +6,10 @@ 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';
12
+ import { PROC_LIMITS } from '../proc/limits.js';
11
13
  /** Below this the `eco` chip costs the context gauge more than it is worth. */
12
14
  const ECO_MIN_COLS = 72;
13
15
  /**
@@ -29,14 +31,22 @@ function redrawChar(nonce) {
29
31
  return nonce % 2 === 0 ? ' ' : NBSP;
30
32
  }
31
33
  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;
34
+ const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, servicesActive, retryActive, ecoRung = 0, fastActive, compactionActive, gaugeMarks, contextEstimated = false, } = props;
33
35
  const { stdout } = useStdout();
34
36
  const cols = stdout?.columns ?? 80;
35
37
  const running = status === 'running';
36
38
  const glyphs = pickGlyphs(caps);
37
39
  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}%`;
40
+ // `gaugeMarks` is spread rather than passed as `undefined`, so a session with
41
+ // compaction off takes `buildGauge`'s own default and the bar is byte-identical
42
+ // to the pre-feature build.
43
+ const gauge = gaugeMarks
44
+ ? buildGauge(pct, cols < 72 ? 8 : 12, theme, caps, gaugeMarks)
45
+ : buildGauge(pct, cols < 72 ? 8 : 12, theme, caps);
46
+ // ONE `~` FOR TWO KINDS OF APPROXIMATION (§6.2). A second marker would be a
47
+ // second thing to explain for a distinction the user does not act on
48
+ // differently: either way the number is a guess.
49
+ const pctLabel = `${contextWindowKnown && !contextEstimated ? '' : '~'}${gauge.pct}%`;
40
50
  const tokens = `${formatTokens(usageTotal.inputTokens)}${glyphs.arrowUp} ${formatTokens(usageTotal.outputTokens)}${glyphs.arrowDown}`;
41
51
  const statusGlyph = running ? theme.symbols.toolRunning : theme.symbols.toolPending;
42
52
  // Nothing at all in build mode with nothing pending: the default posture is
@@ -57,5 +67,7 @@ export function StatusBar(props) {
57
67
  ? `agents ${teamActive.running}/${teamActive.total}`
58
68
  : `[${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
69
  ? `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)] }))] })] }));
70
+ : `[${todoActive.done}/${todoActive.total}]`] })), servicesActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= PROC_LIMITS.statusCompactCols
71
+ ? `svc ${servicesActive.live}`
72
+ : `[${servicesActive.live}]`] })), 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
73
  }
@@ -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,12 @@ 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';
43
+ import { ServiceCard, serviceCardColor, serviceGlyph } from './entries/ServiceCard.js';
42
44
  import { MeasuredEntry } from './layout/MeasuredEntry.js';
43
45
  import { useViewportGeometry } from './layout/viewport-geometry.js';
44
46
  import { estimateEntryRows, heightKey, selectWindow, VIRTUAL_LIMITS, } from './layout/virtual-window.js';
47
+ import { advanceTailRows } from './layout/follow-state.js';
45
48
  import { useTerminalSize } from './layout/useTerminalSize.js';
46
49
  /** Keep the last K entries in the live region so a mutating tail stays hot. */
47
50
  const LIVE_TAIL = 1;
@@ -99,6 +102,37 @@ export function computeSettledCount(entries, expandedToolIds) {
99
102
  // other half.
100
103
  if (e.kind === 'fast' && e.live)
101
104
  break;
105
+ // Nor is a compaction card whose call is still open: it is rewritten once,
106
+ // when the engine reports its verdict, and `<Static>` cannot un-print what it
107
+ // has already drawn (C-8). `session/persist.ts` forces `live: false` on load,
108
+ // which is the other half.
109
+ if (e.kind === 'compaction' && e.live)
110
+ break;
111
+ /**
112
+ * A SERVICE BLOCKS ONLY WHILE IT IS `starting` (D-11 / P0-4).
113
+ *
114
+ * Every clause above is bounded by an OPERATION — a dispatch, a turn, one
115
+ * LLM call, one countdown — which is why each of them can safely pin this
116
+ * prefix scan: the wait is short and it ends. A service is bounded by the
117
+ * USER'S INTENT. A dev server left up for an hour is the normal case, not
118
+ * the pathological one, so treating it like the other five would pin the
119
+ * boundary at the card and re-render the whole transcript from there on
120
+ * EVERY FRAME, FOREVER, on exactly the long sessions where it is least
121
+ * likely to be noticed.
122
+ *
123
+ * `starting` is different, and it is different for the reason that makes
124
+ * every other clause here legitimate: it is bounded by `readyTimeoutMs`,
125
+ * after which the supervisor calls the service `running` whatever happened.
126
+ *
127
+ * The cost of the rest is that a printed card stops mirroring live state —
128
+ * which is what a transcript IS. A terminal transition appends a NEW one-row
129
+ * entry rather than rewriting a card `<Static>` has already drawn, and live
130
+ * state lives in the status chip, `/bg` and `bash_output`. Weakening this to
131
+ * "any non-terminal service blocks" is the change D-11 exists to prevent;
132
+ * see condition 5 of the design's review verdict before making it.
133
+ */
134
+ if (e.kind === 'service' && e.status === 'starting')
135
+ break;
102
136
  count = i + 1;
103
137
  }
104
138
  return count;
@@ -111,7 +145,14 @@ export function computeSettledCount(entries, expandedToolIds) {
111
145
  * comparator, on every frame, forever.
112
146
  */
113
147
  function nowSecFor(entry, nowSec) {
114
- return entry.kind === 'tool' && entry.status === 'running' ? nowSec : undefined;
148
+ if (entry.kind === 'tool' && entry.status === 'running')
149
+ return nowSec;
150
+ // THE LIVE COMPACTION CARD IS THE SECOND CONSUMER (hardening W5). It is at most
151
+ // ONE entry in the whole transcript, and it settles - so the memo boundary this
152
+ // function protects is unchanged for every other card, forever.
153
+ if (entry.kind === 'compaction' && entry.live)
154
+ return nowSec;
155
+ return undefined;
115
156
  }
116
157
  function noticeColor(level, theme) {
117
158
  if (level === 'error')
@@ -133,7 +174,7 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
133
174
  const frame = (glyph, color, children) => (_jsx(EntryFrame, { glyph: glyph, color: color, continuation: glyphs.railVertical, separation: separation, spacerColor: theme.border, children: children }));
134
175
  switch (entry.kind) {
135
176
  case 'user':
136
- return frame(glyphs.user, theme.user, _jsx(UserEntry, { text: entry.text, theme: theme }));
177
+ return frame(glyphs.user, theme.user, _jsx(UserEntry, { text: entry.text, theme: theme, caps: caps }));
137
178
  case 'assistant': {
138
179
  // While text streams, the spinner IS the role marker. Braille dots are
139
180
  // both an animation and a Unicode-only glyph, so reduced motion and an
@@ -185,6 +226,16 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
185
226
  reducedMotion: reducedMotion, theme: theme, caps: caps }));
186
227
  case 'fast':
187
228
  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 }));
229
+ case 'compaction':
230
+ 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,
231
+ // ONLY WHILE LIVE, and only when the clock has ticked at least once:
232
+ // `nowSec` is `undefined` until then, and a settled card must keep
233
+ // rendering `durationMs` rather than a figure that would drift.
234
+ elapsedMs: entry.live && nowSec !== undefined && entry.startedAt !== undefined
235
+ ? Math.max(0, nowSec * 1000 - entry.startedAt)
236
+ : undefined, live: entry.live, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
237
+ case 'service':
238
+ return frame(serviceGlyph(entry, glyphs), serviceCardColor(entry, theme), _jsx(ServiceCard, { serviceId: entry.serviceId, command: entry.command, status: entry.status, url: entry.url, port: entry.port, exitCode: entry.exitCode, startedAt: entry.startedAt, readyAt: entry.readyAt, endedAt: entry.endedAt, rows: entry.rows, terminal: entry.terminal, killIncomplete: entry.killIncomplete, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
188
239
  case 'notice':
189
240
  return frame(noticeSymbol(entry.level, theme), noticeColor(entry.level, theme), _jsx(Text, { color: noticeColor(entry.level, theme), children: entry.text }));
190
241
  default:
@@ -248,7 +299,7 @@ export const EntryView = React.memo(EntryViewImpl, (a, b) => {
248
299
  * `selectWindow`, and the two spacers are what keep the scroll maths honest
249
300
  * while they are absent.
250
301
  */
251
- export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, }) {
302
+ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, tailSink, }) {
252
303
  const size = Math.max(1, Math.floor(windowSize));
253
304
  const visible = entries.length > size ? entries.slice(-size) : entries;
254
305
  const collapsed = entries.length - visible.length;
@@ -291,6 +342,13 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
291
342
  const slice = visible.slice(selection.startIndex, selection.endIndex);
292
343
  if (mountedSink)
293
344
  mountedSink.current = slice.length;
345
+ // One more pass over the array `selectWindow` has already walked, reading the
346
+ // SAME height table and never the geometry context (§4.3.1a). `visible` is
347
+ // what is drawn, so its last entry is the tail; entries the horizon dropped are
348
+ // not "removed rows" and must not register as one.
349
+ if (tailSink) {
350
+ tailSink.current = advanceTailRows(tailSink.current, visible.map((entry) => entry.id), heightOf);
351
+ }
294
352
  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
353
  const index = selection.startIndex + i;
296
354
  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,31 @@
1
+ /**
2
+ * Composer and transcript row ceilings (tui-paste-handling section 5.5 / 5.6).
3
+ *
4
+ * Structural, not policy (D-10): the only user-facing key this feature adds is
5
+ * the boolean `paste`. Everything here is a bound that keeps the frame's row
6
+ * arithmetic true, and every one of them is switchable by returning a larger
7
+ * number rather than by a config flag.
8
+ */
9
+ /** At or below BOTH of these a paste is inserted verbatim rather than collapsed. */
10
+ export declare const PASTE_INLINE_MAX_LINES = 6;
11
+ export declare const PASTE_INLINE_MAX_CHARS = 400;
12
+ /**
13
+ * Rows one submitted user message may RENDER in the transcript, before the
14
+ * `... +N more lines` tail (G7).
15
+ *
16
+ * `ui/layout/virtual-window.ts` imports this rather than duplicating the number,
17
+ * unlike `DIFF_COLLAPSED_LINES` / `TOOL_LIVE_TAIL_ROWS` beside it: I-13 makes the
18
+ * render cap and the height estimate THE SAME NUMBER, and there is no
19
+ * import-cycle reason here for them to be two.
20
+ */
21
+ export declare const USER_ENTRY_MAX_ROWS = 40;
22
+ /**
23
+ * Ceiling on the composer's DRAFT rows at a given terminal height.
24
+ *
25
+ * Non-decreasing in `terminalRows`. Returning `Infinity` reduces the whole
26
+ * height-bound layer to the rung below it (section 12, S4), which is what makes
27
+ * that stage revertible on its own.
28
+ */
29
+ export declare function draftMaxRows(terminalRows: number): number;
30
+ /** The terminal height at which `draftMaxRows` steps from 3 to 6. Pinned by test. */
31
+ export declare const DRAFT_MAX_ROWS_HINT_TIER = 20;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Composer and transcript row ceilings (tui-paste-handling section 5.5 / 5.6).
3
+ *
4
+ * Structural, not policy (D-10): the only user-facing key this feature adds is
5
+ * the boolean `paste`. Everything here is a bound that keeps the frame's row
6
+ * arithmetic true, and every one of them is switchable by returning a larger
7
+ * number rather than by a config flag.
8
+ */
9
+ /** At or below BOTH of these a paste is inserted verbatim rather than collapsed. */
10
+ export const PASTE_INLINE_MAX_LINES = 6;
11
+ export const PASTE_INLINE_MAX_CHARS = 400;
12
+ /**
13
+ * Rows one submitted user message may RENDER in the transcript, before the
14
+ * `... +N more lines` tail (G7).
15
+ *
16
+ * `ui/layout/virtual-window.ts` imports this rather than duplicating the number,
17
+ * unlike `DIFF_COLLAPSED_LINES` / `TOOL_LIVE_TAIL_ROWS` beside it: I-13 makes the
18
+ * render cap and the height estimate THE SAME NUMBER, and there is no
19
+ * import-cycle reason here for them to be two.
20
+ */
21
+ export const USER_ENTRY_MAX_ROWS = 40;
22
+ /**
23
+ * The terminal height at which the composer's hint row starts to fit.
24
+ *
25
+ * DELIBERATELY A SECOND COPY of `ui/layout/budget.ts::HINT_MIN_ROWS` rather than
26
+ * an import: `budget.ts` calls `draftMaxRows` to clamp its composer term, so an
27
+ * import in this direction would close an ESM cycle whose only protection is
28
+ * that neither side reads the other at module scope -- true today, and a
29
+ * confusing TDZ crash the first time someone hoists a use. `budget.test.ts`
30
+ * asserts the two numbers are equal, so they cannot drift in silence.
31
+ */
32
+ const DRAFT_TIER_MIN_ROWS = 20;
33
+ /**
34
+ * Ceiling on the composer's DRAFT rows at a given terminal height.
35
+ *
36
+ * Non-decreasing in `terminalRows`. Returning `Infinity` reduces the whole
37
+ * height-bound layer to the rung below it (section 12, S4), which is what makes
38
+ * that stage revertible on its own.
39
+ */
40
+ export function draftMaxRows(terminalRows) {
41
+ if (!Number.isFinite(terminalRows))
42
+ return 3;
43
+ if (terminalRows >= 30)
44
+ return 10;
45
+ if (terminalRows >= DRAFT_TIER_MIN_ROWS)
46
+ return 6;
47
+ return 3;
48
+ }
49
+ /** The terminal height at which `draftMaxRows` steps from 3 to 6. Pinned by test. */
50
+ export const DRAFT_MAX_ROWS_HINT_TIER = DRAFT_TIER_MIN_ROWS;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Composer layout (tui-paste-handling section 5.5) -- pure. No Ink, no React.
3
+ *
4
+ * THE ONE PLACE THAT MAPS A BUFFER INDEX TO A SCREEN CELL (I-9). The composer
5
+ * used to hand its buffer to Ink one `<Text>` per logical line and let Yoga wrap
6
+ * it, which is fine until the draft is taller than the frame: `AppShell` gives
7
+ * the bottom chrome `flexShrink={0}`, so a tall composer does not grow the frame
8
+ * -- Yoga takes the rows out of the transcript and `overflow: hidden` clips them
9
+ * away, while `layout/budget.ts` keeps telling five consumers the composer is
10
+ * three rows tall. Four hundred presses of `Shift+Enter` do it without pasting
11
+ * anything.
12
+ *
13
+ * So the wrap happens HERE, the row count is a number this module returns, and
14
+ * `budget.ts` is told that number (I-8).
15
+ *
16
+ * HARD WRAP BY DISPLAY COLUMN, NEVER `layout/wrap-rows.ts::wrapToRows` (D-15).
17
+ * That function is a PROSE wrapper: it splits on `/\s+/` and rejoins with single
18
+ * spaces, so it destroys indentation and makes the character-index-to-screen-cell
19
+ * mapping the caret depends on impossible to state, let alone compute.
20
+ */
21
+ export interface ComposerSegment {
22
+ readonly text: string;
23
+ readonly kind: 'text' | 'token';
24
+ }
25
+ export interface ComposerRow {
26
+ readonly segments: ComposerSegment[];
27
+ }
28
+ export interface ComposerLayout {
29
+ /** At most `maxRows`, and always containing the caret when `active`. */
30
+ readonly rows: ComposerRow[];
31
+ /** Index into `rows`, or -1 when the composer is not focused. */
32
+ readonly cursorRow: number;
33
+ /** Display column within that row. */
34
+ readonly cursorCol: number;
35
+ /** Rows the buffer occupies BEFORE the window was applied. */
36
+ readonly totalRows: number;
37
+ readonly hiddenAbove: number;
38
+ readonly hiddenBelow: number;
39
+ }
40
+ export interface ComposerLayoutInput {
41
+ readonly buffer: string;
42
+ readonly cursor: number;
43
+ readonly cols: number;
44
+ readonly maxRows: number;
45
+ readonly active: boolean;
46
+ }
47
+ /** Total display width of a run, using the same table the layout used. */
48
+ export declare function displayWidth(text: string): number;
49
+ /**
50
+ * Wrap `buffer` into rows, locate the caret, and select a window of at most
51
+ * `maxRows` rows that contains it.
52
+ *
53
+ * The window is pinned to the BOTTOM -- where the user is typing -- and pulled up
54
+ * only far enough to keep the caret visible. That is deterministic, which matters
55
+ * because this function holds no state between calls: a scroll offset would have
56
+ * to live in the reducer and would then need its own invalidation rules.
57
+ */
58
+ export declare function layoutComposer(input: ComposerLayoutInput): ComposerLayout;
59
+ export interface RowSplit {
60
+ readonly before: ComposerSegment[];
61
+ /** The one cell the caret sits on, or `null` when it sits past the last one. */
62
+ readonly at: ComposerSegment | null;
63
+ readonly after: ComposerSegment[];
64
+ }
65
+ /**
66
+ * Split one row at a display column so the renderer can draw an inverse caret on
67
+ * exactly one cell.
68
+ *
69
+ * Lives here rather than in the component because it walks the SAME width table
70
+ * `layoutComposer` walked (I-9): a second, subtly different measurement is how
71
+ * the caret ends up drawn on a cell that does not hold the character it is on.
72
+ */
73
+ export declare function splitRowAtColumn(row: ComposerRow, column: number): RowSplit;