@scira/cli 0.1.0 → 0.1.2

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.
@@ -1,15 +1,68 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
+ import { LLM_PROVIDER_LABELS } from "../../../providers/llm/registry.js";
3
4
  import { pkgVersion, relativeTime } from "../lib/utils.js";
4
5
  import { HOME_TIPS } from "../constants.js";
6
+ import { useTheme } from "../hooks/use-theme.js";
7
+ function enabledMcpCount(config) {
8
+ return (config.mcp.chromeDevtools.enabled ? 1 : 0) + config.mcp.servers.filter((s) => s.enabled).length;
9
+ }
10
+ function computeHeroLayout(bodyRows, bodyCols, hasNotice) {
11
+ const showTip = bodyRows >= 20 && bodyCols >= 58 && !hasNotice;
12
+ const showArt = bodyRows >= 17 && bodyCols >= 72;
13
+ const showSubtitle = bodyRows >= 14 && bodyCols >= 50;
14
+ const showTagline = bodyRows >= 16 && bodyCols >= 64;
15
+ const showHint = bodyRows >= 15 && bodyCols >= 58;
16
+ const showConfig = bodyRows >= 12 && bodyCols >= 46;
17
+ const showConfigDetail = bodyRows >= 14 && bodyCols >= 56;
18
+ const showProviderLabel = bodyCols >= 62;
19
+ const showVersion = bodyRows >= 13 && bodyCols >= 54;
20
+ const showActionHints = bodyCols >= 52;
21
+ let contentRows = 1; // title
22
+ if (showTagline)
23
+ contentRows++;
24
+ if (showHint)
25
+ contentRows++;
26
+ if (showConfig || showConfigDetail) {
27
+ contentRows++; // spacer
28
+ if (showConfig)
29
+ contentRows++;
30
+ if (showConfigDetail)
31
+ contentRows++;
32
+ }
33
+ const newResearchRowOffset = contentRows + 1; // spacer before actions, then new research
34
+ contentRows = newResearchRowOffset + 2; // new + quit
35
+ const cardRows = contentRows + 2; // border padding
36
+ return {
37
+ showArt,
38
+ showSubtitle,
39
+ showTagline,
40
+ showHint,
41
+ showConfig,
42
+ showConfigDetail,
43
+ showProviderLabel,
44
+ showVersion,
45
+ showActionHints,
46
+ showTip,
47
+ cardRows,
48
+ newResearchRowOffset,
49
+ };
50
+ }
5
51
  /** Home screen body: branding card, browse modal, notice, and tip line.
6
52
  * Also (re)builds the mouse click/hover row maps as a render side-effect. */
