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