@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.
- package/CHANGELOG.md +321 -0
- package/README.md +297 -2
- package/dist/agent/controller.d.ts +193 -1
- package/dist/agent/controller.js +475 -2
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +189 -0
- package/dist/agent/reducer.js +301 -2
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +297 -16
- package/dist/commands/builtins.js +165 -24
- package/dist/commands/registry.d.ts +27 -0
- package/dist/compaction/archive.d.ts +175 -0
- package/dist/compaction/archive.js +267 -0
- package/dist/compaction/child.d.ts +97 -0
- package/dist/compaction/child.js +118 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +355 -0
- package/dist/compaction/compactor.d.ts +302 -0
- package/dist/compaction/compactor.js +810 -0
- package/dist/compaction/digest.d.ts +62 -0
- package/dist/compaction/digest.js +164 -0
- package/dist/compaction/limits.d.ts +165 -0
- package/dist/compaction/limits.js +172 -0
- package/dist/compaction/pressure.d.ts +135 -0
- package/dist/compaction/pressure.js +167 -0
- package/dist/compaction/summarize-call.d.ts +70 -0
- package/dist/compaction/summarize-call.js +86 -0
- package/dist/compaction/summary-prompt.d.ts +85 -0
- package/dist/compaction/summary-prompt.js +197 -0
- package/dist/compaction/tail-budget.d.ts +52 -0
- package/dist/compaction/tail-budget.js +67 -0
- package/dist/compaction/types.d.ts +215 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +264 -0
- package/dist/compaction/wiring.js +577 -0
- package/dist/config/app-paths.d.ts +14 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/cli-commands.d.ts +41 -0
- package/dist/config/cli-commands.js +77 -1
- package/dist/config/env.d.ts +5 -2
- package/dist/config/env.js +140 -3
- package/dist/config/load.d.ts +42 -0
- package/dist/config/load.js +101 -1
- package/dist/config/schema.d.ts +244 -0
- package/dist/config/schema.js +170 -0
- package/dist/config/store.js +37 -1
- package/dist/config/ui-state.d.ts +28 -1
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/runner.js +42 -0
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +53 -1
- package/dist/logging/install.js +81 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +55 -1
- package/dist/team/report.js +7 -1
- package/dist/team/runtime.d.ts +10 -0
- package/dist/team/runtime.js +7 -0
- package/dist/team/subagent.d.ts +31 -1
- package/dist/team/subagent.js +42 -15
- package/dist/team/types.d.ts +8 -0
- package/dist/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +62 -2
- package/dist/ui/App.js +546 -16
- package/dist/ui/Composer.d.ts +66 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +82 -3
- package/dist/ui/PromptInput.js +232 -49
- package/dist/ui/StatusBar.d.ts +59 -0
- package/dist/ui/StatusBar.js +16 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +61 -3
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- package/dist/ui/frame-differ.d.ts +46 -0
- package/dist/ui/frame-differ.js +94 -10
- package/dist/ui/gauge.d.ts +16 -1
- package/dist/ui/gauge.js +25 -6
- package/dist/ui/glyphs.d.ts +11 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/ScrollViewport.d.ts +43 -6
- package/dist/ui/layout/ScrollViewport.js +128 -21
- package/dist/ui/layout/budget.d.ts +44 -9
- package/dist/ui/layout/budget.js +42 -10
- package/dist/ui/layout/follow-state.d.ts +137 -0
- package/dist/ui/layout/follow-state.js +150 -0
- package/dist/ui/layout/virtual-window.d.ts +11 -0
- package/dist/ui/layout/virtual-window.js +85 -1
- package/dist/ui/overlays/HelpOverlay.js +9 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +74 -2
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +38 -0
- package/dist/ui/screen.js +113 -8
- package/dist/ui/selection/highlight.d.ts +39 -0
- package/dist/ui/selection/highlight.js +64 -0
- package/dist/ui/selection/screen-mirror.d.ts +24 -0
- package/dist/ui/selection/screen-mirror.js +35 -0
- package/dist/ui/selection/selection-controller.d.ts +88 -0
- package/dist/ui/selection/selection-controller.js +282 -0
- package/dist/ui/selection/selection.d.ts +90 -0
- package/dist/ui/selection/selection.js +223 -0
- package/dist/ui/stdout-frame-writer.d.ts +33 -0
- package/dist/ui/stdout-frame-writer.js +15 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +2 -0
- package/dist/ui/transcript-text.js +72 -0
- package/dist/ui/use-startup-notices.d.ts +27 -1
- package/dist/ui/use-startup-notices.js +43 -11
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/dist/ui/use-wheel-routing.js +8 -0
- package/package.json +4 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -42
- package/dist/input/stdin-mouse-filter.js +0 -153
package/dist/ui/StatusBar.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
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}]`] })),
|
|
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
|
}
|
package/dist/ui/TeamPanel.js
CHANGED
|
@@ -88,7 +88,13 @@ export function activityLine(run, cols) {
|
|
|
88
88
|
? 'waiting for mail'
|
|
89
89
|
: 'tool';
|
|
90
90
|
case 'done':
|
|
91
|
-
|
|
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':
|
package/dist/ui/Transcript.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -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
|
-
|
|
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;
|