7
- export function HomeScreen({ cols, rows, sessions, selectedIdx, hoveredIdx, heroHidden, notice, tipIndex, commandMenuHeight, sessionsModalOpen, sessionsModalIdx, inputText, clickMapRef, hoverMapRef, setSelectedIdx, setSessionsModalOpen, setSessionsModalIdx, setNotice, openRun, submitHome, exit, }) {
8
- const cardW = Math.min(Math.max(52, cols - 4), 90);
9
- const heroRows = heroHidden ? 0 : 2;
10
- const cardH = heroHidden ? 0 : 10;
53
+ export function HomeScreen({ cols, rows, sessions, selectedIdx, hoveredIdx, heroHidden, notice, tipIndex, commandMenuHeight, mcpOpen, sessionsModalOpen, sessionsModalIdx, inputText, config, modelName, clickMapRef, hoverMapRef, setSelectedIdx, setSessionsModalOpen, setSessionsModalIdx, setNotice, openRun, submitHome, exit, }) {
54
+ const theme = useTheme();
55
+ const bodyCols = Math.max(32, cols - 4);
56
+ const cardW = Math.min(Math.max(36, bodyCols), 90);
57
+ const mcpCount = enabledMcpCount(config);
11
58
  const bHeight = rows - 6 - commandMenuHeight;
12
- const contentH = cardH + (notice ? 2 : 0) + 2;
59
+ const heroLayout = heroHidden ? null : computeHeroLayout(bHeight, bodyCols, !!notice);
60
+ const providerLabel = heroLayout?.showProviderLabel
61
+ ? LLM_PROVIDER_LABELS[config.llmProvider]
62
+ : config.llmProvider;
63
+ const cardH = heroLayout?.cardRows ?? 0;
64
+ const tipRows = heroLayout?.showTip ? 2 : 0;
65
+ const contentH = cardH + (notice ? 2 : 0) + tipRows;
13
66
  const topGap = Math.max(0, Math.floor((bHeight - contentH) / 2));
14
67
  const cardTop0 = 2 + topGap;
15
68
  const newIdx = 0;
@@ -18,8 +71,8 @@ export function HomeScreen({ cols, rows, sessions, selectedIdx, hoveredIdx, hero
18
71
  const quitActive = selectedIdx === quitIdx || hoveredIdx === quitIdx;
19
72
  const clickMap = new Map();
20
73
  const hoverMap = new Map();
21
- const rowBase = cardTop0 + heroRows + 4;
22
- if (sessionsModalOpen) {
74
+ const rowBase = cardTop0 + 1 + (heroLayout?.newResearchRowOffset ?? 4);
75
+ if (!mcpOpen && sessionsModalOpen) {
23
76
  const modalVisible = Math.max(5, rows - 12);
24
77
  const half = Math.floor(modalVisible / 2);
25
78
  const windowStart = Math.max(0, Math.min(sessionsModalIdx - half, Math.max(0, sessions.length - modalVisible)));
@@ -38,8 +91,8 @@ export function HomeScreen({ cols, rows, sessions, selectedIdx, hoveredIdx, hero
38
91
  hoverMap.set(row, idx);
39
92
  });
40
93
  }
41
- else if (!heroHidden) {
42
- clickMap.set(rowBase, () => {
94
+ else if (!mcpOpen && !heroHidden) {
95
+ clickMap.set(rowBase, (_x) => {
43
96
  setSelectedIdx(newIdx);
44
97
  if (inputText.trim())
45
98
  void submitHome(inputText);
@@ -47,11 +100,13 @@ export function HomeScreen({ cols, rows, sessions, selectedIdx, hoveredIdx, hero
47
100
  setNotice("Type a question below to start a new research run.");
48
101
  });
49
102
  hoverMap.set(rowBase, newIdx);
50
- clickMap.set(rowBase + 1, () => exit());
103
+ clickMap.set(rowBase + 1, (_x) => exit());
51
104
  hoverMap.set(rowBase + 1, quitIdx);
52
105
  }
53
- clickMapRef.current = clickMap;
54
- hoverMapRef.current = hoverMap;
106
+ if (!mcpOpen) {
107
+ clickMapRef.current = clickMap;
108
+ hoverMapRef.current = hoverMap;
109
+ }
55
110
  return (_jsx(Box, { flexGrow: 1, flexDirection: "column", justifyContent: "center", alignItems: "center", children: sessionsModalOpen ? (() => {
56
111
  const modalVisible = Math.max(5, rows - 12);
57
112
  const half = Math.floor(modalVisible / 2);
@@ -59,11 +114,13 @@ export function HomeScreen({ cols, rows, sessions, selectedIdx, hoveredIdx, hero
59
114
  const windowEnd = Math.min(sessions.length, windowStart + modalVisible);
60
115
  const above = windowStart;
61
116
  const below = sessions.length - windowEnd;
62
- return (_jsxs(Box, { borderStyle: "round", borderColor: "#FFE0C2", width: cardW, flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: "#FFE0C2", bold: true, children: "All sessions" }), _jsx(Text, { color: "gray", dimColor: true, children: `${sessions.length} total · ${sessionsModalIdx + 1}/${sessions.length} · ↑↓ navigate · ⏎ open · esc close` }), _jsx(Box, { height: 1 }), above > 0 && _jsx(Text, { color: "gray", dimColor: true, children: ` ↑ ${above} more above` }), sessions.slice(windowStart, windowEnd).map((s, i) => {
117
+ return (_jsxs(Box, { borderStyle: "round", borderColor: theme.border, width: cardW, flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "All sessions" }), _jsx(Text, { color: theme.textDim, wrap: "truncate", children: bodyCols >= 58
118
+ ? `${sessions.length} total · ${sessionsModalIdx + 1}/${sessions.length} · ↑↓ navigate · ⏎ open · esc close`
119
+ : `${sessions.length} sessions · ${sessionsModalIdx + 1}/${sessions.length} · esc close` }), _jsx(Box, { height: 1 }), above > 0 && _jsx(Text, { color: theme.textDim, children: ` ↑ ${above} more above` }), sessions.slice(windowStart, windowEnd).map((s, i) => {
63
120
  const idx = windowStart + i;
64
121
  const active = idx === sessionsModalIdx;
65
122
  const display = (s.title || s.goal || s.id).slice(0, cardW - 22);
66
- return (_jsxs(Box, { children: [_jsx(Text, { color: active ? "white" : "gray", children: active ? "❯ [ " : " [ " }), _jsx(Text, { bold: active, color: active ? "white" : "gray", wrap: "truncate", children: display }), _jsx(Box, { flexGrow: 1 }), s.isFull && _jsx(Text, { color: s.reportDirty ? "yellow" : "green", dimColor: true, children: s.reportDirty ? "draft " : "ready " }), _jsx(Text, { color: "gray", dimColor: true, children: relativeTime(s.updatedAt) }), _jsx(Text, { color: active ? "white" : "gray", children: " ]" })] }, s.id));
67
- }), below > 0 && _jsx(Text, { color: "gray", dimColor: true, children: ` ↓ ${below} more below` })] }));
68
- })() : heroHidden ? null : (_jsxs(Box, { flexDirection: "column", alignItems: "center", width: cardW, children: [_jsxs(Box, { borderStyle: "round", borderColor: "gray", width: cardW, flexDirection: "row", paddingX: 2, paddingY: 1, children: [_jsx(Box, { flexDirection: "column", justifyContent: "center", marginRight: 3, children: [" ·:::· ", "·: ", " ·:::· ", " ·:", " ·:::· "].map((line, i) => (_jsx(Text, { color: "#FFE0C2", dimColor: true, children: line }, i))) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "white", children: "scira" }), _jsx(Text, { color: "gray", dimColor: true, children: "research agent" }), _jsxs(Text, { color: "gray", dimColor: true, children: ["v", pkgVersion] })] }), _jsx(Text, { color: "white", children: "Research and coding agent with real sources and tools." }), _jsx(Text, { color: "gray", dimColor: true, children: "Type a question below to start, or use # to browse past sessions." }), _jsx(Box, { height: 1 }), _jsxs(Box, { children: [_jsx(Text, { bold: newActive, color: newActive ? "white" : "gray", children: "New research" }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: "gray", dimColor: true, children: "\u23CE enter" })] }), _jsxs(Box, { children: [_jsx(Text, { bold: quitActive, color: quitActive ? "white" : "gray", children: "Quit" }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: "gray", dimColor: true, children: "ctrl+d" })] })] })] }), notice ? (_jsx(Box, { paddingTop: 1, children: _jsx(Text, { color: "yellow", children: notice }) })) : null, _jsx(Box, { paddingTop: 2, children: _jsxs(Text, { color: "gray", dimColor: true, children: ["Tip: ", HOME_TIPS[tipIndex % HOME_TIPS.length]] }) })] })) }));
123
+ return (_jsxs(Box, { children: [_jsx(Text, { color: active ? theme.text : theme.textDim, children: active ? "❯ [ " : " [ " }), _jsx(Text, { bold: active, color: active ? theme.text : theme.textDim, wrap: "truncate", children: display }), _jsx(Box, { flexGrow: 1 }), s.isFull && _jsx(Text, { color: s.reportDirty ? theme.warning : theme.success, children: s.reportDirty ? "draft " : "ready " }), _jsx(Text, { color: theme.textDim, children: relativeTime(s.updatedAt) }), _jsx(Text, { color: active ? theme.text : theme.textDim, children: " ]" })] }, s.id));
124
+ }), below > 0 && _jsx(Text, { color: theme.textDim, children: ` ↓ ${below} more below` })] }));
125
+ })() : heroHidden || !heroLayout ? null : (_jsxs(Box, { flexDirection: "column", alignItems: "center", width: cardW, children: [_jsxs(Box, { borderStyle: "round", borderColor: theme.border, width: cardW, flexDirection: "row", paddingX: 2, paddingY: 1, children: [heroLayout.showArt ? (_jsx(Box, { flexDirection: "column", justifyContent: "center", marginRight: 3, children: [" ·:::· ", "·: ", " ·:::· ", " ·:", " ·:::· "].map((line, i) => (_jsx(Text, { color: theme.accent, children: line }, i))) })) : null, _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { gap: heroLayout.showSubtitle || heroLayout.showVersion ? 2 : 0, marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.text, children: "scira" }), heroLayout.showSubtitle ? _jsx(Text, { color: theme.textDim, children: "research agent" }) : null, heroLayout.showVersion ? _jsxs(Text, { color: theme.textDim, children: ["v", pkgVersion] }) : null] }), heroLayout.showTagline ? (_jsx(Text, { color: theme.text, wrap: "truncate", children: "Research and coding agent with real sources and tools." })) : null, heroLayout.showHint ? (_jsx(Text, { color: theme.textDim, wrap: "truncate", children: "Type a question below to start, or use # to browse past sessions." })) : null, (heroLayout.showConfig || heroLayout.showConfigDetail) ? _jsx(Box, { height: 1 }) : null, heroLayout.showConfig ? (_jsxs(Text, { color: theme.textDim, wrap: "truncate", children: [_jsx(Text, { color: theme.accent, children: "model " }), modelName, _jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", providerLabel] })] })) : null, heroLayout.showConfigDetail ? (_jsxs(Text, { color: theme.textDim, wrap: "truncate", children: [_jsx(Text, { color: theme.accent, children: "search " }), config.search.provider, " \u00D7", config.search.maxResults, _jsxs(Text, { color: theme.textDim, children: [" \u00B7 theme ", config.theme, " \u00B7 ", config.approvalMode] }), mcpCount > 0 ? _jsxs(Text, { color: theme.textDim, children: [" \u00B7 ", mcpCount, " mcp"] }) : null] })) : null, _jsx(Box, { height: 1 }), _jsxs(Box, { children: [_jsx(Text, { bold: newActive, color: newActive ? theme.text : theme.textDim, children: "New research" }), heroLayout.showActionHints ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: theme.textDim, children: "\u23CE enter" })] })) : null] }), _jsxs(Box, { children: [_jsx(Text, { bold: quitActive, color: quitActive ? theme.text : theme.textDim, children: "Quit" }), heroLayout.showActionHints ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: theme.textDim, children: "ctrl+d" })] })) : null] })] })] }), notice ? (_jsx(Box, { paddingTop: 1, children: _jsx(Text, { color: theme.warning, children: notice }) })) : null, heroLayout.showTip ? (_jsx(Box, { paddingTop: 2, width: cardW, children: _jsxs(Text, { color: theme.textDim, wrap: "truncate", children: ["Tip: ", HOME_TIPS[tipIndex % HOME_TIPS.length]] }) })) : null] })) }));
69
126
  }
@@ -3,26 +3,33 @@ import { Box, Text } from "ink";
3
3
  import { SPINNER_FRAMES, CHAT_COMMANDS, COMMAND_DESCRIPTIONS, MENU_VISIBLE } from "../constants.js";
4
4
  import { fmtTokens, wrapText } from "../lib/utils.js";
5
5
  import { LLM_PROVIDER_LABELS } from "../../../providers/llm/registry.js";
6
- export function TopBar({ screen, runState, fullMode, activeUsage, busy, frame, cwdDisplay }) {
7
- return (_jsxs(Box, { paddingTop: 1, justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 1, minWidth: 0, marginRight: 2, children: _jsx(Text, { color: "gray", dimColor: true, wrap: "truncate-end", children: screen === "chat" ? (runState?.title || runState?.goal || cwdDisplay) : cwdDisplay }) }), screen === "chat" && (_jsxs(Box, { flexShrink: 0, gap: 1, children: [_jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsx(Text, { color: fullMode ? "magenta" : "#FFE0C2", children: fullMode ? "full" : "quick" }), activeUsage && (_jsx(Text, { color: "gray", dimColor: true, children: `↑${fmtTokens(activeUsage.input)} ↓${fmtTokens(activeUsage.output)}` })), fullMode && (_jsxs(Text, { color: "gray", dimColor: true, children: [`src:${runState?.sourceCount ?? 0}`, (runState?.claimCount ?? 0) > 0 ? ` · claims:${runState?.claimCount}` : ""] })), fullMode && (_jsx(Text, { color: runState?.reportDirty ? "yellow" : "green", dimColor: true, children: runState?.reportDirty ? "draft" : "ready" })), busy && _jsx(Text, { color: "#FFE0C2", children: SPINNER_FRAMES[frame % SPINNER_FRAMES.length] }), _jsx(Text, { color: "gray", dimColor: true, children: "|" })] }))] }));
6
+ import { useTheme } from "../hooks/use-theme.js";
7
+ export function TopBar({ screen, runState, fullMode, activeUsage, busy, frame, cwdDisplay, config }) {
8
+ const theme = useTheme();
9
+ return (_jsxs(Box, { paddingTop: 1, justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 1, minWidth: 0, marginRight: 2, children: _jsx(Text, { color: theme.textDim, wrap: "truncate-end", children: screen === "chat" ? (runState?.title || runState?.goal || cwdDisplay) : cwdDisplay }) }), screen === "chat" && (_jsxs(Box, { flexShrink: 0, gap: 1, children: [_jsx(Text, { color: theme.textDim, children: "|" }), _jsx(Text, { color: fullMode ? "magenta" : theme.accent, children: fullMode ? "full" : "quick" }), activeUsage && (_jsx(Text, { color: theme.textDim, children: `↑${fmtTokens(activeUsage.input)} ↓${fmtTokens(activeUsage.output)}` })), fullMode && (_jsxs(Text, { color: theme.textDim, children: [`src:${runState?.sourceCount ?? 0}`, (runState?.claimCount ?? 0) > 0 ? ` · claims:${runState?.claimCount}` : ""] })), fullMode && (_jsx(Text, { color: runState?.reportDirty ? theme.warning : theme.success, children: runState?.reportDirty ? "draft" : "ready" })), busy && _jsx(Text, { color: theme.accent, children: SPINNER_FRAMES[frame % SPINNER_FRAMES.length] }), _jsx(Text, { color: theme.textDim, children: "|" })] }))] }));
8
10
  }
9
- export function InputBar({ inputLines, cursorLine, cursorCol, showCursor, approvalPending, busy, frame, boxWidth, modelName }) {
10
- const accentColor = approvalPending ? "yellowBright" : busy ? "#C2AA93" : "gray";
11
+ export function InputBar({ inputLines, cursorLine, cursorCol, showCursor, approvalPending, busy, frame, boxWidth, modelName, config }) {
12
+ const theme = useTheme();
13
+ const borderColor = approvalPending ? theme.warning : busy ? theme.accentDim : theme.textDim;
14
+ const promptColor = approvalPending ? theme.warning : busy ? theme.accentDim : theme.accent;
15
+ const inputColor = approvalPending ? theme.textDim : theme.inputText;
11
16
  const borderLabel = busy ? `${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${modelName}` : modelName;
12
17
  const labelMax = Math.max(0, boxWidth - 6);
13
18
  const label = borderLabel.length > labelMax ? borderLabel.slice(0, labelMax) : borderLabel;
14
19
  const dashCount = Math.max(1, boxWidth - label.length - 5);
15
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: accentColor, children: "╭" + "─".repeat(Math.max(0, boxWidth - 2)) + "╮" }), inputLines.map((line, i) => (_jsxs(Box, { width: boxWidth, children: [_jsx(Text, { color: accentColor, children: "│ " }), _jsx(Text, { color: i === 0 ? (approvalPending ? "yellowBright" : busy ? "#C2AA93" : "#FFE0C2") : accentColor, children: i === 0 ? "❯ " : " " }), showCursor && i === cursorLine ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: line.slice(0, cursorCol) }), _jsx(Text, { color: "white", inverse: true, children: line[cursorCol] ?? " " }), _jsx(Text, { color: "white", children: line.slice(cursorCol + 1) })] })) : (_jsx(Text, { color: approvalPending ? "gray" : "white", children: line })), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: accentColor, children: " \u2502" })] }, i))), _jsxs(Box, { width: boxWidth, children: [_jsx(Text, { color: accentColor, children: "╰" + "─".repeat(dashCount) + " " }), _jsx(Text, { color: "#FFE0C2", children: label }), _jsx(Text, { color: accentColor, children: " ─╯" })] })] }));
20
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: borderColor, children: "╭" + "─".repeat(Math.max(0, boxWidth - 2)) + "╮" }), inputLines.map((line, i) => (_jsxs(Box, { width: boxWidth, children: [_jsx(Text, { color: borderColor, children: "│ " }), _jsx(Text, { color: i === 0 ? promptColor : borderColor, children: i === 0 ? "❯ " : " " }), _jsx(Text, { color: inputColor, wrap: "truncate", children: showCursor && i === cursorLine ? (_jsxs(_Fragment, { children: [line.slice(0, cursorCol), _jsx(Text, { backgroundColor: theme.cursorBackground, color: theme.cursorForeground, children: line[cursorCol] ?? " " }), line.slice(cursorCol + 1)] })) : (line) }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: borderColor, children: " \u2502" })] }, i))), _jsxs(Box, { width: boxWidth, children: [_jsx(Text, { color: borderColor, children: "╰" + "─".repeat(dashCount) + " " }), _jsx(Text, { color: theme.accent, children: label }), _jsx(Text, { color: borderColor, children: " ─╯" })] })] }));
16
21
  }
