@aragon-agent/cli 0.6.4 → 0.6.8
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 +37 -0
- package/README.md +163 -37
- package/dist/agent/controller.d.ts +49 -8
- package/dist/agent/controller.js +160 -48
- package/dist/agent/headless.d.ts +2 -1
- package/dist/agent/prompt-options.d.ts +15 -0
- package/dist/agent/prompt-options.js +1 -0
- package/dist/agent/reducer.d.ts +33 -19
- package/dist/agent/reducer.js +62 -71
- package/dist/agent/usage.d.ts +34 -0
- package/dist/agent/usage.js +43 -0
- package/dist/cli.js +131 -54
- package/dist/commands/builtins.js +12 -1
- package/dist/commands/registry.d.ts +2 -3
- package/dist/compaction/compactor.d.ts +60 -24
- package/dist/compaction/compactor.js +89 -61
- package/dist/compaction/context-command.d.ts +33 -0
- package/dist/compaction/context-command.js +166 -0
- package/dist/compaction/meter.d.ts +251 -0
- package/dist/compaction/meter.js +428 -0
- package/dist/compaction/pressure.d.ts +6 -0
- package/dist/compaction/pressure.js +4 -0
- package/dist/compaction/types.d.ts +41 -0
- package/dist/compaction/wiring.d.ts +47 -0
- package/dist/compaction/wiring.js +88 -27
- package/dist/config/cli-commands.js +5 -0
- package/dist/config/env.js +15 -1
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +45 -1
- package/dist/config/schema.d.ts +44 -0
- package/dist/config/schema.js +36 -0
- package/dist/config/store.js +27 -1
- package/dist/exec/events.d.ts +138 -1
- package/dist/exec/events.js +42 -0
- package/dist/exec/index.js +43 -6
- package/dist/exec/progress-reporter.d.ts +23 -0
- package/dist/exec/progress-reporter.js +88 -0
- package/dist/exec/runner.d.ts +34 -0
- package/dist/exec/runner.js +137 -25
- package/dist/fast/reviewer.d.ts +19 -1
- package/dist/fast/reviewer.js +22 -0
- package/dist/fast/types.d.ts +15 -0
- package/dist/fast/wiring.d.ts +10 -1
- package/dist/fast/wiring.js +18 -1
- package/dist/input/interrupt-gesture.d.ts +18 -0
- package/dist/input/interrupt-gesture.js +19 -0
- package/dist/input/pointer-router.d.ts +11 -0
- package/dist/input/pointer-router.js +14 -0
- package/dist/todo/limits.d.ts +4 -17
- package/dist/todo/limits.js +4 -17
- package/dist/todo/panel-rows.d.ts +8 -6
- package/dist/todo/panel-rows.js +19 -6
- package/dist/todo/store.d.ts +8 -13
- package/dist/todo/store.js +13 -14
- package/dist/todo/types.d.ts +2 -1
- package/dist/ui/App.d.ts +2 -0
- package/dist/ui/App.js +233 -183
- package/dist/ui/AutocompletePopup.d.ts +3 -1
- package/dist/ui/AutocompletePopup.js +9 -11
- package/dist/ui/BottomStatusRow.d.ts +1 -1
- package/dist/ui/Composer.d.ts +13 -5
- package/dist/ui/Composer.js +7 -11
- package/dist/ui/PromptCaret.d.ts +11 -0
- package/dist/ui/PromptCaret.js +31 -0
- package/dist/ui/PromptInput.d.ts +12 -6
- package/dist/ui/PromptInput.js +50 -16
- package/dist/ui/StatusBar.d.ts +22 -15
- package/dist/ui/StatusBar.js +61 -16
- package/dist/ui/TeamPanel.d.ts +3 -1
- package/dist/ui/TeamPanel.js +13 -9
- package/dist/ui/TodoPanel.d.ts +5 -5
- package/dist/ui/TodoPanel.js +56 -24
- package/dist/ui/Transcript.js +1 -0
- package/dist/ui/UpdateLine.d.ts +1 -1
- package/dist/ui/capabilities.js +5 -5
- package/dist/ui/composer-rows.js +50 -9
- package/dist/ui/exit-snapshot.d.ts +9 -0
- package/dist/ui/frame-differ.js +2 -19
- package/dist/ui/frame-observer.d.ts +17 -0
- package/dist/ui/frame-observer.js +81 -0
- package/dist/ui/frame-parser.d.ts +7 -0
- package/dist/ui/frame-parser.js +18 -0
- package/dist/ui/layout/AppShell.d.ts +7 -52
- package/dist/ui/layout/AppShell.js +2 -2
- package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
- package/dist/ui/layout/ScrollIndicator.js +4 -4
- package/dist/ui/layout/ScrollViewport.d.ts +13 -66
- package/dist/ui/layout/ScrollViewport.js +183 -185
- package/dist/ui/layout/autocomplete.d.ts +18 -0
- package/dist/ui/layout/autocomplete.js +26 -0
- package/dist/ui/layout/budget.d.ts +3 -51
- package/dist/ui/layout/budget.js +4 -53
- package/dist/ui/layout/document-layout.d.ts +13 -0
- package/dist/ui/layout/document-layout.js +11 -0
- package/dist/ui/layout/follow-state.d.ts +1 -0
- package/dist/ui/layout/follow-state.js +1 -3
- package/dist/ui/layout/rail.d.ts +4 -53
- package/dist/ui/layout/rail.js +10 -62
- package/dist/ui/layout/scroll-indicator.d.ts +1 -1
- package/dist/ui/layout/scroll-indicator.js +3 -2
- package/dist/ui/layout/scrollbar-geometry.d.ts +17 -0
- package/dist/ui/layout/scrollbar-geometry.js +13 -0
- package/dist/ui/layout/team-panel.d.ts +18 -0
- package/dist/ui/layout/team-panel.js +22 -0
- package/dist/ui/layout/todo-layout.d.ts +25 -0
- package/dist/ui/layout/todo-layout.js +28 -0
- package/dist/ui/layout/viewport-geometry.d.ts +1 -0
- package/dist/ui/layout/viewport-geometry.js +1 -0
- package/dist/ui/layout/virtual-window.d.ts +1 -0
- package/dist/ui/layout/virtual-window.js +1 -1
- package/dist/ui/overlays/HelpOverlay.js +5 -2
- package/dist/ui/screen.d.ts +1 -1
- package/dist/ui/screen.js +4 -6
- package/dist/ui/scrollbar-controller.d.ts +30 -0
- package/dist/ui/scrollbar-controller.js +124 -0
- package/dist/ui/transcript-text.js +6 -2
- package/dist/ui/use-wheel-routing.d.ts +2 -0
- package/dist/ui/use-wheel-routing.js +6 -0
- package/package.json +2 -2
package/dist/ui/StatusBar.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import stringWidth from 'string-width';
|
|
2
3
|
import { Box, Text, useStdout } from 'ink';
|
|
3
4
|
import { pickGlyphs } from './glyphs.js';
|
|
4
|
-
import { formatCost, formatTokens, formatDuration } from '../agent/usage.js';
|
|
5
|
+
import { formatCost, formatTokens, formatDuration, promptTokensOf } from '../agent/usage.js';
|
|
5
6
|
import { buildGauge } from './gauge.js';
|
|
6
7
|
import { MODE_LABEL } from '../agent/agent-mode.js';
|
|
7
8
|
import { formatRetryChip } from '../agent/retry-view.js';
|
|
@@ -12,6 +13,26 @@ import { TODO_LIMITS } from '../todo/limits.js';
|
|
|
12
13
|
import { PROC_LIMITS } from '../proc/limits.js';
|
|
13
14
|
/** Below this the `eco` chip costs the context gauge more than it is worth. */
|
|
14
15
|
const ECO_MIN_COLS = 72;
|
|
16
|
+
/**
|
|
17
|
+
* Where `86k/200k` starts being shown (context-usage-gauge-accuracy §3.6 / W5).
|
|
18
|
+
*
|
|
19
|
+
* IT TAKES THE COLUMNS THE SESSION TOTAL USED TO HAVE, and the ordering is the
|
|
20
|
+
* point rather than a layout accident (D-6). "How full am I, out of how much" is
|
|
21
|
+
* the question a user asks the gauge; "what has this session cost in tokens" is
|
|
22
|
+
* a different one, and putting the second in front of the first spends the
|
|
23
|
+
* scarce columns on the number the user is more likely to MISREAD - the total
|
|
24
|
+
* looks like an occupancy until you notice it exceeds the window.
|
|
25
|
+
*/
|
|
26
|
+
const ABSOLUTE_PAIR_MIN_COLS = 72;
|
|
27
|
+
/**
|
|
28
|
+
* Where the session total returns, now prefixed.
|
|
29
|
+
*
|
|
30
|
+
* `total ` IS AN ASCII WORD, NOT A SIGMA. `ui/**` is inside the glyph scanner's
|
|
31
|
+
* scope, so a new symbol would need an entry in `glyphs.ts` with an ASCII
|
|
32
|
+
* fallback for a legacy `cmd.exe` - a real cost for a three-character prefix
|
|
33
|
+
* that reads worse.
|
|
34
|
+
*/
|
|
35
|
+
const SESSION_TOTAL_MIN_COLS = 96;
|
|
15
36
|
/**
|
|
16
37
|
* I-5 — DO NOT "CLEAN THIS UP". Ink short-circuits identical output at TWO
|
|
17
38
|
* gates (`ink.js:132` and `log-update.js:13`), so a fixed frame whose React
|
|
@@ -31,23 +52,42 @@ function redrawChar(nonce) {
|
|
|
31
52
|
return nonce % 2 === 0 ? ' ' : NBSP;
|
|
32
53
|
}
|
|
33
54
|
export function StatusBar(props) {
|
|
34
|
-
const { model, provider, usageTotal,
|
|
55
|
+
const { model, provider, usageTotal, context, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, todoActive, servicesActive, retryActive, ecoRung = 0, fastActive, compactionActive, gaugeMarks, } = props;
|
|
35
56
|
const { stdout } = useStdout();
|
|
36
57
|
const cols = stdout?.columns ?? 80;
|
|
37
58
|
const running = status === 'running';
|
|
38
59
|
const glyphs = pickGlyphs(caps);
|
|
39
|
-
|
|
60
|
+
// THE PERCENTAGE IS NOT RECOMPUTED HERE ANY MORE. It arrives already divided
|
|
61
|
+
// and clamped by the same code the compaction trigger reads, which is the
|
|
62
|
+
// whole of AC-2: a user watching 78 % and a `/compact status` reporting 91 %
|
|
63
|
+
// would rightly conclude one of them is lying.
|
|
64
|
+
const pct = context.pct;
|
|
40
65
|
// `gaugeMarks` is spread rather than passed as `undefined`, so a session with
|
|
41
66
|
// compaction off takes `buildGauge`'s own default and the bar is byte-identical
|
|
42
67
|
// to the pre-feature build.
|
|
43
68
|
const gauge = gaugeMarks
|
|
44
69
|
? buildGauge(pct, cols < 72 ? 8 : 12, theme, caps, gaugeMarks)
|
|
45
70
|
: buildGauge(pct, cols < 72 ? 8 : 12, theme, caps);
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
// `~` BELONGS TO THE NUMERATOR, `?` TO THE DENOMINATOR (P2-7). One glyph used
|
|
72
|
+
// to carry three meanings - the window is a guess, the occupancy is a guess,
|
|
73
|
+
// the occupancy contains an appended-message estimate - and a user could not
|
|
74
|
+
// tell from it which half to distrust. The `~` keeps every case it had: an
|
|
75
|
+
// unknown window still produces it, so a session that never touches the new
|
|
76
|
+
// absolute pair sees the identical string it saw before.
|
|
77
|
+
const occupancyApproximate = context.source === 'estimate' || context.deltaTokens > 0;
|
|
78
|
+
const pctLabel = `${context.windowKnown && !occupancyApproximate ? '' : '~'}${gauge.pct}%`;
|
|
79
|
+
// `?` ON THE WINDOW, AND ONLY ON THE NEW READOUT. `windowKnown` is false
|
|
80
|
+
// exactly when `buildRuntimeModel` invented the 128k placeholder, and saying
|
|
81
|
+
// `128k?` is the difference between a denominator the user can act on and one
|
|
82
|
+
// they will assume came from their model card.
|
|
83
|
+
const windowLabel = `${formatTokens(context.window)}${context.windowKnown ? '' : '?'}`;
|
|
84
|
+
const absolutePair = `${formatTokens(context.occupied)}/${windowLabel}`;
|
|
85
|
+
// THE PROMPT-SIDE TOTAL, NOT `inputTokens` (P1-3 / AC-11). Anthropic reports
|
|
86
|
+
// `input_tokens` EXCLUDING cached tokens, so the old expression under-reported
|
|
87
|
+
// for anyone behind a caching gateway - on the same row as a `$` that has
|
|
88
|
+
// always priced the cache. Any session with cache hits sees this number grow,
|
|
89
|
+
// and that is the fix rather than a regression.
|
|
90
|
+
const tokens = `total ${formatTokens(promptTokensOf(usageTotal))}${glyphs.arrowUp} ${formatTokens(usageTotal.outputTokens)}${glyphs.arrowDown}`;
|
|
51
91
|
const statusGlyph = running ? theme.symbols.toolRunning : theme.symbols.toolPending;
|
|
52
92
|
// Nothing at all in build mode with nothing pending: the default posture is
|
|
53
93
|
// not news, and a permanent word here would be furniture.
|
|
@@ -56,6 +96,11 @@ export function StatusBar(props) {
|
|
|
56
96
|
: pendingAgentMode
|
|
57
97
|
? `${MODE_LABEL[agentMode]} ${glyphs.arrowRight} ${MODE_LABEL[pendingAgentMode]}`
|
|
58
98
|
: MODE_LABEL[agentMode];
|
|
99
|
+
const actionHints = [running ? (props.interruptHint ?? 'Esc abort') : '',
|
|
100
|
+
servicesActive ? `Ctrl+C stop ${servicesActive.live}` : '',
|
|
101
|
+
scrolledLines > 0 ? 'PgDn down' : ''].filter(Boolean).join(' | ');
|
|
102
|
+
const hintCols = actionHints ? Math.min(Math.max(0, cols - 1), stringWidth(actionHints) + 2) : 0;
|
|
103
|
+
const detailCols = Math.max(0, cols - 1 - hintCols);
|
|
59
104
|
return (
|
|
60
105
|
// `flexShrink={0}` on the left cluster is not cosmetic. When the two clusters
|
|
61
106
|
// over-subscribe the row, yoga shrinks the flexible children and Ink's text
|
|
@@ -63,11 +108,11 @@ export function StatusBar(props) {
|
|
|
63
108
|
// and "? hel" rather than truncating cleanly at one end. Pinning the left
|
|
64
109
|
// cluster keeps the run status and model intact and makes the degradation
|
|
65
110
|
// land in one predictable place.
|
|
66
|
-
_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
111
|
+
_jsxs(Box, { width: cols, flexDirection: "row", justifyContent: "space-between", flexShrink: 0, height: 1, overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(redrawNonce) }) }), hintCols > 0 && (_jsx(Box, { width: hintCols, flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [actionHints, ' '] }) })), _jsxs(Box, { display: detailCols > 0 ? 'flex' : 'none', width: detailCols, flexShrink: 0, flexDirection: "row", justifyContent: "space-between", overflow: "hidden", children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, overflow: "hidden", children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [statusGlyph, " ", running ? 'running' : 'idle'] }), modeWord && (_jsxs(Text, { color: theme.accent, bold: true, children: [' ', modeWord] })), teamActive && (_jsxs(Text, { color: theme.toolRunning, bold: true, children: [' ', cols >= TEAM_LIMITS.statusCompactCols
|
|
112
|
+
? `agents ${teamActive.running}/${teamActive.total}`
|
|
113
|
+
: `[${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
|
|
114
|
+
? `todo ${todoActive.done}/${todoActive.total}`
|
|
115
|
+
: `[${todoActive.done}/${todoActive.total}]`] })), servicesActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= PROC_LIMITS.statusCompactCols
|
|
116
|
+
? `svc ${servicesActive.live}`
|
|
117
|
+
: `[${servicesActive.live}]`] })), cols >= 60 && (cols >= 110 || (!running && !servicesActive && scrolledLines === 0)) && (_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 >= ABSOLUTE_PAIR_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: [" ", absolutePair] })), cols >= SESSION_TOTAL_MIN_COLS && _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)] }))] })] })] }));
|
|
73
118
|
}
|
package/dist/ui/TeamPanel.d.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import React from 'react';
|
|
20
20
|
import type { Theme } from './theme.js';
|
|
21
21
|
import type { TermCapabilities } from './capabilities.js';
|
|
22
|
+
import { type TeamPanelLayout } from './layout/team-panel.js';
|
|
22
23
|
import type { SubagentRun, TeamSnapshot } from '../team/types.js';
|
|
23
24
|
export interface TeamPanelProps {
|
|
24
25
|
snapshot: TeamSnapshot;
|
|
@@ -35,6 +36,7 @@ export interface TeamPanelProps {
|
|
|
35
36
|
caps: TermCapabilities;
|
|
36
37
|
/** Injectable clock so the panel test does not depend on wall time. */
|
|
37
38
|
now?: number;
|
|
39
|
+
layout?: TeamPanelLayout;
|
|
38
40
|
}
|
|
39
41
|
export declare function activityBudget(cols: number): number;
|
|
40
42
|
/**
|
|
@@ -55,4 +57,4 @@ export declare function activityBudget(cols: number): number;
|
|
|
55
57
|
* columns and break at 80.
|
|
56
58
|
*/
|
|
57
59
|
export declare function activityLine(run: SubagentRun, cols: number): string;
|
|
58
|
-
export declare function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, }: TeamPanelProps): React.ReactElement;
|
|
60
|
+
export declare function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, layout, }: TeamPanelProps): React.ReactElement | null;
|
package/dist/ui/TeamPanel.js
CHANGED
|
@@ -5,7 +5,7 @@ import { pickGlyphs } from './glyphs.js';
|
|
|
5
5
|
import { formatDuration } from '../agent/usage.js';
|
|
6
6
|
import { describeToolActivity, sanitizeActivity } from '../team/activity.js';
|
|
7
7
|
import { TEAM_LIMITS } from '../team/limits.js';
|
|
8
|
-
import {
|
|
8
|
+
import { buildTeamPanelLayout } from './layout/team-panel.js';
|
|
9
9
|
function isRunning(run) {
|
|
10
10
|
return run.phase === 'starting' || run.phase === 'thinking' || run.phase === 'tool' ||
|
|
11
11
|
run.phase === 'waiting';
|
|
@@ -101,24 +101,28 @@ export function activityLine(run, cols) {
|
|
|
101
101
|
return 'aborted';
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, }) {
|
|
104
|
+
export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, layout, }) {
|
|
105
|
+
const projection = layout ?? buildTeamPanelLayout({
|
|
106
|
+
snapshot, terminalRows: rows, availableRows: Infinity,
|
|
107
|
+
});
|
|
108
|
+
if (projection.rowCount === 0)
|
|
109
|
+
return null;
|
|
105
110
|
const glyphs = pickGlyphs(caps);
|
|
106
111
|
const at = now ?? Date.now();
|
|
107
112
|
const running = snapshot.runs.filter(isRunning).length;
|
|
108
113
|
const done = snapshot.runs.filter(isSettled).length;
|
|
109
114
|
const elapsed = formatDuration(Math.max(0, at - snapshot.startedAt));
|
|
110
115
|
const header = (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate", color: theme.accent, bold: true, children: [glyphs.teamAgent, " team", ' ', _jsxs(Text, { color: theme.muted, children: [running, " running ", glyphs.midDot, " ", done, " done ", glyphs.midDot, " ", elapsed] })] }), snapshot.messageCount > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.teamMail, " ", snapshot.messageCount] }))] }));
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
if (rows < TEAM_LIMITS.panelCollapseRows) {
|
|
116
|
+
// Short terminals or a long draft leave only the shared header projection,
|
|
117
|
+
// which still answers whether anything is running and for how long.
|
|
118
|
+
if (projection.collapsed) {
|
|
115
119
|
return _jsx(Box, { flexDirection: "column", flexShrink: 0, children: header });
|
|
116
120
|
}
|
|
117
121
|
// RANKED, not sliced by array index (F-2). The slot pool hands out indices in
|
|
118
122
|
// order, so `slice(0, 5)` showed the five children that finished FIRST and hid
|
|
119
123
|
// everything still working behind `+N more` - at exactly the fan-out widths
|
|
120
124
|
// the requirement permits.
|
|
121
|
-
const { visible, hiddenTotal, hiddenRunning } =
|
|
125
|
+
const { visible, hiddenTotal, hiddenRunning } = projection;
|
|
122
126
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [header, visible.map((run) => {
|
|
123
127
|
const color = statusColor(run, theme);
|
|
124
128
|
// One spinner per RUNNING row, and the row cap is 5 precisely because
|
|
@@ -126,8 +130,8 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
|
|
|
126
130
|
// `reducedMotion` and a non-Unicode terminal fall back to the same
|
|
127
131
|
// static marker, exactly as `AssistantEntry` and `ToolCard` already do.
|
|
128
132
|
const animate = isRunning(run) && !reducedMotion && caps.unicode;
|
|
129
|
-
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: `${run.label}${run.tier === 'fast' ? '~' : ''}`.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
|
|
130
|
-
}), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject] }))] }));
|
|
133
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: `${run.label}${run.tier === 'fast' ? '~' : ''}`.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description.replace(/\s+/g, ' ') }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityLine(run, cols)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
|
|
134
|
+
}), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject.replace(/\s+/g, ' ')] }))] }));
|
|
131
135
|
}
|
|
132
136
|
function marker(run, glyphs) {
|
|
133
137
|
if (run.phase === 'done')
|
package/dist/ui/TodoPanel.d.ts
CHANGED
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
* stop being a rail and start eating viewport height, growing and shrinking the
|
|
15
15
|
* transcript every time the list changed length.
|
|
16
16
|
*
|
|
17
|
-
* WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is
|
|
17
|
+
* WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 14-36
|
|
18
18
|
* columns wide and the system prompt's last line is "Respond in the language the
|
|
19
19
|
* user writes in", so CJK item text in an 18-column rail is the NORMAL case
|
|
20
20
|
* rather than the edge case. Every text cell is `wrap="truncate"` and Ink's
|
|
21
21
|
* truncation is display-width aware, so nothing can overflow the box — and NO
|
|
22
|
-
* ARITHMETIC HERE MAY BUDGET USER TEXT BY `.length`. The
|
|
23
|
-
* `padStart(2)` cells below are ASCII
|
|
22
|
+
* ARITHMETIC HERE MAY BUDGET USER TEXT BY `.length`. The count string and
|
|
23
|
+
* `padStart(2)` cells below are ASCII counts and indices, which is why they are
|
|
24
24
|
* safe; a "characters that fit" calculation over `content` would not be. That is
|
|
25
25
|
* the whole of the lesson `TeamPanel`'s `WIDE_CHAR_ALLOWANCE` records at length,
|
|
26
26
|
* and the mitigation here is to have no such budget at all.
|
|
@@ -34,7 +34,7 @@ export interface TodoPanelProps {
|
|
|
34
34
|
/** From `todoRailWidth(cols)`. The rail's whole column budget, separator included. */
|
|
35
35
|
width: number;
|
|
36
36
|
/**
|
|
37
|
-
* From `
|
|
37
|
+
* From `buildTodoRailLayout` after actual team/menu rows are subtracted.
|
|
38
38
|
* (I-10 / P1-3). The gate that mounts this component and the number it lays
|
|
39
39
|
* out against are ONE value computed once in `App`: a panel that mounts on one
|
|
40
40
|
* number and lays out against another renders nothing and reports nothing.
|
|
@@ -46,4 +46,4 @@ export interface TodoPanelProps {
|
|
|
46
46
|
theme: Theme;
|
|
47
47
|
caps: TermCapabilities;
|
|
48
48
|
}
|
|
49
|
-
export declare function TodoPanel({ snapshot, width, rows, running, reducedMotion, theme, caps, }: TodoPanelProps): React.ReactElement;
|
|
49
|
+
export declare function TodoPanel({ snapshot, width, rows, running, reducedMotion, theme, caps, }: TodoPanelProps): React.ReactElement | null;
|
package/dist/ui/TodoPanel.js
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* TodoPanel — the right rail (todo-plan-execution §6.1).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: every glyph comes from `pickGlyphs`; a literal here bypasses the
|
|
6
|
+
* capability probe and shows mojibake on a legacy console (`glyphs.ts` header).
|
|
7
|
+
*
|
|
8
|
+
* MOUNTED ONLY WHEN A LIST EXISTS. `App` passes `null` the rest of the time and
|
|
9
|
+
* this component is not rendered at all, so a session that never plans has zero
|
|
10
|
+
* columns and zero layout shift (R-g's "no furniture when idle" / AC-24).
|
|
11
|
+
*
|
|
12
|
+
* IT MUST BE MOUNTED IN `AppShell`'s MIDDLE BAND, NEVER IN THE BOTTOM CHROME
|
|
13
|
+
* (C-6 / I-4). This is a COLUMN BESIDE the viewport, and the middle band is the
|
|
14
|
+
* only row-wrapped region there is; moved down into the bottom chrome it would
|
|
15
|
+
* stop being a rail and start eating viewport height, growing and shrinking the
|
|
16
|
+
* transcript every time the list changed length.
|
|
17
|
+
*
|
|
18
|
+
* WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 14-36
|
|
19
|
+
* columns wide and the system prompt's last line is "Respond in the language the
|
|
20
|
+
* user writes in", so CJK item text in an 18-column rail is the NORMAL case
|
|
21
|
+
* rather than the edge case. Every text cell is `wrap="truncate"` and Ink's
|
|
22
|
+
* truncation is display-width aware, so nothing can overflow the box — and NO
|
|
23
|
+
* ARITHMETIC HERE MAY BUDGET USER TEXT BY `.length`. The count string and
|
|
24
|
+
* `padStart(2)` cells below are ASCII counts and indices, which is why they are
|
|
25
|
+
* safe; a "characters that fit" calculation over `content` would not be. That is
|
|
26
|
+
* the whole of the lesson `TeamPanel`'s `WIDE_CHAR_ALLOWANCE` records at length,
|
|
27
|
+
* and the mitigation here is to have no such budget at all.
|
|
28
|
+
*/
|
|
29
|
+
import { useMemo } from 'react';
|
|
2
30
|
import { Box, Text } from 'ink';
|
|
3
31
|
import Spinner from 'ink-spinner';
|
|
4
32
|
import { pickGlyphs } from './glyphs.js';
|
|
@@ -6,7 +34,7 @@ import { buildGauge } from './gauge.js';
|
|
|
6
34
|
import { railBorderProps } from './layout/Gutter.js';
|
|
7
35
|
import { TODO_RAIL_INDEX_MIN_COLS } from './layout/rail.js';
|
|
8
36
|
import { TODO_LIMITS } from '../todo/limits.js';
|
|
9
|
-
import { selectTodoRows } from '../todo/panel-rows.js';
|
|
37
|
+
import { selectCompactTodoRows, selectTodoRows } from '../todo/panel-rows.js';
|
|
10
38
|
/** Header row + the blank row under the gauge. */
|
|
11
39
|
const FIXED_ROWS = 2;
|
|
12
40
|
/** The gauge is the one purely decorative element, so it is the first to go. */
|
|
@@ -19,8 +47,11 @@ const MARKER_COLS = 3;
|
|
|
19
47
|
/** Index cell: two digits plus the space that separates it from the text. */
|
|
20
48
|
const INDEX_COLS = 3;
|
|
21
49
|
function marker(item, glyphs) {
|
|
22
|
-
|
|
23
|
-
|
|
50
|
+
// Ink's tokenizer otherwise treats the bare emoji-capable check as one cell,
|
|
51
|
+
// while string-width counts two, adding an extra visible space to each row.
|
|
52
|
+
if (item.status === 'completed') {
|
|
53
|
+
return glyphs.todoDone.length === 1 ? `${glyphs.todoDone}\uFE0F` : glyphs.todoDone;
|
|
54
|
+
}
|
|
24
55
|
if (item.status === 'in_progress')
|
|
25
56
|
return glyphs.todoActive;
|
|
26
57
|
return glyphs.todoPending;
|
|
@@ -34,41 +65,42 @@ function rowColor(item, theme) {
|
|
|
34
65
|
}
|
|
35
66
|
export function TodoPanel({ snapshot, width, rows, running, reducedMotion, theme, caps, }) {
|
|
36
67
|
const glyphs = pickGlyphs(caps);
|
|
68
|
+
// Ink reapplies all four Yoga borders when the borderStyle object changes,
|
|
69
|
+
// but unchanged borderTop/Bottom=false props are not reapplied on that update.
|
|
70
|
+
const border = useMemo(() => railBorderProps(glyphs.railVertical), [glyphs.railVertical]);
|
|
71
|
+
if (snapshot.items.length === 0 || rows < TODO_LIMITS.panelMinRows)
|
|
72
|
+
return null;
|
|
73
|
+
const compact = rows < TODO_LIMITS.panelFullRows;
|
|
37
74
|
const showGauge = width >= GAUGE_MIN_COLS && rows >= GAUGE_MIN_ROWS;
|
|
38
|
-
const showIndex = width >= TODO_RAIL_INDEX_MIN_COLS;
|
|
39
|
-
const
|
|
40
|
-
const
|
|
75
|
+
const showIndex = !compact && width >= TODO_RAIL_INDEX_MIN_COLS;
|
|
76
|
+
const compactFooter = compact && snapshot.items.length > rows - 1;
|
|
77
|
+
const itemRows = compact ? rows - 1 - Number(compactFooter)
|
|
78
|
+
: rows - FIXED_ROWS - Number(showGauge);
|
|
79
|
+
const { visible, hiddenAbove, hiddenBelow } = compact
|
|
80
|
+
? selectCompactTodoRows(snapshot.items, itemRows)
|
|
81
|
+
: selectTodoRows(snapshot.items, itemRows);
|
|
41
82
|
const pct = snapshot.total > 0 ? Math.round((snapshot.doneCount / snapshot.total) * 100) : 0;
|
|
42
83
|
const gauge = buildGauge(pct, Math.max(4, width - CHROME_COLS - 2), theme, caps);
|
|
43
84
|
const complete = snapshot.doneCount === snapshot.total;
|
|
85
|
+
const countText = `${snapshot.doneCount}/${snapshot.total}`;
|
|
86
|
+
const showDone = complete
|
|
87
|
+
&& width - CHROME_COLS >= 'TODO '.length + countText.length + ' done'.length;
|
|
88
|
+
const count = `${countText}${showDone ? ' done' : ''}`;
|
|
44
89
|
return (
|
|
45
90
|
// `flexShrink={0}` IS LOAD-BEARING (P1-4). Ink's `Box.defaultProps` is
|
|
46
91
|
// `{ flexWrap:'nowrap', flexDirection:'row', flexGrow:0, flexShrink:1 }`, so
|
|
47
92
|
// a `width={n}` box inside a shrinking row is SQUEEZED under pressure — and
|
|
48
93
|
// every piece of arithmetic in `rail.ts` silently becomes a suggestion at
|
|
49
94
|
// exactly the narrow widths where the guarantee matters. `ScrollIndicator`
|
|
50
|
-
// spells the same pair one level down.
|
|
51
|
-
//
|
|
52
|
-
_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, overflow: "hidden", paddingLeft: 1, ...
|
|
95
|
+
// spells the same pair one level down. Height uses the same row budget as
|
|
96
|
+
// selection, so content cannot silently enlarge the middle band.
|
|
97
|
+
_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, height: rows, overflow: "hidden", paddingLeft: 1, ...border, borderColor: theme.border, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: "TODO" }) }), _jsx(Box, { width: count.length, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", color: complete ? theme.toolDone : theme.muted, children: count }) })] }), showGauge && (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty })] })), !compact && _jsx(Text, { children: " " }), !compact && hiddenAbove > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenAbove, " above"] })), visible.map(({ item, index }) => {
|
|
53
98
|
const active = item.status === 'in_progress';
|
|
54
99
|
// The spinner is gated on `running`: a spinner while the agent is idle
|
|
55
100
|
// is a lie about the state of the world, and this panel outlives the run
|
|
56
101
|
// that filled it. Reduced motion and a non-Unicode terminal fall back to
|
|
57
102
|
// the same static marker, exactly as `AssistantEntry` and `TeamPanel` do.
|
|
58
103
|
const animate = active && running && !reducedMotion && caps.unicode;
|
|
59
|
-
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, width: MARKER_COLS, children: _jsx(Text, { color: rowColor(item, theme), bold: active, children: animate ? _jsx(Spinner, { type: "dots" }) : marker(item, glyphs) }) }), showIndex && (_jsx(Box, { flexShrink: 0, width: INDEX_COLS, children: _jsx(Text, { color: theme.muted, children: String(index + 1).padStart(2) }) })), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap:
|
|
60
|
-
}), hiddenBelow > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenBelow, " more"] }))] }));
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Keep the wrapping row inside `TODO_LIMITS.activeWrapRows`.
|
|
64
|
-
*
|
|
65
|
-
* A CHARACTER BUDGET IS NOT A COLUMN BUDGET (P2-3), so this is deliberately
|
|
66
|
-
* generous rather than exact: it exists to stop a maximum-length `activeForm`
|
|
67
|
-
* from claiming five rows of an 18-column rail, and Ink's display-width-aware
|
|
68
|
-
* truncation is what actually keeps the row inside the box. Erring long costs
|
|
69
|
-
* nothing — the surrounding `overflow="hidden"` clips it.
|
|
70
|
-
*/
|
|
71
|
-
function clampWrapped(text, width) {
|
|
72
|
-
const budget = Math.max(1, width - CHROME_COLS) * TODO_LIMITS.activeWrapRows;
|
|
73
|
-
return text.length > budget ? text.slice(0, budget) : text;
|
|
104
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, width: MARKER_COLS, children: _jsx(Text, { color: rowColor(item, theme), bold: active, children: animate ? _jsx(Spinner, { type: "dots" }) : marker(item, glyphs) }) }), showIndex && (_jsx(Box, { flexShrink: 0, width: INDEX_COLS, children: _jsx(Text, { color: theme.muted, children: String(index + 1).padStart(2) }) })), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: rowColor(item, theme), bold: active, children: active ? item.activeForm : item.content }) })] }, `${index}-${item.content}`));
|
|
105
|
+
}), compactFooter && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: ["-", hiddenAbove, " +", hiddenBelow] })), !compact && hiddenBelow > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenBelow, " more"] }))] }, glyphs.railVertical));
|
|
74
106
|
}
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -336,6 +336,7 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
|
|
|
336
336
|
entries: visible,
|
|
337
337
|
heightOf,
|
|
338
338
|
viewportRows: geometry.viewportRows,
|
|
339
|
+
trailingContentRows: geometry.trailingContentRows,
|
|
339
340
|
offset: geometry.offset,
|
|
340
341
|
overscan: VIRTUAL_LIMITS.overscan,
|
|
341
342
|
});
|
package/dist/ui/UpdateLine.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* still truthy. Taking that branch would emit a box of ZERO rows - the frame
|
|
10
10
|
* keeps its fixed height, Yoga hands the row to the transcript, and
|
|
11
11
|
* `viewportRows()` goes on reporting the old number to `ScrollViewport`,
|
|
12
|
-
* `selectWindow`, `overlayMaxRows`, `popupMaxRows` and `
|
|
12
|
+
* `selectWindow`, `overlayMaxRows`, `popupMaxRows` and `buildTodoRailLayout`. The
|
|
13
13
|
* transcript would gain a row while the updater is idle and lose it the moment
|
|
14
14
|
* it has news. Presence is the CALLER's decision, through
|
|
15
15
|
* `shouldRenderUpdateLine`, exactly as `App.tsx` already decides the activity
|
package/dist/ui/capabilities.js
CHANGED
|
@@ -15,10 +15,6 @@ function detectColorLevel(env) {
|
|
|
15
15
|
return 0;
|
|
16
16
|
if (env.TERM === 'dumb')
|
|
17
17
|
return 0;
|
|
18
|
-
// A truecolor-capable terminal advertises itself.
|
|
19
|
-
const colorterm = env.COLORTERM?.toLowerCase();
|
|
20
|
-
if (colorterm === 'truecolor' || colorterm === '24bit')
|
|
21
|
-
return 3;
|
|
22
18
|
// FORCE_COLOR is an explicit override of the auto-detected depth.
|
|
23
19
|
const force = env.FORCE_COLOR;
|
|
24
20
|
if (force !== undefined) {
|
|
@@ -30,6 +26,10 @@ function detectColorLevel(env) {
|
|
|
30
26
|
return 3;
|
|
31
27
|
return 1; // '1' | 'true' | '' → at least basic color
|
|
32
28
|
}
|
|
29
|
+
// A truecolor-capable terminal advertises itself.
|
|
30
|
+
const colorterm = env.COLORTERM?.toLowerCase();
|
|
31
|
+
if (colorterm === 'truecolor' || colorterm === '24bit')
|
|
32
|
+
return 3;
|
|
33
33
|
// Fall back to chalk's own platform detection.
|
|
34
34
|
const level = chalk.level;
|
|
35
35
|
return level >= 0 && level <= 3 ? level : 0;
|
|
@@ -42,7 +42,7 @@ function detectUnicode(env) {
|
|
|
42
42
|
if (env.WT_SESSION)
|
|
43
43
|
return true; // Windows Terminal
|
|
44
44
|
const program = env.TERM_PROGRAM;
|
|
45
|
-
if (program === 'vscode' || program === 'iTerm.app' || program === 'Apple_Terminal')
|
|
45
|
+
if (program === 'aragonmesh' || program === 'vscode' || program === 'iTerm.app' || program === 'Apple_Terminal')
|
|
46
46
|
return true;
|
|
47
47
|
// Legacy cmd.exe (no TERM, no WT_SESSION) and unknown terminals fall back to ASCII.
|
|
48
48
|
return false;
|
package/dist/ui/composer-rows.js
CHANGED
|
@@ -52,6 +52,32 @@ export function displayWidth(text) {
|
|
|
52
52
|
w += charWidth(ch);
|
|
53
53
|
return w;
|
|
54
54
|
}
|
|
55
|
+
/** Normalize only the visual projection; the editor keeps its UTF-16 index. */
|
|
56
|
+
function visualCursor(buffer, cursor) {
|
|
57
|
+
let index = Math.max(0, Math.min(buffer.length, cursor));
|
|
58
|
+
const code = buffer.charCodeAt(index);
|
|
59
|
+
const previous = buffer.charCodeAt(index - 1);
|
|
60
|
+
if (code >= 0xdc00 && code <= 0xdfff && previous >= 0xd800 && previous <= 0xdbff)
|
|
61
|
+
index -= 1;
|
|
62
|
+
if (index === buffer.length || buffer[index] === '\n')
|
|
63
|
+
return index;
|
|
64
|
+
const ch = String.fromCodePoint(buffer.codePointAt(index));
|
|
65
|
+
if (ch === '\t' || charWidth(ch) > 0)
|
|
66
|
+
return index;
|
|
67
|
+
const start = buffer.lastIndexOf('\n', index - 1) + 1;
|
|
68
|
+
let nearest = -1;
|
|
69
|
+
let i = start;
|
|
70
|
+
while (i < buffer.length && buffer[i] !== '\n') {
|
|
71
|
+
const point = String.fromCodePoint(buffer.codePointAt(i));
|
|
72
|
+
if (point === '\t' || charWidth(point) > 0) {
|
|
73
|
+
if (i > index)
|
|
74
|
+
return nearest >= 0 ? nearest : i;
|
|
75
|
+
nearest = i;
|
|
76
|
+
}
|
|
77
|
+
i += point.length;
|
|
78
|
+
}
|
|
79
|
+
return nearest >= 0 ? nearest : i;
|
|
80
|
+
}
|
|
55
81
|
/**
|
|
56
82
|
* Wrap `buffer` into rows, locate the caret, and select a window of at most
|
|
57
83
|
* `maxRows` rows that contains it.
|
|
@@ -62,7 +88,8 @@ export function displayWidth(text) {
|
|
|
62
88
|
* to live in the reducer and would then need its own invalidation rules.
|
|
63
89
|
*/
|
|
64
90
|
export function layoutComposer(input) {
|
|
65
|
-
const { buffer,
|
|
91
|
+
const { buffer, active } = input;
|
|
92
|
+
const cursor = visualCursor(buffer, input.cursor);
|
|
66
93
|
const cols = Math.max(1, Math.floor(input.cols));
|
|
67
94
|
const maxRows = input.maxRows >= 1 ? Math.floor(input.maxRows) : 1;
|
|
68
95
|
const spans = tokenSpans(buffer);
|
|
@@ -111,6 +138,7 @@ export function layoutComposer(input) {
|
|
|
111
138
|
const ch = String.fromCodePoint(buffer.codePointAt(i));
|
|
112
139
|
if (ch === '\n') {
|
|
113
140
|
if (atCursor) {
|
|
141
|
+
ensureRoom(1);
|
|
114
142
|
cursorRowAbs = rows.length;
|
|
115
143
|
cursorCol = col;
|
|
116
144
|
}
|
|
@@ -182,20 +210,33 @@ export function splitRowAtColumn(row, column) {
|
|
|
182
210
|
else
|
|
183
211
|
list.push({ text, kind });
|
|
184
212
|
};
|
|
213
|
+
const cells = [];
|
|
214
|
+
let leading = '';
|
|
185
215
|
for (const segment of row.segments) {
|
|
186
|
-
if (at !== null) {
|
|
187
|
-
push(after, segment.text, segment.kind);
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
216
|
for (const ch of segment.text) {
|
|
191
|
-
if (
|
|
192
|
-
|
|
217
|
+
if (charWidth(ch) === 0) {
|
|
218
|
+
const last = cells[cells.length - 1];
|
|
219
|
+
if (last)
|
|
220
|
+
cells[cells.length - 1] = { ...last, text: last.text + ch };
|
|
221
|
+
else
|
|
222
|
+
leading += ch;
|
|
193
223
|
}
|
|
194
224
|
else {
|
|
195
|
-
push(
|
|
225
|
+
cells.push({ text: leading + ch, kind: segment.kind });
|
|
226
|
+
leading = '';
|
|
196
227
|
}
|
|
197
|
-
x += charWidth(ch);
|
|
198
228
|
}
|
|
199
229
|
}
|
|
230
|
+
// A zero-width-only row keeps its text before the explicitly budgeted space.
|
|
231
|
+
if (leading)
|
|
232
|
+
push(before, leading, row.segments[0]?.kind ?? 'text');
|
|
233
|
+
for (const cell of cells) {
|
|
234
|
+
const width = displayWidth(cell.text);
|
|
235
|
+
if (at === null && column >= x && column < x + width)
|
|
236
|
+
at = cell;
|
|
237
|
+
else
|
|
238
|
+
push(at === null ? before : after, cell.text, cell.kind);
|
|
239
|
+
x += width;
|
|
240
|
+
}
|
|
200
241
|
return { before, at, after };
|
|
201
242
|
}
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
import type { Entry, UsageTotal } from '../agent/reducer.js';
|
|
15
15
|
export interface ExitSnapshot {
|
|
16
16
|
entries: Entry[];
|
|
17
|
+
/**
|
|
18
|
+
* The session total, FORWARDED not copied.
|
|
19
|
+
*
|
|
20
|
+
* It gained `cacheReadTokens` / `cacheWriteTokens`
|
|
21
|
+
* (context-usage-gauge-accuracy §3.6) and this file needed no change for it,
|
|
22
|
+
* which is the point of referencing the reducer's type rather than restating
|
|
23
|
+
* its fields: `renderTranscriptText` reads the total through `promptTokensOf`,
|
|
24
|
+
* so the exit summary and the status bar cannot drift apart.
|
|
25
|
+
*/
|
|
17
26
|
usageTotal: UsageTotal;
|
|
18
27
|
provider: string;
|
|
19
28
|
model: string;
|
package/dist/ui/frame-differ.js
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
* mode of it firing spuriously is one extra full repaint.
|
|
39
39
|
*/
|
|
40
40
|
import { MIN_FULLSCREEN_ROWS } from './layout/frame.js';
|
|
41
|
+
import { matchInkErasePrefix } from './frame-parser.js';
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
42
43
|
// Constants — exact, so no call site ever re-derives an escape sequence.
|
|
43
44
|
// ---------------------------------------------------------------------------
|
|
@@ -55,18 +56,6 @@ const ERASE_DOWN = `${CSI}J`;
|
|
|
55
56
|
/** DEC private mode 2026 — begin/end synchronized update (F2, §3.3). */
|
|
56
57
|
const SYNC_BEGIN = `${CSI}?2026h`;
|
|
57
58
|
const SYNC_END = `${CSI}?2026l`;
|
|
58
|
-
/**
|
|
59
|
-
* `eraseLines(n)` for any n ≥ 1, anchored at the start of the chunk.
|
|
60
|
-
*
|
|
61
|
-
* FALLBACK ONLY (P2-3). Whenever `prev !== null` the differ already knows the
|
|
62
|
-
* exact prefix it expects — `eraseLinesPrefix(prev.length + 1)` is a
|
|
63
|
-
* deterministic string — so the hot path is `chunk.startsWith(expected)`:
|
|
64
|
-
* linear, unambiguous, and it collapses step 7's `erased !== prev.length + 1`
|
|
65
|
-
* test into the same comparison. This regex is reached only on the first
|
|
66
|
-
* diffable chunk after a session start or an `invalidate()`, where no
|
|
67
|
-
* expectation exists yet.
|
|
68
|
-
*/
|
|
69
|
-
const ERASE_PREFIX_RE = /^(?:\x1b\[2K(?:\x1b\[1A)?)+\x1b\[G/;
|
|
70
59
|
function cursorTo(row) {
|
|
71
60
|
return `${CSI}${row};1H`;
|
|
72
61
|
}
|
|
@@ -88,9 +77,6 @@ export function eraseLinesPrefix(count) {
|
|
|
88
77
|
}
|
|
89
78
|
return out + CURSOR_LEFT;
|
|
90
79
|
}
|
|
91
|
-
function countErases(prefix) {
|
|
92
|
-
return prefix.split(ERASE_LINE).length - 1;
|
|
93
|
-
}
|
|
94
80
|
export function createFrameDiffer(options) {
|
|
95
81
|
/**
|
|
96
82
|
* Last frame AS PAINTED — what is actually on the screen.
|
|
@@ -200,10 +186,7 @@ export function createFrameDiffer(options) {
|
|
|
200
186
|
if (chunk.startsWith(expected))
|
|
201
187
|
return { prefix: expected, erased: prev.length + 1 };
|
|
202
188
|
}
|
|
203
|
-
|
|
204
|
-
if (m === null)
|
|
205
|
-
return null;
|
|
206
|
-
return { prefix: m[0], erased: countErases(m[0]) };
|
|
189
|
+
return matchInkErasePrefix(chunk);
|
|
207
190
|
};
|
|
208
191
|
/**
|
|
209
192
|
* Full repaint — the first diffable frame of a session, every invalidation
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ScrollbarBridge } from './scrollbar-controller.js';
|
|
2
|
+
export interface FrameSummary {
|
|
3
|
+
columns: number;
|
|
4
|
+
rows: number;
|
|
5
|
+
trackTop: number;
|
|
6
|
+
trackRows: number;
|
|
7
|
+
}
|
|
8
|
+
/** Observe before differ transformation, but acknowledge only after downstream write returns. */
|
|
9
|
+
export declare function createFrameObserver(options: {
|
|
10
|
+
stdout: NodeJS.WriteStream;
|
|
11
|
+
terminal: NodeJS.WriteStream;
|
|
12
|
+
scrollbar: ScrollbarBridge;
|
|
13
|
+
}): {
|
|
14
|
+
stdout: NodeJS.WriteStream;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
invalidate(): void;
|
|
17
|
+
};
|