omniharness-cli 0.1.80 → 0.1.82

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/cli.js CHANGED
@@ -7,23 +7,6 @@ import { ownVersion, runUpdate } from './update.js';
7
7
  import { readActiveCombo } from './config/settings.js';
8
8
  import { OmniRouteClient } from './config/omniRoute.js';
9
9
  import { doctor, helpText, models } from './doctor.js';
10
- const ALT_ENTER = '\x1b[?1049h\x1b[H';
11
- const ALT_LEAVE = '\x1b[?1049l';
12
- /** Plain-text transcript flushed to the primary buffer on exit — the audit trail. */
13
- function dumpTranscript(messages) {
14
- if (messages.length === 0)
15
- return;
16
- const label = {
17
- user: 'you', assistant: 'assistant', thought: 'think', action: 'action',
18
- tool: 'tool', command: 'command', error: 'error',
19
- };
20
- process.stdout.write('\n');
21
- for (const message of messages) {
22
- const who = message.model ?? label[message.role] ?? message.role;
23
- process.stdout.write(`\n─ ${who} ─────\n${message.content.trim()}\n`);
24
- }
25
- process.stdout.write('\n');
26
- }
27
10
  // A crash anywhere below would otherwise surface as a raw Node stack trace, or
28
11
  // as an unhandled rejection that terminates the process without saying why.
29
12
  // A CLI should fail with a sentence.
@@ -81,23 +64,26 @@ else {
81
64
  void (async () => {
82
65
  const saved = await readActiveCombo();
83
66
  const engine = await createMastraEngine({ workspaceRoot: process.cwd(), model: saved });
84
- // Render the structured UI in the alternate screen buffer so that, on exit,
85
- // the primary buffer's scrollback is left holding the plain-text transcript —
86
- // the raw audit trail, without maintaining a parallel log.
87
- const alt = process.stdout.isTTY === true;
88
- if (alt)
89
- process.stdout.write(ALT_ENTER);
67
+ // Render in the primary buffer, not the alternate screen.
68
+ //
69
+ // The alternate screen has no scrollback that is what it is for — so
70
+ // while the harness was running you could not scroll back to anything: not
71
+ // a file the agent read three steps ago, not the reasoning behind an edit.
72
+ // The transcript only appeared once you quit, dumped into the primary
73
+ // buffer on exit, which is the wrong time to want it.
74
+ //
75
+ // Staying in the primary buffer means the TUI's <Static> region writes
76
+ // settled turns straight into real scrollback, where the terminal scrolls
77
+ // them like any other output and they are still there afterwards. The exit
78
+ // dump goes with it: scrollback already holds the transcript, and printing
79
+ // it again would duplicate every line.
80
+ //
81
+ // The cost is that quitting no longer restores the pre-launch screen. For
82
+ // a tool whose output you are meant to read back, that is the right trade.
83
+ //
90
84
  // The app owns Ctrl+C so idle quits but an in-flight run is cancelled first.
91
85
  const { waitUntilExit } = render(_jsx(TerminalInterface, { engine: engine }), { exitOnCtrlC: false });
92
- try {
93
- await waitUntilExit();
94
- }
95
- finally {
96
- if (alt) {
97
- process.stdout.write(ALT_LEAVE);
98
- dumpTranscript(engine.state.messages);
99
- }
100
- }
86
+ await waitUntilExit();
101
87
  })();
102
88
  }
103
89
  //# sourceMappingURL=cli.js.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Data shaping for the sidebar. The layout lives in the Ink component; every