17
- export function HintLine({ screen, busy, scrollLabel, hasDoneGroups, hasFocusedGroup }) {
22
+ export function HintLine({ screen, busy, scrollLabel, hasDoneGroups, hasFocusedGroup, config }) {
23
+ const theme = useTheme();
18
24
  if (screen === "chat") {
19
- return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "gray", dimColor: true, children: _jsx(Text, { bold: true, color: "#FFE0C2", children: "/HELP" }) }), _jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsx(Text, { color: "gray", dimColor: true, children: _jsx(Text, { bold: true, color: "#FFE0C2", children: "/REPORT" }) }), _jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsx(Text, { color: "gray", dimColor: true, children: _jsx(Text, { bold: true, color: "#FFE0C2", children: "/NEW" }) }), busy && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsx(Text, { color: "gray", dimColor: true, children: _jsx(Text, { bold: true, color: "#FFE0C2", children: "/STOP" }) })] })), hasDoneGroups && !busy ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsx(Text, { color: "gray", dimColor: true, children: hasFocusedGroup
20
- ? _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "#FFE0C2", children: "C" }), " toggle \u00B7 ", _jsx(Text, { bold: true, color: "#FFE0C2", children: "ESC" }), " unfocus"] })
21
- : _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "#FFE0C2", children: "[ ]" }), " \u00B7 ", _jsx(Text, { bold: true, color: "#FFE0C2", children: "C" }), " groups"] }) })] })) : null, scrollLabel ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: "gray", dimColor: true, children: scrollLabel })] })) : null] }));
25
+ return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: theme.textDim, children: _jsx(Text, { bold: true, color: theme.accent, children: "/HELP" }) }), _jsx(Text, { color: theme.textDim, children: "|" }), _jsx(Text, { color: theme.textDim, children: _jsx(Text, { bold: true, color: theme.accent, children: "/REPORT" }) }), _jsx(Text, { color: theme.textDim, children: "|" }), _jsx(Text, { color: theme.textDim, children: _jsx(Text, { bold: true, color: theme.accent, children: "/NEW" }) }), busy && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.textDim, children: "|" }), _jsx(Text, { color: theme.textDim, children: _jsx(Text, { bold: true, color: theme.accent, children: "/STOP" }) })] })), hasDoneGroups && !busy ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.textDim, children: "|" }), _jsx(Text, { color: theme.textDim, children: hasFocusedGroup
26
+ ? _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: theme.accent, children: "C" }), " toggle \u00B7 ", _jsx(Text, { bold: true, color: theme.accent, children: "ESC" }), " unfocus"] })
27
+ : _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: theme.accent, children: "[ ]" }), " \u00B7 ", _jsx(Text, { bold: true, color: theme.accent, children: "C" }), " groups"] }) })] })) : null, scrollLabel ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: theme.textDim, children: scrollLabel })] })) : null] }));
22
28
  }
