omniharness-cli 0.1.82 → 0.1.83
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/dist/ui/terminalInterface.js +33 -9
- package/dist/ui/toolrow.js +53 -0
- package/dist/ui/viewport.js +62 -0
- package/package.json +1 -1
|
@@ -10,6 +10,8 @@ import { looksLikeDiff, diffSegments } from './diff.js';
|
|
|
10
10
|
import { palette } from './palette.js';
|
|
11
11
|
import { capabilityLine, recentRows, shortenPath, twoColumn } from './home.js';
|
|
12
12
|
import { conversationWidth, overflowCount, sidebarMode, SIDEBAR_WIDTH, todoRows, usageRows, clip as clipRow } from './sidebar.js';
|
|
13
|
+
import { planViewport } from './viewport.js';
|
|
14
|
+
import { statusMarker, toolHead } from './toolrow.js';
|
|
13
15
|
import { contextMeter, meterBar } from './modelWindows.js';
|
|
14
16
|
import { BEL, SYNC_QUERY, isSyncOutputReply, osc9Notify, osc52Copy, shouldNudgeOnFinish, wrapSynchronizedOutput } from './termcaps.js';
|
|
15
17
|
import { KITTY_POP, KITTY_PUSH, KITTY_QUERY, isEncodedKey, isKittyQueryResponse, parseRawKey } from './keys.js';
|
|
@@ -87,8 +89,12 @@ const PERM_COLOR = (p) => (p === 'bypass' ? PALETTE.error : p === 'acceptEdits'
|
|
|
87
89
|
* the end of it, and the chrome ends up further from what it describes.
|
|
88
90
|
*/
|
|
89
91
|
const MAX_MEASURE = 100;
|
|
92
|
+
/** Shown after an expanded tool head; the head reserves room for it. */
|
|
93
|
+
const COLLAPSE_HINT = ' Ctrl+T collapse';
|
|
90
94
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
91
95
|
const widthOf = (stdout) => Math.max(48, stdout.columns ?? 80);
|
|
96
|
+
/** Terminal height, floored so the layout maths never goes negative. */
|
|
97
|
+
const rowsOf = (stdout) => Math.max(8, stdout.rows ?? 24);
|
|
92
98
|
const clip = (text, width) => text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}…`;
|
|
93
99
|
/** Word-wrap text to width, honoring existing newlines and hard-breaking long words. */
|
|
94
100
|
function wrap(text, width) {
|
|
@@ -244,6 +250,12 @@ export function TerminalInterface({ engine }) {
|
|
|
244
250
|
const { stdout } = useStdout();
|
|
245
251
|
const { stdin } = useStdin();
|
|
246
252
|
const [width, setWidth] = useState(() => widthOf(stdout));
|
|
253
|
+
// Height has to be state for the same reason width is. Maximising and
|
|
254
|
+
// then floating a window changes rows without necessarily changing
|
|
255
|
+
// columns, and setting width to the value it already had re-renders
|
|
256
|
+
// nothing — so the live region kept the height budget of the old
|
|
257
|
+
// terminal and overran the viewport.
|
|
258
|
+
const [rows, setRows] = useState(() => rowsOf(stdout));
|
|
247
259
|
const [edit, setEdit] = useState({ value: '', cursor: 0 });
|
|
248
260
|
const inputWidth = Math.max(16, Math.min(width, MAX_MEASURE) - 12); // recomputed below once the split is known
|
|
249
261
|
// Settled transcript. Rendered once each into <Static> — the terminal's own
|
|
@@ -301,7 +313,10 @@ export function TerminalInterface({ engine }) {
|
|
|
301
313
|
return () => { alive = false; };
|
|
302
314
|
}, []);
|
|
303
315
|
useEffect(() => {
|
|
304
|
-
const onResize = () =>
|
|
316
|
+
const onResize = () => {
|
|
317
|
+
setWidth(widthOf(stdout));
|
|
318
|
+
setRows(rowsOf(stdout));
|
|
319
|
+
};
|
|
305
320
|
stdout.on('resize', onResize);
|
|
306
321
|
stdout.write(KITTY_PUSH);
|
|
307
322
|
let kittyTimer;
|
|
@@ -964,7 +979,7 @@ export function TerminalInterface({ engine }) {
|
|
|
964
979
|
const gutter = Math.max(2, Math.floor((width - measure) / 2));
|
|
965
980
|
const convoWidth = conversationWidth(measure, sideMode);
|
|
966
981
|
const contentWidth = Math.max(20, convoWidth - 6);
|
|
967
|
-
const terminalRows =
|
|
982
|
+
const terminalRows = rows;
|
|
968
983
|
const metrics = engine.client.snapshotMetrics();
|
|
969
984
|
const meter = contextMeter(metrics.compression.inputTokens, engine.state.activeModel, metrics.fallback.activeProvider);
|
|
970
985
|
const meterColor = meter.zone === 'danger' ? PALETTE.error : meter.zone === 'warn' ? PALETTE.warn : PALETTE.muted;
|
|
@@ -980,18 +995,27 @@ export function TerminalInterface({ engine }) {
|
|
|
980
995
|
const liveAnswerLines = useMemo(() => renderMarkdown(liveAnswer, contentWidth), [liveAnswer, contentWidth]);
|
|
981
996
|
// Cap the streaming region so a long think/answer can't crowd out the chrome;
|
|
982
997
|
// the complete text lands in <Static> once the event fires.
|
|
983
|
-
|
|
998
|
+
// What fits. The old budget subtracted a fixed 14 rows of guesswork and
|
|
999
|
+
// never bounded the sections it was competing with, so a short terminal
|
|
1000
|
+
// still produced a frame taller than itself.
|
|
1001
|
+
const view = planViewport({
|
|
1002
|
+
rows: terminalRows,
|
|
1003
|
+
editorLines: editorLayout.lines.length,
|
|
1004
|
+
toolCards: toolCards.length,
|
|
1005
|
+
todos: taskQueue.length,
|
|
1006
|
+
wantHero: lines.length === 0 && !busy,
|
|
1007
|
+
});
|
|
1008
|
+
const liveBudget = view.liveLines;
|
|
984
1009
|
const liveThinkView = liveThinkLines.slice(-Math.max(2, Math.floor(liveBudget / 2)));
|
|
985
1010
|
const liveAnswerView = liveAnswerLines.slice(-liveBudget);
|
|
986
1011
|
const doneAgents = agents.filter((lane) => lane.status === 'done').length;
|
|
987
1012
|
const panel = _jsx(SidebarPanel, { width: SIDEBAR_WIDTH, model: engine.state.activeModel, mode: mode, perm: permMode, workspace: engine.state.workspace.root, usage: { tokensIn: metrics.compression.inputTokens, requests: metrics.requestCount }, agents: agents, todos: taskQueue, skills: engine.skills.length, plugins: new Set(engine.skills.map((skill) => skill.source).filter((source) => source !== undefined)).size });
|
|
988
|
-
return _jsxs(Box, { flexDirection: "column", width: width, paddingLeft: gutter, paddingRight: gutter, children: [_jsx(Static, { items: lines, children: (line, index) => _jsx(TranscriptEntry, { line: line, width: contentWidth, fallbackModel: engine.state.activeModel }, index) }, staticKey), sideMode === 'replace' && _jsx(Box, { marginBottom: 1, children: panel }), _jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Box, { flexDirection: "column", width: sideMode === 'split' ? convoWidth : undefined, flexGrow: sideMode === 'split' ? 0 : 1, children: [
|
|
1013
|
+
return _jsxs(Box, { flexDirection: "column", width: width, paddingLeft: gutter, paddingRight: gutter, children: [_jsx(Static, { items: lines, children: (line, index) => _jsx(TranscriptEntry, { line: line, width: contentWidth, fallbackModel: engine.state.activeModel }, index) }, staticKey), sideMode === 'replace' && _jsx(Box, { marginBottom: 1, children: panel }), _jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Box, { flexDirection: "column", width: sideMode === 'split' ? convoWidth : undefined, flexGrow: sideMode === 'split' ? 0 : 1, children: [view.showHero && _jsx(Hero, { width: contentWidth + 2, endpoint: engine.client.endpoint ?? 'omniroute', model: engine.state.activeModel, mode: mode, perm: permMode, workspace: engine.state.workspace.root, sessions: recentSessions, skills: engine.skills.length, plugins: new Set(engine.skills.map((skill) => skill.source).filter((source) => source !== undefined)).size, mcpTools: engine.mcpTools.length }), liveThink !== '' && _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: PALETTE.warn, children: "\u00B7 thinking" }), liveThinkView.map((segments, index) => _jsx(SegmentText, { segments: segments, role: "thinking" }, index))] }), liveAnswer !== '' && _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: PALETTE.accent, children: engine.state.activeModel }), liveAnswerView.map((segments, index) => _jsx(SegmentText, { segments: segments, role: "assistant" }, index))] }), (view.toolCards > 0 ? toolCards.slice(-view.toolCards) : []).map((card) => {
|
|
989
1014
|
const expanded = expandedTool === card.id;
|
|
990
|
-
const
|
|
991
|
-
const
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
return _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [dot, " ", head, expanded ? ' Ctrl+T collapse' : ''] }), expanded && renderToolBody(card, contentWidth, PALETTE)] }, card.id);
|
|
1015
|
+
const status = card.status === 'running' ? 'running' : card.status === 'error' ? 'error' : 'done';
|
|
1016
|
+
const statusColor = status === 'running' ? PALETTE.warn : status === 'error' ? PALETTE.error : PALETTE.success;
|
|
1017
|
+
const head = toolHead(card.name === 'run_command' ? '$' : toolVerb(card.name), card.name === 'run_command' ? (card.target || '…') : card.target, contentWidth, expanded ? COLLAPSE_HINT.length : 0);
|
|
1018
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: statusColor, children: statusMarker(status) }), _jsx(Text, { dimColor: true, children: head }), expanded ? _jsx(Text, { dimColor: true, children: COLLAPSE_HINT }) : null] }), expanded && _jsx(Box, { borderStyle: "round", borderColor: statusColor, borderTop: false, borderBottom: false, borderRight: false, paddingLeft: 1, marginLeft: 1, flexDirection: "column", children: renderToolBody(card, Math.max(10, contentWidth - 4), PALETTE) })] }, card.id);
|
|
995
1019
|
}), sideMode === 'hidden' && agents.length > 0 && _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.error, paddingX: 2, marginTop: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: PALETTE.error, children: "swarm" }), _jsxs(Text, { dimColor: true, children: [doneAgents, "/", agents.length, " lanes done"] })] }), agents.map((lane, index) => {
|
|
996
1020
|
const color = AGENT_COLORS[index % AGENT_COLORS.length];
|
|
997
1021
|
const glyph = lane.status === 'done' ? 'ok' : lane.status === 'error' ? 'FAIL' : lane.status === 'working' ? '..' : '--';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool row composition.
|
|
3
|
+
*
|
|
4
|
+
* Follows the two shapes OpenCode uses (MIT, anomalyco/opencode): a one-line
|
|
5
|
+
* inline row with a fixed-width status column so descriptions align down the
|
|
6
|
+
* page, and a block form for output, marked with a rule down its left edge
|
|
7
|
+
* rather than boxed on all four sides — a full border around every tool
|
|
8
|
+
* result turns a transcript into a stack of crates.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Width of the status column. Every marker is padded to it, so the tool
|
|
12
|
+
* descriptions start at the same column whatever happened to the call. The
|
|
13
|
+
* markers were 'ok' (2), 'FAIL' (4) and '..' (2), which meant every failure
|
|
14
|
+
* shunted its own description two columns right.
|
|
15
|
+
*/
|
|
16
|
+
// Five, not four: the longest marker is 'FAIL', and padding to its own
|
|
17
|
+
// length leaves no gap, so a failed call rendered as "FAIL$ go test ...".
|
|
18
|
+
// The extra column is the separator, which is why the head is not padded
|
|
19
|
+
// again on the other side.
|
|
20
|
+
export const STATUS_WIDTH = 5;
|
|
21
|
+
/** Plain-word status marker, padded to a fixed column. No glyphs. */
|
|
22
|
+
export function statusMarker(status) {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case 'running': return '..'.padEnd(STATUS_WIDTH);
|
|
25
|
+
case 'error': return 'FAIL'.padEnd(STATUS_WIDTH);
|
|
26
|
+
case 'denied': return 'no'.padEnd(STATUS_WIDTH);
|
|
27
|
+
default: return 'ok'.padEnd(STATUS_WIDTH);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The text beside the marker: a verb and its target, clipped to the room
|
|
32
|
+
* actually left once the status column and the indent are accounted for.
|
|
33
|
+
* Sizing this to the full width is what makes rows wrap and the column
|
|
34
|
+
* collapse.
|
|
35
|
+
*/
|
|
36
|
+
export function toolHead(verb, target, width, reserve = 0) {
|
|
37
|
+
// `reserve` is room kept for something drawn after the head on the same
|
|
38
|
+
// row — the collapse hint, today. Without it the hint pushed itself onto a
|
|
39
|
+
// line of its own, which is worse than not showing it.
|
|
40
|
+
const room = Math.max(8, width - STATUS_WIDTH - 2 - Math.max(0, reserve));
|
|
41
|
+
if (target === '')
|
|
42
|
+
return clip(verb, room);
|
|
43
|
+
const head = `${verb} ${target}`;
|
|
44
|
+
return clip(head, room);
|
|
45
|
+
}
|
|
46
|
+
export function clip(text, width) {
|
|
47
|
+
const flat = text.replace(/\s+/g, ' ');
|
|
48
|
+
const runes = [...flat];
|
|
49
|
+
if (runes.length <= width)
|
|
50
|
+
return flat;
|
|
51
|
+
return `${runes.slice(0, Math.max(0, width - 1)).join('')}…`;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=toolrow.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much of the live region fits in the terminal.
|
|
3
|
+
*
|
|
4
|
+
* Ink redraws its live region by walking the cursor up over the lines it
|
|
5
|
+
* wrote last time and erasing them. That accounting only holds while the
|
|
6
|
+
* frame fits on screen: once it is taller than the viewport the terminal
|
|
7
|
+
* scrolls it, the cursor no longer lands where Ink expects, and the redraw
|
|
8
|
+
* eats the transcript above or leaves a trail of half-erased frames. It shows
|
|
9
|
+
* up as the display "going weird" after a resize, because maximising and then
|
|
10
|
+
* floating a window is the ordinary way to make the viewport smaller than the
|
|
11
|
+
* frame that was drawn for it.
|
|
12
|
+
*
|
|
13
|
+
* So the sections that can grow are given a budget rather than a fixed cap.
|
|
14
|
+
*/
|
|
15
|
+
/** Rows the input frame, status line and key hints always need. */
|
|
16
|
+
const CHROME_ROWS = 6;
|
|
17
|
+
/** Rows the home screen occupies when it is drawn. */
|
|
18
|
+
const HERO_ROWS = 12;
|
|
19
|
+
/** Streaming never drops below this, or a running turn looks like a hang. */
|
|
20
|
+
const LIVE_MIN = 2;
|
|
21
|
+
const MAX_TOOL_CARDS = 5;
|
|
22
|
+
const MAX_TODO_ROWS = 6;
|
|
23
|
+
/**
|
|
24
|
+
* Decide what fits. Order of sacrifice, least useful first: the home screen
|
|
25
|
+
* goes before tool cards, tool cards before the queue, and streaming text
|
|
26
|
+
* keeps a floor because a turn with nothing visible reads as a hang.
|
|
27
|
+
*/
|
|
28
|
+
export function planViewport(input) {
|
|
29
|
+
const rows = Math.max(8, Math.floor(input.rows));
|
|
30
|
+
const editor = Math.max(1, Math.floor(input.editorLines));
|
|
31
|
+
let free = rows - CHROME_ROWS - editor;
|
|
32
|
+
// The home screen only appears before the first turn, and only when there
|
|
33
|
+
// is genuinely room: half a home screen is worse than none.
|
|
34
|
+
const showHero = input.wantHero && free >= HERO_ROWS + LIVE_MIN;
|
|
35
|
+
if (showHero)
|
|
36
|
+
free -= HERO_ROWS;
|
|
37
|
+
// The bounded sections are allocated first and streaming absorbs the rest.
|
|
38
|
+
// Taking a share of the remainder for streaming up front looks fair and is
|
|
39
|
+
// not: on a tall terminal it starved the queue of rows there was plenty of
|
|
40
|
+
// room for.
|
|
41
|
+
let budget = free - LIVE_MIN;
|
|
42
|
+
const toolCards = clamp(input.toolCards, 0, Math.max(0, Math.min(MAX_TOOL_CARDS, budget)));
|
|
43
|
+
budget -= toolCards;
|
|
44
|
+
const todoRows = clamp(input.todos, 0, Math.max(0, Math.min(MAX_TODO_ROWS, budget)));
|
|
45
|
+
budget -= todoRows;
|
|
46
|
+
// Whatever is left goes to the running turn, never below the floor — a turn
|
|
47
|
+
// with nothing visible reads as a hang.
|
|
48
|
+
const liveLines = LIVE_MIN + Math.max(0, budget);
|
|
49
|
+
return { toolCards, todoRows, liveLines, showHero };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The smallest frame this can produce: the input and the chrome around it,
|
|
53
|
+
* plus the streaming floor. Below this the terminal is simply too short and
|
|
54
|
+
* nothing can be given up to fix it.
|
|
55
|
+
*/
|
|
56
|
+
export function minimumHeight(editorLines) {
|
|
57
|
+
return CHROME_ROWS + Math.max(1, Math.floor(editorLines)) + LIVE_MIN;
|
|
58
|
+
}
|
|
59
|
+
function clamp(value, min, max) {
|
|
60
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=viewport.js.map
|