3
+ * decision it has to make is here so it can be tested without a terminal.
4
+ *
5
+ * The shape follows OpenCode's session sidebar (MIT, sst/opencode): a fixed
6
+ * narrow panel carrying what the conversation column should not have to —
7
+ * session identity, what is running, and what is queued. The implementation is
8
+ * ours; Ink has no scrollbox and no absolute positioning, so the behaviour
9
+ * differs where it has to.
10
+ */
11
+ /** Fixed width of the panel, matching the measure OpenCode settled on. */
12
+ export const SIDEBAR_WIDTH = 34;
13
+ /**
14
+ * Below this the terminal cannot hold a sidebar and a readable conversation
15
+ * at once, so the toggle shows the sidebar *instead of* the conversation
16
+ * rather than beside it. Ink cannot overlay one panel on another, which is
17
+ * what OpenCode does at this size.
18
+ */
19
+ export const SIDEBAR_MIN_SPLIT = 92;
20
+ /**
21
+ * How the sidebar should be shown, given the terminal width and whether the
22
+ * user has it toggled on.
23
+ */
24
+ export function sidebarMode(width, wanted) {
25
+ if (!wanted)
26
+ return 'hidden';
27
+ return width >= SIDEBAR_MIN_SPLIT ? 'split' : 'replace';
28
+ }
29
+ /** Width left for the conversation once the sidebar has taken its share. */
30
+ export function conversationWidth(width, mode) {
31
+ if (mode !== 'split')
32
+ return width;
33
+ return Math.max(20, width - SIDEBAR_WIDTH - 1);
34
+ }
35
+ /**
36
+ * Render a token count the way it is read: exact while small, then thousands.
37
+ * A six-digit number in a 34-column panel is noise, not information.
38
+ */
39
+ export function compactTokens(n) {
40
+ if (!Number.isFinite(n) || n < 0)
41
+ return '0';
42
+ if (n < 1000)
43
+ return String(Math.round(n));
44
+ if (n < 1_000_000) {
45
+ const k = n / 1000;
46
+ return `${k < 10 ? k.toFixed(1) : Math.round(k)}k`;
47
+ }
48
+ return `${(n / 1_000_000).toFixed(1)}M`;
49
+ }
50
+ /**
51
+ * Cost to four places under a cent, two above. Sub-cent spend is the normal
52
+ * case for a single turn and "$0.00" would report every one of them as free.
53
+ */
54
+ export function formatCost(usd) {
55
+ if (!Number.isFinite(usd) || usd <= 0)
56
+ return '$0';
57
+ return usd < 0.01 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;
58
+ }
59
+ /**
60
+ * The usage rows, or none at all when nothing has been spent yet. An empty
61
+ * panel section reads as broken; a missing one reads as "not yet".
62
+ */
63
+ export function usageRows(usage) {
64
+ const rows = [];
65
+ const inTokens = usage.tokensIn ?? 0;
66
+ const outTokens = usage.tokensOut ?? 0;
67
+ if (inTokens > 0 || outTokens > 0) {
68
+ // Only name a direction that was measured: "1.2k in · 0 out" claims zero
69
+ // output rather than admitting it is not counted here.
70
+ const parts = [];
71
+ if (inTokens > 0)
72
+ parts.push(`${compactTokens(inTokens)} in`);
73
+ if (outTokens > 0)
74
+ parts.push(`${compactTokens(outTokens)} out`);
75
+ rows.push({ label: 'tokens', value: parts.join(' · ') });
76
+ }
77
+ if ((usage.costUSD ?? 0) > 0)
78
+ rows.push({ label: 'cost', value: formatCost(usage.costUSD ?? 0) });
79
+ if ((usage.requests ?? 0) > 0)
80
+ rows.push({ label: 'calls', value: String(usage.requests) });
81
+ return rows;
82
+ }
83
+ /**
84
+ * Task queue rows. Plain-word markers rather than glyphs, matching the rest of
85
+ * the interface. The active item is marked so the eye finds it without colour,
86
+ * which matters under NO_COLOR.
87
+ */
88
+ export function todoRows(todos, limit, width) {
89
+ return todos.slice(0, Math.max(0, limit)).map((todo) => ({
90
+ marker: todo.status === 'done' ? 'x' : todo.status === 'active' ? '>' : '-',
91
+ title: clip(todo.title, Math.max(4, width)),
92
+ active: todo.status === 'active',
93
+ }));
94
+ }
95
+ /** How many queued items were not shown, for a "+N more" line. */
96
+ export function overflowCount(total, shown) {
97
+ return Math.max(0, total - shown);
98
+ }
99
+ export function clip(text, width) {
100
+ const runes = [...text.replace(/\s+/g, ' ')];
101
+ if (runes.length <= width)
102
+ return runes.join('');
103
+ return `${runes.slice(0, Math.max(0, width - 1)).join('')}…`;
104
+ }
105
+ //# sourceMappingURL=sidebar.js.map
@@ -9,6 +9,7 @@ import { renderMarkdown } from './markdown.js';
9
9
  import { looksLikeDiff, diffSegments } from './diff.js';
10
10
  import { palette } from './palette.js';
11
11
  import { capabilityLine, recentRows, shortenPath, twoColumn } from './home.js';
