@scira/cli 0.1.0

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/dist/agent/research-agent.js +253 -0
  4. package/dist/agent/skills.js +265 -0
  5. package/dist/agent/tools.js +429 -0
  6. package/dist/agent/tools.test.js +27 -0
  7. package/dist/cli/commands/init.js +370 -0
  8. package/dist/cli/index.js +445 -0
  9. package/dist/cli/shell/shell.js +76 -0
  10. package/dist/cli/shell/tui.js +11 -0
  11. package/dist/config/env-store.js +47 -0
  12. package/dist/config/load-config.js +58 -0
  13. package/dist/export/formatters.js +37 -0
  14. package/dist/providers/llm/gateway.js +64 -0
  15. package/dist/providers/llm/huggingface.js +33 -0
  16. package/dist/providers/llm/models.js +97 -0
  17. package/dist/providers/llm/readiness.js +50 -0
  18. package/dist/providers/llm/registry.js +56 -0
  19. package/dist/storage/jsonl.js +29 -0
  20. package/dist/storage/jsonl.test.js +38 -0
  21. package/dist/storage/run-store.js +134 -0
  22. package/dist/storage/run-store.test.js +65 -0
  23. package/dist/tools/chrome-devtools-mcp.js +61 -0
  24. package/dist/tools/file-tools.js +128 -0
  25. package/dist/tools/mcp-bridge.js +118 -0
  26. package/dist/tools/mcp-oauth.js +276 -0
  27. package/dist/tools/open-url.js +99 -0
  28. package/dist/tools/search-web.js +153 -0
  29. package/dist/types/index.js +91 -0
  30. package/dist/types/schema.test.js +60 -0
  31. package/dist/ui/ink/SciraApp.js +274 -0
  32. package/dist/ui/ink/components/effects.js +44 -0
  33. package/dist/ui/ink/components/home-screen.js +69 -0
  34. package/dist/ui/ink/components/overlays.js +111 -0
  35. package/dist/ui/ink/constants.js +56 -0
  36. package/dist/ui/ink/hooks/use-agent-turn.js +186 -0
  37. package/dist/ui/ink/hooks/use-feed-lines.js +186 -0
  38. package/dist/ui/ink/hooks/use-feed.js +69 -0
  39. package/dist/ui/ink/hooks/use-keyboard.js +315 -0
  40. package/dist/ui/ink/hooks/use-mouse.js +31 -0
  41. package/dist/ui/ink/hooks/use-session.js +103 -0
  42. package/dist/ui/ink/hooks/use-settings.js +155 -0
  43. package/dist/ui/ink/hooks/use-submit.js +366 -0
  44. package/dist/ui/ink/hooks/use-suggestions.js +91 -0
  45. package/dist/ui/ink/lib/file-mentions.js +71 -0
  46. package/dist/ui/ink/lib/markdown.js +245 -0
  47. package/dist/ui/ink/lib/utils.js +224 -0
  48. package/dist/ui/ink/session-manager.js +160 -0
  49. package/dist/ui/ink/types.js +1 -0
  50. package/dist/utils/ids.js +15 -0
  51. package/dist/utils/markdown-joiner.js +249 -0
  52. package/dist/watch/runner.js +65 -0
  53. package/package.json +74 -0
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { pkgVersion, relativeTime } from "../lib/utils.js";
4
+ import { HOME_TIPS } from "../constants.js";
5
+ /** Home screen body: branding card, browse modal, notice, and tip line.
6
+ * Also (re)builds the mouse click/hover row maps as a render side-effect. */
7
+ export function HomeScreen({ cols, rows, sessions, selectedIdx, hoveredIdx, heroHidden, notice, tipIndex, commandMenuHeight, sessionsModalOpen, sessionsModalIdx, inputText, clickMapRef, hoverMapRef, setSelectedIdx, setSessionsModalOpen, setSessionsModalIdx, setNotice, openRun, submitHome, exit, }) {
8
+ const cardW = Math.min(Math.max(52, cols - 4), 90);
9
+ const heroRows = heroHidden ? 0 : 2;
10
+ const cardH = heroHidden ? 0 : 10;
11
+ const bHeight = rows - 6 - commandMenuHeight;
12
+ const contentH = cardH + (notice ? 2 : 0) + 2;
13
+ const topGap = Math.max(0, Math.floor((bHeight - contentH) / 2));
14
+ const cardTop0 = 2 + topGap;
15
+ const newIdx = 0;
16
+ const quitIdx = 1;
17
+ const newActive = selectedIdx === newIdx || hoveredIdx === newIdx;
18
+ const quitActive = selectedIdx === quitIdx || hoveredIdx === quitIdx;
19
+ const clickMap = new Map();
20
+ const hoverMap = new Map();
21
+ const rowBase = cardTop0 + heroRows + 4;
22
+ if (sessionsModalOpen) {
23
+ const modalVisible = Math.max(5, rows - 12);
24
+ const half = Math.floor(modalVisible / 2);
25
+ const windowStart = Math.max(0, Math.min(sessionsModalIdx - half, Math.max(0, sessions.length - modalVisible)));
26
+ const windowEnd = Math.min(sessions.length, windowStart + modalVisible);
27
+ const modalTop = cardTop0;
28
+ const headerRows = 3;
29
+ const above = windowStart;
30
+ sessions.slice(windowStart, windowEnd).forEach((s, i) => {
31
+ const idx = windowStart + i;
32
+ const row = modalTop + headerRows + (above > 0 ? 1 : 0) + i;
33
+ clickMap.set(row, () => {
34
+ setSessionsModalIdx(idx);
35
+ setSessionsModalOpen(false);
36
+ void openRun(s.path);
37
+ });
38
+ hoverMap.set(row, idx);
39
+ });
40
+ }
41
+ else if (!heroHidden) {
42
+ clickMap.set(rowBase, () => {
43
+ setSelectedIdx(newIdx);
44
+ if (inputText.trim())
45
+ void submitHome(inputText);
46
+ else
47
+ setNotice("Type a question below to start a new research run.");
48
+ });
49
+ hoverMap.set(rowBase, newIdx);
50
+ clickMap.set(rowBase + 1, () => exit());
51
+ hoverMap.set(rowBase + 1, quitIdx);
52
+ }
53
+ clickMapRef.current = clickMap;
54
+ hoverMapRef.current = hoverMap;
55
+ return (_jsx(Box, { flexGrow: 1, flexDirection: "column", justifyContent: "center", alignItems: "center", children: sessionsModalOpen ? (() => {
56
+ const modalVisible = Math.max(5, rows - 12);
57
+ const half = Math.floor(modalVisible / 2);
58
+ const windowStart = Math.max(0, Math.min(sessionsModalIdx - half, Math.max(0, sessions.length - modalVisible)));
59
+ const windowEnd = Math.min(sessions.length, windowStart + modalVisible);
60
+ const above = windowStart;
61
+ const below = sessions.length - windowEnd;
62
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "#FFE0C2", width: cardW, flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: "#FFE0C2", bold: true, children: "All sessions" }), _jsx(Text, { color: "gray", dimColor: true, children: `${sessions.length} total · ${sessionsModalIdx + 1}/${sessions.length} · ↑↓ navigate · ⏎ open · esc close` }), _jsx(Box, { height: 1 }), above > 0 && _jsx(Text, { color: "gray", dimColor: true, children: ` ↑ ${above} more above` }), sessions.slice(windowStart, windowEnd).map((s, i) => {
63
+ const idx = windowStart + i;
64
+ const active = idx === sessionsModalIdx;
65
+ const display = (s.title || s.goal || s.id).slice(0, cardW - 22);
66
+ return (_jsxs(Box, { children: [_jsx(Text, { color: active ? "white" : "gray", children: active ? "❯ [ " : " [ " }), _jsx(Text, { bold: active, color: active ? "white" : "gray", wrap: "truncate", children: display }), _jsx(Box, { flexGrow: 1 }), s.isFull && _jsx(Text, { color: s.reportDirty ? "yellow" : "green", dimColor: true, children: s.reportDirty ? "draft " : "ready " }), _jsx(Text, { color: "gray", dimColor: true, children: relativeTime(s.updatedAt) }), _jsx(Text, { color: active ? "white" : "gray", children: " ]" })] }, s.id));
67
+ }), below > 0 && _jsx(Text, { color: "gray", dimColor: true, children: ` ↓ ${below} more below` })] }));
68
+ })() : heroHidden ? null : (_jsxs(Box, { flexDirection: "column", alignItems: "center", width: cardW, children: [_jsxs(Box, { borderStyle: "round", borderColor: "gray", width: cardW, flexDirection: "row", paddingX: 2, paddingY: 1, children: [_jsx(Box, { flexDirection: "column", justifyContent: "center", marginRight: 3, children: [" ·:::· ", "·: ", " ·:::· ", " ·:", " ·:::· "].map((line, i) => (_jsx(Text, { color: "#FFE0C2", dimColor: true, children: line }, i))) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "white", children: "scira" }), _jsx(Text, { color: "gray", dimColor: true, children: "research agent" }), _jsxs(Text, { color: "gray", dimColor: true, children: ["v", pkgVersion] })] }), _jsx(Text, { color: "white", children: "Research and coding agent with real sources and tools." }), _jsx(Text, { color: "gray", dimColor: true, children: "Type a question below to start, or use # to browse past sessions." }), _jsx(Box, { height: 1 }), _jsxs(Box, { children: [_jsx(Text, { bold: newActive, color: newActive ? "white" : "gray", children: "New research" }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: "gray", dimColor: true, children: "\u23CE enter" })] }), _jsxs(Box, { children: [_jsx(Text, { bold: quitActive, color: quitActive ? "white" : "gray", children: "Quit" }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: "gray", dimColor: true, children: "ctrl+d" })] })] })] }), notice ? (_jsx(Box, { paddingTop: 1, children: _jsx(Text, { color: "yellow", children: notice }) })) : null, _jsx(Box, { paddingTop: 2, children: _jsxs(Text, { color: "gray", dimColor: true, children: ["Tip: ", HOME_TIPS[tipIndex % HOME_TIPS.length]] }) })] })) }));
69
+ }
@@ -0,0 +1,111 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { SPINNER_FRAMES, CHAT_COMMANDS, COMMAND_DESCRIPTIONS, MENU_VISIBLE } from "../constants.js";
4
+ import { fmtTokens, wrapText } from "../lib/utils.js";
5
+ import { LLM_PROVIDER_LABELS } from "../../../providers/llm/registry.js";
6
+ export function TopBar({ screen, runState, fullMode, activeUsage, busy, frame, cwdDisplay }) {
7
+ return (_jsxs(Box, { paddingTop: 1, justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 1, minWidth: 0, marginRight: 2, children: _jsx(Text, { color: "gray", dimColor: true, wrap: "truncate-end", children: screen === "chat" ? (runState?.title || runState?.goal || cwdDisplay) : cwdDisplay }) }), screen === "chat" && (_jsxs(Box, { flexShrink: 0, gap: 1, children: [_jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsx(Text, { color: fullMode ? "magenta" : "#FFE0C2", children: fullMode ? "full" : "quick" }), activeUsage && (_jsx(Text, { color: "gray", dimColor: true, children: `↑${fmtTokens(activeUsage.input)} ↓${fmtTokens(activeUsage.output)}` })), fullMode && (_jsxs(Text, { color: "gray", dimColor: true, children: [`src:${runState?.sourceCount ?? 0}`, (runState?.claimCount ?? 0) > 0 ? ` · claims:${runState?.claimCount}` : ""] })), fullMode && (_jsx(Text, { color: runState?.reportDirty ? "yellow" : "green", dimColor: true, children: runState?.reportDirty ? "draft" : "ready" })), busy && _jsx(Text, { color: "#FFE0C2", children: SPINNER_FRAMES[frame % SPINNER_FRAMES.length] }), _jsx(Text, { color: "gray", dimColor: true, children: "|" })] }))] }));
8
+ }
9
+ export function InputBar({ inputLines, cursorLine, cursorCol, showCursor, approvalPending, busy, frame, boxWidth, modelName }) {
10
+ const accentColor = approvalPending ? "yellowBright" : busy ? "#C2AA93" : "gray";
11
+ const borderLabel = busy ? `${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${modelName}` : modelName;
12
+ const labelMax = Math.max(0, boxWidth - 6);
13
+ const label = borderLabel.length > labelMax ? borderLabel.slice(0, labelMax) : borderLabel;
14
+ const dashCount = Math.max(1, boxWidth - label.length - 5);
15
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: accentColor, children: "╭" + "─".repeat(Math.max(0, boxWidth - 2)) + "╮" }), inputLines.map((line, i) => (_jsxs(Box, { width: boxWidth, children: [_jsx(Text, { color: accentColor, children: "│ " }), _jsx(Text, { color: i === 0 ? (approvalPending ? "yellowBright" : busy ? "#C2AA93" : "#FFE0C2") : accentColor, children: i === 0 ? "❯ " : " " }), showCursor && i === cursorLine ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: line.slice(0, cursorCol) }), _jsx(Text, { color: "white", inverse: true, children: line[cursorCol] ?? " " }), _jsx(Text, { color: "white", children: line.slice(cursorCol + 1) })] })) : (_jsx(Text, { color: approvalPending ? "gray" : "white", children: line })), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: accentColor, children: " \u2502" })] }, i))), _jsxs(Box, { width: boxWidth, children: [_jsx(Text, { color: accentColor, children: "╰" + "─".repeat(dashCount) + " " }), _jsx(Text, { color: "#FFE0C2", children: label }), _jsx(Text, { color: accentColor, children: " ─╯" })] })] }));
16
+ }
17
+ export function HintLine({ screen, busy, scrollLabel, hasDoneGroups, hasFocusedGroup }) {
18
+ if (screen === "chat") {
19
+ return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "gray", dimColor: true, children: _jsx(Text, { bold: true, color: "#FFE0C2", children: "/HELP" }) }), _jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsx(Text, { color: "gray", dimColor: true, children: _jsx(Text, { bold: true, color: "#FFE0C2", children: "/REPORT" }) }), _jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsx(Text, { color: "gray", dimColor: true, children: _jsx(Text, { bold: true, color: "#FFE0C2", children: "/NEW" }) }), busy && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsx(Text, { color: "gray", dimColor: true, children: _jsx(Text, { bold: true, color: "#FFE0C2", children: "/STOP" }) })] })), hasDoneGroups && !busy ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsx(Text, { color: "gray", dimColor: true, children: hasFocusedGroup
20
+ ? _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "#FFE0C2", children: "C" }), " toggle \u00B7 ", _jsx(Text, { bold: true, color: "#FFE0C2", children: "ESC" }), " unfocus"] })
21
+ : _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "#FFE0C2", children: "[ ]" }), " \u00B7 ", _jsx(Text, { bold: true, color: "#FFE0C2", children: "C" }), " groups"] }) })] })) : null, scrollLabel ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: "gray", dimColor: true, children: scrollLabel })] })) : null] }));
22
+ }
23
+ return (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [_jsx(Text, { bold: true, color: "#FFE0C2", children: "\u2191\u2193" }), ":navigate"] }), _jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsxs(Text, { color: "gray", dimColor: true, children: [_jsx(Text, { bold: true, color: "#FFE0C2", children: "\u23CE" }), ":open"] }), _jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsxs(Text, { color: "gray", dimColor: true, children: [_jsx(Text, { bold: true, color: "#FFE0C2", children: "ESC" }), ":close"] }), _jsx(Text, { color: "gray", dimColor: true, children: "|" }), _jsxs(Text, { color: "gray", dimColor: true, children: [_jsx(Text, { bold: true, color: "#FFE0C2", children: "Q" }), ":quit"] })] }));
24
+ }
25
+ export function CommandMenuBox({ activeSuggestions, activeSuggestionKind, commandMenuIndex, innerWidth, sessions }) {
26
+ if (activeSuggestions.length === 0)
27
+ return null;
28
+ const total = activeSuggestions.length;
29
+ const clampedIdx = Math.min(Math.max(0, commandMenuIndex), total - 1);
30
+ const windowStart = Math.max(0, Math.min(clampedIdx - MENU_VISIBLE + 1, total - MENU_VISIBLE));
31
+ const visible = activeSuggestions.slice(windowStart, windowStart + MENU_VISIBLE);
32
+ const nameWidth = Math.min(40, Math.max(...visible.map((c) => c.length)));
33
+ const innerCols = Math.max(20, innerWidth - 4);
34
+ const descMax = Math.max(10, innerCols - nameWidth - 4);
35
+ const isFileMenu = activeSuggestionKind === "file";
36
+ const isSessionMenu = activeSuggestionKind === "session";
37
+ const baseHeader = isFileMenu ? "files ↑↓ move · tab complete"
38
+ : isSessionMenu ? "sessions ↑↓ move · ⏎ open"
39
+ : "commands ↑↓ move · tab complete";
40
+ const header = total > MENU_VISIBLE ? `${baseHeader} · ${clampedIdx + 1}/${total}` : baseHeader;
41
+ const sessionDesc = (label) => {
42
+ const s = sessions?.find((r) => (r.title ?? r.goal ?? r.id).replace(/\s+/gu, " ").trim() === label);
43
+ if (!s)
44
+ return "";
45
+ const bits = [s.isFull ? "full" : "quick"];
46
+ if (s.sourceCount > 0)
47
+ bits.push(`${s.sourceCount} src`);
48
+ if (s.claimCount > 0)
49
+ bits.push(`${s.claimCount} claims`);
50
+ return bits.join(" · ");
51
+ };
52
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginX: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [header, windowStart > 0 ? " ↑" : "", windowStart + MENU_VISIBLE < total ? " ↓" : ""] }), visible.map((item, i) => {
53
+ const gi = windowStart + i;
54
+ const active = gi === clampedIdx;
55
+ const name = isSessionMenu && item.length > nameWidth ? item.slice(0, Math.max(0, nameWidth - 1)) + "…" : item;
56
+ const label = isFileMenu ? `@${name}` : isSessionMenu ? `# ${name}` : name;
57
+ const desc = isFileMenu ? "Attach file as model context."
58
+ : isSessionMenu ? sessionDesc(item)
59
+ : COMMAND_DESCRIPTIONS[item] ?? "";
60
+ const trimmed = desc.length > descMax ? desc.slice(0, Math.max(0, descMax - 1)) + "…" : desc;
61
+ const namePad = " ".repeat(Math.max(1, nameWidth - name.length + 2));
62
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: active ? "white" : "gray", bold: active, children: [active ? "❯ " : " ", label] }), _jsxs(Text, { color: "gray", dimColor: true, children: [namePad, trimmed] })] }, `${item}-${gi}`));
63
+ })] }));
64
+ }
65
+ export function HelpBox({ open, innerWidth }) {
66
+ if (!open)
67
+ return null;
68
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginX: 1, children: [_jsxs(Text, { bold: true, color: "white", children: ["help ", _jsx(Text, { color: "gray", dimColor: true, children: "esc close" })] }), _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(Math.max(10, innerWidth - 6)) }), _jsx(Text, { color: "gray", dimColor: true, children: "scroll \u2191/\u2193 k/j u/d pgup/pgdn" }), _jsx(Text, { color: "gray", dimColor: true, children: "autocomplete / commands \u00B7 @ files \u00B7 # sessions" }), _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(Math.max(10, innerWidth - 6)) }), CHAT_COMMANDS.map((cmd) => (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: "#FFE0C2", children: cmd }), _jsx(Text, { color: "gray", dimColor: true, children: COMMAND_DESCRIPTIONS[cmd] })] }, cmd)))] }));
69
+ }
70
+ export function ApprovalBox({ toolName, description, innerWidth }) {
71
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellowBright", paddingX: 1, marginX: 1, children: [_jsxs(Text, { bold: true, color: "yellowBright", children: ["\u26A0 ", toolName, _jsx(Text, { color: "gray", dimColor: true, children: " y approve \u00B7 n reject" })] }), _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(Math.max(10, innerWidth - 6)) }), wrapText(description, Math.max(10, innerWidth - 4)).slice(0, 6).map((line, i) => {
72
+ const isAdded = line.startsWith("+ ");
73
+ const isRemoved = line.startsWith("- ");
74
+ return (_jsx(Text, { color: isAdded ? "green" : isRemoved ? "red" : "gray", wrap: "truncate", children: line }, i));
75
+ })] }));
76
+ }
77
+ export function MenuDialog({ menu, cols, rows }) {
78
+ if (!menu)
79
+ return null;
80
+ const DIALOG_W = Math.min(64, Math.max(40, cols - 4));
81
+ const DIALOG_ITEMS = 10;
82
+ const displayName = (item) => menu.type === "llm" ? LLM_PROVIDER_LABELS[item] ?? item : item;
83
+ const menuFiltered = !menu.loading
84
+ ? (menu.query
85
+ ? menu.items.filter((item) => item.toLowerCase().includes(menu.query.toLowerCase()) ||
86
+ displayName(item).toLowerCase().includes(menu.query.toLowerCase()))
87
+ : menu.items)
88
+ : [];
89
+ const menuStart = Math.min(Math.max(0, menu.index - Math.floor(DIALOG_ITEMS / 2)), Math.max(0, menuFiltered.length - DIALOG_ITEMS));
90
+ const dialogLeft = Math.max(0, Math.floor((cols - 4 - DIALOG_W) / 2));
91
+ 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
+ const dialogTop = Math.max(1, Math.floor((rows - dialogH) / 2));
93
+ return (_jsxs(Box, { position: "absolute", marginLeft: dialogLeft, marginTop: dialogTop, width: DIALOG_W, flexDirection: "column", borderStyle: "round", borderColor: "#FFE0C2", backgroundColor: "#0d0d0d", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "white", children: [menu.type === "model" ? "Select model" : menu.type === "llm" ? "Select LLM provider" : "Select search provider", " ", _jsx(Text, { color: "gray", dimColor: true, children: "\u2191\u2193 navigate \u00B7 \u23CE apply \u00B7 esc close" })] }), !menu.loading && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: "#FFE0C2", children: "⌕ " }), _jsx(Text, { color: "white", children: menu.query }), !menu.query && _jsx(Text, { color: "gray", dimColor: true, children: "type to filter\u2026" })] }), _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(Math.max(4, DIALOG_W - 4)) })] })), menu.loading ? (_jsx(Text, { color: "gray", dimColor: true, children: " loading models\u2026" })) : menuFiltered.length === 0 ? (_jsxs(Text, { color: "gray", dimColor: true, children: [" no matches for \"", menu.query, "\""] })) : (_jsxs(_Fragment, { children: [menuStart > 0 && _jsxs(Text, { color: "gray", dimColor: true, children: [" \u2191 ", menuStart, " more"] }), menuFiltered.slice(menuStart, menuStart + DIALOG_ITEMS).map((item, i) => {
94
+ const idx = menuStart + i;
95
+ const active = idx === menu.index;
96
+ return (_jsxs(Text, { color: active ? "#FFE0C2" : "gray", bold: active, wrap: "truncate", children: [active ? "❯ " : " ", displayName(item), menu.type === "llm" ? _jsx(Text, { color: "gray", dimColor: true, children: " " + item }) : null] }, item));
97
+ }), menuFiltered.length - (menuStart + DIALOG_ITEMS) > 0 && (_jsxs(Text, { color: "gray", dimColor: true, children: [" \u2193 ", menuFiltered.length - (menuStart + DIALOG_ITEMS), " more"] }))] }))] }));
98
+ }
99
+ export function McpDialog({ open, config, cols, rows }) {
100
+ if (!open)
101
+ return null;
102
+ const W = Math.min(92, Math.max(52, cols - 8));
103
+ const left = Math.max(0, Math.floor((cols - 4 - W) / 2));
104
+ const servers = config.mcp.servers;
105
+ const top = Math.max(1, Math.floor((rows - (8 + servers.length)) / 2));
106
+ const dt = config.mcp.chromeDevtools;
107
+ return (_jsxs(Box, { position: "absolute", marginLeft: left, marginTop: top, width: W, flexDirection: "column", borderStyle: "round", borderColor: "#FFE0C2", backgroundColor: "#0d0d0d", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "white", children: ["MCP servers ", _jsx(Text, { color: "gray", dimColor: true, children: "esc/q/enter close" })] }), _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(Math.max(4, W - 4)) }), _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: dt.enabled ? "green" : "gray", children: dt.enabled ? "●" : "○" }), _jsx(Text, { color: "white", children: " chromeDevtools " }), _jsx(Text, { color: "#FFE0C2", children: "[stdio] " }), _jsx(Text, { color: "gray", dimColor: true, children: [dt.command, ...dt.args].join(" ") })] }), servers.length === 0 ? (_jsx(Text, { color: "gray", dimColor: true, children: " no user-defined servers" })) : servers.map((s) => {
108
+ const target = s.transport === "stdio" ? [s.command, ...s.args].filter(Boolean).join(" ") : s.url ?? "(missing url)";
109
+ return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: s.enabled ? "green" : "gray", children: s.enabled ? "●" : "○" }), _jsxs(Text, { color: "white", children: [" ", s.name, " "] }), _jsxs(Text, { color: "#FFE0C2", children: ["[", s.transport, "] "] }), _jsx(Text, { color: "gray", dimColor: true, children: target })] }, s.name));
110
+ }), _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(Math.max(4, W - 4)) }), _jsx(Text, { color: "gray", dimColor: true, children: "/mcp add http exa https://mcp.exa.ai/mcp" }), _jsx(Text, { color: "gray", dimColor: true, children: "/mcp enable <name> \u00B7 /mcp disable <name>" })] }));
111
+ }
@@ -0,0 +1,56 @@
1
+ export const USER_BAND_BG = "#1c1c1c";
2
+ export const S_BAR = "│";
3
+ export const MENU_VISIBLE = 8;
4
+ export const FILE_MENTION_MAX_CHARS = 20000;
5
+ export const FILE_MENTION_SKIP = new Set([".git", "node_modules", "dist", ".scira"]);
6
+ 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"];
8
+ export const COMMAND_DESCRIPTIONS = {
9
+ "/help": "Show command and keyboard shortcuts.",
10
+ "/new": "Go to the home screen to start a new research run.",
11
+ "/rerun": "Run the research agent again for this run.",
12
+ "/report": "Show the generated report.md in the timeline.",
13
+ "/sources": "List the run's gathered sources with links.",
14
+ "/claims": "List all claims with id, confidence, status, and text.",
15
+ "/why": "Show full detail for a claim: /why <claim-id>",
16
+ "/mcp": "Manage MCP servers: /mcp list · /mcp enable/disable <name> · /mcp add <type> <name> <cmd|url>",
17
+ "/copy": "Copy the last answer (or report) to the clipboard.",
18
+ "/usage": "Show token usage per model for this session.",
19
+ "/rename": "Set a title for this session, e.g. /rename SpaceX IPO analysis",
20
+ "/model": "Open the model selector dropup.",
21
+ "/llm": "Switch the LLM provider (gateway, xai, workers-ai).",
22
+ "/provider": "Open the search provider selector.",
23
+ "/key": "Save an API key, e.g. /key EXA_API_KEY ...",
24
+ "/keys": "Show which required API keys are set.",
25
+ "/stop": "Abort the currently running agent turn.",
26
+ "/back": "Return to the sessions list.",
27
+ "/quit": "Quit the TUI."
28
+ };
29
+ export const TOOL_ICONS = {
30
+ bash: "$",
31
+ writeFile: "✎",
32
+ editFile: "✎",
33
+ readFile: "▤",
34
+ createClaim: "◎",
35
+ verifyClaim: "✓",
36
+ webSearch: "⌕",
37
+ readUrl: "↗",
38
+ listSkills: "★",
39
+ readSkill: "★"
40
+ };
41
+ export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
42
+ export const HOME_TIPS = [
43
+ "Type a question and press ⏎ to start a new research run.",
44
+ "Say \"deep research …\" or \"compare …\" to trigger the full research harness.",
45
+ "↑↓ navigate · ⏎ open · type to start a new run.",
46
+ "/model · /provider · /key NAME value to configure.",
47
+ "Browse all sessions to find older runs.",
48
+ "ready / draft badges show full-research runs and their report state."
49
+ ];
50
+ export const FULL_MODE_TRIGGERS = [
51
+ "deep research", "deep dive", "deep-dive", "do research", "research about",
52
+ "research on", "in depth", "in-depth", "comprehensive", "thorough",
53
+ "detailed report", "full report", "write a report", "literature review",
54
+ "investigate", "analyze", "analyse", "analysis of", "compare", "comparison",
55
+ "pros and cons", "state of the art", "survey of", "everything about"
56
+ ];
@@ -0,0 +1,186 @@
1
+ import { useCallback, useRef } from "react";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { createResearchAgent, createOneShotAgent } from "../../../agent/research-agent.js";
5
+ import { generateWithGateway } from "../../../providers/llm/gateway.js";
6
+ import { setRunTitle, summarizeRun } from "../../../storage/run-store.js";
7
+ import { fmtDuration, fmtTokens, aggregateTurns, wantsFullResearch, summarizeToolInput } from "../lib/utils.js";
8
+ import { promptWithFileMentions } from "../lib/file-mentions.js";
9
+ import { markdownJoinerTransform } from "../../../utils/markdown-joiner.js";
10
+ import { createSession, getSession, removeSession, attachSubscriber, sessionPushFeed, sessionSetBusy, sessionSetApproval, sessionFinishReasoning, sessionNotifyEscalate, sessionNotifyModeChange, } from "../session-manager.js";
11
+ export function useAgentTurn({ config, currentRunPath, queuedPromptRef, fullModeRef, conversationRef, turnsRef, feedRef, setBusy, setScrollOffset, refreshRun, recordUsage, setMode, getSubscriber, }) {
12
+ const runTurn = useCallback(async (prompt) => {
13
+ const runPath = currentRunPath;
14
+ if (!runPath)
15
+ return;
16
+ const existing = getSession(runPath);
17
+ if (existing?.busy)
18
+ return;
19
+ const session = createSession(runPath);
20
+ // Always re-attach subscriber so follow-up turns have a live listener.
21
+ attachSubscriber(runPath, getSubscriber());
22
+ const controller = new AbortController();
23
+ session.abort = controller;
24
+ setBusy(true);
25
+ setScrollOffset(0);
26
+ sessionSetBusy(runPath, true);
27
+ const modelId = config.model;
28
+ const turnStartedAt = Date.now();
29
+ const turnUsage = { input: 0, output: 0, total: 0 };
30
+ let summary;
31
+ try {
32
+ summary = await summarizeRun(runPath);
33
+ if (summary && !summary.title && conversationRef.current.length === 0) {
34
+ void (async () => {
35
+ try {
36
+ const title = await generateWithGateway(config, `Summarize this research topic into a very short title (3-5 words). Output ONLY the title, nothing else.\n\nTopic: ${summary.goal}`);
37
+ const cleanTitle = title.trim().replace(/^["']+|["']+$/gu, "").slice(0, 60);
38
+ if (cleanTitle)
39
+ await setRunTitle(runPath, cleanTitle);
40
+ }
41
+ catch { /* non-fatal */ }
42
+ })();
43
+ }
44
+ const onApprovalRequired = (toolName, description) => new Promise((resolve) => sessionSetApproval(runPath, { toolName, description, resolve }));
45
+ const consume = async (result) => {
46
+ for await (const part of result.fullStream) {
47
+ if (part.type !== "reasoning-delta" && part.type !== "reasoning-start")
48
+ sessionFinishReasoning(runPath);
49
+ switch (part.type) {
50
+ case "text-delta":
51
+ sessionPushFeed(runPath, { kind: "text", text: part.text });
52
+ break;
53
+ case "reasoning-delta":
54
+ sessionPushFeed(runPath, { kind: "reasoning", text: part.text });
55
+ break;
56
+ case "reasoning-end":
57
+ sessionFinishReasoning(runPath);
58
+ break;
59
+ case "tool-call":
60
+ sessionPushFeed(runPath, { kind: "tool", name: part.toolName, toolCallId: part.toolCallId, summary: summarizeToolInput(part.toolName, part.input), status: "running" });
61
+ break;
62
+ case "tool-result": {
63
+ const toolResultId = part.toolCallId;
64
+ const resultText = String(part.output).slice(0, 200);
65
+ sessionPushFeed(runPath, { kind: "tool", name: "", toolCallId: toolResultId, summary: resultText, status: "done", result: resultText });
66
+ void refreshRun();
67
+ break;
68
+ }
69
+ case "tool-error": {
70
+ const toolErrId = part.toolCallId ?? "";
71
+ const errText = String(part.error).slice(0, 200);
72
+ sessionPushFeed(runPath, { kind: "tool", name: "", toolCallId: toolErrId, summary: errText, status: "error", result: errText });
73
+ break;
74
+ }
75
+ case "error":
76
+ sessionPushFeed(runPath, { kind: "status", text: `Error: ${String(part.error)}` });
77
+ break;
78
+ default: break;
79
+ }
80
+ }
81
+ sessionFinishReasoning(runPath);
82
+ try {
83
+ const u = await result.totalUsage;
84
+ recordUsage(modelId, u);
85
+ turnUsage.input += u.inputTokens ?? 0;
86
+ turnUsage.output += u.outputTokens ?? 0;
87
+ turnUsage.total += u.totalTokens ?? (u.inputTokens ?? 0) + (u.outputTokens ?? 0);
88
+ }
89
+ catch { /* usage is best-effort */ }
90
+ return result.text;
91
+ };
92
+ const mentioned = await promptWithFileMentions(prompt);
93
+ if (mentioned.files.length > 0)
94
+ sessionPushFeed(runPath, { kind: "status", text: `Attached ${mentioned.files.map((f) => `@${f}`).join(", ")}.` });
95
+ let messages = [...conversationRef.current, { role: "user", content: mentioned.prompt }];
96
+ let finalText = "";
97
+ if (!fullModeRef.current && wantsFullResearch(prompt)) {
98
+ setMode(true);
99
+ fullModeRef.current = true;
100
+ sessionNotifyModeChange(runPath, true);
101
+ sessionPushFeed(runPath, { kind: "status", text: "Detected a research request — switching to the full research harness." });
102
+ }
103
+ if (fullModeRef.current) {
104
+ const bundle = await createResearchAgent(runPath, summary.goal, config, onApprovalRequired);
105
+ try {
106
+ finalText = await consume(await bundle.agent.stream({ messages, abortSignal: controller.signal, experimental_transform: markdownJoinerTransform() }));
107
+ }
108
+ finally {
109
+ await bundle.close();
110
+ }
111
+ }
112
+ else {
113
+ const escalate = { requested: false };
114
+ const oneShot = await createOneShotAgent(runPath, summary.goal, config, onApprovalRequired, () => { escalate.requested = true; });
115
+ try {
116
+ finalText = await consume(await oneShot.agent.stream({ messages, abortSignal: controller.signal, experimental_transform: markdownJoinerTransform() }));
117
+ }
118
+ finally {
119
+ await oneShot.close();
120
+ }
121
+ if (escalate.requested && !controller.signal.aborted) {
122
+ setMode(true);
123
+ fullModeRef.current = true;
124
+ sessionNotifyEscalate(runPath);
125
+ sessionNotifyModeChange(runPath, true);
126
+ sessionPushFeed(runPath, { kind: "status", text: "Escalated to the full research harness." });
127
+ messages = [
128
+ ...messages,
129
+ { role: "assistant", content: finalText },
130
+ { role: "user", content: "Approved. Now run the full research harness: discover skills, write plan.md, gather and read grounded sources, extract and verify claims, write sources.jsonl and a complete report.md, then give a short summary." }
131
+ ];
132
+ const full = await createResearchAgent(runPath, summary.goal, config, onApprovalRequired);
133
+ try {
134
+ finalText = await consume(await full.agent.stream({ messages, abortSignal: controller.signal, experimental_transform: markdownJoinerTransform() }));
135
+ }
136
+ finally {
137
+ await full.close();
138
+ }
139
+ }
140
+ }
141
+ conversationRef.current = [...messages, { role: "assistant", content: finalText }];
142
+ await refreshRun();
143
+ }
144
+ catch (error) {
145
+ if (!controller.signal.aborted) {
146
+ sessionPushFeed(runPath, { kind: "status", text: error instanceof Error ? error.message : String(error) });
147
+ }
148
+ }
149
+ finally {
150
+ session.abort = null;
151
+ setBusy(false);
152
+ sessionSetBusy(runPath, false);
153
+ sessionSetApproval(runPath, null);
154
+ const elapsedMs = Date.now() - turnStartedAt;
155
+ const parts = [];
156
+ if (turnUsage.input > 0)
157
+ parts.push(`↑${fmtTokens(turnUsage.input)}`);
158
+ if (turnUsage.output > 0)
159
+ parts.push(`↓${fmtTokens(turnUsage.output)}`);
160
+ parts.push(`◌ ${fmtDuration(elapsedMs)}`);
161
+ if (controller.signal.aborted)
162
+ parts.push("stopped");
163
+ sessionPushFeed(runPath, { kind: "status", text: parts.join(" · ") });
164
+ if (turnUsage.input + turnUsage.output + turnUsage.total > 0) {
165
+ turnsRef.current = [...turnsRef.current, { model: modelId, input: turnUsage.input, output: turnUsage.output, total: turnUsage.total, ts: Date.now() }];
166
+ }
167
+ const snapshot = feedRef.current
168
+ .filter((item) => !(item.kind === "status" && item.text === "This will wipe the conversation history. Press /rerun again to confirm."))
169
+ .map((item) => item.kind === "tool" && item.status === "running" ? { ...item, status: "error" } : item);
170
+ try {
171
+ await writeFile(join(runPath, "convo.json"), JSON.stringify({ feed: snapshot, messages: conversationRef.current, usage: aggregateTurns(turnsRef.current) }, null, 2));
172
+ }
173
+ catch { /* non-fatal */ }
174
+ removeSession(runPath);
175
+ const queued = queuedPromptRef.current;
176
+ if (queued && !controller.signal.aborted) {
177
+ queuedPromptRef.current = null;
178
+ sessionPushFeed(runPath, { kind: "user", text: queued, ts: Date.now() });
179
+ void runTurnRef.current(queued);
180
+ }
181
+ }
182
+ }, [config, currentRunPath, refreshRun, recordUsage, setMode, getSubscriber]);
183
+ const runTurnRef = useRef(runTurn);
184
+ runTurnRef.current = runTurn;
185
+ return { runTurn, runTurnRef };
186
+ }
@@ -0,0 +1,186 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { Text } from "ink";
4
+ import { S_BAR, TOOL_ICONS, USER_BAND_BG, SPINNER_FRAMES } from "../constants.js";
5
+ import { formatTime, fmtDuration, wrapText, hyperlink, displayWidth } from "../lib/utils.js";
6
+ import { markdownToSegLines } from "../lib/markdown.js";
7
+ export function computeGroups(feed) {
8
+ const groupOf = new Array(feed.length).fill(-1);
9
+ const groups = new Map();
10
+ let gs = -1;
11
+ for (let i = 0; i < feed.length; i++) {
12
+ const k = feed[i].kind;
13
+ if (k === "tool" || k === "reasoning") {
14
+ if (gs === -1) {
15
+ gs = i;
16
+ groups.set(gs, { toolNames: [], itemCount: 0, active: false, end: i });
17
+ }
18
+ const g = groups.get(gs);
19
+ g.end = i;
20
+ g.itemCount++;
21
+ groupOf[i] = gs;
22
+ if (k === "tool") {
23
+ const it = feed[i];
24
+ if (!g.toolNames.includes(it.name))
25
+ g.toolNames.push(it.name);
26
+ if (it.status === "running")
27
+ g.active = true;
28
+ }
29
+ else {
30
+ const it = feed[i];
31
+ if (it.durationMs === undefined)
32
+ g.active = true;
33
+ }
34
+ }
35
+ else {
36
+ gs = -1;
37
+ }
38
+ }
39
+ return { groupOf, groups };
40
+ }
41
+ const isGH = (item) => "_tag" in item;
42
+ export function useFeedLines(feed, innerWidth,
43
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
44
+ reasoningTick, spinnerFrame, collapsedGroups, focusedGroupKey) {
45
+ return useMemo(() => {
46
+ const lines = [];
47
+ let key = 0;
48
+ const { groupOf, groups } = computeGroups(feed);
49
+ const eff = [];
50
+ for (let i = 0; i < feed.length; i++) {
51
+ const gs = groupOf[i];
52
+ if (gs !== -1) {
53
+ const info = groups.get(gs);
54
+ const collapsed = !info.active && collapsedGroups.has(gs);
55
+ if (gs === i) {
56
+ eff.push({ _tag: "gh", info, key: gs, collapsed, focused: focusedGroupKey === gs });
57
+ if (!collapsed)
58
+ eff.push(feed[i]);
59
+ }
60
+ else if (!collapsed) {
61
+ eff.push(feed[i]);
62
+ }
63
+ }
64
+ else {
65
+ eff.push(feed[i]);
66
+ }
67
+ }
68
+ eff.forEach((item, ei) => {
69
+ const currKind = isGH(item) ? "gh" : item.kind;
70
+ if (ei === 0 && currKind === "user") {
71
+ lines.push(_jsx(Text, { children: " " }, key++));
72
+ }
73
+ if (ei > 0) {
74
+ const prev = eff[ei - 1];
75
+ const prevGH = isGH(prev);
76
+ const currGH = isGH(item);
77
+ const prevKind = prevGH ? "gh" : prev.kind;
78
+ const currKind = currGH ? "gh" : item.kind;
79
+ const prevTool = prevKind === "tool" || prevKind === "reasoning";
80
+ const currTool = currKind === "tool" || currKind === "reasoning";
81
+ if (currKind === "gh") {
82
+ if (prevTool) {
83
+ lines.push(_jsx(Text, { color: "gray", dimColor: true, children: S_BAR }, key++));
84
+ lines.push(_jsx(Text, { children: " " }, key++));
85
+ }
86
+ else if (prevKind !== "gh") {
87
+ lines.push(_jsx(Text, { children: " " }, key++));
88
+ }
89
+ }
90
+ else if (prevKind === "gh") {
91
+ if (prev.collapsed) {
92
+ if (currKind !== "user")
93
+ lines.push(_jsx(Text, { children: " " }, key++));
94
+ }
95
+ }
96
+ else if (prevTool && currTool) {
97
+ if (!(prevKind === "reasoning" && currKind === "reasoning")) {
98
+ lines.push(_jsx(Text, { color: "gray", dimColor: true, children: S_BAR }, key++));
99
+ }
100
+ }
101
+ else if (prevTool) {
102
+ if (currKind !== "user")
103
+ lines.push(_jsx(Text, { children: " " }, key++));
104
+ }
105
+ else if (currTool) {
106
+ lines.push(_jsx(Text, { children: " " }, key++));
107
+ }
108
+ else if (prevKind === "status" && currKind === "status") {
109
+ }
110
+ else if (currKind === "user") {
111
+ if (prevKind !== "user") {
112
+ lines.push(_jsx(Text, { children: " " }, key++));
113
+ lines.push(_jsx(Text, { children: " " }, key++));
114
+ }
115
+ }
116
+ else {
117
+ lines.push(_jsx(Text, { children: " " }, key++));
118
+ }
119
+ }
120
+ if (isGH(item)) {
121
+ const { info, collapsed, focused } = item;
122
+ const icon = info.active ? "◎" : collapsed ? "▶" : "▼";
123
+ const hc = focused ? "#FFE0C2" : "gray";
124
+ const names = info.toolNames.slice(0, 5).join(", ") + (info.toolNames.length > 5 ? ", …" : "");
125
+ lines.push(_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: info.active ? "#FFE0C2" : hc, bold: info.active || focused, children: [icon, " "] }), _jsxs(Text, { color: info.active ? "white" : hc, bold: info.active || focused, dimColor: !info.active && !focused, children: [info.itemCount, " step", info.itemCount !== 1 ? "s" : ""] }), (collapsed || info.active) && names ? (_jsxs(Text, { color: "gray", dimColor: true, children: [" ", names] })) : null, focused && !collapsed && !info.active ? (_jsx(Text, { color: "gray", dimColor: true, children: " [c] collapse · [esc] unfocus" })) : null] }, key++));
126
+ return;
127
+ }
128
+ const fi = item;
129
+ if (fi.kind === "tool") {
130
+ const toolIcon = fi.status === "running"
131
+ ? SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length]
132
+ : TOOL_ICONS[fi.name] ?? "·";
133
+ const symColor = "#CFB59D";
134
+ const nameColor = fi.status === "running" ? "white" : "gray";
135
+ const summaryLine = fi.summary.replace(/\s+/gu, " ").trim();
136
+ const toolSummary = summaryLine.length > innerWidth - fi.name.length - 6
137
+ ? summaryLine.slice(0, Math.max(0, innerWidth - fi.name.length - 7)) + "…"
138
+ : summaryLine;
139
+ lines.push(_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: symColor, bold: fi.status === "running", children: toolIcon }), _jsxs(Text, { color: nameColor, bold: fi.status === "running", dimColor: fi.status === "done", children: [" ", fi.name] }), _jsxs(Text, { color: "gray", dimColor: true, children: [" ", toolSummary] })] }, key++));
140
+ }
141
+ else if (fi.kind === "user") {
142
+ const bandW = innerWidth;
143
+ const time = formatTime(fi.ts);
144
+ const rightPad = time ? time.length + 1 : 0;
145
+ const wrapped = wrapText(fi.text, Math.max(10, bandW - 4 - rightPad));
146
+ const blank = " ".repeat(bandW);
147
+ lines.push(_jsx(Text, { backgroundColor: USER_BAND_BG, children: blank }, key++));
148
+ wrapped.forEach((l, idx) => {
149
+ const isFirst = idx === 0;
150
+ const prefix = isFirst ? " ❯ " : " ";
151
+ const left = prefix + l;
152
+ const pad = Math.max(1, bandW - displayWidth(left) - (isFirst ? rightPad : 0));
153
+ lines.push(_jsxs(Text, { backgroundColor: USER_BAND_BG, wrap: "truncate", children: [_jsx(Text, { color: isFirst ? "#FFE0C2" : "white", children: prefix }), _jsx(Text, { color: "white", children: l }), _jsx(Text, { children: " ".repeat(pad) }), isFirst && time ? _jsx(Text, { color: "gray", dimColor: true, children: time + " " }) : null] }, key++));
154
+ });
155
+ lines.push(_jsx(Text, { backgroundColor: USER_BAND_BG, children: blank }, key++));
156
+ }
157
+ else if (fi.kind === "status") {
158
+ lines.push(_jsxs(Text, { color: "gray", dimColor: true, wrap: "truncate", children: [" · ", fi.text] }, key++));
159
+ }
160
+ else if (fi.kind === "reasoning") {
161
+ const isOpen = fi.durationMs === undefined;
162
+ const elapsedMs = fi.durationMs ?? (fi.startedAt ? Date.now() - fi.startedAt : 0);
163
+ const titleText = isOpen ? `Thinking… ${fmtDuration(elapsedMs)}` : `Thought for ${fmtDuration(elapsedMs)}`;
164
+ lines.push(_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "gray", dimColor: true, children: "\u25CC " }), _jsx(Text, { color: "gray", bold: isOpen, dimColor: !isOpen, children: titleText })] }, key++));
165
+ for (const segLine of markdownToSegLines(fi.text, innerWidth - 4)) {
166
+ if (segLine.length === 0) {
167
+ lines.push(_jsx(Text, { color: "gray", dimColor: true, children: S_BAR }, key++));
168
+ continue;
169
+ }
170
+ lines.push(_jsxs(Text, { color: "gray", dimColor: true, italic: true, wrap: "truncate-end", children: [_jsx(Text, { color: "gray", dimColor: true, children: "│ " }), segLine.map((s, i) => (_jsx(Text, { color: "gray", bold: s.bold, italic: s.italic ?? true, underline: s.underline, dimColor: true, children: hyperlink(s.text, s.url) }, i)))] }, key++));
171
+ }
172
+ }
173
+ else {
174
+ for (const segLine of markdownToSegLines(fi.text, innerWidth - 2)) {
175
+ if (segLine.length === 0) {
176
+ lines.push(_jsx(Text, { children: " " }, key++));
177
+ continue;
178
+ }
179
+ lines.push(_jsx(Text, { wrap: "truncate-end", children: segLine.map((s, i) => (_jsx(Text, { color: s.color ?? "white", bold: s.bold, italic: s.italic, underline: s.underline, dimColor: s.dim, children: hyperlink(s.text, s.url) }, i))) }, key++));
180
+ }
181
+ }
182
+ });
183
+ return lines;
184
+ // eslint-disable-next-line react-hooks/exhaustive-deps
185
+ }, [feed, innerWidth, reasoningTick, spinnerFrame, collapsedGroups, focusedGroupKey]);
186
+ }