@scira/cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/research-agent.js +4 -3
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/index.js +96 -103
- package/dist/types/index.js +2 -0
- package/dist/ui/ink/SciraApp.js +102 -13
- package/dist/ui/ink/components/home-screen.js +74 -17
- package/dist/ui/ink/components/overlays.js +85 -31
- package/dist/ui/ink/constants.js +5 -2
- package/dist/ui/ink/hooks/use-agent-turn.js +25 -8
- package/dist/ui/ink/hooks/use-feed-lines.js +65 -39
- package/dist/ui/ink/hooks/use-feed-lines.test.js +16 -0
- package/dist/ui/ink/hooks/use-feed.js +18 -18
- package/dist/ui/ink/hooks/use-keyboard.js +36 -4
- package/dist/ui/ink/hooks/use-mcp-actions.js +44 -0
- package/dist/ui/ink/hooks/use-mouse.js +1 -1
- package/dist/ui/ink/hooks/use-settings.js +16 -0
- package/dist/ui/ink/hooks/use-submit.js +2 -2
- package/dist/ui/ink/hooks/use-theme.js +1 -0
- package/dist/ui/ink/lib/markdown.js +14 -14
- package/dist/ui/ink/lib/tool-result.js +319 -0
- package/dist/ui/ink/lib/tool-result.test.js +60 -0
- package/dist/ui/ink/lib/utils.js +88 -6
- package/dist/ui/ink/lib/utils.test.js +31 -0
- package/dist/ui/ink/session-manager.js +41 -4
- package/dist/ui/ink/session-manager.test.js +31 -0
- package/dist/ui/ink/terminal-probe.js +53 -0
- package/dist/ui/ink/terminal-probe.test.js +12 -0
- package/dist/ui/ink/theme-context.js +33 -0
- package/dist/ui/ink/theme.js +183 -0
- package/dist/ui/ink/theme.test.js +41 -0
- package/package.json +5 -6
|
@@ -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
|
|
9
|
-
const
|
|
10
|
-
const
|
|
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
|
|
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 +
|
|
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
|
-
|
|
54
|
-
|
|
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:
|
|
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 ?
|
|
67
|
-
}), below > 0 && _jsx(Text, { color:
|
|
68
|
-
})() : heroHidden ? null : (_jsxs(Box, { flexDirection: "column", alignItems: "center", width: cardW, children: [_jsxs(Box, { borderStyle: "round", borderColor:
|
|
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
|
-
|
|
7
|
-
|
|
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
|
|
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:
|
|
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:
|
|
20
|
-
? _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color:
|
|
21
|
-
: _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color:
|
|
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:
|
|
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:
|
|
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 ?
|
|
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:
|
|
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
|
-
|
|
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 ?
|
|
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:
|
|
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 ?
|
|
97
|
-
}), menuFiltered.length - (menuStart + DIALOG_ITEMS) > 0 && (_jsxs(Text, { color:
|
|
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
|
|
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
|
|
105
|
-
const top = Math.max(1, Math.floor((rows - (8 +
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
}
|
package/dist/ui/ink/constants.js
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
71
|
-
const errText = String(
|
|
72
|
-
sessionPushFeed(runPath, {
|
|
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
|
|
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 {
|