12
+ import { conversationWidth, overflowCount, sidebarMode, SIDEBAR_WIDTH, todoRows, usageRows, clip as clipRow } from './sidebar.js';
12
13
  import { contextMeter, meterBar } from './modelWindows.js';
13
14
  import { BEL, SYNC_QUERY, isSyncOutputReply, osc9Notify, osc52Copy, shouldNudgeOnFinish, wrapSynchronizedOutput } from './termcaps.js';
14
15
  import { KITTY_POP, KITTY_PUSH, KITTY_QUERY, isEncodedKey, isKittyQueryResponse, parseRawKey } from './keys.js';
@@ -80,6 +81,12 @@ const MODE_SEQ = ['plan', 'build', 'research', 'crazy'];
80
81
  const PERM_SEQ = ['ask', 'acceptEdits', 'bypass'];
81
82
  const PERM_LABEL = { ask: 'manual', acceptEdits: 'accept edits', bypass: 'bypass' };
82
83
  const PERM_COLOR = (p) => (p === 'bypass' ? PALETTE.error : p === 'acceptEdits' ? PALETTE.warn : PALETTE.muted);
84
+ /**
85
+ * The widest the content is allowed to get, however wide the terminal is.
86
+ * Past roughly this the eye loses the start of a line on the way back from
87
+ * the end of it, and the chrome ends up further from what it describes.
88
+ */
89
+ const MAX_MEASURE = 100;
83
90
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
84
91
  const widthOf = (stdout) => Math.max(48, stdout.columns ?? 80);
