@scira/cli 0.1.4 → 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 +53 -12
- 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 +37 -7
- package/dist/ui/ink/hooks/use-agent-turn.js +17 -6
- package/dist/ui/ink/hooks/use-feed-lines.js +34 -7
- 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 +205 -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 +15 -13
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { Box, useApp, useStdout, useStdin } from "ink";
|
|
4
|
-
import { CHAT_COMMANDS, MENU_VISIBLE } from "./constants.js";
|
|
3
|
+
import { Box, Text, useApp, useStdout, useStdin } from "ink";
|
|
4
|
+
import { CHAT_COMMANDS, MENU_VISIBLE, SPINNER_FRAMES, LOADING_PHRASES } from "./constants.js";
|
|
5
5
|
import { CWD_DISPLAY, wrapText, wrapInputWithCursor, loadInputHistory, saveInputHistory, linkAtMouseColumn, openExternalUrl } from "./lib/utils.js";
|
|
6
6
|
import { deleteRun } from "../../storage/run-store.js";
|
|
7
7
|
import { saveGlobalConfig } from "../../config/load-config.js";
|
|
@@ -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 },
|
|
@@ -346,12 +350,25 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
346
350
|
const menuHeight = commandMenuHeight + helpHeight + approvalHeight + linkHeight;
|
|
347
351
|
const feedRows = Math.max(3, rows - 6 - inputLines.length - menuHeight);
|
|
348
352
|
const hasRunningTool = feed.some((it) => it.kind === "tool" && it.status === "running");
|
|
349
|
-
const { lines: feedLines, toggleAtLine, groupToggleAtLine, linkAtLine } = useFeedLines(feed, innerWidth, reasoningTick, hasRunningTool ? frame : 0, collapsedGroups, focusedGroupKey, itemExpandState, hoveredIdx, config);
|
|
353
|
+
const { lines: feedLines, toggleAtLine, groupToggleAtLine, linkAtLine, lastUserLineStart } = useFeedLines(feed, innerWidth, reasoningTick, hasRunningTool ? frame : 0, collapsedGroups, focusedGroupKey, itemExpandState, hoveredIdx, config);
|
|
350
354
|
const contentRows = Math.max(1, feedRows);
|
|
351
355
|
const maxScrollOffset = Math.max(0, feedLines.length - contentRows);
|
|
352
356
|
wheelStateRef.current = { screen, maxScrollOffset };
|
|
353
|
-
|
|
354
|
-
|
|
357
|
+
// scrollOffset < 0 is a sentinel meaning "pin the most recent user message to
|
|
358
|
+
// the top of the viewport" (with empty space below for the incoming reply).
|
|
359
|
+
// Once the reply grows past the viewport we fall back to bottom-anchoring so
|
|
360
|
+
// the streaming output stays visible. Any manual scroll clears the sentinel.
|
|
361
|
+
const pinUserToTop = scrollOffset < 0 && lastUserLineStart >= 0;
|
|
362
|
+
let startIdx;
|
|
363
|
+
if (pinUserToTop) {
|
|
364
|
+
const fitsBelow = feedLines.length - lastUserLineStart <= contentRows;
|
|
365
|
+
startIdx = fitsBelow ? lastUserLineStart : Math.max(0, feedLines.length - contentRows);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
const off = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
|
|
369
|
+
startIdx = Math.max(0, feedLines.length - contentRows - off);
|
|
370
|
+
}
|
|
371
|
+
const clampedOffset = Math.max(0, feedLines.length - contentRows - startIdx);
|
|
355
372
|
const hasLinkHover = hoveredIdx !== null && (linkAtLine.get(hoveredIdx)?.length ?? 0) > 0;
|
|
356
373
|
const feedStartRow = 3;
|
|
357
374
|
if (screen === "chat") {
|
|
@@ -384,7 +401,31 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
384
401
|
clickMapRef.current = clickMap;
|
|
385
402
|
hoverMapRef.current = hoverMap;
|
|
386
403
|
}
|
|
387
|
-
const
|
|
404
|
+
const slicedLines = feedLines.slice(startIdx, startIdx + contentRows);
|
|
405
|
+
// The feed box is bottom-aligned (justifyContent="flex-end"). When pinning the
|
|
406
|
+
// user message to the top, pad blank lines below it so short content is pushed
|
|
407
|
+
// up — the user message sits at the top with empty room below for the reply.
|
|
408
|
+
const blankLine = (k) => _jsx(Text, { children: " " }, k);
|
|
409
|
+
// Show an animated loading line whenever the agent is still doing non-text
|
|
410
|
+
// work (reasoning, tool calls, status) — i.e. the latest feed item isn't the
|
|
411
|
+
// streamed answer text. It stays pinned below the latest content for the whole
|
|
412
|
+
// turn, and the phrase rotates slowly so it doesn't feel frozen. The feed box
|
|
413
|
+
// is bottom-aligned with overflow hidden, so when the timeline is long the
|
|
414
|
+
// loader stays visible and the oldest lines scroll off the top instead.
|
|
415
|
+
const lastItem = feed[feed.length - 1];
|
|
416
|
+
const showLoader = busy && lastItem !== undefined && lastItem.kind !== "text";
|
|
417
|
+
const phrase = LOADING_PHRASES[Math.floor(frame / 24) % LOADING_PHRASES.length];
|
|
418
|
+
const loadingLine = (_jsx(Text, { dimColor: true, children: `${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${phrase}` }, "loading"));
|
|
419
|
+
const contentWithLoader = showLoader
|
|
420
|
+
? (slicedLines.length > 0 ? [...slicedLines, blankLine("loading-gap"), loadingLine] : [loadingLine])
|
|
421
|
+
: slicedLines;
|
|
422
|
+
const visibleLines = pinUserToTop && contentWithLoader.length < contentRows
|
|
423
|
+
? [
|
|
424
|
+
blankLine("pad-top"),
|
|
425
|
+
...contentWithLoader,
|
|
426
|
+
...Array.from({ length: Math.max(0, contentRows - contentWithLoader.length - 1) }, (_, i) => blankLine(`pad-${i}`)),
|
|
427
|
+
]
|
|
428
|
+
: contentWithLoader;
|
|
388
429
|
const scrollLabel = clampedOffset > 0
|
|
389
430
|
? (startIdx > 0 ? `↑ ${startIdx} · ↓ ${clampedOffset} · wheel/⇞⇟` : `top · ↓ ${clampedOffset} · wheel/⇞⇟`)
|
|
390
431
|
: "";
|
|
@@ -406,9 +447,9 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
406
447
|
const activeUsage = usage[config.model];
|
|
407
448
|
const themed = (node) => (_jsx(ThemeProvider, { config: config, children: node }));
|
|
408
449
|
if (screen === "home") {
|
|
409
|
-
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 })] }));
|
|
410
451
|
}
|
|
411
|
-
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 })] }));
|
|
412
453
|
}
|
|
413
454
|
function ChatInputChrome({ children }) {
|
|
414
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: "★",
|
|
@@ -51,12 +54,39 @@ export const TOOL_ICONS = {
|
|
|
51
54
|
grepWorkspace: "⌕"
|
|
52
55
|
};
|
|
53
56
|
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
57
|
+
export const LOADING_PHRASES = [
|
|
58
|
+
"Thinking it through…",
|
|
59
|
+
"Digging into it…",
|
|
60
|
+
"Connecting the dots…",
|
|
61
|
+
"Gathering context…",
|
|
62
|
+
"Working through it…",
|
|
63
|
+
"Sifting the details…",
|
|
64
|
+
"Putting it together…",
|
|
65
|
+
"Chasing down answers…",
|
|
66
|
+
"Mulling it over…",
|
|
67
|
+
"Lining things up…",
|
|
68
|
+
"Reading the room…",
|
|
69
|
+
"Scanning the sources…",
|
|
70
|
+
"Cross-checking facts…",
|
|
71
|
+
"Tracing the threads…",
|
|
72
|
+
"Weighing the options…",
|
|
73
|
+
"Following the trail…",
|
|
74
|
+
"Piecing it together…",
|
|
75
|
+
"Untangling the details…",
|
|
76
|
+
"Skimming the fine print…",
|
|
77
|
+
"Joining the dots…",
|
|
78
|
+
"Hunting for specifics…",
|
|
79
|
+
"Sorting signal from noise…",
|
|
80
|
+
"Drafting the answer…",
|
|
81
|
+
"Double-checking the work…",
|
|
82
|
+
"Wrapping my head around it…",
|
|
83
|
+
];
|
|
54
84
|
export const HOME_TIPS = [
|
|
55
|
-
"Type a question and press ⏎ to start a new
|
|
85
|
+
"Type a question and press ⏎ to start a new session.",
|
|
56
86
|
"Say \"deep research …\" or \"compare …\" to trigger the full research harness.",
|
|
57
|
-
"↑↓ navigate · ⏎ open · type to start a new
|
|
87
|
+
"↑↓ navigate · ⏎ open · type to start a new session.",
|
|
58
88
|
"/model · /provider · /key NAME value to configure.",
|
|
59
|
-
"Browse all sessions to find older
|
|
89
|
+
"Browse all sessions to find older ones.",
|
|
60
90
|
"ready / draft badges show full-research runs and their report state."
|
|
61
91
|
];
|
|
62
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";
|
|
@@ -26,7 +25,9 @@ export function useAgentTurn({ config, currentRunPath, queuedPromptRef, fullMode
|
|
|
26
25
|
const controller = new AbortController();
|
|
27
26
|
session.abort = controller;
|
|
28
27
|
setBusy(true);
|
|
29
|
-
|
|
28
|
+
// Pin the just-sent user message to the top of the viewport, leaving room
|
|
29
|
+
// below for the incoming assistant reply (-1 sentinel; see SciraApp).
|
|
30
|
+
setScrollOffset(-1);
|
|
30
31
|
sessionSetBusy(runPath, true);
|
|
31
32
|
const modelId = config.model;
|
|
32
33
|
const turnStartedAt = Date.now();
|
|
@@ -60,9 +61,19 @@ export function useAgentTurn({ config, currentRunPath, queuedPromptRef, fullMode
|
|
|
60
61
|
case "reasoning-end":
|
|
61
62
|
sessionFinishReasoning(runPath);
|
|
62
63
|
break;
|
|
63
|
-
case "tool-call":
|
|
64
|
-
|
|
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 });
|
|
65
75
|
break;
|
|
76
|
+
}
|
|
66
77
|
case "tool-result": {
|
|
67
78
|
if (part.preliminary)
|
|
68
79
|
break;
|
|
@@ -204,7 +215,7 @@ export function useAgentTurn({ config, currentRunPath, queuedPromptRef, fullMode
|
|
|
204
215
|
.filter((item) => !(item.kind === "status" && item.text === "This will wipe the conversation history. Press /rerun again to confirm."))
|
|
205
216
|
.map((item) => item.kind === "tool" && item.status === "running" ? { ...item, status: "error" } : item);
|
|
206
217
|
try {
|
|
207
|
-
await
|
|
218
|
+
await Bun.write(join(runPath, "convo.json"), JSON.stringify({ feed: snapshot, messages: conversationRef.current, usage: aggregateTurns(turnsRef.current) }, null, 2));
|
|
208
219
|
}
|
|
209
220
|
catch { /* non-fatal */ }
|
|
210
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);
|
|
@@ -63,6 +80,7 @@ reasoningTick, spinnerFrame, collapsedGroups, focusedGroupKey, itemExpandState,
|
|
|
63
80
|
const toggleAtLine = new Map();
|
|
64
81
|
const groupToggleAtLine = new Map();
|
|
65
82
|
const linkAtLine = new Map();
|
|
83
|
+
let lastUserLineStart = -1;
|
|
66
84
|
let key = 0;
|
|
67
85
|
const { groupOf, groups } = computeGroups(feed);
|
|
68
86
|
const eff = [];
|
|
@@ -154,21 +172,29 @@ reasoningTick, spinnerFrame, collapsedGroups, focusedGroupKey, itemExpandState,
|
|
|
154
172
|
const running = fi.status === "running";
|
|
155
173
|
const failed = fi.status === "error";
|
|
156
174
|
const itemId = feedToolItemId(feedIdx, fi.toolCallId);
|
|
157
|
-
|
|
158
|
-
|
|
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);
|
|
159
182
|
const headerLineIdx = lines.length;
|
|
160
183
|
const hovered = hoveredLineIdx === headerLineIdx;
|
|
161
184
|
const toolIcon = running
|
|
162
185
|
? SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length]
|
|
163
|
-
: TOOL_ICONS[
|
|
186
|
+
: TOOL_ICONS[canon] ?? "·";
|
|
164
187
|
const symColor = failed ? theme.error : theme.accentDim;
|
|
165
188
|
const nameColor = running ? theme.text : failed ? theme.error : theme.textDim;
|
|
166
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.
|
|
167
193
|
const preview = formatToolResultPreview(fi.name, fi.summary, fi.result, fi.status);
|
|
168
|
-
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);
|
|
169
195
|
if (collapsible)
|
|
170
196
|
toggleAtLine.set(headerLineIdx, itemId);
|
|
171
|
-
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++));
|
|
172
198
|
for (const row of bodyLines) {
|
|
173
199
|
if (row.length === 0) {
|
|
174
200
|
lines.push(_jsx(Text, { color: theme.textDim, children: S_BAR }, key++));
|
|
@@ -188,6 +214,7 @@ reasoningTick, spinnerFrame, collapsedGroups, focusedGroupKey, itemExpandState,
|
|
|
188
214
|
const rightPad = time ? time.length + 1 : 0;
|
|
189
215
|
const wrapped = wrapText(fi.text, Math.max(10, bandW - 4 - rightPad));
|
|
190
216
|
const blank = " ".repeat(bandW);
|
|
217
|
+
lastUserLineStart = lines.length;
|
|
191
218
|
lines.push(_jsx(Text, { ...bandBg, children: blank }, key++));
|
|
192
219
|
wrapped.forEach((l, idx) => {
|
|
193
220
|
const isFirst = idx === 0;
|
|
@@ -233,7 +260,7 @@ reasoningTick, spinnerFrame, collapsedGroups, focusedGroupKey, itemExpandState,
|
|
|
233
260
|
}
|
|
234
261
|
}
|
|
235
262
|
});
|
|
236
|
-
return { lines, toggleAtLine, groupToggleAtLine, linkAtLine };
|
|
263
|
+
return { lines, toggleAtLine, groupToggleAtLine, linkAtLine, lastUserLineStart };
|
|
237
264
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
238
265
|
}, [feed, innerWidth, reasoningTick, spinnerFrame, collapsedGroups, focusedGroupKey, itemExpandState, hoveredLineIdx, config, theme]);
|
|
239
266
|
}
|
|
@@ -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
|
}
|