23
- return (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [_jsx(Text, { bold: true, color: "#FFE0C2", children: "\u2191\u2193" }), ":navigate"] }), _jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsxs(Text, { color: "gray", dimColor: true, children: [_jsx(Text, { bold: true, color: "#FFE0C2", children: "\u23CE" }), ":open"] }), _jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsxs(Text, { color: "gray", dimColor: true, children: [_jsx(Text, { bold: true, color: "#FFE0C2", children: "ESC" }), ":close"] }), _jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsxs(Text, { color: "gray", dimColor: true, children: [_jsx(Text, { bold: true, color: "#FFE0C2", children: "Q" }), ":quit"] })] }));
29
+ return (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: theme.textDim, children: [_jsx(Text, { bold: true, color: theme.accent, children: "\u2191\u2193" }), ":navigate"] }), _jsx(Text, { color: theme.textDim, children: "|" }), _jsxs(Text, { color: theme.textDim, children: [_jsx(Text, { bold: true, color: theme.accent, children: "\u23CE" }), ":open"] }), _jsx(Text, { color: theme.textDim, children: "|" }), _jsxs(Text, { color: theme.textDim, children: [_jsx(Text, { bold: true, color: theme.accent, children: "ESC" }), ":close"] }), _jsx(Text, { color: theme.textDim, children: "|" }), _jsxs(Text, { color: theme.textDim, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Q" }), ":quit"] })] }));
24
30
  }
25
- export function CommandMenuBox({ activeSuggestions, activeSuggestionKind, commandMenuIndex, innerWidth, sessions }) {
31
+ export function CommandMenuBox({ activeSuggestions, activeSuggestionKind, commandMenuIndex, innerWidth, sessions, config }) {
32
+ const theme = useTheme();
26
33
  if (activeSuggestions.length === 0)
27
34
  return null;
28
35
  const total = activeSuggestions.length;
@@ -36,7 +43,7 @@ export function CommandMenuBox({ activeSuggestions, activeSuggestionKind, comman
36
43
  const isSessionMenu = activeSuggestionKind === "session";
37
44
  const baseHeader = isFileMenu ? "files ↑↓ move · tab complete"
38
45
  : isSessionMenu ? "sessions ↑↓ move · ⏎ open"
39
- : "commands ↑↓ move · tab complete";
46
+ : "commands ↑↓ move · tab complete · ⏎ run";
40
47
  const header = total > MENU_VISIBLE ? `${baseHeader} · ${clampedIdx + 1}/${total}` : baseHeader;
41
48
  const sessionDesc = (label) => {
42
49
  const s = sessions?.find((r) => (r.title ?? r.goal ?? r.id).replace(/\s+/gu, " ").trim() === label);
@@ -49,7 +56,7 @@ export function CommandMenuBox({ activeSuggestions, activeSuggestionKind, comman
49
56
  bits.push(`${s.claimCount} claims`);
50
57
  return bits.join(" · ");
51
58
  };
52
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginX: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [header, windowStart > 0 ? " ↑" : "", windowStart + MENU_VISIBLE < total ? " ↓" : ""] }), visible.map((item, i) => {
59
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, marginX: 1, children: [_jsxs(Text, { color: theme.textDim, children: [header, windowStart > 0 ? " ↑" : "", windowStart + MENU_VISIBLE < total ? " ↓" : ""] }), visible.map((item, i) => {
53
60
  const gi = windowStart + i;
54
61
  const active = gi === clampedIdx;
55
62
  const name = isSessionMenu && item.length > nameWidth ? item.slice(0, Math.max(0, nameWidth - 1)) + "…" : item;
@@ -59,22 +66,25 @@ export function CommandMenuBox({ activeSuggestions, activeSuggestionKind, comman
59
66
  : COMMAND_DESCRIPTIONS[item] ?? "";
60
67
  const trimmed = desc.length > descMax ? desc.slice(0, Math.max(0, descMax - 1)) + "…" : desc;
61
68
  const namePad = " ".repeat(Math.max(1, nameWidth - name.length + 2));
62
- return (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: active ? "white" : "gray", bold: active, children: [active ? "❯ " : " ", label] }), _jsxs(Text, { color: "gray", dimColor: true, children: [namePad, trimmed] })] }, `${item}-${gi}`));
69
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: active ? theme.text : theme.textDim, bold: active, children: [active ? "❯ " : " ", label] }), _jsxs(Text, { color: theme.textDim, children: [namePad, trimmed] })] }, `${item}-${gi}`));
63
70
  })] }));
