omniharness-cli 0.1.78 → 0.1.79

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.
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Data shaping for the home screen — the layout itself lives in the Ink
3
+ * component, but everything it has to decide is pure and lives here so it can
4
+ * be tested without rendering a terminal.
5
+ */
6
+ /**
7
+ * Render an age the way someone reads it at a glance: the largest unit that
8
+ * still says something useful, never more than one unit deep.
9
+ *
10
+ * Anything not yet a minute old is "now" rather than "0m ago", because a
11
+ * session saved seconds ago reading as zero looks like a bug.
12
+ */
13
+ export function relativeTime(savedAt, now = Date.now()) {
14
+ const then = Date.parse(savedAt);
15
+ if (!Number.isFinite(then))
16
+ return '';
17
+ const seconds = Math.max(0, Math.round((now - then) / 1000));
18
+ if (seconds < 60)
19
+ return 'now';
20
+ const minutes = Math.floor(seconds / 60);
21
+ if (minutes < 60)
22
+ return `${minutes}m ago`;
23
+ const hours = Math.floor(minutes / 60);
24
+ if (hours < 24)
25
+ return `${hours}h ago`;
26
+ const days = Math.floor(hours / 24);
27
+ if (days < 7)
28
+ return `${days}d ago`;
29
+ const weeks = Math.floor(days / 7);
30
+ if (weeks < 52)
31
+ return `${weeks}w ago`;
32
+ return `${Math.floor(days / 365)}y ago`;
33
+ }
34
+ /**
35
+ * The recent-session rows to show, newest first and capped. Names are clipped
36
+ * rather than wrapped: a single line per session keeps the block a predictable
37
+ * height, which matters because the home screen shares the window with the
38
+ * input.
39
+ */
40
+ export function recentRows(sessions, limit, nameWidth, now = Date.now()) {
41
+ return sessions.slice(0, Math.max(0, limit)).map((session) => ({
42
+ name: clipName(session.name, nameWidth),
43
+ age: relativeTime(session.savedAt, now),
44
+ }));
45
+ }
46
+ function clipName(name, width) {
47
+ const runes = [...name];
48
+ if (width <= 1 || runes.length <= width)
49
+ return name;
50
+ return `${runes.slice(0, width - 1).join('')}…`;
51
+ }
52
+ /**
53
+ * Whether there is room for the two-column home. Below this the columns would
54
+ * be too narrow to hold a session name or a path, so the blocks stack instead.
55
+ */
56
+ export const TWO_COLUMN_MIN_WIDTH = 76;
57
+ export function twoColumn(width) {
58
+ return width >= TWO_COLUMN_MIN_WIDTH;
59
+ }
60
+ /**
61
+ * Shorten a workspace path from the left, keeping the end. The tail is the
62
+ * part that identifies the project; the head is usually /Users/someone.
63
+ */
64
+ export function shortenPath(p, width) {
65
+ if (p.length <= width)
66
+ return p;
67
+ if (width <= 1)
68
+ return p.slice(-width);
69
+ return `…${p.slice(-(width - 1))}`;
70
+ }
71
+ /**
72
+ * A one-line summary of what the agent can reach beyond its built-in tools.
73
+ * Says nothing at all when there is nothing to say, rather than "0 skills".
74
+ */
75
+ export function capabilityLine(skills, plugins, mcpTools) {
76
+ const parts = [];
77
+ if (skills > 0)
78
+ parts.push(`${skills} skill${skills === 1 ? '' : 's'}${plugins > 0 ? ` from ${plugins} plugin${plugins === 1 ? '' : 's'}` : ''}`);
79
+ if (mcpTools > 0)
80
+ parts.push(`${mcpTools} mcp tool${mcpTools === 1 ? '' : 's'}`);
81
+ return parts.join(' · ');
82
+ }
83
+ //# sourceMappingURL=home.js.map
@@ -8,6 +8,7 @@ import { deleteAt, deleteBefore, insertAt, layoutEditor, lineEndAt, lineStartAt,
8
8
  import { renderMarkdown } from './markdown.js';
9
9
  import { looksLikeDiff, diffSegments } from './diff.js';
10
10
  import { palette } from './palette.js';
11
+ import { capabilityLine, recentRows, shortenPath, twoColumn } from './home.js';
11
12
  import { contextMeter, meterBar } from './modelWindows.js';
12
13
  import { BEL, SYNC_QUERY, isSyncOutputReply, osc9Notify, osc52Copy, shouldNudgeOnFinish, wrapSynchronizedOutput } from './termcaps.js';
13
14
  import { KITTY_POP, KITTY_PUSH, KITTY_QUERY, isEncodedKey, isKittyQueryResponse, parseRawKey } from './keys.js';
@@ -173,9 +174,39 @@ function TranscriptEntry({ line, width, fallbackModel }) {
173
174
  const bullet = line.role === 'user' ? '>' : line.role === 'error' ? '!' : '-';
174
175
  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] });
175
176
  }