85
92
  const clip = (text, width) => text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}…`;
@@ -174,6 +181,30 @@ function TranscriptEntry({ line, width, fallbackModel }) {
174
181
  const bullet = line.role === 'user' ? '>' : line.role === 'error' ? '!' : '-';
175
182
  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] });
176
183
  }
184
+ /**
185
+ * The session panel, shown beside the live region on Ctrl+B.
186
+ *
187
+ * It carries what the conversation column should not have to: what is
188
+ * running, what is queued, and what the turn has cost. The shape follows
189
+ * OpenCode's session sidebar (MIT); the implementation is ours, because Ink
190
+ * has neither a scrollbox nor absolute positioning, so it cannot scroll
191
+ * independently or overlay the conversation.
192
+ *
193
+ * Sections are absent rather than empty. A heading with nothing under it
194
+ * reads as something failing to load.
195
+ */
196
+ function SidebarPanel(props) {
197
+ const { width, model, mode, perm, workspace, usage, agents, todos, skills, plugins } = props;
198
+ const inner = Math.max(10, width - 4);
199
+ // Two columns of slack, not one. At an exact fit Ink still wraps, and a
200
+ // wrapped value in a 34-column panel shows as a stray blank line under a
201
+ // bare label — measured, not reasoned about.
202
+ const value = Math.max(6, inner - LABEL_WIDTH - 2);
203
+ const usageLines = usageRows(usage);
204
+ const shownTodos = todoRows(todos, 6, inner - 2);
205
+ const moreTodos = overflowCount(todos.length, shownTodos.length);
206
+ return _jsxs(Box, { flexDirection: "column", width: width, borderStyle: "round", borderColor: PALETTE.muted, paddingX: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "session" }), _jsx(Field, { label: "model", children: clip(model, value) }), _jsx(Field, { label: "mode", children: _jsx(Text, { color: MODE_ACCENT[mode], children: mode }) }), _jsx(Field, { label: "perms", children: _jsx(Text, { color: mode === 'crazy' ? PALETTE.error : PERM_COLOR(perm), children: mode === 'crazy' ? 'bypass' : PERM_LABEL[perm] }) }), _jsx(Field, { label: "cwd", children: shortenPath(workspace, value) }), skills > 0 && _jsx(Field, { label: "skills", children: clip(plugins > 0 ? `${skills} · ${plugins} plugins` : String(skills), value) }), usageLines.length > 0 && _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "usage" }), usageLines.map((row) => _jsx(Field, { label: row.label, children: clip(row.value, value) }, row.label))] }), agents.length > 0 && _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "agents" }), agents.slice(-6).map((lane, index) => _jsxs(Text, { children: [_jsx(Text, { color: AGENT_COLORS[index % AGENT_COLORS.length], children: lane.status === 'error' ? 'FAIL' : lane.status === 'done' ? 'ok ' : '.. ' }), _jsx(Text, { dimColor: true, children: clipRow(lane.label, inner - 5) })] }, lane.id))] }), shownTodos.length > 0 && _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "plan" }), shownTodos.map((row) => _jsxs(Text, { children: [_jsxs(Text, { color: row.active ? PALETTE.accent : PALETTE.muted, children: [row.marker, " "] }), _jsx(Text, { dimColor: !row.active, children: row.title })] }, row.title)), moreTodos > 0 && _jsxs(Text, { dimColor: true, children: ["+", moreTodos, " more"] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Ctrl+B hides this" }) })] });
207
+ }
177
208
  /**
178
209
  * Width of the dim label column on the home screen. The value gets whatever is
179
210
  * left, and every caller has to subtract this: a value sized to the whole box
@@ -197,7 +228,7 @@ function Field({ label, children }) {
197
228
  export function Hero(props) {
198
229
  const { width, endpoint, model, mode, perm, workspace, sessions, skills, plugins, mcpTools } = props;
199
230
  const wide = twoColumn(width);
200
- const outer = Math.min(width - 2, 84);
231
+ const outer = width;
201
232
  const column = wide ? Math.floor((outer - 3) / 2) : outer;
202
233
  const inner = Math.max(12, column - 4);
203
234
  // Values sit to the right of the label, so that is the room they actually get.
@@ -214,7 +245,7 @@ export function TerminalInterface({ engine }) {
214
245
  const { stdin } = useStdin();
215
246
  const [width, setWidth] = useState(() => widthOf(stdout));
216
247
  const [edit, setEdit] = useState({ value: '', cursor: 0 });
217
- const inputWidth = Math.max(16, width - 12);
248
+ const inputWidth = Math.max(16, Math.min(width, MAX_MEASURE) - 12); // recomputed below once the split is known
218
249
  // Settled transcript. Rendered once each into <Static> — the terminal's own
219
250
  // scrollback is the history; there is no in-app viewport to scroll.
220
251
  const [lines, setLines] = useState(() => engine.state.messages.map(lineFromMessage));
@@ -250,6 +281,9 @@ export function TerminalInterface({ engine }) {
250
281
  // Recent snapshots for the home screen. Read once on mount and left
251
282
  // alone: the home screen is only on screen before the first turn.
252
283
  const [recentSessions, setRecentSessions] = useState([]);
284
+ // Off by default: it is a second thing to read, and the conversation is
285
+ // the first. Ctrl+B brings it in.
286
+ const [sidebarWanted, setSidebarWanted] = useState(false);
253
287
  const [layoutDebug, setLayoutDebug] = useState(false);
254
288
  const syncRestoreRef = useRef(null);
255
289
  const pushLine = (line) => setLines((current) => [...current, line]);
@@ -690,6 +724,9 @@ export function TerminalInterface({ engine }) {
690
724
  case 'ctrlE':
691
725
  cycleMode();
692
726
  return;
727
+ case 'ctrlB':
728
+ setSidebarWanted((on) => !on);
729
+ return;
693
730
  case 'up':
694
731
  if (sessionsOpen) {
695
732
  setSessionsIndex((current) => clamp(current - 1, 0, Math.max(0, sessionsList.length - 1)));
@@ -783,6 +820,10 @@ export function TerminalInterface({ engine }) {
783
820
  applyAction({ kind: 'ctrlE' });
784
821
  return;
785
822
  }
823
+ if (key.ctrl && value.toLowerCase() === 'b') {
824
+ applyAction({ kind: 'ctrlB' });
825
+ return;
826
+ }
786
827
  if (pickerOpen) {
787
828
  if (key.escape) {
788
829
  applyAction({ kind: 'escape' });
@@ -912,7 +953,17 @@ export function TerminalInterface({ engine }) {
912
953
  }, [busy]);
913
954
  const modeKey = kitty === true ? 'M' : 'E';
914
955
  const modeAccent = MODE_ACCENT[mode];
915
- const contentWidth = Math.max(20, width - 6);
956
+ // On a wide terminal the UI used to stretch edge to edge, which puts the
957
+ // input box and the text you are reading at opposite ends of the screen.
958
+ // Hold the content to a readable measure and centre it instead; below that
959
+ // measure the gutter collapses and nothing changes.
960
+ const sideMode = sidebarMode(width, sidebarWanted);
961
+ // With the panel beside it the conversation gets what is left, so the
962
+ // centred measure applies to the pair rather than to the text alone.
963
+ const measure = Math.min(width, sideMode === 'split' ? MAX_MEASURE + SIDEBAR_WIDTH + 1 : MAX_MEASURE);
964
+ const gutter = Math.max(2, Math.floor((width - measure) / 2));
965
+ const convoWidth = conversationWidth(measure, sideMode);
966
+ const contentWidth = Math.max(20, convoWidth - 6);
916
967
  const terminalRows = stdout.rows ?? 24;
917
968
  const metrics = engine.client.snapshotMetrics();
918
969
  const meter = contextMeter(metrics.compression.inputTokens, engine.state.activeModel, metrics.fallback.activeProvider);
@@ -933,29 +984,32 @@ export function TerminalInterface({ engine }) {
933
984
  const liveThinkView = liveThinkLines.slice(-Math.max(2, Math.floor(liveBudget / 2)));
934
985
  const liveAnswerView = liveAnswerLines.slice(-liveBudget);
935
986
  const doneAgents = agents.filter((lane) => lane.status === 'done').length;
936
- 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, 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))] }), toolCards.slice(-5).map((card) => {
937
- const expanded = expandedTool === card.id;
938
- const dot = card.status === 'running' ? _jsx(Text, { color: PALETTE.warn, children: ".." }) : card.status === 'error' ? _jsx(Text, { color: PALETTE.error, children: "FAIL" }) : _jsx(Text, { color: PALETTE.success, children: "ok" });
939
- const head = card.name === 'run_command'
940
- ? `$ ${clip(card.target || '', Math.max(10, contentWidth - 20))}`
941
- : `${toolVerb(card.name)}${card.target ? ` ${clip(card.target, Math.max(10, contentWidth - 24))}` : ''}`;
942
- return _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [dot, " ", head, expanded ? ' Ctrl+T collapse' : ''] }), expanded && renderToolBody(card, contentWidth, PALETTE)] }, card.id);
943
- }), 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) => {
944
- const color = AGENT_COLORS[index % AGENT_COLORS.length];
945
- const glyph = lane.status === 'done' ? 'ok' : lane.status === 'error' ? 'FAIL' : lane.status === 'working' ? '..' : '--';
946
- const detail = lane.note ?? (lane.label !== lane.id ? lane.label : lane.status);
947
- return _jsxs(Text, { color: color, children: [glyph, " ", lane.id, " ", _jsx(Text, { dimColor: true, children: clip(detail, Math.max(12, contentWidth - 8)) })] }, lane.id);
948
- })] }), 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: "plan" }), _jsxs(Text, { dimColor: true, children: [taskQueue.filter((item) => item.status === 'done').length, "/", taskQueue.length, " done"] })] }), taskQueue.slice(-6).map((item) => {
949
- const marker = item.status === 'done' ? 'ok' : item.status === 'active' ? '>' : '-';
950
- const color = item.status === 'done' ? PALETTE.success : item.status === 'active' ? PALETTE.accent : undefined;
951
- return _jsxs(Text, { color: color, dimColor: item.status === 'done', children: [marker, " ", clip(item.title, contentWidth - 4)] }, item.id);
952
- })] }), engine.state.preview && _jsxs(Text, { color: PALETTE.success, children: ["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: "up/down 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: "up/down / 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) => {
953
- const header = index === 0 || pickerItems[index - 1].group !== item.group
954
- ? _jsx(Text, { dimColor: true, bold: true, children: item.group === 'combos' ? 'your combos' : 'auto engine' }, `h-${item.group}`)
955
- : null;
956
- 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);
957
- })] }), 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: ["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 === ''
958
- ? _jsxs(Text, { color: modeAccent, children: ['>', " ", _jsx(Text, { dimColor: true, children: busy ? 'type to queue the next task' : 'describe the work and press enter' })] })
959
- : 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: "line" }), busy ? ' ' : '', phase, elapsed ? ` · ${elapsed}` : '', agents.length > 0 ? ` · swarm ${doneAgents}/${agents.length}` : ''] }), _jsxs(Text, { color: PALETTE.muted, children: [_jsx(Text, { color: modeAccent, children: mode }), _jsx(Text, { children: " \u00B7 " }), _jsx(Text, { color: mode === 'crazy' ? PALETTE.error : PERM_COLOR(permMode), children: mode === 'crazy' ? 'bypass' : PERM_LABEL[permMode] }), _jsxs(Text, { children: [" \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 Shift+Tab perms \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] })] })] })] });
987
+ 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: [lines.length === 0 && !busy && _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))] }), toolCards.slice(-5).map((card) => {
989
+ const expanded = expandedTool === card.id;
990
+ const dot = card.status === 'running' ? _jsx(Text, { color: PALETTE.warn, children: ".." }) : card.status === 'error' ? _jsx(Text, { color: PALETTE.error, children: "FAIL" }) : _jsx(Text, { color: PALETTE.success, children: "ok" });
991
+ const head = card.name === 'run_command'
992
+ ? `$ ${clip(card.target || '…', Math.max(10, contentWidth - 20))}`
993
+ : `${toolVerb(card.name)}${card.target ? ` ${clip(card.target, Math.max(10, contentWidth - 24))}` : ''}`;
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);
995
+ }), 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
+ const color = AGENT_COLORS[index % AGENT_COLORS.length];
997
+ const glyph = lane.status === 'done' ? 'ok' : lane.status === 'error' ? 'FAIL' : lane.status === 'working' ? '..' : '--';
998
+ const detail = lane.note ?? (lane.label !== lane.id ? lane.label : lane.status);
999
+ return _jsxs(Text, { color: color, children: [glyph, " ", lane.id, " ", _jsx(Text, { dimColor: true, children: clip(detail, Math.max(12, contentWidth - 8)) })] }, lane.id);
1000
+ })] }), sideMode === 'hidden' && 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: "plan" }), _jsxs(Text, { dimColor: true, children: [taskQueue.filter((item) => item.status === 'done').length, "/", taskQueue.length, " done"] })] }), taskQueue.slice(-6).map((item) => {
1001
+ const marker = item.status === 'done' ? 'ok' : item.status === 'active' ? '>' : '-';
1002
+ const color = item.status === 'done' ? PALETTE.success : item.status === 'active' ? PALETTE.accent : undefined;
1003
+ return _jsxs(Text, { color: color, dimColor: item.status === 'done', children: [marker, " ", clip(item.title, contentWidth - 4)] }, item.id);
1004
+ })] }), engine.state.preview && _jsxs(Text, { color: PALETTE.success, children: ["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: "up/down 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: "up/down / 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) => {
1005
+ const header = index === 0 || pickerItems[index - 1].group !== item.group
1006
+ ? _jsx(Text, { dimColor: true, bold: true, children: item.group === 'combos' ? 'your combos' : 'auto engine' }, `h-${item.group}`)
1007
+ : null;
1008
+ 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);
1009
+ })] }), 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: ["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 === ''
1010
+ ? _jsxs(Text, { color: modeAccent, children: ['>', " ", _jsx(Text, { dimColor: true, children: busy ? 'type to queue the next task' : 'describe the work and press enter' })] })
1011
+ : 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: "line" }), busy ? ' ' : '', phase, elapsed ? ` · ${elapsed}` : '', agents.length > 0 ? ` · swarm ${doneAgents}/${agents.length}` : ''] }), _jsxs(Text, { color: PALETTE.muted, children: [_jsx(Text, { color: modeAccent, children: mode }), _jsx(Text, { children: " \u00B7 " }), _jsx(Text, { color: mode === 'crazy' ? PALETTE.error : PERM_COLOR(permMode), children: mode === 'crazy' ? 'bypass' : PERM_LABEL[permMode] }), _jsxs(Text, { children: [" \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: [_jsx(Text, { dimColor: true, children: sideMode === 'split'
1012
+ ? `Ctrl+B panel · Ctrl+${modeKey} mode · Ctrl+C ${busy ? 'cancel' : 'quit'} · /help`
1013
+ : `Enter send · Ctrl+J newline · Ctrl+O models · Ctrl+${modeKey} mode · Shift+Tab perms · Ctrl+B panel · Ctrl+T tool · Ctrl+Y copy · Ctrl+C ${busy ? 'cancel' : 'quit'} · /help` }), compression ? _jsxs(Text, { dimColor: true, children: ["saved ", compression, metrics.remainingQuota !== undefined ? ` · quota ${metrics.remainingQuota}` : ''] }) : null] })] })] }), sideMode === 'split' && _jsx(Box, { marginLeft: 1, children: panel })] })] });
960
1014
  }
961
1015
  //# sourceMappingURL=terminalInterface.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omniharness-cli",
3
- "version": "0.1.80",
3
+ "version": "0.1.82",
4
4
  "description": "OmniHarness — local-first agent orchestration harness for OmniRoute.",
5
5
  "license": "MIT",
6
6
  "type": "module",