64
71
  }
65
- export function HelpBox({ open, innerWidth }) {
72
+ export function HelpBox({ open, innerWidth, config }) {
73
+ const theme = useTheme();
66
74
  if (!open)
67
75
  return null;
68
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginX: 1, children: [_jsxs(Text, { bold: true, color: "white", children: ["help ", _jsx(Text, { color: "gray", dimColor: true, children: "esc close" })] }), _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(Math.max(10, innerWidth - 6)) }), _jsx(Text, { color: "gray", dimColor: true, children: "scroll \u2191/\u2193 k/j u/d pgup/pgdn" }), _jsx(Text, { color: "gray", dimColor: true, children: "autocomplete / commands \u00B7 @ files \u00B7 # sessions" }), _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(Math.max(10, innerWidth - 6)) }), CHAT_COMMANDS.map((cmd) => (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: "#FFE0C2", children: cmd }), _jsx(Text, { color: "gray", dimColor: true, children: COMMAND_DESCRIPTIONS[cmd] })] }, cmd)))] }));
76
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, marginX: 1, children: [_jsxs(Text, { bold: true, color: theme.text, children: ["help ", _jsx(Text, { color: theme.textDim, children: "esc close" })] }), _jsx(Text, { color: theme.textDim, children: "─".repeat(Math.max(10, innerWidth - 6)) }), _jsx(Text, { color: theme.textDim, children: "scroll \u2191/\u2193 k/j u/d pgup/pgdn" }), _jsx(Text, { color: theme.textDim, children: "autocomplete / commands \u00B7 @ files \u00B7 # sessions" }), _jsx(Text, { color: theme.textDim, children: "─".repeat(Math.max(10, innerWidth - 6)) }), CHAT_COMMANDS.map((cmd) => (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: theme.accent, children: cmd }), _jsx(Text, { color: theme.textDim, children: COMMAND_DESCRIPTIONS[cmd] })] }, cmd)))] }));
69
77
  }