176
- /** Shown until the first prompt: what the harness is pointed at, and how to drive it. */
177
- function Hero({ width, endpoint, model, mode, perm }) {
178
- return _jsxs(Box, { flexDirection: "column", marginBottom: 1, width: Math.min(width, 76), children: [_jsxs(Text, { bold: true, color: PALETTE.accent, children: ["omniharness ", ownVersion()] }), _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" })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "perms " }), _jsx(Text, { color: mode === 'crazy' ? PALETTE.error : PERM_COLOR(perm), children: mode === 'crazy' ? 'bypass' : PERM_LABEL[perm] }), _jsx(Text, { dimColor: true, children: " \u00B7 Shift+Tab cycles manual \u00B7 accept edits \u00B7 bypass" })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "describe the work and press enter \u00B7 /help for commands" }) })] });
177
+ /**
178
+ * Width of the dim label column on the home screen. The value gets whatever is
179
+ * left, and every caller has to subtract this: a value sized to the whole box
180
+ * wraps onto a second line and the block stops being a fixed height.
181
+ */
182
+ const LABEL_WIDTH = 10;
183
+ /** One labelled line: a dim fixed-width label, then the value. */
184
+ function Field({ label, children }) {
185
+ return _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: label.padEnd(LABEL_WIDTH) }), children] });
186
+ }
187
+ /**
188
+ * The home screen, shown while the transcript is empty.
189
+ *
190
+ * Two columns when the terminal is wide enough — what this session is on the
191
+ * left, what you can pick up and what you can press on the right — stacking
192
+ * below that rather than squeezing. Everything on it is read from the running
193
+ * session: there is no placeholder copy and no invented "what's new" feed,
194
+ * because a home screen that shows things which are not true is worse than a
195
+ * plain one.
196
+ */
197
+ export function Hero(props) {
198
+ const { width, endpoint, model, mode, perm, workspace, sessions, skills, plugins, mcpTools } = props;
199
+ const wide = twoColumn(width);
200
+ const outer = Math.min(width - 2, 84);
201
+ const column = wide ? Math.floor((outer - 3) / 2) : outer;
202
+ const inner = Math.max(12, column - 4);
203
+ // Values sit to the right of the label, so that is the room they actually get.
204
+ const value = Math.max(8, inner - LABEL_WIDTH);
205
+ const capability = capabilityLine(skills, plugins, mcpTools);
206
+ const recent = recentRows(sessions, 4, Math.max(8, inner - 9));
207
+ const session = _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.accent, paddingX: 1, width: column, children: [_jsxs(Text, { bold: true, color: PALETTE.accent, children: ["omniharness ", ownVersion()] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Field, { label: "workspace", children: shortenPath(workspace, value) }), _jsx(Field, { label: "gateway", children: shortenPath(endpoint, value) }), _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] }) }), capability !== '' && _jsx(Field, { label: "loaded", children: clip(capability, value) })] })] });
208
+ const aside = _jsxs(Box, { flexDirection: "column", width: column, marginLeft: wide ? 1 : 0, marginTop: wide ? 0 : 1, children: [recent.length > 0 && _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.muted, paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "recent" }), recent.map((row) => _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: row.age.padEnd(8) }), row.name] }, row.name)), _jsx(Text, { dimColor: true, children: "/sessions for all" })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: PALETTE.muted, paddingX: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "keys" }), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.accent, children: 'Ctrl+E'.padEnd(LABEL_WIDTH) }), _jsx(Text, { dimColor: true, children: "cycle mode" })] }), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.accent, children: 'Shift+Tab'.padEnd(LABEL_WIDTH) }), _jsx(Text, { dimColor: true, children: "cycle perms" })] }), _jsxs(Text, { children: [_jsx(Text, { color: PALETTE.accent, children: 'Ctrl+O'.padEnd(LABEL_WIDTH) }), _jsx(Text, { dimColor: true, children: "pick a model" })] }), _jsx(Text, { dimColor: true, children: "/help for the rest" })] })] });
209
+ return _jsxs(Box, { flexDirection: "column", marginBottom: 1, width: outer, children: [_jsxs(Box, { flexDirection: wide ? 'row' : 'column', alignItems: "flex-start", children: [session, aside] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "describe the work and press enter" }) })] });
179
210
  }
180
211
  export function TerminalInterface({ engine }) {
181
212
  const { exit } = useApp();
@@ -216,6 +247,9 @@ export function TerminalInterface({ engine }) {
216
247
  const [now, setNow] = useState(() => Date.now());
217
248
  const queuedRef = useRef(null);
218
249
  const [queued, setQueued] = useState();
250
+ // Recent snapshots for the home screen. Read once on mount and left
251
+ // alone: the home screen is only on screen before the first turn.
252
+ const [recentSessions, setRecentSessions] = useState([]);
219
253
  const [layoutDebug, setLayoutDebug] = useState(false);
220
254
  const syncRestoreRef = useRef(null);
221
255
  const pushLine = (line) => setLines((current) => [...current, line]);
@@ -226,6 +260,10 @@ export function TerminalInterface({ engine }) {
226
260
  if (alive && promptHistoryRef.current.length === 0)
227
261
  syncPromptHistory(history);
228
262
  }).catch(() => { });
263
+ void listSessions(engine.state.workspace.root).then((found) => {
264
+ if (alive)
265
+ setRecentSessions(found);
266
+ }).catch(() => { });
229
267
  return () => { alive = false; };
230
268
  }, []);
231
269
  useEffect(() => {
@@ -895,7 +933,7 @@ export function TerminalInterface({ engine }) {
895
933
  const liveThinkView = liveThinkLines.slice(-Math.max(2, Math.floor(liveBudget / 2)));
896
934
  const liveAnswerView = liveAnswerLines.slice(-liveBudget);
897
935
  const doneAgents = agents.filter((lane) => lane.status === 'done').length;
898
- 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 }), 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) => {
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) => {
899
937
  const expanded = expandedTool === card.id;
900
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" });
901
939
  const head = card.name === 'run_command'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omniharness-cli",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "description": "OmniHarness — local-first agent orchestration harness for OmniRoute.",
5
5
  "license": "MIT",
6
6
  "type": "module",