@scira/cli 0.1.5 → 0.1.6
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/harness-agent.js +206 -0
- package/dist/agent/{research-agent.js → main-agent.js} +20 -1
- package/dist/cli/commands/init.js +7 -5
- package/dist/cli/index.js +52 -11
- package/dist/cli/shell/shell.js +4 -5
- package/dist/cli/shell/tui.js +5 -2
- package/dist/config/env-guide.js +24 -0
- package/dist/config/env-store.js +5 -3
- package/dist/config/load-config.js +9 -14
- package/dist/providers/harness/local-sandbox.js +143 -0
- package/dist/providers/llm/gateway.js +5 -2
- package/dist/providers/llm/models.js +13 -0
- package/dist/providers/llm/readiness.js +5 -1
- package/dist/providers/llm/registry.js +24 -3
- package/dist/storage/jsonl.js +2 -2
- package/dist/storage/run-store.js +15 -15
- package/dist/tools/agent-tools.js +7 -7
- package/dist/tools/background-tasks.js +4 -5
- package/dist/tools/mcp-oauth.js +29 -25
- package/dist/tools/open-url.js +1 -2
- package/dist/tools/todos.js +3 -3
- package/dist/types/index.js +13 -1
- package/dist/ui/ink/SciraApp.js +10 -6
- package/dist/ui/ink/components/home-screen.js +2 -2
- package/dist/ui/ink/components/overlays.js +73 -15
- package/dist/ui/ink/constants.js +10 -7
- package/dist/ui/ink/hooks/use-agent-turn.js +14 -5
- package/dist/ui/ink/hooks/use-feed-lines.js +31 -6
- package/dist/ui/ink/hooks/use-keyboard.js +28 -5
- package/dist/ui/ink/hooks/use-session.js +7 -5
- package/dist/ui/ink/hooks/use-settings.js +20 -0
- package/dist/ui/ink/hooks/use-submit.js +15 -8
- package/dist/ui/ink/lib/file-mentions.js +1 -2
- package/dist/ui/ink/lib/tool-result.js +201 -2
- package/dist/ui/ink/lib/utils.js +52 -28
- package/dist/ui/ink/theme.js +5 -10
- package/dist/watch/runner.js +2 -2
- package/package.json +13 -11
- package/dist/agent/background-tasks.js +0 -173
- package/dist/agent/todos.js +0 -140
- package/dist/agent/tools.js +0 -432
- package/dist/agent/tools.test.js +0 -60
- package/dist/agent/workspace.js +0 -85
- package/dist/config/env-guide.test.js +0 -18
- package/dist/config/env-store.test.js +0 -60
- package/dist/storage/jsonl.test.js +0 -38
- package/dist/storage/run-store.test.js +0 -65
- package/dist/tools/bash-policy.test.js +0 -38
- package/dist/tools/search-web.test.js +0 -24
- package/dist/tools/workspace.test.js +0 -75
- package/dist/types/schema.test.js +0 -61
- package/dist/ui/ink/hooks/use-feed-lines.test.js +0 -16
- package/dist/ui/ink/lib/tool-result.test.js +0 -60
- package/dist/ui/ink/lib/utils.test.js +0 -48
- package/dist/ui/ink/session-manager.test.js +0 -31
- package/dist/ui/ink/terminal-probe.test.js +0 -12
- package/dist/ui/ink/theme.test.js +0 -68
package/dist/ui/ink/SciraApp.js
CHANGED
|
@@ -56,6 +56,10 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
56
56
|
const planModeRef = useRef(false);
|
|
57
57
|
const [planMode, setPlanModeState] = useState(false);
|
|
58
58
|
const setPlanMode = useCallback((active) => { planModeRef.current = active; setPlanModeState(active); }, []);
|
|
59
|
+
// Plan-mode preference armed from the home screen, applied when the next run opens.
|
|
60
|
+
const pendingPlanModeRef = useRef(false);
|
|
61
|
+
const [pendingPlanMode, setPendingPlanModeState] = useState(false);
|
|
62
|
+
const setPendingPlanMode = useCallback((active) => { pendingPlanModeRef.current = active; setPendingPlanModeState(active); }, []);
|
|
59
63
|
const [usage, setUsage] = useState({});
|
|
60
64
|
const turnsRef = useRef([]);
|
|
61
65
|
const recordUsage = useCallback((model, u) => {
|
|
@@ -226,9 +230,9 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
226
230
|
}), [pushFeed, appendText, appendReasoning, finishReasoning, markToolDone, setBusy, setApprovalPending, setMode]);
|
|
227
231
|
const runTurnRef = useRef(async () => { });
|
|
228
232
|
const { refreshSessions, refreshRun, openRun: openRunBase } = useSession({
|
|
229
|
-
config, currentRunPath, conversationRef, feedRef, turnsRef, startedRef,
|
|
233
|
+
config, currentRunPath, conversationRef, feedRef, turnsRef, startedRef, pendingPlanModeRef,
|
|
230
234
|
setSessions, setRunState, setCurrentRunPath, setInputText, setCursorPos,
|
|
231
|
-
setFeed, setUsage, setScrollOffset, setScreen, setMode, setPlanMode,
|
|
235
|
+
setFeed, setUsage, setScrollOffset, setScreen, setMode, setPlanMode, setPendingPlanMode,
|
|
232
236
|
setBusy, setApprovalPending, getSubscriber,
|
|
233
237
|
});
|
|
234
238
|
const { runTurn } = useAgentTurn({
|
|
@@ -314,10 +318,10 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
314
318
|
});
|
|
315
319
|
const { submitHome, submitChat, stopTurn } = useSubmit({
|
|
316
320
|
state: { config, currentRunPath, sessions, selectedIdx, busy, usage, pendingRerun },
|
|
317
|
-
refs: { queuedPromptRef, fullModeRef, planModeRef, conversationRef, feedRef },
|
|
321
|
+
refs: { queuedPromptRef, fullModeRef, planModeRef, pendingPlanModeRef, conversationRef, feedRef },
|
|
318
322
|
setters: {
|
|
319
323
|
setApprovalPending, setInputText, setCursorPos, setInputHistory, setHistoryIndex, setHelpOpen,
|
|
320
|
-
setNotice, setBusy, setScreen, setFeed, setRunState, setPendingRerun, setMode, setPlanMode, setConfig, setMcpOpen,
|
|
324
|
+
setNotice, setBusy, setScreen, setFeed, setRunState, setPendingRerun, setMode, setPlanMode, setPendingPlanMode, setConfig, setMcpOpen,
|
|
321
325
|
setHeroHidden,
|
|
322
326
|
},
|
|
323
327
|
actions: { pushFeed, refreshSessions, openRun, openMenu, handleSettings, runTurn, exit },
|
|
@@ -443,9 +447,9 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
443
447
|
const activeUsage = usage[config.model];
|
|
444
448
|
const themed = (node) => (_jsx(ThemeProvider, { config: config, children: node }));
|
|
445
449
|
if (screen === "home") {
|
|
446
|
-
return themed(_jsxs(Box, { flexDirection: "column", width: cols, height: rows, paddingX: 2, children: [_jsx(TipCycler, { setTipIndex: setTipIndex }), (!sessionsModalOpen || mcpOpen) && stdout !== undefined && stdin !== undefined && (_jsx(MouseTracker, { stdout: stdout, stdin: stdin, onData: handleMouseData, onUnmount: () => setHoveredIdx(null) })), busy && _jsx(AnimationTick, { setBlink: setBlink, setFrame: setFrame, setReasoningTick: setReasoningTick }), _jsx(TopBar, { screen: screen, runState: runState, fullMode: fullMode, planMode: planMode, activeUsage: activeUsage, busy: busy, frame: frame, cwdDisplay: CWD_DISPLAY, config: config }), _jsx(HomeScreen, { cols: cols, rows: rows, sessions: sessions, selectedIdx: selectedIdx, hoveredIdx: hoveredIdx, heroHidden: heroHidden, notice: notice, tipIndex: tipIndex, commandMenuHeight: commandMenuHeight, mcpOpen: mcpOpen, sessionsModalOpen: sessionsModalOpen, sessionsModalIdx: sessionsModalIdx, inputText: inputText, config: config, modelName: modelName, clickMapRef: clickMapRef, hoverMapRef: hoverMapRef, setSelectedIdx: setSelectedIdx, setSessionsModalOpen: setSessionsModalOpen, setSessionsModalIdx: setSessionsModalIdx, setNotice: setNotice, openRun: openRun, submitHome: submitHome, exit: exit }), _jsxs(Box, { flexDirection: "column", paddingBottom: 1, children: [_jsx(CommandMenuBox, { activeSuggestions: activeSuggestions, activeSuggestionKind: activeSuggestionKind, commandMenuIndex: commandMenuIndex, innerWidth: innerWidth, sessions: sessions, config: config }), _jsx(InputBar, { inputLines: inputLines, cursorLine: cursorLine, cursorCol: cursorCol, showCursor: showCursor, approvalPending: inputBlocked, busy: busy, frame: frame, boxWidth: boxWidth, modelName: modelName, config: config }), _jsx(HintLine, { screen: screen, busy: busy, config: config })] }), _jsx(MenuDialog, { menu: menu, cols: cols, rows: rows, config: config }), _jsx(McpDialog, { open: mcpOpen, config: config, cols: cols, rows: rows, selectedIdx: mcpRowIdx, hoveredIdx: hoveredIdx, onToggle: handleMcpToggle, onRemove: handleMcpRemove, clickMapRef: clickMapRef, hoverMapRef: hoverMapRef })] }));
|
|
450
|
+
return themed(_jsxs(Box, { flexDirection: "column", width: cols, height: rows, paddingX: 2, children: [_jsx(TipCycler, { setTipIndex: setTipIndex }), (!sessionsModalOpen || mcpOpen) && stdout !== undefined && stdin !== undefined && (_jsx(MouseTracker, { stdout: stdout, stdin: stdin, onData: handleMouseData, onUnmount: () => setHoveredIdx(null) })), busy && _jsx(AnimationTick, { setBlink: setBlink, setFrame: setFrame, setReasoningTick: setReasoningTick }), _jsx(TopBar, { screen: screen, runState: runState, fullMode: fullMode, planMode: planMode, activeUsage: activeUsage, busy: busy, frame: frame, cwdDisplay: CWD_DISPLAY, config: config }), _jsx(HomeScreen, { cols: cols, rows: rows, sessions: sessions, selectedIdx: selectedIdx, hoveredIdx: hoveredIdx, heroHidden: heroHidden, notice: notice, tipIndex: tipIndex, commandMenuHeight: commandMenuHeight, mcpOpen: mcpOpen, sessionsModalOpen: sessionsModalOpen, sessionsModalIdx: sessionsModalIdx, inputText: inputText, config: config, modelName: modelName, clickMapRef: clickMapRef, hoverMapRef: hoverMapRef, setSelectedIdx: setSelectedIdx, setSessionsModalOpen: setSessionsModalOpen, setSessionsModalIdx: setSessionsModalIdx, setNotice: setNotice, openRun: openRun, submitHome: submitHome, exit: exit }), _jsxs(Box, { flexDirection: "column", paddingBottom: 1, children: [_jsx(CommandMenuBox, { activeSuggestions: activeSuggestions, activeSuggestionKind: activeSuggestionKind, commandMenuIndex: commandMenuIndex, innerWidth: innerWidth, sessions: sessions, config: config }), _jsx(InputBar, { inputLines: inputLines, cursorLine: cursorLine, cursorCol: cursorCol, showCursor: showCursor, approvalPending: inputBlocked, busy: busy, frame: frame, boxWidth: boxWidth, modelName: modelName, planMode: planMode, config: config }), _jsx(HintLine, { screen: screen, busy: busy, modeLabel: pendingPlanMode ? "PLAN MODE" : "", modeColor: "cyan", config: config })] }), _jsx(MenuDialog, { menu: menu, cols: cols, rows: rows, config: config }), _jsx(McpDialog, { open: mcpOpen, config: config, cols: cols, rows: rows, selectedIdx: mcpRowIdx, hoveredIdx: hoveredIdx, onToggle: handleMcpToggle, onRemove: handleMcpRemove, clickMapRef: clickMapRef, hoverMapRef: hoverMapRef })] }));
|
|
447
451
|
}
|
|
448
|
-
return themed(_jsxs(Box, { flexDirection: "column", width: cols, height: rows, paddingX: 2, children: [stdout !== undefined && stdin !== undefined && (_jsx(MouseTracker, { stdout: stdout, stdin: stdin, onData: handleMouseData, onUnmount: () => setHoveredIdx(null) })), busy && _jsx(AnimationTick, { setBlink: setBlink, setFrame: setFrame, setReasoningTick: setReasoningTick }), _jsx(TopBar, { screen: screen, runState: runState, fullMode: fullMode, planMode: planMode, activeUsage: activeUsage, busy: busy, frame: frame, cwdDisplay: CWD_DISPLAY, config: config }), _jsx(Box, { flexDirection: "column", height: contentRows, flexShrink: 0, justifyContent: "flex-end", paddingTop: 1, overflow: "hidden", children: visibleLines }), _jsxs(ChatInputChrome, { children: [_jsx(CommandMenuBox, { activeSuggestions: activeSuggestions, activeSuggestionKind: activeSuggestionKind, commandMenuIndex: commandMenuIndex, innerWidth: innerWidth, sessions: sessions, config: config }), _jsx(HelpBox, { open: helpOpen, innerWidth: innerWidth, config: config }), approvalPending && _jsx(ApprovalBox, { toolName: approvalPending.toolName, description: approvalPending.description, innerWidth: innerWidth, config: config }), linkPending && _jsx(LinkOpenBox, { url: linkPending.url, innerWidth: innerWidth, config: config }), _jsx(InputBar, { inputLines: inputLines, cursorLine: cursorLine, cursorCol: cursorCol, showCursor: showCursor, approvalPending: inputBlocked, busy: busy, frame: frame, boxWidth: boxWidth, modelName: modelName, config: config }), _jsx(HintLine, { screen: screen, busy: busy, scrollLabel: scrollLabel, hasDoneGroups: doneGroupKeys.length > 0, hasFocusedGroup: focusedGroupKey !== null, hasLinkHover: hasLinkHover || !!linkPending, alwaysAllowLinks: config.alwaysAllowLinks, config: config })] }), _jsx(MenuDialog, { menu: menu, cols: cols, rows: rows, config: config }), _jsx(McpDialog, { open: mcpOpen, config: config, cols: cols, rows: rows, selectedIdx: mcpRowIdx, hoveredIdx: hoveredIdx, onToggle: handleMcpToggle, onRemove: handleMcpRemove, clickMapRef: clickMapRef, hoverMapRef: hoverMapRef })] }));
|
|
452
|
+
return themed(_jsxs(Box, { flexDirection: "column", width: cols, height: rows, paddingX: 2, children: [stdout !== undefined && stdin !== undefined && (_jsx(MouseTracker, { stdout: stdout, stdin: stdin, onData: handleMouseData, onUnmount: () => setHoveredIdx(null) })), busy && _jsx(AnimationTick, { setBlink: setBlink, setFrame: setFrame, setReasoningTick: setReasoningTick }), _jsx(TopBar, { screen: screen, runState: runState, fullMode: fullMode, planMode: planMode, activeUsage: activeUsage, busy: busy, frame: frame, cwdDisplay: CWD_DISPLAY, config: config }), _jsx(Box, { flexDirection: "column", height: contentRows, flexShrink: 0, justifyContent: feedLines.length > contentRows ? "flex-end" : "flex-start", paddingTop: 1, overflow: "hidden", children: visibleLines }), _jsxs(ChatInputChrome, { children: [_jsx(CommandMenuBox, { activeSuggestions: activeSuggestions, activeSuggestionKind: activeSuggestionKind, commandMenuIndex: commandMenuIndex, innerWidth: innerWidth, sessions: sessions, config: config }), _jsx(HelpBox, { open: helpOpen, innerWidth: innerWidth, config: config }), approvalPending && _jsx(ApprovalBox, { toolName: approvalPending.toolName, description: approvalPending.description, innerWidth: innerWidth, config: config }), linkPending && _jsx(LinkOpenBox, { url: linkPending.url, innerWidth: innerWidth, config: config }), _jsx(InputBar, { inputLines: inputLines, cursorLine: cursorLine, cursorCol: cursorCol, showCursor: showCursor, approvalPending: inputBlocked, busy: busy, frame: frame, boxWidth: boxWidth, modelName: modelName, planMode: planMode, config: config }), _jsx(HintLine, { screen: screen, busy: busy, modeLabel: fullMode ? "FULL RESEARCH" : planMode ? "PLAN MODE" : "", modeColor: fullMode ? "magenta" : "cyan", scrollLabel: scrollLabel, hasDoneGroups: doneGroupKeys.length > 0, hasFocusedGroup: focusedGroupKey !== null, hasLinkHover: hasLinkHover || !!linkPending, alwaysAllowLinks: config.alwaysAllowLinks, config: config })] }), _jsx(MenuDialog, { menu: menu, cols: cols, rows: rows, config: config }), _jsx(McpDialog, { open: mcpOpen, config: config, cols: cols, rows: rows, selectedIdx: mcpRowIdx, hoveredIdx: hoveredIdx, onToggle: handleMcpToggle, onRemove: handleMcpRemove, clickMapRef: clickMapRef, hoverMapRef: hoverMapRef })] }));
|
|
449
453
|
}
|
|
450
454
|
function ChatInputChrome({ children }) {
|
|
451
455
|
const theme = useTheme();
|
|
@@ -97,7 +97,7 @@ export function HomeScreen({ cols, rows, sessions, selectedIdx, hoveredIdx, hero
|
|
|
97
97
|
if (inputText.trim())
|
|
98
98
|
void submitHome(inputText);
|
|
99
99
|
else
|
|
100
|
-
setNotice("Type a question below to start a new
|
|
100
|
+
setNotice("Type a question below to start a new session.");
|
|
101
101
|
});
|
|
102
102
|
hoverMap.set(rowBase, newIdx);
|
|
103
103
|
clickMap.set(rowBase + 1, (_x) => exit());
|
|
@@ -122,5 +122,5 @@ export function HomeScreen({ cols, rows, sessions, selectedIdx, hoveredIdx, hero
|
|
|
122
122
|
const display = (s.title || s.goal || s.id).slice(0, cardW - 22);
|
|
123
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
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
|
|
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 & coding 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 session" }), 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] })) }));
|
|
126
126
|
}
|
|
@@ -1,34 +1,37 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { SPINNER_FRAMES, CHAT_COMMANDS, COMMAND_DESCRIPTIONS, MENU_VISIBLE } from "../constants.js";
|
|
4
|
-
import { fmtTokens, wrapText } from "../lib/utils.js";
|
|
4
|
+
import { fmtTokens, wrapText, displayWidth } from "../lib/utils.js";
|
|
5
5
|
import { LLM_PROVIDER_LABELS } from "../../../providers/llm/registry.js";
|
|
6
6
|
import { useTheme } from "../hooks/use-theme.js";
|
|
7
7
|
export function TopBar({ screen, runState, fullMode, planMode, activeUsage, busy, frame, cwdDisplay, config }) {
|
|
8
8
|
const theme = useTheme();
|
|
9
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" }), planMode && _jsx(Text, { color: "cyan", children: "plan" }), 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: "|" })] }))] }));
|
|
10
10
|
}
|
|
11
|
-
export function InputBar({ inputLines, cursorLine, cursorCol, showCursor, approvalPending, busy, frame, boxWidth, modelName, config }) {
|
|
11
|
+
export function InputBar({ inputLines, cursorLine, cursorCol, showCursor, approvalPending, busy, frame, boxWidth, modelName, planMode, config }) {
|
|
12
12
|
const theme = useTheme();
|
|
13
|
-
|
|
14
|
-
const
|
|
13
|
+
// Plan mode tints the whole input box (unless an approval/busy state takes precedence).
|
|
14
|
+
const borderColor = approvalPending ? theme.warning : busy ? theme.accentDim : planMode ? "cyan" : theme.textDim;
|
|
15
|
+
const promptColor = approvalPending ? theme.warning : busy ? theme.accentDim : planMode ? "cyan" : theme.accent;
|
|
15
16
|
const inputColor = approvalPending ? theme.textDim : theme.inputText;
|
|
16
|
-
const
|
|
17
|
+
const planTag = planMode ? "plan ◆ " : "";
|
|
18
|
+
const borderLabel = busy ? `${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${planTag}${modelName}` : `${planTag}${modelName}`;
|
|
17
19
|
const labelMax = Math.max(0, boxWidth - 6);
|
|
18
20
|
const label = borderLabel.length > labelMax ? borderLabel.slice(0, labelMax) : borderLabel;
|
|
19
21
|
const dashCount = Math.max(1, boxWidth - label.length - 5);
|
|
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(Box, { flexGrow: 1, minWidth: 0, children: showCursor && i === cursorLine ? (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: inputColor, children: line.slice(0, cursorCol) }), _jsx(Text, { backgroundColor: theme.cursorBackground, color: theme.cursorForeground, children: line[cursorCol] ?? " " }), _jsx(Text, { color: inputColor, children: line.slice(cursorCol + 1) })] })) : (_jsx(Text, { color: inputColor, wrap: "truncate", children: 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: " ─╯" })] })] }));
|
|
22
|
+
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(Box, { flexGrow: 1, minWidth: 0, children: showCursor && i === cursorLine ? (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: inputColor, children: line.slice(0, cursorCol) }), _jsx(Text, { backgroundColor: theme.cursorBackground, color: theme.cursorForeground, children: line[cursorCol] ?? " " }), _jsx(Text, { color: inputColor, children: line.slice(cursorCol + 1) })] })) : (_jsx(Text, { color: inputColor, wrap: "truncate", children: 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: planMode ? "cyan" : theme.accent, children: label }), _jsx(Text, { color: borderColor, children: " ─╯" })] })] }));
|
|
21
23
|
}
|
|
22
|
-
export function HintLine({ screen, busy, scrollLabel, hasDoneGroups, hasFocusedGroup, hasLinkHover, alwaysAllowLinks, config }) {
|
|
24
|
+
export function HintLine({ screen, busy, scrollLabel, hasDoneGroups, hasFocusedGroup, hasLinkHover, alwaysAllowLinks, modeLabel, modeColor, config }) {
|
|
23
25
|
const theme = useTheme();
|
|
26
|
+
const modeChip = modeLabel ? _jsx(Text, { backgroundColor: modeColor ?? "cyan", color: theme.background, bold: true, children: ` ${modeLabel} ` }) : null;
|
|
24
27
|
if (screen === "chat") {
|
|
25
28
|
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" }) }), hasLinkHover && !busy ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.textDim, children: "|" }), _jsx(Text, { color: theme.textDim, children: alwaysAllowLinks
|
|
26
29
|
? "click link to open"
|
|
27
30
|
: _jsxs(_Fragment, { children: ["click link \u00B7 ", _jsx(Text, { bold: true, color: theme.accent, children: "a" }), " always \u00B7 ", _jsx(Text, { bold: true, color: theme.accent, children: "y" }), " open \u00B7 ", _jsx(Text, { bold: true, color: theme.accent, children: "n" }), " cancel"] }) })] })) : null, 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
|
|
28
31
|
? _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: theme.accent, children: "C" }), " toggle \u00B7 ", _jsx(Text, { bold: true, color: theme.accent, children: "ESC" }), " unfocus"] })
|
|
29
|
-
: _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: theme.accent, children: "[ ]" }), " \u00B7 ", _jsx(Text, { bold: true, color: theme.accent, children: "C" }), " groups"] }) })] })) : null, scrollLabel ?
|
|
32
|
+
: _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: theme.accent, children: "[ ]" }), " \u00B7 ", _jsx(Text, { bold: true, color: theme.accent, children: "C" }), " groups"] }) })] })) : null, (modeChip || scrollLabel) ? _jsx(Box, { flexGrow: 1 }) : null, modeChip, scrollLabel ? _jsx(Text, { color: theme.textDim, children: scrollLabel }) : null] }));
|
|
30
33
|
}
|
|
31
|
-
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: "
|
|
34
|
+
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: "^D" }), ":quit"] }), modeChip ? _jsxs(_Fragment, { children: [_jsx(Box, { flexGrow: 1 }), modeChip] }) : null] }));
|
|
32
35
|
}
|
|
33
36
|
export function CommandMenuBox({ activeSuggestions, activeSuggestionKind, commandMenuIndex, innerWidth, sessions, config }) {
|
|
34
37
|
const theme = useTheme();
|
|
@@ -106,11 +109,66 @@ export function MenuDialog({ menu, cols, rows, config }) {
|
|
|
106
109
|
const dialogLeft = Math.max(0, Math.floor((cols - 4 - DIALOG_W) / 2));
|
|
107
110
|
const dialogH = 5 + (menu.loading ? 1 : Math.min(DIALOG_ITEMS, menuFiltered.length) + (menuStart > 0 ? 1 : 0) + (menuFiltered.length - (menuStart + DIALOG_ITEMS) > 0 ? 1 : 0));
|
|
108
111
|
const dialogTop = Math.max(1, Math.floor((rows - dialogH) / 2));
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
const bg = theme.userBandBackground ? { backgroundColor: theme.userBandBackground } : {};
|
|
113
|
+
const innerW = DIALOG_W - 2; // cells between the two border columns
|
|
114
|
+
// Draw the border as characters inside full-width background lines (like
|
|
115
|
+
// InputBar). Ink's box border + backgroundColor leaves unfilled gaps, so we
|
|
116
|
+
// compose each line ourselves: every line is one solid Text spanning DIALOG_W.
|
|
117
|
+
const line = (key, visibleLen, content) => (_jsxs(Text, { ...bg, wrap: "truncate", children: [_jsx(Text, { color: theme.accent, children: "\u2502" }), _jsx(Text, { children: " " }), content, _jsx(Text, { children: " ".repeat(Math.max(0, innerW - 1 - visibleLen)) }), _jsx(Text, { color: theme.accent, children: "\u2502" })] }, key));
|
|
118
|
+
// Clip a string to at most `max` display columns (so a row never overruns the
|
|
119
|
+
// border on narrow terminals — wrap="truncate" would eat the closing │).
|
|
120
|
+
const clip = (s, max) => {
|
|
121
|
+
if (max <= 0)
|
|
122
|
+
return "";
|
|
123
|
+
if (displayWidth(s) <= max)
|
|
124
|
+
return s;
|
|
125
|
+
let out = "", w = 0;
|
|
126
|
+
for (const ch of s) {
|
|
127
|
+
const cw = displayWidth(ch);
|
|
128
|
+
if (w + cw > max - 1)
|
|
129
|
+
break; // leave a column for the ellipsis
|
|
130
|
+
out += ch;
|
|
131
|
+
w += cw;
|
|
132
|
+
}
|
|
133
|
+
return out + "…";
|
|
134
|
+
};
|
|
135
|
+
const avail = innerW - 1; // usable columns after the leading space
|
|
136
|
+
const title = menu.type === "model" ? "Select model" : menu.type === "llm" ? "Select LLM provider" : "Select search provider";
|
|
137
|
+
const hint = "↑↓ navigate · ⏎ apply · esc close";
|
|
138
|
+
const dialogLines = [];
|
|
139
|
+
// Title + hint, dropping/clipping the (secondary) hint when space is tight.
|
|
140
|
+
const titleC = clip(title, avail);
|
|
141
|
+
const hintRoom = avail - displayWidth(titleC) - 2;
|
|
142
|
+
const hintC = hintRoom >= 6 ? clip(hint, hintRoom) : "";
|
|
143
|
+
dialogLines.push(line("title", displayWidth(titleC) + (hintC ? 2 + displayWidth(hintC) : 0), (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: theme.text, children: titleC }), hintC ? _jsx(Text, { color: theme.textDim, children: " " + hintC }) : null] }))));
|
|
144
|
+
if (!menu.loading) {
|
|
145
|
+
const filterC = clip(menu.query || "type to filter…", avail - 2);
|
|
146
|
+
dialogLines.push(line("filter", 2 + displayWidth(filterC), (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, children: "⌕ " }), menu.query ? _jsx(Text, { color: theme.inputText, children: filterC }) : _jsx(Text, { color: theme.textDim, children: filterC })] }))));
|
|
147
|
+
dialogLines.push(line("divider", innerW - 1, _jsx(Text, { color: theme.textDim, children: "─".repeat(Math.max(4, innerW - 1)) })));
|
|
148
|
+
}
|
|
149
|
+
if (menu.loading) {
|
|
150
|
+
dialogLines.push(line("loading", displayWidth("loading models…"), _jsx(Text, { color: theme.textDim, children: "loading models\u2026" })));
|
|
151
|
+
}
|
|
152
|
+
else if (menuFiltered.length === 0) {
|
|
153
|
+
const msg = clip(`no matches for "${menu.query}"`, avail);
|
|
154
|
+
dialogLines.push(line("empty", displayWidth(msg), _jsx(Text, { color: theme.textDim, children: msg })));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
if (menuStart > 0)
|
|
158
|
+
dialogLines.push(line("up", displayWidth(`↑ ${menuStart} more`), _jsx(Text, { color: theme.textDim, children: `↑ ${menuStart} more` })));
|
|
159
|
+
menuFiltered.slice(menuStart, menuStart + DIALOG_ITEMS).forEach((item, i) => {
|
|
160
|
+
const active = menuStart + i === menu.index;
|
|
161
|
+
const marker = active ? "❯ " : " ";
|
|
162
|
+
const label = clip(displayName(item), avail - displayWidth(marker));
|
|
163
|
+
const suffixRoom = avail - displayWidth(marker + label);
|
|
164
|
+
const suffix = menu.type === "llm" && suffixRoom >= 4 ? clip(" " + item, suffixRoom) : "";
|
|
165
|
+
dialogLines.push(line(item, displayWidth(marker + label) + displayWidth(suffix), (_jsxs(_Fragment, { children: [_jsx(Text, { color: active ? theme.accent : theme.textDim, bold: active, children: marker + label }), suffix ? _jsx(Text, { color: theme.textDim, children: suffix }) : null] }))));
|
|
166
|
+
});
|
|
167
|
+
const moreBelow = menuFiltered.length - (menuStart + DIALOG_ITEMS);
|
|
168
|
+
if (moreBelow > 0)
|
|
169
|
+
dialogLines.push(line("down", displayWidth(`↓ ${moreBelow} more`), _jsx(Text, { color: theme.textDim, children: `↓ ${moreBelow} more` })));
|
|
170
|
+
}
|
|
171
|
+
return (_jsxs(Box, { position: "absolute", marginLeft: dialogLeft, marginTop: dialogTop, width: DIALOG_W, flexDirection: "column", children: [_jsx(Text, { ...bg, children: _jsx(Text, { color: theme.accent, children: "╭" + "─".repeat(innerW) + "╮" }) }), dialogLines, _jsx(Text, { ...bg, children: _jsx(Text, { color: theme.accent, children: "╰" + "─".repeat(innerW) + "╯" }) })] }));
|
|
114
172
|
}
|
|
115
173
|
export function buildMcpDialogRows(config) {
|
|
116
174
|
const dt = config.mcp.chromeDevtools;
|
|
@@ -163,7 +221,7 @@ export function McpDialog({ open, config, cols, rows, selectedIdx, hoveredIdx, o
|
|
|
163
221
|
});
|
|
164
222
|
clickMapRef.current = clickMap;
|
|
165
223
|
hoverMapRef.current = hoverMap;
|
|
166
|
-
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) => {
|
|
224
|
+
return (_jsxs(Box, { position: "absolute", marginLeft: left, marginTop: top, width: W, flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, paddingY: 1, ...(theme.userBandBackground ? { backgroundColor: theme.userBandBackground } : {}), 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) => {
|
|
167
225
|
const active = i === selectedIdx || hoveredIdx === i;
|
|
168
226
|
const check = row.enabled ? "x" : " ";
|
|
169
227
|
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));
|
package/dist/ui/ink/constants.js
CHANGED
|
@@ -3,15 +3,15 @@ export const MENU_VISIBLE = 8;
|
|
|
3
3
|
export const FILE_MENTION_MAX_CHARS = 20000;
|
|
4
4
|
export const FILE_MENTION_SKIP = new Set([".git", "node_modules", "dist", ".scira"]);
|
|
5
5
|
export const PROVIDERS = ["parallel", "exa", "firecrawl"];
|
|
6
|
-
export const CHAT_COMMANDS = ["/help", "/home", "/new", "/plan", "/rerun", "/report", "/sources", "/claims", "/why", "/mcp", "/copy", "/usage", "/rename", "/model", "/llm", "/provider", "/theme", "/links", "/key", "/keys", "/stop", "/back", "/quit"];
|
|
6
|
+
export const CHAT_COMMANDS = ["/help", "/home", "/new", "/plan", "/rerun", "/report", "/sources", "/claims", "/why", "/mcp", "/copy", "/usage", "/rename", "/model", "/llm", "/provider", "/thinking", "/reasoning", "/theme", "/links", "/key", "/keys", "/stop", "/back", "/quit"];
|
|
7
7
|
/** Slash commands that take an argument; ⏎ from the menu appends a space instead of running. */
|
|
8
8
|
export const COMMANDS_NEEDING_ARGS = new Set(["/theme", "/key", "/rename", "/why", "/links"]);
|
|
9
9
|
export const COMMAND_DESCRIPTIONS = {
|
|
10
10
|
"/help": "Show command and keyboard shortcuts.",
|
|
11
11
|
"/home": "Go to the home screen (or show the welcome card on home).",
|
|
12
|
-
"/new": "Go to the home screen to start a new
|
|
12
|
+
"/new": "Go to the home screen to start a new session.",
|
|
13
13
|
"/plan": "Toggle plan mode (explore and plan before making changes).",
|
|
14
|
-
"/rerun": "Run the research agent again for this
|
|
14
|
+
"/rerun": "Run the research agent again for this session.",
|
|
15
15
|
"/report": "Show the generated report.md in the timeline.",
|
|
16
16
|
"/sources": "List the run's gathered sources with links.",
|
|
17
17
|
"/claims": "List all claims with id, confidence, status, and text.",
|
|
@@ -21,8 +21,10 @@ export const COMMAND_DESCRIPTIONS = {
|
|
|
21
21
|
"/usage": "Show token usage per model for this session.",
|
|
22
22
|
"/rename": "Set a title for this session, e.g. /rename SpaceX IPO analysis",
|
|
23
23
|
"/model": "Open the model selector dropup.",
|
|
24
|
-
"/llm": "Switch the LLM provider (gateway, xai, workers-ai).",
|
|
24
|
+
"/llm": "Switch the LLM provider (gateway, xai, workers-ai, claude-code, codex).",
|
|
25
25
|
"/provider": "Open the search provider selector.",
|
|
26
|
+
"/thinking": "Claude Code thinking: /thinking off · on · adaptive",
|
|
27
|
+
"/reasoning": "Codex reasoning effort: /reasoning low · medium · high",
|
|
26
28
|
"/theme": "Set UI theme: /theme dark · /theme light · /theme auto",
|
|
27
29
|
"/links": "Link opens: /links always · /links ask",
|
|
28
30
|
"/key": "Save an API key, e.g. /key EXA_API_KEY ...",
|
|
@@ -40,6 +42,7 @@ export const TOOL_ICONS = {
|
|
|
40
42
|
createClaim: "◎",
|
|
41
43
|
verifyClaim: "✓",
|
|
42
44
|
webSearch: "⌕",
|
|
45
|
+
multiWebSearch: "⌕",
|
|
43
46
|
readUrl: "↗",
|
|
44
47
|
listSkills: "★",
|
|
45
48
|
readSkill: "★",
|
|
@@ -79,11 +82,11 @@ export const LOADING_PHRASES = [
|
|
|
79
82
|
"Wrapping my head around it…",
|
|
80
83
|
];
|
|
81
84
|
export const HOME_TIPS = [
|
|
82
|
-
"Type a question and press ⏎ to start a new
|
|
85
|
+
"Type a question and press ⏎ to start a new session.",
|
|
83
86
|
"Say \"deep research …\" or \"compare …\" to trigger the full research harness.",
|
|
84
|
-
"↑↓ navigate · ⏎ open · type to start a new
|
|
87
|
+
"↑↓ navigate · ⏎ open · type to start a new session.",
|
|
85
88
|
"/model · /provider · /key NAME value to configure.",
|
|
86
|
-
"Browse all sessions to find older
|
|
89
|
+
"Browse all sessions to find older ones.",
|
|
87
90
|
"ready / draft badges show full-research runs and their report state."
|
|
88
91
|
];
|
|
89
92
|
export const FULL_MODE_TRIGGERS = [
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
|
-
import { writeFile } from "node:fs/promises";
|
|
3
2
|
import { join } from "node:path";
|
|
4
|
-
import { createResearchAgent, createOneShotAgent } from "../../../agent/
|
|
3
|
+
import { createResearchAgent, createOneShotAgent } from "../../../agent/main-agent.js";
|
|
5
4
|
import { createBackgroundTaskManager } from "../../../tools/background-tasks.js";
|
|
6
5
|
import { resolveProjectRoot } from "../../../tools/workspace.js";
|
|
7
6
|
import { generateWithGateway } from "../../../providers/llm/gateway.js";
|
|
@@ -62,9 +61,19 @@ export function useAgentTurn({ config, currentRunPath, queuedPromptRef, fullMode
|
|
|
62
61
|
case "reasoning-end":
|
|
63
62
|
sessionFinishReasoning(runPath);
|
|
64
63
|
break;
|
|
65
|
-
case "tool-call":
|
|
66
|
-
|
|
64
|
+
case "tool-call": {
|
|
65
|
+
// Stash the raw input (capped) so dedicated tool renderers (diffs,
|
|
66
|
+
// checklists, …) can reconstruct it from the feed.
|
|
67
|
+
let inputJson;
|
|
68
|
+
try {
|
|
69
|
+
inputJson = JSON.stringify(part.input)?.slice(0, 32_000);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
inputJson = undefined;
|
|
73
|
+
}
|
|
74
|
+
sessionPushFeed(runPath, { kind: "tool", name: part.toolName, toolCallId: part.toolCallId, summary: summarizeToolInput(part.toolName, part.input), status: "running", input: inputJson });
|
|
67
75
|
break;
|
|
76
|
+
}
|
|
68
77
|
case "tool-result": {
|
|
69
78
|
if (part.preliminary)
|
|
70
79
|
break;
|
|
@@ -206,7 +215,7 @@ export function useAgentTurn({ config, currentRunPath, queuedPromptRef, fullMode
|
|
|
206
215
|
.filter((item) => !(item.kind === "status" && item.text === "This will wipe the conversation history. Press /rerun again to confirm."))
|
|
207
216
|
.map((item) => item.kind === "tool" && item.status === "running" ? { ...item, status: "error" } : item);
|
|
208
217
|
try {
|
|
209
|
-
await
|
|
218
|
+
await Bun.write(join(runPath, "convo.json"), JSON.stringify({ feed: snapshot, messages: conversationRef.current, usage: aggregateTurns(turnsRef.current) }, null, 2));
|
|
210
219
|
}
|
|
211
220
|
catch { /* non-fatal */ }
|
|
212
221
|
removeSession(runPath);
|
|
@@ -4,8 +4,25 @@ import { Text } from "ink";
|
|
|
4
4
|
import Link from "ink-link";
|
|
5
5
|
import { S_BAR, TOOL_ICONS, SPINNER_FRAMES } from "../constants.js";
|
|
6
6
|
import { formatTime, fmtDuration, wrapText, computeLineLinks, displayWidth } from "../lib/utils.js";
|
|
7
|
-
import { formatToolResultLines, formatToolResultPreview, feedToolItemId, isCollapsibleToolName, isToolItemCollapsed, } from "../lib/tool-result.js";
|
|
7
|
+
import { formatToolResultLines, formatToolResultPreview, feedToolItemId, isCollapsibleToolName, isToolItemCollapsed, canonicalToolName, displayToolName, } from "../lib/tool-result.js";
|
|
8
8
|
import { markdownToSegLines } from "../lib/markdown.js";
|
|
9
|
+
// Formatting a tool body (wrapping/parsing the result) is the expensive part of
|
|
10
|
+
// building feed lines. The feed re-renders on every spinner/reasoning tick
|
|
11
|
+
// (~20fps while busy), so without caching a large result would be re-parsed
|
|
12
|
+
// dozens of times a second. Cache by content identity; completed items hit the
|
|
13
|
+
// cache and only the live item (whose result is still growing) recomputes.
|
|
14
|
+
const toolBodyCache = new Map();
|
|
15
|
+
function cachedToolBody(themeKey, name, callId, summary, result, status, width, theme, expanded, input) {
|
|
16
|
+
const key = `${themeKey}|${callId}|${name}|${status}|${width}|${expanded}|${result?.length ?? 0}|${input?.length ?? 0}`;
|
|
17
|
+
const hit = toolBodyCache.get(key);
|
|
18
|
+
if (hit)
|
|
19
|
+
return hit;
|
|
20
|
+
const v = formatToolResultLines(name, summary, result, status, width, theme, expanded, input);
|
|
21
|
+
toolBodyCache.set(key, v);
|
|
22
|
+
if (toolBodyCache.size > 400)
|
|
23
|
+
toolBodyCache.delete(toolBodyCache.keys().next().value);
|
|
24
|
+
return v;
|
|
25
|
+
}
|
|
9
26
|
import { useTheme } from "./use-theme.js";
|
|
10
27
|
export function computeGroups(feed) {
|
|
11
28
|
const groupOf = new Array(feed.length).fill(-1);
|
|
@@ -155,21 +172,29 @@ reasoningTick, spinnerFrame, collapsedGroups, focusedGroupKey, itemExpandState,
|
|
|
155
172
|
const running = fi.status === "running";
|
|
156
173
|
const failed = fi.status === "error";
|
|
157
174
|
const itemId = feedToolItemId(feedIdx, fi.toolCallId);
|
|
158
|
-
|
|
159
|
-
|
|
175
|
+
// Route rendering through the Scira-canonical name (Claude/Codex builtins
|
|
176
|
+
// and `mcp__harness-tools__*` host tools map onto our renderers), but show
|
|
177
|
+
// the cleaned real name in the header.
|
|
178
|
+
const canon = canonicalToolName(fi.name);
|
|
179
|
+
const shownName = displayToolName(fi.name);
|
|
180
|
+
const collapsible = isCollapsibleToolName(canon) && !running;
|
|
181
|
+
const collapsed = collapsible && isToolItemCollapsed(itemId, canon, fi.status, itemExpandState);
|
|
160
182
|
const headerLineIdx = lines.length;
|
|
161
183
|
const hovered = hoveredLineIdx === headerLineIdx;
|
|
162
184
|
const toolIcon = running
|
|
163
185
|
? SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length]
|
|
164
|
-
: TOOL_ICONS[
|
|
186
|
+
: TOOL_ICONS[canon] ?? "·";
|
|
165
187
|
const symColor = failed ? theme.error : theme.accentDim;
|
|
166
188
|
const nameColor = running ? theme.text : failed ? theme.error : theme.textDim;
|
|
167
189
|
const panelWidth = innerWidth - 4;
|
|
190
|
+
// Pass the raw name so dedicated built-in renderers (Edit diff, TodoWrite
|
|
191
|
+
// checklist, …) can key off it; the formatters canonicalize internally
|
|
192
|
+
// for the generic path.
|
|
168
193
|
const preview = formatToolResultPreview(fi.name, fi.summary, fi.result, fi.status);
|
|
169
|
-
const bodyLines =
|
|
194
|
+
const bodyLines = cachedToolBody(theme.accent, fi.name, fi.toolCallId ?? String(feedIdx), fi.summary, fi.result, fi.status, panelWidth, theme, !collapsed, fi.input);
|
|
170
195
|
if (collapsible)
|
|
171
196
|
toggleAtLine.set(headerLineIdx, itemId);
|
|
172
|
-
lines.push(_jsxs(Text, { wrap: "truncate", children: [collapsible ? (_jsx(Text, { color: hovered ? theme.accent : theme.textDim, bold: hovered, children: collapsed ? "▶ " : "▼ " })) : null, _jsx(Text, { color: symColor, bold: running, children: toolIcon }), _jsxs(Text, { color: nameColor, bold: running || failed || hovered, children: [" ",
|
|
197
|
+
lines.push(_jsxs(Text, { wrap: "truncate", children: [collapsible ? (_jsx(Text, { color: hovered ? theme.accent : theme.textDim, bold: hovered, children: collapsed ? "▶ " : "▼ " })) : null, _jsx(Text, { color: symColor, bold: running, children: toolIcon }), _jsxs(Text, { color: nameColor, bold: running || failed || hovered, children: [" ", shownName] }), failed ? _jsx(Text, { color: theme.error, children: " failed" }) : null, running ? _jsx(Text, { color: theme.textDim, children: " \u2026" }) : null, !running && !failed && collapsed && preview ? (_jsxs(Text, { color: theme.textDim, children: [" ", preview] })) : null] }, key++));
|
|
173
198
|
for (const row of bodyLines) {
|
|
174
199
|
if (row.length === 0) {
|
|
175
200
|
lines.push(_jsx(Text, { color: theme.textDim, children: S_BAR }, key++));
|
|
@@ -16,6 +16,9 @@ export function useKeyboard(o) {
|
|
|
16
16
|
const { setScrollOffset, contentRows, maxScrollOffset, pendingRerun, setPendingRerun, busy, stopTurn, submitChat, toggleAllGroups, toggleFocusedGroup, focusPrevGroup, focusNextGroup, unfocusGroup, hasFocusedGroup } = o.chat;
|
|
17
17
|
const { sessionsModalOpen, setSessionsModalOpen, sessionsModalIdx, setSessionsModalIdx, sessions, deleteSession, selectedIdx, setSelectedIdx, setHeroHidden, openRun, submitHome } = o.home;
|
|
18
18
|
const deleteArmedRef = useRef(null);
|
|
19
|
+
// Quit requires confirmation: first Ctrl+C arms, second within the window quits.
|
|
20
|
+
const quitArmedRef = useRef(false);
|
|
21
|
+
const quitTimerRef = useRef(null);
|
|
19
22
|
const editInput = (char, key) => {
|
|
20
23
|
const deleteWordBefore = () => {
|
|
21
24
|
const match = inputText.slice(0, cursorPos).match(/\S+\s*$/u);
|
|
@@ -95,6 +98,30 @@ export function useKeyboard(o) {
|
|
|
95
98
|
// OSC background-color query responses leak as stdin when terminals reply to theme probes.
|
|
96
99
|
if (char && (/\]11;rgb:/u.test(char) || /^11;rgb:/u.test(char)))
|
|
97
100
|
return;
|
|
101
|
+
// Quit handling (all screens): Ctrl+C twice (with warning) or Ctrl+D.
|
|
102
|
+
if (key.ctrl && char === "c") {
|
|
103
|
+
if (busy) {
|
|
104
|
+
stopTurn();
|
|
105
|
+
quitArmedRef.current = false;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (quitArmedRef.current) {
|
|
109
|
+
exit();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
quitArmedRef.current = true;
|
|
113
|
+
setNotice("Press Ctrl+C again to quit (or Ctrl+D).");
|
|
114
|
+
if (quitTimerRef.current)
|
|
115
|
+
clearTimeout(quitTimerRef.current);
|
|
116
|
+
quitTimerRef.current = setTimeout(() => { quitArmedRef.current = false; }, 3000);
|
|
117
|
+
// Don't let the disarm timer keep the process alive after a quit.
|
|
118
|
+
quitTimerRef.current.unref?.();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (key.ctrl && char === "d" && !inputText) {
|
|
122
|
+
exit();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
98
125
|
if (approvalPending) {
|
|
99
126
|
if (char === "y" || char === "Y" || key.return) {
|
|
100
127
|
const p = approvalPending;
|
|
@@ -303,7 +330,7 @@ export function useKeyboard(o) {
|
|
|
303
330
|
setSessionsModalIdx(0);
|
|
304
331
|
}
|
|
305
332
|
else if (selectedIdx === newIdx) {
|
|
306
|
-
setNotice("Type a question below to start a new
|
|
333
|
+
setNotice("Type a question below to start a new session.");
|
|
307
334
|
}
|
|
308
335
|
else if (selectedIdx === quitIdx) {
|
|
309
336
|
exit();
|
|
@@ -312,10 +339,6 @@ export function useKeyboard(o) {
|
|
|
312
339
|
void submitHome("");
|
|
313
340
|
}
|
|
314
341
|
}
|
|
315
|
-
else if (char === "q" && !inputText)
|
|
316
|
-
exit();
|
|
317
|
-
else if (key.ctrl && char === "d" && !inputText)
|
|
318
|
-
exit();
|
|
319
342
|
else
|
|
320
343
|
editInput(char, key);
|
|
321
344
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { listRuns, summarizeRun } from "../../../storage/run-store.js";
|
|
5
4
|
import { getSession, attachSubscriber } from "../session-manager.js";
|
|
6
5
|
export function useSession(o) {
|
|
7
|
-
const { config, currentRunPath, conversationRef, feedRef, turnsRef, startedRef, setSessions, setRunState, setCurrentRunPath, setInputText, setCursorPos, setFeed, setUsage, setScrollOffset, setScreen, setMode, setPlanMode, setBusy, setApprovalPending, getSubscriber, } = o;
|
|
6
|
+
const { config, currentRunPath, conversationRef, feedRef, turnsRef, startedRef, pendingPlanModeRef, setSessions, setRunState, setCurrentRunPath, setInputText, setCursorPos, setFeed, setUsage, setScrollOffset, setScreen, setMode, setPlanMode, setPendingPlanMode, setBusy, setApprovalPending, getSubscriber, } = o;
|
|
8
7
|
const refreshSessions = useCallback(async () => {
|
|
9
8
|
const runs = await listRuns(config);
|
|
10
9
|
setSessions(runs);
|
|
@@ -14,8 +13,11 @@ export function useSession(o) {
|
|
|
14
13
|
setRunState(await summarizeRun(currentRunPath));
|
|
15
14
|
}, [currentRunPath]);
|
|
16
15
|
const openRun = useCallback(async (runPath, initialQuestion) => {
|
|
17
|
-
if (runPath !== currentRunPath)
|
|
18
|
-
|
|
16
|
+
if (runPath !== currentRunPath) {
|
|
17
|
+
// Honor a plan-mode preference armed from the home screen, then disarm it.
|
|
18
|
+
setPlanMode(pendingPlanModeRef.current);
|
|
19
|
+
setPendingPlanMode(false);
|
|
20
|
+
}
|
|
19
21
|
setCurrentRunPath(runPath);
|
|
20
22
|
setInputText("");
|
|
21
23
|
setCursorPos(0);
|
|
@@ -39,7 +41,7 @@ export function useSession(o) {
|
|
|
39
41
|
return undefined;
|
|
40
42
|
}
|
|
41
43
|
try {
|
|
42
|
-
const raw = await
|
|
44
|
+
const raw = await Bun.file(join(runPath, "convo.json")).text();
|
|
43
45
|
const saved = JSON.parse(raw);
|
|
44
46
|
if (saved.feed && saved.feed.length > 0) {
|
|
45
47
|
const filteredFeed = saved.feed.filter((item) => !(item.kind === "status" && item.text === "This will wipe the conversation history. Press /rerun again to confirm."));
|
|
@@ -146,6 +146,26 @@ export function useSettings({ config, setConfig, screen, pushFeed, setNotice })
|
|
|
146
146
|
await setEnvKey(name, value);
|
|
147
147
|
return `${name} saved to ~/.scira/.env and active for this session. Use .scira/.env in a project to scope keys to that repo.`;
|
|
148
148
|
}
|
|
149
|
+
if (cmd === "/thinking") {
|
|
150
|
+
if (!arg)
|
|
151
|
+
return `Claude Code thinking: ${config.harness.thinking}\nOptions: off, on, adaptive`;
|
|
152
|
+
if (!["off", "on", "adaptive"].includes(arg))
|
|
153
|
+
return `Unknown thinking mode "${arg}". Options: off, on, adaptive`;
|
|
154
|
+
const next = { ...config, harness: { ...config.harness, thinking: arg } };
|
|
155
|
+
setConfig(next);
|
|
156
|
+
await saveGlobalConfig(next);
|
|
157
|
+
return `Claude Code thinking set to ${arg}.`;
|
|
158
|
+
}
|
|
159
|
+
if (cmd === "/reasoning") {
|
|
160
|
+
if (!arg)
|
|
161
|
+
return `Codex reasoning effort: ${config.harness.reasoningEffort}\nOptions: low, medium, high`;
|
|
162
|
+
if (!["low", "medium", "high"].includes(arg))
|
|
163
|
+
return `Unknown reasoning effort "${arg}". Options: low, medium, high`;
|
|
164
|
+
const next = { ...config, harness: { ...config.harness, reasoningEffort: arg } };
|
|
165
|
+
setConfig(next);
|
|
166
|
+
await saveGlobalConfig(next);
|
|
167
|
+
return `Codex reasoning effort set to ${arg}.`;
|
|
168
|
+
}
|
|
149
169
|
if (cmd === "/theme") {
|
|
150
170
|
if (!arg) {
|
|
151
171
|
const terminal = detectTerminalTheme();
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
2
|
import { createRun, getRunPaths, setRunTitle } from "../../../storage/run-store.js";
|
|
4
3
|
import { readJsonl } from "../../../storage/jsonl.js";
|
|
5
4
|
import { fmtDuration, fmtTokens, copyToClipboard } from "../lib/utils.js";
|
|
@@ -7,8 +6,8 @@ import { detachSubscriber, abortSession } from "../session-manager.js";
|
|
|
7
6
|
import { saveGlobalMcpConfig } from "../../../config/load-config.js";
|
|
8
7
|
export function useSubmit(o) {
|
|
9
8
|
const { config, currentRunPath, sessions, selectedIdx, busy, usage, pendingRerun } = o.state;
|
|
10
|
-
const { queuedPromptRef, fullModeRef, planModeRef, conversationRef, feedRef } = o.refs;
|
|
11
|
-
const { setApprovalPending, setInputText, setCursorPos, setInputHistory, setHistoryIndex, setHelpOpen, setNotice, setBusy, setScreen, setFeed, setRunState, setPendingRerun, setMode, setPlanMode, setConfig, setMcpOpen, setHeroHidden, } = o.setters;
|
|
9
|
+
const { queuedPromptRef, fullModeRef, planModeRef, pendingPlanModeRef, conversationRef, feedRef } = o.refs;
|
|
10
|
+
const { setApprovalPending, setInputText, setCursorPos, setInputHistory, setHistoryIndex, setHelpOpen, setNotice, setBusy, setScreen, setFeed, setRunState, setPendingRerun, setMode, setPlanMode, setPendingPlanMode, setConfig, setMcpOpen, setHeroHidden, } = o.setters;
|
|
12
11
|
const { pushFeed, refreshSessions, openRun, openMenu, handleSettings, runTurn, exit } = o.actions;
|
|
13
12
|
const rerunConfirmRef = useRef(false);
|
|
14
13
|
const abortTurn = useCallback(() => {
|
|
@@ -27,7 +26,7 @@ export function useSubmit(o) {
|
|
|
27
26
|
void openRun(selected.path);
|
|
28
27
|
return;
|
|
29
28
|
}
|
|
30
|
-
if (text === "
|
|
29
|
+
if (text === "/quit" || text === "/q") {
|
|
31
30
|
exit();
|
|
32
31
|
return;
|
|
33
32
|
}
|
|
@@ -60,7 +59,15 @@ export function useSubmit(o) {
|
|
|
60
59
|
setMcpOpen(true);
|
|
61
60
|
return;
|
|
62
61
|
}
|
|
63
|
-
setNotice("Open a
|
|
62
|
+
setNotice("Open a session first to use /mcp enable/disable/add.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (text === "/plan") {
|
|
66
|
+
const next = !pendingPlanModeRef.current;
|
|
67
|
+
setPendingPlanMode(next);
|
|
68
|
+
setNotice(next
|
|
69
|
+
? "Plan mode armed — the next run you start will open in plan mode."
|
|
70
|
+
: "Plan mode disarmed.");
|
|
64
71
|
return;
|
|
65
72
|
}
|
|
66
73
|
if (text.startsWith("/")) {
|
|
@@ -136,7 +143,7 @@ export function useSubmit(o) {
|
|
|
136
143
|
void openMenu("provider");
|
|
137
144
|
return;
|
|
138
145
|
}
|
|
139
|
-
if (["/key", "/keys", "/llm", "/theme"].includes(text.split(/\s+/u)[0])) {
|
|
146
|
+
if (["/key", "/keys", "/llm", "/theme", "/thinking", "/reasoning"].includes(text.split(/\s+/u)[0])) {
|
|
140
147
|
void (async () => {
|
|
141
148
|
const result = await handleSettings(text);
|
|
142
149
|
if (result)
|
|
@@ -147,7 +154,7 @@ export function useSubmit(o) {
|
|
|
147
154
|
if (text === "/report") {
|
|
148
155
|
void (async () => {
|
|
149
156
|
try {
|
|
150
|
-
const report = await
|
|
157
|
+
const report = await Bun.file(getRunPaths(currentRunPath).report).text();
|
|
151
158
|
pushFeed({ kind: "text", text: report });
|
|
152
159
|
}
|
|
153
160
|
catch {
|
|
@@ -290,7 +297,7 @@ export function useSubmit(o) {
|
|
|
290
297
|
void (async () => {
|
|
291
298
|
const currentSession = sessions.find(s => s.path === currentRunPath);
|
|
292
299
|
const report = currentSession?.isFull
|
|
293
|
-
? await
|
|
300
|
+
? await Bun.file(getRunPaths(currentRunPath).report).text().catch(() => "")
|
|
294
301
|
: "";
|
|
295
302
|
const lastText = [...feedRef.current].reverse().find((it) => it.kind === "text")?.text ?? "";
|
|
296
303
|
const content = report.trim() || lastText;
|