70
- export function ApprovalBox({ toolName, description, innerWidth }) {
71
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellowBright", paddingX: 1, marginX: 1, children: [_jsxs(Text, { bold: true, color: "yellowBright", children: ["\u26A0 ", toolName, _jsx(Text, { color: "gray", dimColor: true, children: " y approve \u00B7 n reject" })] }), _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(Math.max(10, innerWidth - 6)) }), wrapText(description, Math.max(10, innerWidth - 4)).slice(0, 6).map((line, i) => {
78
+ export function ApprovalBox({ toolName, description, innerWidth, config }) {
79
+ const theme = useTheme();
80
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.warning, paddingX: 1, marginX: 1, children: [_jsxs(Text, { bold: true, color: theme.warning, children: ["\u26A0 ", toolName, _jsx(Text, { color: theme.textDim, children: " y approve \u00B7 n reject" })] }), _jsx(Text, { color: theme.textDim, children: "─".repeat(Math.max(10, innerWidth - 6)) }), wrapText(description, Math.max(10, innerWidth - 4)).slice(0, 6).map((line, i) => {
72
81
  const isAdded = line.startsWith("+ ");
73
82
  const isRemoved = line.startsWith("- ");
74
- return (_jsx(Text, { color: isAdded ? "green" : isRemoved ? "red" : "gray", wrap: "truncate", children: line }, i));
83
+ return (_jsx(Text, { color: isAdded ? theme.success : isRemoved ? theme.error : theme.textDim, wrap: "truncate", children: line }, i));
75
84
  })] }));
76
85
  }
77
- export function MenuDialog({ menu, cols, rows }) {
86
+ export function MenuDialog({ menu, cols, rows, config }) {
87
+ const theme = useTheme();
78
88
  if (!menu)
79
89
  return null;
80
90
  const DIALOG_W = Math.min(64, Math.max(40, cols - 4));
@@ -90,22 +100,66 @@ export function MenuDialog({ menu, cols, rows }) {
90
100
  const dialogLeft = Math.max(0, Math.floor((cols - 4 - DIALOG_W) / 2));
91
101
  const dialogH = 5 + (menu.loading ? 1 : Math.min(DIALOG_ITEMS, menuFiltered.length) + (menuStart > 0 ? 1 : 0) + (menuFiltered.length - (menuStart + DIALOG_ITEMS) > 0 ? 1 : 0));
92
102
  const dialogTop = Math.max(1, Math.floor((rows - dialogH) / 2));
93
- return (_jsxs(Box, { position: "absolute", marginLeft: dialogLeft, marginTop: dialogTop, width: DIALOG_W, flexDirection: "column", borderStyle: "round", borderColor: "#FFE0C2", backgroundColor: "#0d0d0d", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "white", children: [menu.type === "model" ? "Select model" : menu.type === "llm" ? "Select LLM provider" : "Select search provider", " ", _jsx(Text, { color: "gray", dimColor: true, children: "\u2191\u2193 navigate \u00B7 \u23CE apply \u00B7 esc close" })] }), !menu.loading && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: "#FFE0C2", children: "⌕ " }), _jsx(Text, { color: "white", children: menu.query }), !menu.query && _jsx(Text, { color: "gray", dimColor: true, children: "type to filter\u2026" })] }), _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(Math.max(4, DIALOG_W - 4)) })] })), menu.loading ? (_jsx(Text, { color: "gray", dimColor: true, children: " loading models\u2026" })) : menuFiltered.length === 0 ? (_jsxs(Text, { color: "gray", dimColor: true, children: [" no matches for \"", menu.query, "\""] })) : (_jsxs(_Fragment, { children: [menuStart > 0 && _jsxs(Text, { color: "gray", dimColor: true, children: [" \u2191 ", menuStart, " more"] }), menuFiltered.slice(menuStart, menuStart + DIALOG_ITEMS).map((item, i) => {
103
+ return (_jsxs(Box, { position: "absolute", marginLeft: dialogLeft, marginTop: dialogTop, width: DIALOG_W, flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, children: [_jsxs(Text, { bold: true, color: theme.text, children: [menu.type === "model" ? "Select model" : menu.type === "llm" ? "Select LLM provider" : "Select search provider", " ", _jsx(Text, { color: theme.textDim, children: "\u2191\u2193 navigate \u00B7 \u23CE apply \u00B7 esc close" })] }), !menu.loading && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: "⌕ " }), _jsx(Text, { color: theme.text, children: menu.query }), !menu.query && _jsx(Text, { color: theme.textDim, children: "type to filter\u2026" })] }), _jsx(Text, { color: theme.textDim, children: "─".repeat(Math.max(4, DIALOG_W - 4)) })] })), menu.loading ? (_jsx(Text, { color: theme.textDim, children: " loading models\u2026" })) : menuFiltered.length === 0 ? (_jsxs(Text, { color: theme.textDim, children: [" no matches for \"", menu.query, "\""] })) : (_jsxs(_Fragment, { children: [menuStart > 0 && _jsxs(Text, { color: theme.textDim, children: [" \u2191 ", menuStart, " more"] }), menuFiltered.slice(menuStart, menuStart + DIALOG_ITEMS).map((item, i) => {
94
104
  const idx = menuStart + i;
95
105
  const active = idx === menu.index;
96
- return (_jsxs(Text, { color: active ? "#FFE0C2" : "gray", bold: active, wrap: "truncate", children: [active ? "❯ " : " ", displayName(item), menu.type === "llm" ? _jsx(Text, { color: "gray", dimColor: true, children: " " + item }) : null] }, item));
97
- }), menuFiltered.length - (menuStart + DIALOG_ITEMS) > 0 && (_jsxs(Text, { color: "gray", dimColor: true, children: [" \u2193 ", menuFiltered.length - (menuStart + DIALOG_ITEMS), " more"] }))] }))] }));
106
+ return (_jsxs(Text, { color: active ? theme.accent : theme.textDim, bold: active, wrap: "truncate", children: [active ? "❯ " : " ", displayName(item), menu.type === "llm" ? _jsx(Text, { color: theme.textDim, children: " " + item }) : null] }, item));
107
+ }), menuFiltered.length - (menuStart + DIALOG_ITEMS) > 0 && (_jsxs(Text, { color: theme.textDim, children: [" \u2193 ", menuFiltered.length - (menuStart + DIALOG_ITEMS), " more"] }))] }))] }));
98
108
  }
