omniharness-cli 0.1.37 → 0.1.39
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/agent/mastraEngine.d.ts +36 -1
- package/dist/agent/mastraEngine.js +144 -6
- package/dist/agent/mastraEngine.js.map +1 -1
- package/dist/cli.js +33 -1
- package/dist/cli.js.map +1 -1
- package/dist/sessionList.d.ts +20 -0
- package/dist/sessionList.js +67 -0
- package/dist/sessionList.js.map +1 -0
- package/dist/ui/groups.d.ts +34 -0
- package/dist/ui/groups.js +50 -0
- package/dist/ui/groups.js.map +1 -0
- package/dist/ui/highlight.d.ts +22 -0
- package/dist/ui/highlight.js +148 -0
- package/dist/ui/highlight.js.map +1 -0
- package/dist/ui/markdown.js +4 -5
- package/dist/ui/markdown.js.map +1 -1
- package/dist/ui/modelWindows.d.ts +29 -0
- package/dist/ui/modelWindows.js +68 -0
- package/dist/ui/modelWindows.js.map +1 -0
- package/dist/ui/palette.d.ts +10 -5
- package/dist/ui/palette.js +36 -14
- package/dist/ui/palette.js.map +1 -1
- package/dist/ui/termcaps.d.ts +56 -0
- package/dist/ui/termcaps.js +76 -0
- package/dist/ui/termcaps.js.map +1 -0
- package/dist/ui/terminalInterface.js +406 -255
- package/dist/ui/terminalInterface.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,26 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { Box, Text, useApp, useInput, useStdin, useStdout } from 'ink';
|
|
3
|
+
import { Box, Static, Text, useApp, useInput, useStdin, useStdout } from 'ink';
|
|
4
4
|
import Spinner from 'ink-spinner';
|
|
5
5
|
import { appendPromptHistory, loadPromptHistory } from '../promptHistory.js';
|
|
6
|
+
import { listSessions, loadSnapshot, saveSnapshot, deleteSnapshot } from '../sessionList.js';
|
|
6
7
|
import { deleteAt, deleteBefore, insertAt, layoutEditor, lineEndAt, lineStartAt, moveHorizontal, moveVerticalWrapped, normalizePaste } from './editor.js';
|
|
7
8
|
import { renderMarkdown } from './markdown.js';
|
|
8
9
|
import { looksLikeDiff, diffSegments } from './diff.js';
|
|
9
10
|
import { palette } from './palette.js';
|
|
11
|
+
import { contextMeter, meterBar } from './modelWindows.js';
|
|
12
|
+
import { BEL, SYNC_QUERY, isSyncOutputReply, osc9Notify, osc52Copy, shouldNudgeOnFinish, wrapSynchronizedOutput } from './termcaps.js';
|
|
10
13
|
import { KITTY_POP, KITTY_PUSH, KITTY_QUERY, isEncodedKey, isKittyQueryResponse, parseRawKey } from './keys.js';
|
|
14
|
+
const PALETTE = palette();
|
|
15
|
+
/** Border / accent colour per working mode — the input frame changes with it. */
|
|
16
|
+
const MODE_ACCENT = {
|
|
17
|
+
plan: PALETTE.info,
|
|
18
|
+
build: PALETTE.success,
|
|
19
|
+
research: PALETTE.accent,
|
|
20
|
+
crazy: PALETTE.error,
|
|
21
|
+
};
|
|
22
|
+
/** Distinct hues for swarm agents — identity, not severity. */
|
|
23
|
+
const AGENT_COLORS = [PALETTE.accent, PALETTE.info, PALETTE.warn, PALETTE.success, 'magenta', PALETTE.error];
|
|
11
24
|
function describeTarget(name, input) {
|
|
12
25
|
if (input && typeof input === 'object') {
|
|
13
26
|
const record = input;
|
|
@@ -20,6 +33,22 @@ function describeTarget(name, input) {
|
|
|
20
33
|
}
|
|
21
34
|
return '';
|
|
22
35
|
}
|
|
36
|
+
/** Short verb shown on a tool card header, per tool type. */
|
|
37
|
+
function toolVerb(tool) {
|
|
38
|
+
switch (tool) {
|
|
39
|
+
case 'read_file': return 'read';
|
|
40
|
+
case 'write_file': return 'edit';
|
|
41
|
+
case 'run_command': return '$';
|
|
42
|
+
case 'git_diff': return 'diff';
|
|
43
|
+
case 'semantic_search': return 'search';
|
|
44
|
+
case 'index_workspace': return 'index';
|
|
45
|
+
case 'update_todo': return 'plan';
|
|
46
|
+
case 'write_memory': return 'memory';
|
|
47
|
+
case 'start_preview': return 'preview';
|
|
48
|
+
case 'route': return 'route';
|
|
49
|
+
default: return tool;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
23
52
|
function phaseFor(tool) {
|
|
24
53
|
switch (tool) {
|
|
25
54
|
case 'read_file': return 'reading files';
|
|
@@ -84,25 +113,66 @@ function labelFor(role, model, toolName, fallback) {
|
|
|
84
113
|
switch (role) {
|
|
85
114
|
case 'user': return 'you';
|
|
86
115
|
case 'error': return 'error';
|
|
87
|
-
case 'thinking': return '
|
|
116
|
+
case 'thinking': return 'thinking';
|
|
88
117
|
case 'tool': return toolName ? `tool · ${toolName}` : 'tool';
|
|
89
118
|
default: return model ?? fallback ?? 'assistant';
|
|
90
119
|
}
|
|
91
120
|
}
|
|
92
|
-
const PALETTE = palette();
|
|
93
121
|
function colorFor(role, p = PALETTE) {
|
|
94
122
|
switch (role) {
|
|
95
123
|
case 'user': return p.info;
|
|
96
124
|
case 'error': return p.error;
|
|
97
125
|
case 'thinking': return p.warn;
|
|
98
126
|
case 'tool': return p.muted;
|
|
99
|
-
default: return p.
|
|
127
|
+
default: return p.accent;
|
|
100
128
|
}
|
|
101
129
|
}
|
|
102
130
|
function SegmentText({ segments, role }) {
|
|
103
|
-
const base = role === 'thinking' ? PALETTE.warn : role === 'tool' ? PALETTE.muted : role === 'assistant' ?
|
|
131
|
+
const base = role === 'thinking' ? PALETTE.warn : role === 'tool' ? PALETTE.muted : role === 'assistant' ? undefined : role === 'user' ? PALETTE.info : undefined;
|
|
104
132
|
return _jsx(Text, { color: base, children: segments.map((segment, index) => (_jsx(Text, { bold: segment.bold, italic: segment.italic, strikethrough: segment.strikethrough, underline: segment.underline, dimColor: segment.dim, color: segment.color ?? base, children: segment.text }, index))) });
|
|
105
133
|
}
|
|
134
|
+
/** Expanded body of a tool card, rendered per tool type. */
|
|
135
|
+
function renderToolBody(card, width, p) {
|
|
136
|
+
const trail = card.trail;
|
|
137
|
+
if (!trail)
|
|
138
|
+
return card.summary ? _jsx(Text, { dimColor: true, children: card.summary.slice(0, width) }) : _jsx(Text, { dimColor: true, children: "(no output)" });
|
|
139
|
+
if (looksLikeDiff(trail)) {
|
|
140
|
+
return _jsx(_Fragment, { children: diffSegments(trail, width).slice(0, 14).map((segments, index) => _jsx(SegmentText, { segments: segments, role: "tool" }, index)) });
|
|
141
|
+
}
|
|
142
|
+
const rows = trail.split('\n').slice(0, 14);
|
|
143
|
+
if (card.name === 'write_file') {
|
|
144
|
+
return _jsx(_Fragment, { children: rows.map((line, index) => _jsx(Text, { color: p.success, children: line.slice(0, width) }, index)) });
|
|
145
|
+
}
|
|
146
|
+
if (card.name === 'run_command') {
|
|
147
|
+
return _jsx(_Fragment, { children: rows.map((line, index) => {
|
|
148
|
+
const exit = /^exit (\d+)/.exec(line);
|
|
149
|
+
const color = exit ? (exit[1] === '0' ? p.success : p.error) : undefined;
|
|
150
|
+
return _jsx(Text, { color: color, dimColor: color === undefined, children: line.slice(0, width) }, index);
|
|
151
|
+
}) });
|
|
152
|
+
}
|
|
153
|
+
return _jsx(_Fragment, { children: rows.map((line, index) => _jsx(Text, { dimColor: true, children: line.slice(0, width) }, index)) });
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* One settled transcript entry, rendered exactly once into `<Static>` (native
|
|
157
|
+
* scrollback). Label row + wrapped body; tool/error lines render literally,
|
|
158
|
+
* everything else as markdown.
|
|
159
|
+
*/
|
|
160
|
+
function TranscriptEntry({ line, width, fallbackModel }) {
|
|
161
|
+
const baseLabel = labelFor(line.role, line.model, line.toolName, fallbackModel);
|
|
162
|
+
const label = line.role === 'assistant' && line.provider
|
|
163
|
+
? `${baseLabel} · via ${line.provider}${line.fallback ? ' (failover)' : ''}`
|
|
164
|
+
: baseLabel;
|
|
165
|
+
const asMarkdown = line.role !== 'tool' && line.role !== 'error';
|
|
166
|
+
const rows = asMarkdown
|
|
167
|
+
? renderMarkdown(line.text, width)
|
|
168
|
+
: wrap(line.text, width).map((text) => [{ text }]);
|
|
169
|
+
const bullet = line.role === 'user' ? '❯' : line.role === 'assistant' ? '◆' : line.role === 'thinking' ? '·' : line.role === 'error' ? '✕' : '⋯';
|
|
170
|
+
return _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: true, color: colorFor(line.role), children: [bullet, " ", label] }), rows.map((segments, index) => _jsx(SegmentText, { segments: segments, role: line.role }, index)), line.saved ? _jsxs(Text, { dimColor: true, children: [" ", line.saved] }) : null] });
|
|
171
|
+
}
|
|
172
|
+
/** Branded splash shown until the first prompt. */
|
|
173
|
+
function Hero({ width, endpoint, model, mode }) {
|
|
174
|
+
return _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.accent, paddingX: 2, paddingY: 1, marginBottom: 1, width: Math.min(width, 76), children: [_jsx(Text, { bold: true, color: PALETTE.accent, children: "\u25C7 OMNIHARNESS" }), _jsx(Text, { dimColor: true, children: "the OmniRoute-native agent harness" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "gateway " }), endpoint] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "model " }), model] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "mode " }), _jsx(Text, { color: MODE_ACCENT[mode], children: mode }), _jsx(Text, { dimColor: true, children: " \u00B7 Ctrl+E cycles plan \u00B7 build \u00B7 research \u00B7 crazy" })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "describe the work and press enter \u00B7 /help for commands" }) })] });
|
|
175
|
+
}
|
|
106
176
|
export function TerminalInterface({ engine }) {
|
|
107
177
|
const { exit } = useApp();
|
|
108
178
|
const { stdout } = useStdout();
|
|
@@ -110,13 +180,10 @@ export function TerminalInterface({ engine }) {
|
|
|
110
180
|
const [width, setWidth] = useState(() => widthOf(stdout));
|
|
111
181
|
const [edit, setEdit] = useState({ value: '', cursor: 0 });
|
|
112
182
|
const inputWidth = Math.max(16, width - 12);
|
|
113
|
-
//
|
|
183
|
+
// Settled transcript. Rendered once each into <Static> — the terminal's own
|
|
184
|
+
// scrollback is the history; there is no in-app viewport to scroll.
|
|
114
185
|
const [lines, setLines] = useState(() => engine.state.messages.map(lineFromMessage));
|
|
115
|
-
const [
|
|
116
|
-
const followTranscriptRef = useRef(true);
|
|
117
|
-
const previousRowCountRef = useRef(0);
|
|
118
|
-
const maxScrollRef = useRef(0);
|
|
119
|
-
const pageSizeRef = useRef(8);
|
|
186
|
+
const [staticKey, setStaticKey] = useState(0); // bumped on /clear to reset <Static>
|
|
120
187
|
const promptHistoryRef = useRef([]);
|
|
121
188
|
const historyIdxRef = useRef(-1);
|
|
122
189
|
const syncPromptHistory = (next) => { promptHistoryRef.current = next; };
|
|
@@ -132,19 +199,24 @@ export function TerminalInterface({ engine }) {
|
|
|
132
199
|
const [liveThink, setLiveThink] = useState('');
|
|
133
200
|
const [liveAnswer, setLiveAnswer] = useState('');
|
|
134
201
|
const [kitty, setKitty] = useState(null);
|
|
202
|
+
const [sessionsList, setSessionsList] = useState([]);
|
|
203
|
+
const [sessionsOpen, setSessionsOpen] = useState(false);
|
|
204
|
+
const [sessionsIndex, setSessionsIndex] = useState(0);
|
|
135
205
|
const [taskQueue, setTaskQueue] = useState(engine.state.taskQueue);
|
|
136
206
|
const [currentTool, setCurrentTool] = useState();
|
|
137
|
-
// Tool activity rendered as collapsible cards: each completed/ongoing tool
|
|
138
|
-
// call carries its target, status, and optional summary/diff trail.
|
|
139
207
|
const [toolCards, setToolCards] = useState([]);
|
|
140
208
|
const [expandedTool, setExpandedTool] = useState();
|
|
141
|
-
|
|
209
|
+
const [agents, setAgents] = useState([]);
|
|
142
210
|
const runStartedAt = useRef(null);
|
|
143
211
|
const [now, setNow] = useState(() => Date.now());
|
|
212
|
+
const queuedRef = useRef(null);
|
|
213
|
+
const [queued, setQueued] = useState();
|
|
214
|
+
const [layoutDebug, setLayoutDebug] = useState(false);
|
|
215
|
+
const syncRestoreRef = useRef(null);
|
|
216
|
+
const pushLine = (line) => setLines((current) => [...current, line]);
|
|
217
|
+
const pushTool = (text, toolName = 'system') => pushLine({ role: 'tool', text, toolName });
|
|
144
218
|
useEffect(() => {
|
|
145
219
|
let alive = true;
|
|
146
|
-
// Seed history from disk, but only if the user hasn't already submitted a
|
|
147
|
-
// prompt this session (handles the async load racing a submit).
|
|
148
220
|
void loadPromptHistory().then((history) => {
|
|
149
221
|
if (alive && promptHistoryRef.current.length === 0)
|
|
150
222
|
syncPromptHistory(history);
|
|
@@ -156,18 +228,23 @@ export function TerminalInterface({ engine }) {
|
|
|
156
228
|
stdout.on('resize', onResize);
|
|
157
229
|
stdout.write(KITTY_PUSH);
|
|
158
230
|
let kittyTimer;
|
|
159
|
-
//
|
|
160
|
-
const
|
|
161
|
-
|
|
231
|
+
// One probe pass: kitty keyboard protocol + synchronized output (DECSET 2026).
|
|
232
|
+
const onProbe = (chunk) => {
|
|
233
|
+
const text = chunk.toString();
|
|
234
|
+
if (isSyncOutputReply(text) && syncRestoreRef.current === null) {
|
|
235
|
+
syncRestoreRef.current = wrapSynchronizedOutput(stdout);
|
|
236
|
+
}
|
|
237
|
+
if (!isKittyQueryResponse(text))
|
|
162
238
|
return;
|
|
163
239
|
if (kittyTimer)
|
|
164
240
|
clearTimeout(kittyTimer);
|
|
165
|
-
stdin?.off('data',
|
|
241
|
+
stdin?.off('data', onProbe);
|
|
166
242
|
setKitty(true);
|
|
167
243
|
};
|
|
168
244
|
if (stdin) {
|
|
169
|
-
stdin.on('data',
|
|
170
|
-
kittyTimer = setTimeout(() => { setKitty(false); stdin.off('data',
|
|
245
|
+
stdin.on('data', onProbe);
|
|
246
|
+
kittyTimer = setTimeout(() => { setKitty(false); stdin.off('data', onProbe); }, 300);
|
|
247
|
+
stdout.write(SYNC_QUERY);
|
|
171
248
|
stdout.write(KITTY_QUERY);
|
|
172
249
|
}
|
|
173
250
|
else {
|
|
@@ -183,16 +260,32 @@ export function TerminalInterface({ engine }) {
|
|
|
183
260
|
break;
|
|
184
261
|
case 'thinking':
|
|
185
262
|
if (event.text)
|
|
186
|
-
|
|
263
|
+
pushLine({ role: 'thinking', text: event.text });
|
|
187
264
|
setLiveThink('');
|
|
188
265
|
break;
|
|
189
266
|
case 'text':
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
267
|
+
pushLine({
|
|
268
|
+
role: 'assistant', text: event.content, model: event.model,
|
|
269
|
+
provider: event.provider, fallback: event.fallback,
|
|
270
|
+
saved: event.compression ? `${Math.round((1 - event.compression.ratio) * 100)}% saved (${event.compression.strategy.toUpperCase()}) · ${event.compression.savedTokens.toLocaleString()} tokens` : undefined,
|
|
271
|
+
});
|
|
194
272
|
setLiveAnswer('');
|
|
195
273
|
break;
|
|
274
|
+
case 'route':
|
|
275
|
+
pushLine({
|
|
276
|
+
role: 'tool', toolName: 'route',
|
|
277
|
+
text: event.fallback
|
|
278
|
+
? `route · failover → ${event.provider ?? 'unknown'} (attempt ${event.attempts + 1})${event.reason ? ` · ${event.reason}` : ''}`
|
|
279
|
+
: `route · ${event.provider ?? 'unknown'}`,
|
|
280
|
+
});
|
|
281
|
+
break;
|
|
282
|
+
case 'agent':
|
|
283
|
+
setAgents((current) => {
|
|
284
|
+
const next = current.filter((lane) => lane.id !== event.id);
|
|
285
|
+
next.push({ id: event.id, label: event.label, status: event.status, note: event.note });
|
|
286
|
+
return next.sort((a, b) => a.id.localeCompare(b.id));
|
|
287
|
+
});
|
|
288
|
+
break;
|
|
196
289
|
case 'tool_start':
|
|
197
290
|
setToolCards((current) => [...current, {
|
|
198
291
|
id: `tc-${Date.now().toString(36)}-${current.length}`, name: event.tool,
|
|
@@ -206,17 +299,19 @@ export function TerminalInterface({ engine }) {
|
|
|
206
299
|
case 'tool_result':
|
|
207
300
|
setToolCards((current) => current.length === 0
|
|
208
301
|
? current
|
|
209
|
-
: current.map((card, index) => index === current.length - 1
|
|
302
|
+
: current.map((card, index) => index === current.length - 1
|
|
303
|
+
? { ...card, status: 'done', summary: event.summary, trail: card.trail ?? event.detail }
|
|
304
|
+
: card));
|
|
210
305
|
setCurrentTool(undefined);
|
|
211
306
|
break;
|
|
212
307
|
case 'todos':
|
|
213
308
|
setTaskQueue(event.todos);
|
|
214
309
|
break;
|
|
215
310
|
case 'preview':
|
|
216
|
-
|
|
311
|
+
pushLine({ role: 'tool', text: `preview: ${event.url}`, toolName: 'preview', url: event.url });
|
|
217
312
|
break;
|
|
218
313
|
case 'attach':
|
|
219
|
-
|
|
314
|
+
pushLine({ role: 'tool', text: `attached: ${event.name} (${event.kind}, ${event.size} bytes)`, toolName: 'attach' });
|
|
220
315
|
break;
|
|
221
316
|
}
|
|
222
317
|
});
|
|
@@ -229,14 +324,16 @@ export function TerminalInterface({ engine }) {
|
|
|
229
324
|
return () => {
|
|
230
325
|
if (kittyTimer)
|
|
231
326
|
clearTimeout(kittyTimer);
|
|
232
|
-
stdin?.off('data',
|
|
327
|
+
stdin?.off('data', onProbe);
|
|
233
328
|
stdout.off('resize', onResize);
|
|
234
329
|
const pendingApproval = approvalResolve.current;
|
|
235
330
|
approvalResolve.current = null;
|
|
236
|
-
pendingApproval?.(false);
|
|
331
|
+
pendingApproval?.({ approved: false });
|
|
237
332
|
engine.stop();
|
|
238
333
|
unsubscribe();
|
|
239
334
|
process.off('exit', onUnload);
|
|
335
|
+
syncRestoreRef.current?.();
|
|
336
|
+
syncRestoreRef.current = null;
|
|
240
337
|
stdout.write(KITTY_POP);
|
|
241
338
|
};
|
|
242
339
|
}, [engine, stdout, stdin]);
|
|
@@ -266,13 +363,83 @@ export function TerminalInterface({ engine }) {
|
|
|
266
363
|
const next = MODE_SEQ[(MODE_SEQ.indexOf(mode) + 1) % MODE_SEQ.length];
|
|
267
364
|
setMode(next);
|
|
268
365
|
engine.state.mode = next;
|
|
269
|
-
|
|
366
|
+
pushTool(`mode → ${next}`, 'mode');
|
|
270
367
|
};
|
|
271
|
-
const approve = (
|
|
368
|
+
const approve = (approved, trust) => {
|
|
272
369
|
const resolve = approvalResolve.current;
|
|
273
370
|
setApproval(null);
|
|
274
371
|
approvalResolve.current = null;
|
|
275
|
-
resolve?.(
|
|
372
|
+
resolve?.({ approved, trust });
|
|
373
|
+
};
|
|
374
|
+
/** Copy the most recent assistant reply to the clipboard via OSC 52. */
|
|
375
|
+
const yankLastBlock = () => {
|
|
376
|
+
const target = [...lines].reverse().find((entry) => entry.role === 'assistant') ?? lines[lines.length - 1];
|
|
377
|
+
if (!target)
|
|
378
|
+
return;
|
|
379
|
+
const seq = osc52Copy(target.text);
|
|
380
|
+
if (!seq) {
|
|
381
|
+
pushTool('clipboard: block too large to copy', 'clipboard');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
stdout.write(seq);
|
|
386
|
+
pushTool(`copied ${target.text.length} chars to clipboard`, 'clipboard');
|
|
387
|
+
}
|
|
388
|
+
catch { /* clipboard write is best-effort */ }
|
|
389
|
+
};
|
|
390
|
+
/** Kick off an engine run for `prompt`, attaching `attachSpec` files first when given. */
|
|
391
|
+
const startRun = (prompt, attachSpec) => {
|
|
392
|
+
setEdit({ value: '', cursor: 0 });
|
|
393
|
+
setBusy(true);
|
|
394
|
+
setError(undefined);
|
|
395
|
+
setToolCards([]);
|
|
396
|
+
setExpandedTool(undefined);
|
|
397
|
+
setAgents([]);
|
|
398
|
+
if (runStartedAt.current === null)
|
|
399
|
+
runStartedAt.current = Date.now();
|
|
400
|
+
setNow(Date.now());
|
|
401
|
+
historyIdxRef.current = -1;
|
|
402
|
+
if (prompt) {
|
|
403
|
+
syncPromptHistory([prompt, ...promptHistoryRef.current.filter((entry) => entry !== prompt)].slice(0, 200));
|
|
404
|
+
void appendPromptHistory(prompt).catch(() => { });
|
|
405
|
+
}
|
|
406
|
+
if (!attachSpec)
|
|
407
|
+
pushLine({ role: 'user', text: prompt });
|
|
408
|
+
void (async () => {
|
|
409
|
+
try {
|
|
410
|
+
if (attachSpec)
|
|
411
|
+
await engine.attach(attachSpec.split(/\s+/).filter(Boolean));
|
|
412
|
+
await engine.run(prompt);
|
|
413
|
+
// CRAZY mode: once a plan exists, fan the rest of it out across parallel workers.
|
|
414
|
+
if (engine.state.mode === 'crazy' && typeof engine.runSwarm === 'function') {
|
|
415
|
+
const pending = engine.state.taskQueue.filter((item) => item.status === 'pending').length;
|
|
416
|
+
if (pending >= 2)
|
|
417
|
+
await engine.runSwarm({ maxAgents: 3 });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (reason) {
|
|
421
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
422
|
+
setError(message);
|
|
423
|
+
pushLine({ role: 'error', text: message });
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
const startedAt = runStartedAt.current;
|
|
427
|
+
setBusy(false);
|
|
428
|
+
setCurrentTool(undefined);
|
|
429
|
+
setToolCards((current) => current.map((card) => card.status === 'running' ? { ...card, status: 'error' } : card));
|
|
430
|
+
setAgents((current) => current.map((lane) => lane.status === 'working' || lane.status === 'spawned' ? { ...lane, status: 'done' } : lane));
|
|
431
|
+
runStartedAt.current = null;
|
|
432
|
+
setLiveThink('');
|
|
433
|
+
setLiveAnswer('');
|
|
434
|
+
if (startedAt !== null && shouldNudgeOnFinish(Date.now() - startedAt)) {
|
|
435
|
+
try {
|
|
436
|
+
stdout.write(osc9Notify('OmniHarness — run finished'));
|
|
437
|
+
stdout.write(BEL);
|
|
438
|
+
}
|
|
439
|
+
catch { /* nudge is best-effort */ }
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
})();
|
|
276
443
|
};
|
|
277
444
|
/** ↑ walks older prompts, ↓ walks newer; ↓ past the newest clears the input. */
|
|
278
445
|
const navigateHistory = (older) => {
|
|
@@ -286,23 +453,29 @@ export function TerminalInterface({ engine }) {
|
|
|
286
453
|
historyIdxRef.current = next;
|
|
287
454
|
setEdit(next < 0 ? { value: '', cursor: 0 } : { value: history[next] ?? '', cursor: (history[next] ?? '').length });
|
|
288
455
|
};
|
|
289
|
-
/** Whether ↑/↓ should browse prompt history instead of moving the text caret. */
|
|
290
456
|
const browsingHistory = () => historyIdxRef.current >= 0 || edit.value === '';
|
|
291
|
-
/**
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
457
|
+
/** Chapters: one per user turn, titled by the prompt's first line. */
|
|
458
|
+
const chapters = () => lines.flatMap((line, index) => line.role === 'user'
|
|
459
|
+
? [{ index, title: line.text.split('\n')[0].slice(0, 60) || '(empty prompt)' }]
|
|
460
|
+
: []);
|
|
461
|
+
const HELP = [
|
|
462
|
+
'/help — show commands',
|
|
463
|
+
'/clear — start a fresh conversation',
|
|
464
|
+
'/sessions — list saved sessions (enter to resume)',
|
|
465
|
+
'/save <name> — snapshot the current session',
|
|
466
|
+
'/forget <name> — delete a saved session',
|
|
467
|
+
'/attach <files> — attach files to the next message',
|
|
468
|
+
'/find <text> — list transcript lines containing <text>',
|
|
469
|
+
'/chapters — list the turns in this session',
|
|
470
|
+
'keys: Ctrl+O models · Ctrl+E mode · Ctrl+T tool card · Ctrl+Y copy reply · Ctrl+L layout · Ctrl+C cancel/quit · ↑/↓ history',
|
|
471
|
+
'history scrolls in your terminal · a prompt typed mid-run is queued and sent when the run ends',
|
|
472
|
+
];
|
|
302
473
|
/** Apply a semantic key action, honoring the active overlay (approval, picker). */
|
|
303
474
|
const applyAction = (action) => {
|
|
304
475
|
if (approval && action.kind !== 'submit' && action.kind !== 'escape' && action.kind !== 'ctrlC')
|
|
305
476
|
return;
|
|
477
|
+
if (sessionsOpen && action.kind !== 'submit' && action.kind !== 'escape' && action.kind !== 'up' && action.kind !== 'down')
|
|
478
|
+
return;
|
|
306
479
|
if (pickerOpen && action.kind !== 'submit' && action.kind !== 'escape' && action.kind !== 'up' && action.kind !== 'down')
|
|
307
480
|
return;
|
|
308
481
|
switch (action.kind) {
|
|
@@ -311,12 +484,28 @@ export function TerminalInterface({ engine }) {
|
|
|
311
484
|
approve(true);
|
|
312
485
|
return;
|
|
313
486
|
}
|
|
487
|
+
if (sessionsOpen) {
|
|
488
|
+
const selected = sessionsList[sessionsIndex];
|
|
489
|
+
if (selected) {
|
|
490
|
+
void loadSnapshot(engine.state.workspace.root, selected.name).then((snapshot) => {
|
|
491
|
+
if (snapshot == null) {
|
|
492
|
+
pushLine({ role: 'error', text: `snapshot ${selected.name} is unreadable` });
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
setStaticKey((k) => k + 1);
|
|
496
|
+
setLines([...snapshot.messages.map(lineFromMessage), { role: 'tool', toolName: 'sessions', text: `session resumed: ${selected.name} (${snapshot.messages.length} messages)` }]);
|
|
497
|
+
setTaskQueue(snapshot.taskQueue);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
setSessionsOpen(false);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
314
503
|
if (pickerOpen) {
|
|
315
504
|
const selected = pickerItems[pickerIndex];
|
|
316
505
|
if (selected) {
|
|
317
506
|
void engine.selectModel(selected.id);
|
|
318
507
|
setPickerOpen(false);
|
|
319
|
-
|
|
508
|
+
pushTool(`model → ${selected.id} (saved as default)`, 'model');
|
|
320
509
|
}
|
|
321
510
|
return;
|
|
322
511
|
}
|
|
@@ -324,75 +513,92 @@ export function TerminalInterface({ engine }) {
|
|
|
324
513
|
const raw = edit.value.trim();
|
|
325
514
|
const attachMatch = /^\/attach\s+(.+)$/.exec(raw);
|
|
326
515
|
if (raw === '/help') {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
516
|
+
HELP.forEach((text) => pushTool(text, 'help'));
|
|
517
|
+
setEdit({ value: '', cursor: 0 });
|
|
518
|
+
historyIdxRef.current = -1;
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const findMatch = /^\/find\s+(.+)$/.exec(raw);
|
|
522
|
+
if (findMatch) {
|
|
523
|
+
const needle = findMatch[1].toLowerCase();
|
|
524
|
+
const hits = lines.filter((line) => line.text.toLowerCase().includes(needle));
|
|
525
|
+
if (hits.length === 0)
|
|
526
|
+
pushTool(`no match for "${findMatch[1]}"`, 'find');
|
|
527
|
+
else {
|
|
528
|
+
pushTool(`find "${findMatch[1]}" · ${hits.length} match${hits.length === 1 ? '' : 'es'}`, 'find');
|
|
529
|
+
hits.slice(-6).forEach((hit) => pushTool(` ${labelFor(hit.role, hit.model, hit.toolName)} · ${clip(hit.text.replace(/\n/g, ' '), Math.max(20, width - 16))}`, 'find'));
|
|
530
|
+
}
|
|
531
|
+
setEdit({ value: '', cursor: 0 });
|
|
532
|
+
historyIdxRef.current = -1;
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (raw === '/chapters' || raw === '/chapter') {
|
|
536
|
+
const list = chapters();
|
|
537
|
+
if (list.length === 0)
|
|
538
|
+
pushTool('no chapters yet — each prompt starts one', 'chapters');
|
|
539
|
+
else
|
|
540
|
+
list.forEach((chapter, order) => pushTool(`${order + 1}. ${chapter.title}`, 'chapters'));
|
|
333
541
|
setEdit({ value: '', cursor: 0 });
|
|
334
542
|
historyIdxRef.current = -1;
|
|
335
543
|
return;
|
|
336
544
|
}
|
|
545
|
+
if (raw === '/sessions') {
|
|
546
|
+
void listSessions(engine.state.workspace.root).then((sessions) => {
|
|
547
|
+
setSessionsList(sessions);
|
|
548
|
+
setSessionsIndex(0);
|
|
549
|
+
setSessionsOpen(sessions.length > 0);
|
|
550
|
+
if (sessions.length === 0)
|
|
551
|
+
pushTool('no saved sessions — use /save <name> to snapshot this one', 'sessions');
|
|
552
|
+
});
|
|
553
|
+
setEdit({ value: '', cursor: 0 });
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const saveMatch = /^\/save\s+([\w.-]+)$/.exec(raw);
|
|
557
|
+
if (saveMatch) {
|
|
558
|
+
const name = saveMatch[1];
|
|
559
|
+
void saveSnapshot(engine.state.workspace.root, name, {
|
|
560
|
+
messages: [...engine.state.messages], taskQueue: [...engine.state.taskQueue], savedAt: new Date().toISOString(),
|
|
561
|
+
}).then(() => pushTool(`session saved: ${name}`, 'sessions'))
|
|
562
|
+
.catch((reason) => pushLine({ role: 'error', text: `save failed: ${reason instanceof Error ? reason.message : String(reason)}` }));
|
|
563
|
+
setEdit({ value: '', cursor: 0 });
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const delMatch = /^\/forget\s+([\w.-]+)$/.exec(raw);
|
|
567
|
+
if (delMatch) {
|
|
568
|
+
void deleteSnapshot(engine.state.workspace.root, delMatch[1]).then(() => pushTool(`session deleted: ${delMatch[1]}`, 'sessions'));
|
|
569
|
+
setEdit({ value: '', cursor: 0 });
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
337
572
|
if (raw === '/clear') {
|
|
338
573
|
historyIdxRef.current = -1;
|
|
339
|
-
followTranscriptRef.current = true;
|
|
340
|
-
setScrollOffset(0);
|
|
341
574
|
setTaskQueue([]);
|
|
342
575
|
setError(undefined);
|
|
343
576
|
setCurrentTool(undefined);
|
|
344
577
|
setToolCards([]);
|
|
345
578
|
setExpandedTool(undefined);
|
|
579
|
+
setAgents([]);
|
|
346
580
|
runStartedAt.current = null;
|
|
347
581
|
setLiveThink('');
|
|
348
582
|
setLiveAnswer('');
|
|
349
583
|
syncPromptHistory([]);
|
|
584
|
+
setStaticKey((k) => k + 1);
|
|
350
585
|
setLines([]);
|
|
351
586
|
void engine.clearHistory().catch(() => { });
|
|
352
587
|
setEdit({ value: '', cursor: 0 });
|
|
353
588
|
return;
|
|
354
589
|
}
|
|
355
590
|
const prompt = attachMatch ? '' : raw;
|
|
356
|
-
|
|
591
|
+
const attachSpec = attachMatch ? attachMatch[1] : undefined;
|
|
592
|
+
if (!prompt && !attachSpec)
|
|
593
|
+
return;
|
|
594
|
+
if (busy) {
|
|
595
|
+
queuedRef.current = { prompt, attachSpec };
|
|
596
|
+
setQueued(prompt || `/attach ${attachSpec ?? ''}`.trim());
|
|
597
|
+
setEdit({ value: '', cursor: 0 });
|
|
598
|
+
historyIdxRef.current = -1;
|
|
357
599
|
return;
|
|
358
|
-
followTranscriptRef.current = true;
|
|
359
|
-
setScrollOffset(0);
|
|
360
|
-
setEdit({ value: '', cursor: 0 });
|
|
361
|
-
setBusy(true);
|
|
362
|
-
setError(undefined);
|
|
363
|
-
setToolCards([]);
|
|
364
|
-
setExpandedTool(undefined);
|
|
365
|
-
if (runStartedAt.current === null)
|
|
366
|
-
runStartedAt.current = Date.now();
|
|
367
|
-
setNow(Date.now());
|
|
368
|
-
historyIdxRef.current = -1;
|
|
369
|
-
if (prompt) {
|
|
370
|
-
syncPromptHistory([prompt, ...promptHistoryRef.current.filter((entry) => entry !== prompt)].slice(0, 200));
|
|
371
|
-
void appendPromptHistory(prompt).catch(() => { });
|
|
372
600
|
}
|
|
373
|
-
|
|
374
|
-
setLines((current) => [...current, { role: 'user', text: prompt }]);
|
|
375
|
-
void (async () => {
|
|
376
|
-
try {
|
|
377
|
-
if (attachMatch)
|
|
378
|
-
await engine.attach(attachMatch[1].split(/\s+/).filter(Boolean));
|
|
379
|
-
await engine.run(prompt);
|
|
380
|
-
/* answer is streamed live via text_delta / text events */
|
|
381
|
-
}
|
|
382
|
-
catch (reason) {
|
|
383
|
-
const message = reason instanceof Error ? reason.message : String(reason);
|
|
384
|
-
setError(message);
|
|
385
|
-
setLines((current) => [...current, { role: 'error', text: message }]);
|
|
386
|
-
}
|
|
387
|
-
finally {
|
|
388
|
-
setBusy(false);
|
|
389
|
-
setCurrentTool(undefined);
|
|
390
|
-
setToolCards((current) => current.map((card) => card.status === 'running' ? { ...card, status: 'error' } : card));
|
|
391
|
-
runStartedAt.current = null;
|
|
392
|
-
setLiveThink('');
|
|
393
|
-
setLiveAnswer('');
|
|
394
|
-
}
|
|
395
|
-
})();
|
|
601
|
+
startRun(prompt, attachSpec);
|
|
396
602
|
}
|
|
397
603
|
return;
|
|
398
604
|
case 'escape':
|
|
@@ -400,13 +606,16 @@ export function TerminalInterface({ engine }) {
|
|
|
400
606
|
approve(false);
|
|
401
607
|
return;
|
|
402
608
|
}
|
|
609
|
+
if (sessionsOpen) {
|
|
610
|
+
setSessionsOpen(false);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
403
613
|
if (pickerOpen) {
|
|
404
614
|
setPickerOpen(false);
|
|
405
615
|
return;
|
|
406
616
|
}
|
|
407
617
|
return;
|
|
408
618
|
case 'ctrlC':
|
|
409
|
-
// While a run is in flight, Ctrl+C cancels that run; when idle it quits.
|
|
410
619
|
if (busy) {
|
|
411
620
|
engine.cancel();
|
|
412
621
|
return;
|
|
@@ -424,6 +633,10 @@ export function TerminalInterface({ engine }) {
|
|
|
424
633
|
cycleMode();
|
|
425
634
|
return;
|
|
426
635
|
case 'up':
|
|
636
|
+
if (sessionsOpen) {
|
|
637
|
+
setSessionsIndex((current) => clamp(current - 1, 0, Math.max(0, sessionsList.length - 1)));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
427
640
|
if (pickerOpen) {
|
|
428
641
|
setPickerIndex((current) => clamp(current - 1, 0, Math.max(0, pickerItems.length - 1)));
|
|
429
642
|
return;
|
|
@@ -435,6 +648,10 @@ export function TerminalInterface({ engine }) {
|
|
|
435
648
|
setEdit((current) => ({ ...current, cursor: moveVerticalWrapped(current.value, current.cursor, -1, inputWidth) }));
|
|
436
649
|
return;
|
|
437
650
|
case 'down':
|
|
651
|
+
if (sessionsOpen) {
|
|
652
|
+
setSessionsIndex((current) => clamp(current + 1, 0, Math.max(0, sessionsList.length - 1)));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
438
655
|
if (pickerOpen) {
|
|
439
656
|
setPickerIndex((current) => clamp(current + 1, 0, Math.max(0, pickerItems.length - 1)));
|
|
440
657
|
return;
|
|
@@ -471,13 +688,25 @@ export function TerminalInterface({ engine }) {
|
|
|
471
688
|
return;
|
|
472
689
|
}
|
|
473
690
|
};
|
|
474
|
-
// Legacy keys Ink parses correctly (\r, \n, \x08, ESC[A arrows, ctrl+letters) plus text and paste.
|
|
475
691
|
useInput((value, key) => {
|
|
476
692
|
if (approval) {
|
|
477
|
-
if (value === 'y' || value === 'Y' || key.return)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
693
|
+
if (value === 'y' || value === 'Y' || key.return) {
|
|
694
|
+
approve(true);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (value === 'n' || value === 'N' || key.escape) {
|
|
698
|
+
approve(false);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (value === 't' || value === 'T') {
|
|
702
|
+
approve(true, approval.scopes[0]?.id);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const digit = Number(value);
|
|
706
|
+
if (Number.isInteger(digit) && digit >= 1 && digit <= approval.scopes.length) {
|
|
707
|
+
approve(true, approval.scopes[digit - 1]?.id);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
481
710
|
return;
|
|
482
711
|
}
|
|
483
712
|
if (key.ctrl && value === 'c') {
|
|
@@ -515,15 +744,33 @@ export function TerminalInterface({ engine }) {
|
|
|
515
744
|
}
|
|
516
745
|
return;
|
|
517
746
|
}
|
|
518
|
-
if (
|
|
519
|
-
|
|
747
|
+
if (sessionsOpen) {
|
|
748
|
+
if (key.escape) {
|
|
749
|
+
applyAction({ kind: 'escape' });
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (key.upArrow) {
|
|
753
|
+
applyAction({ kind: 'up' });
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (key.downArrow) {
|
|
757
|
+
applyAction({ kind: 'down' });
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (key.return) {
|
|
761
|
+
applyAction({ kind: 'submit' });
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (key.ctrl && value.toLowerCase() === 'l') {
|
|
767
|
+
setLayoutDebug((current) => !current);
|
|
520
768
|
return;
|
|
521
769
|
}
|
|
522
|
-
if (key.
|
|
523
|
-
|
|
770
|
+
if (key.ctrl && value.toLowerCase() === 'y') {
|
|
771
|
+
yankLastBlock();
|
|
524
772
|
return;
|
|
525
773
|
}
|
|
526
|
-
// Ctrl+T toggles the most recent tool card between collapsed and expanded.
|
|
527
774
|
if (key.ctrl && value.toLowerCase() === 't') {
|
|
528
775
|
const latest = toolCards[toolCards.length - 1];
|
|
529
776
|
setExpandedTool((current) => (latest && current === latest.id) ? undefined : (latest ? latest.id : undefined));
|
|
@@ -550,7 +797,7 @@ export function TerminalInterface({ engine }) {
|
|
|
550
797
|
return;
|
|
551
798
|
}
|
|
552
799
|
if (!key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow)
|
|
553
|
-
historyIdxRef.current = -1;
|
|
800
|
+
historyIdxRef.current = -1;
|
|
554
801
|
if (key.upArrow) {
|
|
555
802
|
applyAction({ kind: 'up' });
|
|
556
803
|
return;
|
|
@@ -567,7 +814,6 @@ export function TerminalInterface({ engine }) {
|
|
|
567
814
|
applyAction({ kind: 'right' });
|
|
568
815
|
return;
|
|
569
816
|
}
|
|
570
|
-
// 0x7f is Backspace on Windows ConPTY and the kitty-encoded keys are owned by the raw stdin listener.
|
|
571
817
|
if (value.length > 1 && !isEncodedKey(value)) {
|
|
572
818
|
setEdit((current) => insertAt(current.value, current.cursor, normalizePaste(value)));
|
|
573
819
|
return;
|
|
@@ -575,7 +821,6 @@ export function TerminalInterface({ engine }) {
|
|
|
575
821
|
if (!key.ctrl && !key.meta && value && !isEncodedKey(value))
|
|
576
822
|
setEdit((current) => insertAt(current.value, current.cursor, value));
|
|
577
823
|
});
|
|
578
|
-
// Raw stdin: disambiguate Windows Backspace (0x7f), the real Delete key, and kitty-protocol keys.
|
|
579
824
|
useEffect(() => {
|
|
580
825
|
if (!stdin)
|
|
581
826
|
return;
|
|
@@ -587,161 +832,67 @@ export function TerminalInterface({ engine }) {
|
|
|
587
832
|
stdin.on('data', onData);
|
|
588
833
|
return () => { stdin.off('data', onData); };
|
|
589
834
|
}, [stdin, applyAction]);
|
|
590
|
-
|
|
591
|
-
|
|
835
|
+
useEffect(() => {
|
|
836
|
+
if (!busy)
|
|
837
|
+
return;
|
|
838
|
+
const id = setInterval(() => setNow(Date.now()), 250);
|
|
839
|
+
return () => clearInterval(id);
|
|
840
|
+
}, [busy]);
|
|
841
|
+
useEffect(() => {
|
|
842
|
+
if (busy)
|
|
843
|
+
return;
|
|
844
|
+
const pending = queuedRef.current;
|
|
845
|
+
if (!pending)
|
|
846
|
+
return;
|
|
847
|
+
queuedRef.current = null;
|
|
848
|
+
setQueued(undefined);
|
|
849
|
+
startRun(pending.prompt, pending.attachSpec);
|
|
850
|
+
}, [busy]);
|
|
592
851
|
const modeKey = kitty === true ? 'M' : 'E';
|
|
852
|
+
const modeAccent = MODE_ACCENT[mode];
|
|
853
|
+
const contentWidth = Math.max(20, width - 6);
|
|
854
|
+
const terminalRows = stdout.rows ?? 24;
|
|
593
855
|
const metrics = engine.client.snapshotMetrics();
|
|
594
|
-
const
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (metrics.remainingQuota !== undefined)
|
|
599
|
-
hud.push(`quota ${metrics.remainingQuota}`);
|
|
600
|
-
if (metrics.fallback.attempts > 0)
|
|
601
|
-
hud.push(`fb ${metrics.fallback.attempts}`);
|
|
602
|
-
hud.push(`saved ${compression}`);
|
|
603
|
-
// Live statusline fields: model, mode, running phase, tokens/context, elapsed.
|
|
604
|
-
const runningTool = toolCards[toolCards.length - 1];
|
|
856
|
+
const meter = contextMeter(metrics.compression.inputTokens, engine.state.activeModel, metrics.fallback.activeProvider);
|
|
857
|
+
const meterColor = meter.zone === 'danger' ? PALETTE.error : meter.zone === 'warn' ? PALETTE.warn : PALETTE.muted;
|
|
858
|
+
const contextLabel = metrics.compression.inputTokens > 0 ? `ctx ${meterBar(meter.fraction, 8)} ${Math.round(meter.fraction * 100)}%` : '';
|
|
859
|
+
const compression = metrics.compression.inputTokens > 0 ? `${Math.round((1 - metrics.compression.ratio) * 100)}% ${metrics.compression.strategy.toUpperCase()}` : '';
|
|
605
860
|
const phase = busy && currentTool ? phaseFor(currentTool) : (busy ? 'working' : 'ready');
|
|
606
|
-
const elapsedMs = busy && runStartedAt.current !== null
|
|
607
|
-
? Math.max(0, now - runStartedAt.current)
|
|
608
|
-
: 0;
|
|
861
|
+
const elapsedMs = busy && runStartedAt.current !== null ? Math.max(0, now - runStartedAt.current) : 0;
|
|
609
862
|
const elapsed = busy
|
|
610
863
|
? (elapsedMs >= 60_000 ? `${Math.floor(elapsedMs / 60_000)}m${String(Math.floor((elapsedMs % 60_000) / 1000)).padStart(2, '0')}s` : `${Math.floor(elapsedMs / 1000)}s`)
|
|
611
864
|
: '';
|
|
612
|
-
const contextLabel = metrics.compression.inputTokens > 0
|
|
613
|
-
? `${(metrics.compression.inputTokens / 1000).toFixed(1)}k in`
|
|
614
|
-
: '';
|
|
615
|
-
const contentWidth = Math.max(20, width - 8);
|
|
616
|
-
const terminalRows = stdout.rows ?? 24;
|
|
617
865
|
const editorLayout = useMemo(() => layoutEditor(edit.value, edit.cursor, inputWidth), [edit.value, edit.cursor, inputWidth]);
|
|
618
866
|
const liveThinkLines = useMemo(() => renderMarkdown(liveThink, contentWidth), [liveThink, contentWidth]);
|
|
619
867
|
const liveAnswerLines = useMemo(() => renderMarkdown(liveAnswer, contentWidth), [liveAnswer, contentWidth]);
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
rows.push({ key: 'live-answer-label', kind: 'label', role: 'assistant' });
|
|
628
|
-
liveAnswerLines.forEach((segments, index) => rows.push({ key: `live-answer-${index}`, kind: 'content', role: 'assistant', segments }));
|
|
629
|
-
}
|
|
630
|
-
return rows;
|
|
631
|
-
}, [liveThink, liveAnswer, liveThinkLines, liveAnswerLines]);
|
|
632
|
-
// Cache markdown/wrapping per Line object. Streaming updates create new live
|
|
633
|
-
// text, but completed historical lines retain their rendered rows.
|
|
634
|
-
const rowCacheRef = useRef(null);
|
|
635
|
-
const allRows = useMemo(() => {
|
|
636
|
-
const fallback = engine.state.activeModel;
|
|
637
|
-
let cache = rowCacheRef.current;
|
|
638
|
-
if (cache === null || cache.width !== contentWidth || cache.fallback !== fallback) {
|
|
639
|
-
cache = { width: contentWidth, fallback, rows: new WeakMap() };
|
|
640
|
-
rowCacheRef.current = cache;
|
|
641
|
-
}
|
|
642
|
-
const out = [];
|
|
643
|
-
lines.forEach((line, lineIndex) => {
|
|
644
|
-
let rendered = cache.rows.get(line);
|
|
645
|
-
if (rendered === undefined) {
|
|
646
|
-
const label = labelFor(line.role, line.model, line.toolName, fallback);
|
|
647
|
-
const markdown = line.role !== 'tool' && line.role !== 'error';
|
|
648
|
-
const wrapped = markdown
|
|
649
|
-
? renderMarkdown(line.text, contentWidth)
|
|
650
|
-
: wrap(line.text, contentWidth).map((text) => [{ text }]);
|
|
651
|
-
const next = [];
|
|
652
|
-
wrapped.forEach((segments, rowIndex) => next.push({ key: `message-${lineIndex}-${rowIndex}`, role: line.role, segments, label, first: rowIndex === 0 }));
|
|
653
|
-
if (line.saved)
|
|
654
|
-
next.push({ key: `message-${lineIndex}-saved`, role: 'assistant', segments: [{ text: line.saved }], label: '', first: false, saved: line.saved });
|
|
655
|
-
rendered = next;
|
|
656
|
-
cache.rows.set(line, rendered);
|
|
657
|
-
}
|
|
658
|
-
out.push(...rendered);
|
|
659
|
-
});
|
|
660
|
-
return out;
|
|
661
|
-
}, [lines, contentWidth, engine.state.activeModel]);
|
|
662
|
-
const planRows = taskQueue.length > 0 ? 6 + Math.min(6, taskQueue.length) : 0;
|
|
663
|
-
const pickerGroups = new Set(pickerItems.map((item) => item.group)).size;
|
|
664
|
-
const pickerRows = pickerOpen ? 6 + pickerItems.length + pickerGroups + (pickerError || pickerItems.length === 0 ? 1 : 0) : 0;
|
|
665
|
-
const approvalRows = approval ? 8 : 0;
|
|
666
|
-
const inputRows = 3 + editorLayout.lines.length;
|
|
667
|
-
const footerRows = 3;
|
|
668
|
-
const chromeRows = 3 + footerRows + inputRows + (kitty !== null ? 1 : 0) + planRows + pickerRows + approvalRows;
|
|
669
|
-
const messageHeight = Math.max(3, terminalRows - chromeRows);
|
|
670
|
-
const toolRows = toolCards.length > 0 ? toolCards.length : 0; // one collapsed card per tool call
|
|
671
|
-
const statusRows = (engine.state.preview ? 1 : 0) + (busy ? 1 : 0) + toolRows;
|
|
672
|
-
const storedHeight = Math.max(0, messageHeight - Math.min(messageHeight, liveRows.length + statusRows));
|
|
673
|
-
const liveHeight = Math.max(0, messageHeight - storedHeight - statusRows);
|
|
674
|
-
const visibleLiveRows = liveHeight > 0 ? liveRows.slice(-liveHeight) : [];
|
|
675
|
-
const maxScroll = Math.max(0, allRows.length - storedHeight);
|
|
676
|
-
maxScrollRef.current = maxScroll;
|
|
677
|
-
pageSizeRef.current = Math.max(1, storedHeight - 2);
|
|
678
|
-
const boundedScroll = clamp(scrollOffset, 0, maxScroll);
|
|
679
|
-
const endRow = allRows.length - boundedScroll;
|
|
680
|
-
const startRow = Math.max(0, endRow - storedHeight);
|
|
681
|
-
const visibleRows = allRows.slice(startRow, endRow);
|
|
682
|
-
const hiddenAbove = startRow;
|
|
683
|
-
const hiddenBelow = boundedScroll;
|
|
684
|
-
const scrollStatus = allRows.length === 0
|
|
685
|
-
? 'transcript empty'
|
|
686
|
-
: storedHeight === 0
|
|
687
|
-
? `live output · ${allRows.length} transcript rows`
|
|
688
|
-
: hiddenBelow === 0
|
|
689
|
-
? `showing rows ${startRow + 1}-${endRow} of ${allRows.length} · following latest`
|
|
690
|
-
: `showing rows ${startRow + 1}-${endRow} of ${allRows.length} · ${hiddenAbove} older · ${hiddenBelow} newer`;
|
|
691
|
-
useEffect(() => {
|
|
692
|
-
const previous = previousRowCountRef.current;
|
|
693
|
-
const added = allRows.length - previous;
|
|
694
|
-
previousRowCountRef.current = allRows.length;
|
|
695
|
-
if (previous === 0 || added <= 0)
|
|
696
|
-
return;
|
|
697
|
-
if (followTranscriptRef.current) {
|
|
698
|
-
setScrollOffset(0);
|
|
699
|
-
}
|
|
700
|
-
else {
|
|
701
|
-
setScrollOffset((current) => clamp(current + added, 0, maxScrollRef.current));
|
|
702
|
-
}
|
|
703
|
-
}, [allRows.length]);
|
|
704
|
-
useEffect(() => {
|
|
705
|
-
setScrollOffset((current) => clamp(current, 0, maxScroll));
|
|
706
|
-
}, [maxScroll]);
|
|
707
|
-
// Tick a clock while a run is in flight so the statusline can show elapsed time
|
|
708
|
-
// without re-rendering on every event. runStartedAt is set when a run begins.
|
|
709
|
-
useEffect(() => {
|
|
710
|
-
if (!busy)
|
|
711
|
-
return;
|
|
712
|
-
const id = setInterval(() => setNow(Date.now()), 250);
|
|
713
|
-
return () => clearInterval(id);
|
|
714
|
-
}, [busy]);
|
|
715
|
-
return _jsxs(Box, { flexDirection: "column", width: width, height: terminalRows, paddingX: 2, overflow: "hidden", children: [_jsxs(Box, { justifyContent: "space-between", paddingY: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["OMNIHARNESS ", _jsxs(Text, { dimColor: true, children: ["\u00B7 ", mode, " mode"] })] }), _jsx(Text, { dimColor: true, children: "OMNIROUTE :20128" })] }), _jsxs(Box, { flexDirection: "column", height: messageHeight, overflow: "hidden", children: [lines.length === 0 && _jsxs(Box, { flexDirection: "column", marginTop: 2, children: [_jsx(Text, { color: "cyan", bold: true, children: "Ready when you are." }), _jsxs(Text, { dimColor: true, children: ["Describe the work. OmniHarness routes it through your OmniRoute account. Ctrl+", modeKey, " cycles plan \u00B7 build \u00B7 research \u00B7 crazy."] })] }), visibleRows.map((row, index) => row.saved
|
|
716
|
-
? _jsx(Text, { dimColor: true, children: row.saved }, row.key)
|
|
717
|
-
: row.first
|
|
718
|
-
? _jsxs(Box, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [_jsx(Text, { color: colorFor(row.role), bold: true, children: row.label }), _jsx(SegmentText, { segments: row.segments, role: row.role })] }, row.key)
|
|
719
|
-
: _jsx(SegmentText, { segments: row.segments, role: row.role }, row.key)), visibleLiveRows.map((row) => row.kind === 'label'
|
|
720
|
-
? _jsx(Text, { color: colorFor(row.role), bold: true, children: row.role === 'thinking' ? 'think' : engine.state.activeModel }, row.key)
|
|
721
|
-
: _jsx(SegmentText, { segments: row.segments, role: row.role }, row.key)), toolCards.slice(-6).map((card) => {
|
|
868
|
+
// Cap the streaming region so a long think/answer can't crowd out the chrome;
|
|
869
|
+
// the complete text lands in <Static> once the event fires.
|
|
870
|
+
const liveBudget = Math.max(3, terminalRows - 14 - Math.min(6, taskQueue.length) - Math.min(4, agents.length) - editorLayout.lines.length);
|
|
871
|
+
const liveThinkView = liveThinkLines.slice(-Math.max(2, Math.floor(liveBudget / 2)));
|
|
872
|
+
const liveAnswerView = liveAnswerLines.slice(-liveBudget);
|
|
873
|
+
const doneAgents = agents.filter((lane) => lane.status === 'done').length;
|
|
874
|
+
return _jsxs(Box, { flexDirection: "column", width: width, paddingX: 2, children: [_jsx(Static, { items: lines, children: (line, index) => _jsx(TranscriptEntry, { line: line, width: contentWidth, fallbackModel: engine.state.activeModel }, index) }, staticKey), _jsxs(Box, { flexDirection: "column", children: [lines.length === 0 && !busy && _jsx(Hero, { width: width, endpoint: engine.client.endpoint ?? 'omniroute', model: engine.state.activeModel, mode: mode }), 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: [_jsxs(Text, { bold: true, color: PALETTE.accent, children: ["\u25C6 ", engine.state.activeModel] }), liveAnswerView.map((segments, index) => _jsx(SegmentText, { segments: segments, role: "assistant" }, index))] }), toolCards.slice(-5).map((card) => {
|
|
722
875
|
const expanded = expandedTool === card.id;
|
|
723
|
-
const
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
:
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
? _jsxs(Text, { color: "cyan", children: ["\u203A ", _jsx(Text, { dimColor: true, children: "type a task and press enter" })] })
|
|
745
|
-
: editorLayout.lines.map((text, index) => _jsxs(Text, { color: "cyan", children: [index === 0 ? '› ' : ' ', text] }, index)) }), kitty !== null && _jsx(Text, { dimColor: true, children: kitty ? 'kitty protocol active — Shift+Enter makes a new line' : 'this terminal can\'t distinguish Shift+Enter from Enter — use Ctrl+J for a new line' }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: busy ? PALETTE.info : PALETTE.muted, children: [busy && _jsx(Spinner, { type: "dots" }), phase, elapsed ? ` · ${elapsed}` : ''] }), _jsx(Text, { color: PALETTE.muted, children: [engine.state.activeModel, mode, contextLabel, scrollStatus].filter(Boolean).join(' · ') })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { dimColor: true, children: ["Enter send \u00B7 Shift+Enter/Ctrl+J new line \u00B7 Ctrl+O models \u00B7 Ctrl+", modeKey, " mode \u00B7 Ctrl+C cancel/quit \u00B7 /help \u00B7 Ctrl+T tool"] }), hud.length > 0 && _jsx(Text, { dimColor: true, children: hud.join(' · ') })] })] })] });
|
|
876
|
+
const dot = card.status === 'running' ? _jsx(Text, { color: PALETTE.warn, children: "\u25CD" }) : card.status === 'error' ? _jsx(Text, { color: PALETTE.error, children: "\u2715" }) : _jsx(Text, { color: PALETTE.success, children: "\u2713" });
|
|
877
|
+
const head = card.name === 'run_command'
|
|
878
|
+
? `$ ${clip(card.target || '…', Math.max(10, contentWidth - 20))}`
|
|
879
|
+
: `${toolVerb(card.name)}${card.target ? ` ${clip(card.target, Math.max(10, contentWidth - 24))}` : ''}`;
|
|
880
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [dot, " ", head, expanded ? ' Ctrl+T collapse' : ''] }), expanded && renderToolBody(card, contentWidth, PALETTE)] }, card.id);
|
|
881
|
+
}), 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: "\u26A1 swarm" }), _jsxs(Text, { dimColor: true, children: [doneAgents, "/", agents.length, " lanes done"] })] }), agents.map((lane, index) => {
|
|
882
|
+
const color = AGENT_COLORS[index % AGENT_COLORS.length];
|
|
883
|
+
const glyph = lane.status === 'done' ? '✓' : lane.status === 'error' ? '✕' : lane.status === 'working' ? '◍' : '○';
|
|
884
|
+
return _jsxs(Text, { color: color, children: [glyph, " ", lane.id, " ", _jsx(Text, { dimColor: true, children: clip(lane.note ?? lane.label, Math.max(12, contentWidth - 8)) })] }, lane.id);
|
|
885
|
+
})] }), taskQueue.length > 0 && _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.accent, paddingX: 2, marginTop: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: PALETTE.accent, children: "\u25C7 plan" }), _jsxs(Text, { dimColor: true, children: [taskQueue.filter((item) => item.status === 'done').length, "/", taskQueue.length, " done"] })] }), taskQueue.slice(-6).map((item) => {
|
|
886
|
+
const marker = item.status === 'done' ? '✓' : item.status === 'active' ? '◈' : '○';
|
|
887
|
+
const color = item.status === 'done' ? PALETTE.success : item.status === 'active' ? PALETTE.accent : undefined;
|
|
888
|
+
return _jsxs(Text, { color: color, dimColor: item.status === 'done', children: [marker, " ", clip(item.title, contentWidth - 4)] }, item.id);
|
|
889
|
+
})] }), engine.state.preview && _jsxs(Text, { color: PALETTE.success, children: ["\u25B8 preview live \u00B7 ", engine.state.preview.url] }), sessionsOpen && _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.info, paddingX: 2, marginTop: 1, children: [_jsx(Text, { bold: true, color: PALETTE.info, children: "saved sessions" }), _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u00B7 enter resume \u00B7 esc close" }), sessionsList.map((session, index) => (_jsxs(Text, { color: index === sessionsIndex ? PALETTE.info : undefined, children: [index === sessionsIndex ? '❯ ' : ' ', session.name, session.savedAt ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", session.savedAt] }) : null] }, session.name)))] }), pickerOpen && _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.accent, paddingX: 2, marginTop: 1, children: [_jsx(Text, { bold: true, color: PALETTE.accent, children: "choose an OmniRoute model" }), _jsx(Text, { dimColor: true, children: "\u2191\u2193 / j k navigate \u00B7 enter select \u00B7 esc close" }), pickerError && _jsx(Text, { color: PALETTE.error, children: clip(pickerError, contentWidth) }), pickerItems.length === 0 && !pickerError && _jsx(Text, { dimColor: true, children: "no models returned by OmniRoute." }), pickerItems.map((item, index) => {
|
|
890
|
+
const header = index === 0 || pickerItems[index - 1].group !== item.group
|
|
891
|
+
? _jsx(Text, { dimColor: true, bold: true, children: item.group === 'combos' ? 'your combos' : 'auto engine' }, `h-${item.group}`)
|
|
892
|
+
: null;
|
|
893
|
+
return _jsxs(Box, { flexDirection: "column", children: [header, _jsxs(Text, { color: index === pickerIndex ? PALETTE.accent : undefined, children: [index === pickerIndex ? '❯ ' : ' ', item.id, item.strategy ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", item.strategy] }) : null, item.id === engine.state.activeModel ? ' ✓' : ''] })] }, item.id);
|
|
894
|
+
})] }), approval && _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.warn, paddingX: 2, marginTop: 1, children: [_jsxs(Text, { bold: true, color: PALETTE.warn, children: ["approve ", approval.tool, "?"] }), _jsxs(Text, { dimColor: true, children: ["args: ", clip(JSON.stringify(approval.input), contentWidth)] }), approval.scopes.map((scope, index) => _jsxs(Text, { dimColor: true, children: [" ", index + 1, " \u00B7 ", clip(scope.label, Math.max(12, contentWidth - 6))] }, scope.id)), _jsxs(Text, { dimColor: true, children: ["y allow once \u00B7 n deny \u00B7 t always allow \u00B7 1\u2013", approval.scopes.length, " pick a trust scope"] })] }), layoutDebug && _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.muted, paddingX: 2, marginTop: 1, children: [_jsxs(Text, { bold: true, dimColor: true, children: ["layout \u00B7 ", width, "\u00D7", terminalRows, " \u00B7 Ctrl+L to hide"] }), _jsxs(Text, { dimColor: true, children: ["static entries ", lines.length, " \u00B7 live budget ", liveBudget, " \u00B7 think ", liveThinkLines.length, " \u00B7 answer ", liveAnswerLines.length] }), _jsxs(Text, { dimColor: true, children: ["plan ", taskQueue.length, " \u00B7 swarm ", agents.length, " \u00B7 tool cards ", toolCards.length, " \u00B7 editor rows ", editorLayout.lines.length] })] }), queued && _jsxs(Text, { color: PALETTE.warn, children: ["\u23CE queued \u00B7 ", clip(queued, Math.max(12, contentWidth - 12))] }), _jsx(Box, { borderStyle: "round", borderColor: error ? PALETTE.error : modeAccent, paddingX: 1, marginTop: 1, flexDirection: "column", children: edit.value === ''
|
|
895
|
+
? _jsxs(Text, { color: modeAccent, children: ["\u276F ", _jsx(Text, { dimColor: true, children: busy ? 'type to queue the next task' : 'describe the work and press enter' })] })
|
|
896
|
+
: editorLayout.lines.map((text, index) => _jsxs(Text, { color: modeAccent, children: [index === 0 ? '❯ ' : ' ', text] }, index)) }), kitty !== null && _jsx(Text, { dimColor: true, children: kitty ? 'kitty protocol active — Shift+Enter makes a new line' : 'this terminal can\'t distinguish Shift+Enter from Enter — use Ctrl+J for a new line' }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: busy ? modeAccent : PALETTE.muted, children: [busy && _jsx(Spinner, { type: "dots" }), busy ? ' ' : '', phase, elapsed ? ` · ${elapsed}` : '', agents.length > 0 ? ` · swarm ${doneAgents}/${agents.length}` : ''] }), _jsxs(Text, { color: PALETTE.muted, children: [_jsx(Text, { color: modeAccent, children: mode }), " \u00B7 ", engine.state.activeModel, metrics.fallback.activeProvider ? _jsxs(Text, { children: [" \u00B7 via ", metrics.fallback.activeProvider] }) : null, contextLabel ? _jsxs(Text, { color: meterColor, children: [" \u00B7 ", contextLabel] }) : null] })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { dimColor: true, children: ["Enter send \u00B7 Ctrl+J newline \u00B7 Ctrl+O models \u00B7 Ctrl+", modeKey, " mode \u00B7 Ctrl+T tool \u00B7 Ctrl+Y copy \u00B7 Ctrl+C ", busy ? 'cancel' : 'quit', " \u00B7 /help"] }), compression ? _jsxs(Text, { dimColor: true, children: ["saved ", compression, metrics.remainingQuota !== undefined ? ` · quota ${metrics.remainingQuota}` : ''] }) : null] })] })] })] });
|
|
746
897
|
}
|
|
747
898
|
//# sourceMappingURL=terminalInterface.js.map
|