99
- export function McpDialog({ open, config, cols, rows }) {
109
+ export function buildMcpDialogRows(config) {
110
+ const dt = config.mcp.chromeDevtools;
111
+ const rows = [{
112
+ key: "chromeDevtools",
113
+ name: "chromeDevtools",
114
+ transport: "stdio",
115
+ target: [dt.command, ...dt.args].join(" "),
116
+ enabled: dt.enabled,
117
+ removable: false,
118
+ }];
119
+ for (const s of config.mcp.servers) {
120
+ rows.push({
121
+ key: s.name,
122
+ name: s.name,
123
+ transport: s.transport,
124
+ target: s.transport === "stdio"
125
+ ? [s.command, ...s.args].filter(Boolean).join(" ")
126
+ : s.url ?? "(missing url)",
127
+ enabled: s.enabled,
128
+ removable: true,
129
+ });
130
+ }
131
+ return rows;
132
+ }
133
+ export function McpDialog({ open, config, cols, rows, selectedIdx, hoveredIdx, onToggle, onRemove, clickMapRef, hoverMapRef, }) {
134
+ const theme = useTheme();
100
135
  if (!open)
101
136
  return null;
102
137
  const W = Math.min(92, Math.max(52, cols - 8));
103
138
  const left = Math.max(0, Math.floor((cols - 4 - W) / 2));
104
- const servers = config.mcp.servers;
105
- const top = Math.max(1, Math.floor((rows - (8 + servers.length)) / 2));
106
- const dt = config.mcp.chromeDevtools;
107
- return (_jsxs(Box, { position: "absolute", marginLeft: left, marginTop: top, width: W, flexDirection: "column", borderStyle: "round", borderColor: "#FFE0C2", backgroundColor: "#0d0d0d", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "white", children: ["MCP servers ", _jsx(Text, { color: "gray", dimColor: true, children: "esc/q/enter close" })] }), _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(Math.max(4, W - 4)) }), _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: dt.enabled ? "green" : "gray", children: dt.enabled ? "●" : "○" }), _jsx(Text, { color: "white", children: " chromeDevtools " }), _jsx(Text, { color: "#FFE0C2", children: "[stdio] " }), _jsx(Text, { color: "gray", dimColor: true, children: [dt.command, ...dt.args].join(" ") })] }), servers.length === 0 ? (_jsx(Text, { color: "gray", dimColor: true, children: " no user-defined servers" })) : servers.map((s) => {
108
- const target = s.transport === "stdio" ? [s.command, ...s.args].filter(Boolean).join(" ") : s.url ?? "(missing url)";
109
- return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: s.enabled ? "green" : "gray", children: s.enabled ? "●" : "○" }), _jsxs(Text, { color: "white", children: [" ", s.name, " "] }), _jsxs(Text, { color: "#FFE0C2", children: ["[", s.transport, "] "] }), _jsx(Text, { color: "gray", dimColor: true, children: target })] }, s.name));
110
- }), _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(Math.max(4, W - 4)) }), _jsx(Text, { color: "gray", dimColor: true, children: "/mcp add http exa https://mcp.exa.ai/mcp" }), _jsx(Text, { color: "gray", dimColor: true, children: "/mcp enable <name> \u00B7 /mcp disable <name>" })] }));
139
+ const entries = buildMcpDialogRows(config);
140
+ const top = Math.max(1, Math.floor((rows - (8 + entries.length)) / 2));
141
+ const deleteCol = left + W - 5;
142
+ const checkboxCol = left + 4;
143
+ const clickMap = new Map();
144
+ const hoverMap = new Map();
145
+ const firstRow = top + 2;
146
+ entries.forEach((row, i) => {
147
+ const termRow = firstRow + i;
148
+ hoverMap.set(termRow, i);
149
+ clickMap.set(termRow, (x) => {
150
+ if (row.removable && x >= deleteCol)
151
+ onRemove(row);
152
+ else if (x <= checkboxCol + 2)
153
+ onToggle(row);
154
+ else
155
+ onToggle(row);
156
+ });
157
+ });
158
+ clickMapRef.current = clickMap;
159
+ hoverMapRef.current = hoverMap;
160
+ return (_jsxs(Box, { position: "absolute", marginLeft: left, marginTop: top, width: W, flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, paddingY: 1, children: [_jsxs(Text, { bold: true, color: theme.text, children: ["MCP servers ", _jsx(Text, { color: theme.textDim, children: "\u2191\u2193 \u00B7 space toggle \u00B7 x remove \u00B7 esc close" })] }), _jsx(Text, { color: theme.textDim, children: "─".repeat(Math.max(4, W - 4)) }), entries.map((row, i) => {
161
+ const active = i === selectedIdx || hoveredIdx === i;
162
+ const check = row.enabled ? "x" : " ";
163
+ return (_jsxs(Box, { width: W - 2, flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, minWidth: 0, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: active ? theme.text : theme.textDim, children: active ? "❯ " : " " }), _jsxs(Text, { color: active ? theme.accent : theme.textDim, bold: active, children: ["[", check, "]"] }), _jsxs(Text, { color: theme.text, children: [" ", row.name, " "] }), _jsxs(Text, { color: theme.accent, children: ["[", row.transport, "] "] }), _jsx(Text, { color: theme.textDim, children: row.target })] }) }), row.removable ? (_jsx(Text, { color: active ? theme.warning : theme.textDim, children: " [x]" })) : null] }, row.key));
164
+ }), entries.length === 1 ? (_jsx(Text, { color: theme.textDim, children: " no user-defined servers" })) : null, _jsx(Text, { color: theme.textDim, children: "─".repeat(Math.max(4, W - 4)) }), _jsx(Text, { color: theme.textDim, children: "/mcp add http exa https://mcp.exa.ai/mcp" })] }));
111
165
  }
@@ -1,12 +1,14 @@
1
- export const USER_BAND_BG = "#1c1c1c";
2
1
  export const S_BAR = "│";
3
2
  export const MENU_VISIBLE = 8;
4
3
  export const FILE_MENTION_MAX_CHARS = 20000;
5
4
  export const FILE_MENTION_SKIP = new Set([".git", "node_modules", "dist", ".scira"]);
6
5
  export const PROVIDERS = ["parallel", "exa", "firecrawl"];
7
- export const CHAT_COMMANDS = ["/help", "/new", "/rerun", "/report", "/sources", "/claims", "/why", "/mcp", "/copy", "/usage", "/rename", "/model", "/llm", "/provider", "/key", "/keys", "/stop", "/back", "/quit"];
6
+ export const CHAT_COMMANDS = ["/help", "/home", "/new", "/rerun", "/report", "/sources", "/claims", "/why", "/mcp", "/copy", "/usage", "/rename", "/model", "/llm", "/provider", "/theme", "/key", "/keys", "/stop", "/back", "/quit"];
7
+ /** Slash commands that take an argument; ⏎ from the menu appends a space instead of running. */
8
+ export const COMMANDS_NEEDING_ARGS = new Set(["/theme", "/key", "/rename", "/why"]);
8
9
  export const COMMAND_DESCRIPTIONS = {
9
10
  "/help": "Show command and keyboard shortcuts.",
11
+ "/home": "Go to the home screen (or show the welcome card on home).",
10
12
  "/new": "Go to the home screen to start a new research run.",
11
13
  "/rerun": "Run the research agent again for this run.",
12
14
  "/report": "Show the generated report.md in the timeline.",
@@ -20,6 +22,7 @@ export const COMMAND_DESCRIPTIONS = {
20
22
  "/model": "Open the model selector dropup.",
21
23
  "/llm": "Switch the LLM provider (gateway, xai, workers-ai).",
22
24
  "/provider": "Open the search provider selector.",
25
+ "/theme": "Set UI theme: /theme dark · /theme light · /theme auto",
23
26
  "/key": "Save an API key, e.g. /key EXA_API_KEY ...",
24
27
  "/keys": "Show which required API keys are set.",
25
28
  "/stop": "Abort the currently running agent turn.",
@@ -7,7 +7,7 @@ import { setRunTitle, summarizeRun } from "../../../storage/run-store.js";
7
7
  import { fmtDuration, fmtTokens, aggregateTurns, wantsFullResearch, summarizeToolInput } from "../lib/utils.js";
8
8
  import { promptWithFileMentions } from "../lib/file-mentions.js";
9
9
  import { markdownJoinerTransform } from "../../../utils/markdown-joiner.js";
10
- import { createSession, getSession, removeSession, attachSubscriber, sessionPushFeed, sessionSetBusy, sessionSetApproval, sessionFinishReasoning, sessionNotifyEscalate, sessionNotifyModeChange, } from "../session-manager.js";
10
+ import { createSession, getSession, removeSession, attachSubscriber, sessionPushFeed, sessionSetBusy, sessionSetApproval, sessionFinishReasoning, sessionNotifyEscalate, sessionNotifyModeChange, mergeFeedToolResults, getSessionFeedBuffer, } from "../session-manager.js";
11
11
  export function useAgentTurn({ config, currentRunPath, queuedPromptRef, fullModeRef, conversationRef, turnsRef, feedRef, setBusy, setScrollOffset, refreshRun, recordUsage, setMode, getSubscriber, }) {
12
12
  const runTurn = useCallback(async (prompt) => {
13
13
  const runPath = currentRunPath;
@@ -60,16 +60,32 @@ export function useAgentTurn({ config, currentRunPath, queuedPromptRef, fullMode
60
60
  sessionPushFeed(runPath, { kind: "tool", name: part.toolName, toolCallId: part.toolCallId, summary: summarizeToolInput(part.toolName, part.input), status: "running" });
61
61
  break;
62
62
  case "tool-result": {
63
- const toolResultId = part.toolCallId;
64
- const resultText = String(part.output).slice(0, 200);
65
- sessionPushFeed(runPath, { kind: "tool", name: "", toolCallId: toolResultId, summary: resultText, status: "done", result: resultText });
63
+ if (part.preliminary)
64
+ break;
65
+ const raw = part.output;
66
+ const resultText = typeof raw === "string" ? raw : JSON.stringify(raw);
67
+ sessionPushFeed(runPath, {
68
+ kind: "tool",
69
+ name: "",
70
+ toolCallId: part.toolCallId,
71
+ summary: "",
72
+ status: "done",
73
+ result: resultText,
74
+ });
66
75
  void refreshRun();
67
76
  break;
68
77
  }
69
78
  case "tool-error": {
70
- const toolErrId = part.toolCallId ?? "";
71
- const errText = String(part.error).slice(0, 200);
72
- sessionPushFeed(runPath, { kind: "tool", name: "", toolCallId: toolErrId, summary: errText, status: "error", result: errText });
79
+ const errRaw = part.error;
80
+ const errText = errRaw instanceof Error ? errRaw.message : String(errRaw);
81
+ sessionPushFeed(runPath, {
82
+ kind: "tool",
83
+ name: "",
84
+ toolCallId: part.toolCallId ?? "",
85
+ summary: errText,
86
+ status: "error",
87
+ result: errText,
88
+ });
73
89
  break;
74
90
  }
75
91
  case "error":
@@ -164,7 +180,8 @@ export function useAgentTurn({ config, currentRunPath, queuedPromptRef, fullMode
164
180
  if (turnUsage.input + turnUsage.output + turnUsage.total > 0) {
165
181
  turnsRef.current = [...turnsRef.current, { model: modelId, input: turnUsage.input, output: turnUsage.output, total: turnUsage.total, ts: Date.now() }];
166
182
  }
167
- const snapshot = feedRef.current
183
+ const merged = mergeFeedToolResults(feedRef.current, getSessionFeedBuffer(runPath));
184
+ const snapshot = merged
168
185
  .filter((item) => !(item.kind === "status" && item.text === "This will wipe the conversation history. Press /rerun again to confirm."))
169
186
  .map((item) => item.kind === "tool" && item.status === "running" ? { ...item, status: "error" } : item);
170
187
  try {