cray-code 1.0.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.
- package/README.md +316 -0
- package/dist/Tool.d.ts +217 -0
- package/dist/Tool.d.ts.map +1 -0
- package/dist/Tool.js +89 -0
- package/dist/Tool.js.map +1 -0
- package/dist/branding/logo.d.ts +8 -0
- package/dist/branding/logo.d.ts.map +1 -0
- package/dist/branding/logo.js +26 -0
- package/dist/branding/logo.js.map +1 -0
- package/dist/branding/theme.d.ts +27 -0
- package/dist/branding/theme.d.ts.map +1 -0
- package/dist/branding/theme.js +28 -0
- package/dist/branding/theme.js.map +1 -0
- package/dist/commands/registry.d.ts +32 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +759 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/components/MessageView.d.ts +12 -0
- package/dist/components/MessageView.d.ts.map +1 -0
- package/dist/components/MessageView.js +35 -0
- package/dist/components/MessageView.js.map +1 -0
- package/dist/components/PermissionPrompt.d.ts +11 -0
- package/dist/components/PermissionPrompt.d.ts.map +1 -0
- package/dist/components/PermissionPrompt.js +6 -0
- package/dist/components/PermissionPrompt.js.map +1 -0
- package/dist/components/PluginManager.d.ts +27 -0
- package/dist/components/PluginManager.d.ts.map +1 -0
- package/dist/components/PluginManager.js +391 -0
- package/dist/components/PluginManager.js.map +1 -0
- package/dist/components/ThinkingBlock.d.ts +27 -0
- package/dist/components/ThinkingBlock.d.ts.map +1 -0
- package/dist/components/ThinkingBlock.js +29 -0
- package/dist/components/ThinkingBlock.js.map +1 -0
- package/dist/components/ToolCallBlock.d.ts +14 -0
- package/dist/components/ToolCallBlock.d.ts.map +1 -0
- package/dist/components/ToolCallBlock.js +83 -0
- package/dist/components/ToolCallBlock.js.map +1 -0
- package/dist/components/TrustDialog.d.ts +20 -0
- package/dist/components/TrustDialog.d.ts.map +1 -0
- package/dist/components/TrustDialog.js +80 -0
- package/dist/components/TrustDialog.js.map +1 -0
- package/dist/context.d.ts +25 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +268 -0
- package/dist/context.js.map +1 -0
- package/dist/cray.d.ts +114 -0
- package/dist/cray.d.ts.map +1 -0
- package/dist/cray.js +338 -0
- package/dist/cray.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +122 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/registry.d.ts +106 -0
- package/dist/plugins/registry.d.ts.map +1 -0
- package/dist/plugins/registry.js +695 -0
- package/dist/plugins/registry.js.map +1 -0
- package/dist/query.d.ts +31 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +637 -0
- package/dist/query.js.map +1 -0
- package/dist/queryStream.d.ts +36 -0
- package/dist/queryStream.d.ts.map +1 -0
- package/dist/queryStream.js +704 -0
- package/dist/queryStream.js.map +1 -0
- package/dist/screens/ReplScreen.d.ts +22 -0
- package/dist/screens/ReplScreen.d.ts.map +1 -0
- package/dist/screens/ReplScreen.js +763 -0
- package/dist/screens/ReplScreen.js.map +1 -0
- package/dist/services/agentRunner.d.ts +39 -0
- package/dist/services/agentRunner.d.ts.map +1 -0
- package/dist/services/agentRunner.js +115 -0
- package/dist/services/agentRunner.js.map +1 -0
- package/dist/services/compact.d.ts +34 -0
- package/dist/services/compact.d.ts.map +1 -0
- package/dist/services/compact.js +179 -0
- package/dist/services/compact.js.map +1 -0
- package/dist/services/loadPluginCommands.d.ts +55 -0
- package/dist/services/loadPluginCommands.d.ts.map +1 -0
- package/dist/services/loadPluginCommands.js +219 -0
- package/dist/services/loadPluginCommands.js.map +1 -0
- package/dist/services/mcp/index.d.ts +3 -0
- package/dist/services/mcp/index.d.ts.map +1 -0
- package/dist/services/mcp/index.js +3 -0
- package/dist/services/mcp/index.js.map +1 -0
- package/dist/services/mcp/manager.d.ts +24 -0
- package/dist/services/mcp/manager.d.ts.map +1 -0
- package/dist/services/mcp/manager.js +138 -0
- package/dist/services/mcp/manager.js.map +1 -0
- package/dist/services/mcp/types.d.ts +52 -0
- package/dist/services/mcp/types.d.ts.map +1 -0
- package/dist/services/mcp/types.js +5 -0
- package/dist/services/mcp/types.js.map +1 -0
- package/dist/services/memory.d.ts +38 -0
- package/dist/services/memory.d.ts.map +1 -0
- package/dist/services/memory.js +181 -0
- package/dist/services/memory.js.map +1 -0
- package/dist/services/permissionPrompt.d.ts +38 -0
- package/dist/services/permissionPrompt.d.ts.map +1 -0
- package/dist/services/permissionPrompt.js +83 -0
- package/dist/services/permissionPrompt.js.map +1 -0
- package/dist/services/permissions.d.ts +15 -0
- package/dist/services/permissions.d.ts.map +1 -0
- package/dist/services/permissions.js +237 -0
- package/dist/services/permissions.js.map +1 -0
- package/dist/services/sessionStorage.d.ts +51 -0
- package/dist/services/sessionStorage.d.ts.map +1 -0
- package/dist/services/sessionStorage.js +266 -0
- package/dist/services/sessionStorage.js.map +1 -0
- package/dist/setup.d.ts +22 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +160 -0
- package/dist/setup.js.map +1 -0
- package/dist/skills/bundledSkills.d.ts +18 -0
- package/dist/skills/bundledSkills.d.ts.map +1 -0
- package/dist/skills/bundledSkills.js +277 -0
- package/dist/skills/bundledSkills.js.map +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +3 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/loadSkillsDir.d.ts +45 -0
- package/dist/skills/loadSkillsDir.d.ts.map +1 -0
- package/dist/skills/loadSkillsDir.js +233 -0
- package/dist/skills/loadSkillsDir.js.map +1 -0
- package/dist/state/AppState.d.ts +70 -0
- package/dist/state/AppState.d.ts.map +1 -0
- package/dist/state/AppState.js +106 -0
- package/dist/state/AppState.js.map +1 -0
- package/dist/tools/AgentTool.d.ts +62 -0
- package/dist/tools/AgentTool.d.ts.map +1 -0
- package/dist/tools/AgentTool.js +133 -0
- package/dist/tools/AgentTool.js.map +1 -0
- package/dist/tools/AskUserQuestionTool.d.ts +60 -0
- package/dist/tools/AskUserQuestionTool.d.ts.map +1 -0
- package/dist/tools/AskUserQuestionTool.js +52 -0
- package/dist/tools/AskUserQuestionTool.js.map +1 -0
- package/dist/tools/BashTool.d.ts +33 -0
- package/dist/tools/BashTool.d.ts.map +1 -0
- package/dist/tools/BashTool.js +211 -0
- package/dist/tools/BashTool.js.map +1 -0
- package/dist/tools/EditTool.d.ts +24 -0
- package/dist/tools/EditTool.d.ts.map +1 -0
- package/dist/tools/EditTool.js +102 -0
- package/dist/tools/EditTool.js.map +1 -0
- package/dist/tools/GlobTool.d.ts +17 -0
- package/dist/tools/GlobTool.d.ts.map +1 -0
- package/dist/tools/GlobTool.js +65 -0
- package/dist/tools/GlobTool.js.map +1 -0
- package/dist/tools/GrepTool.d.ts +30 -0
- package/dist/tools/GrepTool.d.ts.map +1 -0
- package/dist/tools/GrepTool.js +140 -0
- package/dist/tools/GrepTool.js.map +1 -0
- package/dist/tools/MCPTool.d.ts +24 -0
- package/dist/tools/MCPTool.d.ts.map +1 -0
- package/dist/tools/MCPTool.js +67 -0
- package/dist/tools/MCPTool.js.map +1 -0
- package/dist/tools/NotebookEditTool.d.ts +28 -0
- package/dist/tools/NotebookEditTool.d.ts.map +1 -0
- package/dist/tools/NotebookEditTool.js +213 -0
- package/dist/tools/NotebookEditTool.js.map +1 -0
- package/dist/tools/NotebookReadTool.d.ts +19 -0
- package/dist/tools/NotebookReadTool.d.ts.map +1 -0
- package/dist/tools/NotebookReadTool.js +191 -0
- package/dist/tools/NotebookReadTool.js.map +1 -0
- package/dist/tools/PlanTools.d.ts +17 -0
- package/dist/tools/PlanTools.d.ts.map +1 -0
- package/dist/tools/PlanTools.js +65 -0
- package/dist/tools/PlanTools.js.map +1 -0
- package/dist/tools/ReadTool.d.ts +21 -0
- package/dist/tools/ReadTool.d.ts.map +1 -0
- package/dist/tools/ReadTool.js +202 -0
- package/dist/tools/ReadTool.js.map +1 -0
- package/dist/tools/SkillTool.d.ts +32 -0
- package/dist/tools/SkillTool.d.ts.map +1 -0
- package/dist/tools/SkillTool.js +217 -0
- package/dist/tools/SkillTool.js.map +1 -0
- package/dist/tools/TodoWriteTool.d.ts +35 -0
- package/dist/tools/TodoWriteTool.d.ts.map +1 -0
- package/dist/tools/TodoWriteTool.js +58 -0
- package/dist/tools/TodoWriteTool.js.map +1 -0
- package/dist/tools/WebFetchTool.d.ts +17 -0
- package/dist/tools/WebFetchTool.d.ts.map +1 -0
- package/dist/tools/WebFetchTool.js +97 -0
- package/dist/tools/WebFetchTool.js.map +1 -0
- package/dist/tools/WebSearchTool.d.ts +18 -0
- package/dist/tools/WebSearchTool.d.ts.map +1 -0
- package/dist/tools/WebSearchTool.js +76 -0
- package/dist/tools/WebSearchTool.js.map +1 -0
- package/dist/tools/WriteTool.d.ts +17 -0
- package/dist/tools/WriteTool.d.ts.map +1 -0
- package/dist/tools/WriteTool.js +84 -0
- package/dist/tools/WriteTool.js.map +1 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +20 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools.d.ts +34 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +102 -0
- package/dist/tools.js.map +1 -0
- package/dist/types/events.d.ts +85 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +12 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/message.d.ts +71 -0
- package/dist/types/message.d.ts.map +1 -0
- package/dist/types/message.js +5 -0
- package/dist/types/message.js.map +1 -0
- package/dist/types/permission.d.ts +56 -0
- package/dist/types/permission.d.ts.map +1 -0
- package/dist/types/permission.js +46 -0
- package/dist/types/permission.js.map +1 -0
- package/dist/types/processUserInput.d.ts +18 -0
- package/dist/types/processUserInput.d.ts.map +1 -0
- package/dist/types/processUserInput.js +8 -0
- package/dist/types/processUserInput.js.map +1 -0
- package/dist/types/tool.d.ts +32 -0
- package/dist/types/tool.d.ts.map +1 -0
- package/dist/types/tool.js +5 -0
- package/dist/types/tool.js.map +1 -0
- package/dist/utils/compactBoundary.d.ts +11 -0
- package/dist/utils/compactBoundary.d.ts.map +1 -0
- package/dist/utils/compactBoundary.js +26 -0
- package/dist/utils/compactBoundary.js.map +1 -0
- package/dist/utils/configStore.d.ts +41 -0
- package/dist/utils/configStore.d.ts.map +1 -0
- package/dist/utils/configStore.js +111 -0
- package/dist/utils/configStore.js.map +1 -0
- package/dist/utils/forkedAgent.d.ts +40 -0
- package/dist/utils/forkedAgent.d.ts.map +1 -0
- package/dist/utils/forkedAgent.js +231 -0
- package/dist/utils/forkedAgent.js.map +1 -0
- package/dist/utils/messages.d.ts +14 -0
- package/dist/utils/messages.d.ts.map +1 -0
- package/dist/utils/messages.js +29 -0
- package/dist/utils/messages.js.map +1 -0
- package/dist/utils/sandbox.d.ts +22 -0
- package/dist/utils/sandbox.d.ts.map +1 -0
- package/dist/utils/sandbox.js +59 -0
- package/dist/utils/sandbox.js.map +1 -0
- package/dist/utils/sideQuestion.d.ts +29 -0
- package/dist/utils/sideQuestion.d.ts.map +1 -0
- package/dist/utils/sideQuestion.js +81 -0
- package/dist/utils/sideQuestion.js.map +1 -0
- package/install.ps1 +86 -0
- package/install.sh +92 -0
- package/package.json +68 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ReplScreen — React/Ink REPL for Cray Code.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Streaming thinking display (collapsible "Thought for Xs" block)
|
|
7
|
+
* - Real-time tool call display with spinner
|
|
8
|
+
* - Up/Down arrow key history navigation
|
|
9
|
+
* - Clipboard paste support (text + image paths)
|
|
10
|
+
* - Escape to cancel, Ctrl+C to cancel or exit
|
|
11
|
+
*/
|
|
12
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
13
|
+
import { render, Text, Box, useInput, useApp, Newline } from 'ink';
|
|
14
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { homedir } from 'os';
|
|
17
|
+
import { ThinkingBlock } from '../components/ThinkingBlock.js';
|
|
18
|
+
import { PermissionPrompt } from '../components/PermissionPrompt.js';
|
|
19
|
+
import { CRAY_LOGO, CRAY_TAGLINE } from '../branding/logo.js';
|
|
20
|
+
import { getAllCommands } from '../commands/registry.js';
|
|
21
|
+
import { nextPermissionMode } from '../types/permission.js';
|
|
22
|
+
import { isBtwInput, extractBtwQuestion, runSideQuestion } from '../utils/sideQuestion.js';
|
|
23
|
+
import { queryStream, setPermissionResolver } from '../queryStream.js';
|
|
24
|
+
import { PluginManager } from '../components/PluginManager.js';
|
|
25
|
+
import { TrustDialog, isDirectoryTrusted, trustDirectory } from '../components/TrustDialog.js';
|
|
26
|
+
import { recordMessages, getCurrentSessionId } from '../services/sessionStorage.js';
|
|
27
|
+
/** Seconds counter for thinking display */
|
|
28
|
+
function useElapsedTimer(active) {
|
|
29
|
+
const [elapsed, setElapsed] = useState(0);
|
|
30
|
+
const ref = useRef(0);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!active) {
|
|
33
|
+
setElapsed(0);
|
|
34
|
+
ref.current = 0;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const iv = setInterval(() => { setElapsed(++ref.current); }, 1000);
|
|
38
|
+
return () => clearInterval(iv);
|
|
39
|
+
}, [active]);
|
|
40
|
+
return elapsed;
|
|
41
|
+
}
|
|
42
|
+
/** Persistent history across sessions — saved to ~/.cray/history.json */
|
|
43
|
+
function useHistory() {
|
|
44
|
+
const historyPath = join(homedir(), '.cray', 'history.json');
|
|
45
|
+
// Load existing
|
|
46
|
+
let saved = [];
|
|
47
|
+
if (existsSync(historyPath)) {
|
|
48
|
+
try {
|
|
49
|
+
saved = JSON.parse(readFileSync(historyPath, 'utf-8'));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
saved = [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const [history, setHistory] = useState(saved);
|
|
56
|
+
const [histIdx, setHistIdx] = useState(-1); // -1 = at bottom, typing fresh
|
|
57
|
+
const [draftBeforeNav, setDraftBeforeNav] = useState('');
|
|
58
|
+
const push = useCallback((entry) => {
|
|
59
|
+
if (!entry.trim())
|
|
60
|
+
return;
|
|
61
|
+
setHistory((prev) => {
|
|
62
|
+
const next = [entry, ...prev.filter((h) => h !== entry)].slice(0, 200);
|
|
63
|
+
// Persist
|
|
64
|
+
try {
|
|
65
|
+
mkdirSync(join(homedir(), '.cray'), { recursive: true });
|
|
66
|
+
writeFileSync(historyPath, JSON.stringify(next, null, 2), 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
catch { /* non-fatal */ }
|
|
69
|
+
return next;
|
|
70
|
+
});
|
|
71
|
+
}, []);
|
|
72
|
+
return { history, histIdx, draftBeforeNav, push, setHistIdx, setDraftBeforeNav };
|
|
73
|
+
}
|
|
74
|
+
const ReplScreen = ({ cray, initialPrompt }) => {
|
|
75
|
+
const [messages, setMessages] = useState([]);
|
|
76
|
+
const [loading, setLoading] = useState(false);
|
|
77
|
+
const [cancelling, setCancelling] = useState(false);
|
|
78
|
+
const [error, setError] = useState(null);
|
|
79
|
+
const [inputValue, setInputValue] = useState('');
|
|
80
|
+
const [turns, setTurns] = useState(0);
|
|
81
|
+
const [showWelcome, setShowWelcome] = useState(true);
|
|
82
|
+
// ─── History ──────────────────────────────────────────────────────
|
|
83
|
+
const { history, histIdx, draftBeforeNav, push, setHistIdx, setDraftBeforeNav } = useHistory();
|
|
84
|
+
// ─── Permission state ──────────────────────────────────────────────
|
|
85
|
+
const [permissionMode, setPermissionMode] = useState('default');
|
|
86
|
+
const [askState, setAskState] = useState(null);
|
|
87
|
+
// Ref to avoid stale-closure issues during heavy re-rendering (streaming)
|
|
88
|
+
const askStateRef = useRef(askState);
|
|
89
|
+
const loadingRef = useRef(false);
|
|
90
|
+
askStateRef.current = askState;
|
|
91
|
+
// Live streaming state
|
|
92
|
+
const [thinkingText, setThinkingText] = useState('');
|
|
93
|
+
const [thinkingFirstLine, setThinkingFirstLine] = useState('');
|
|
94
|
+
const [thinkingActive, setThinkingActive] = useState(false);
|
|
95
|
+
const [thinkingDone, setThinkingDone] = useState(false);
|
|
96
|
+
const [thinkingFinalElapsed, setThinkingFinalElapsed] = useState(0);
|
|
97
|
+
const [liveText, setLiveText] = useState('');
|
|
98
|
+
const [liveTools, setLiveTools] = useState([]);
|
|
99
|
+
// ─── Trust dialog at startup ────────────────────────────────────
|
|
100
|
+
const cwd = cray.toolContext?.cwd ?? process.cwd();
|
|
101
|
+
const [trustConfirmed, setTrustConfirmed] = useState(false);
|
|
102
|
+
const [trustChecking, setTrustChecking] = useState(true);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
// Check after mount so cwd is definitely available
|
|
105
|
+
if (isDirectoryTrusted(cwd)) {
|
|
106
|
+
setTrustConfirmed(true);
|
|
107
|
+
}
|
|
108
|
+
setTrustChecking(false);
|
|
109
|
+
}, [cwd]);
|
|
110
|
+
// ─── Plugin manager overlay ────────────────────────────────────
|
|
111
|
+
const [showPluginManager, setShowPluginManager] = useState(false);
|
|
112
|
+
// ─── Side question (/btw) state ──────────────────────────────────
|
|
113
|
+
const [sideQActive, setSideQActive] = useState(false);
|
|
114
|
+
const [sideQResponse, setSideQResponse] = useState(null);
|
|
115
|
+
const [sideQError, setSideQError] = useState(null);
|
|
116
|
+
const [sideQQuestion, setSideQQuestion] = useState('');
|
|
117
|
+
const sideQAbortRef = useRef(null);
|
|
118
|
+
const elapsed = useElapsedTimer(thinkingActive);
|
|
119
|
+
const abortRef = useRef(null);
|
|
120
|
+
const thinkingFirstRef = useRef('');
|
|
121
|
+
const messagesRef = useRef([]);
|
|
122
|
+
messagesRef.current = messages;
|
|
123
|
+
const { exit } = useApp();
|
|
124
|
+
const freezeThinking = useCallback(() => {
|
|
125
|
+
if (thinkingText) {
|
|
126
|
+
setThinkingDone(true);
|
|
127
|
+
setThinkingActive(false);
|
|
128
|
+
setThinkingFinalElapsed(elapsed);
|
|
129
|
+
}
|
|
130
|
+
}, [thinkingText, elapsed]);
|
|
131
|
+
const handleCancel = useCallback(() => {
|
|
132
|
+
if (!loadingRef.current || cancelling)
|
|
133
|
+
return;
|
|
134
|
+
setCancelling(true);
|
|
135
|
+
abortRef.current?.abort();
|
|
136
|
+
}, [cancelling]);
|
|
137
|
+
// ─── Side question (/btw) submission ─────────────────────────────
|
|
138
|
+
const submitSideQuestion = useCallback(async (question) => {
|
|
139
|
+
if (!question.trim() || sideQActive)
|
|
140
|
+
return;
|
|
141
|
+
setSideQActive(true);
|
|
142
|
+
setSideQResponse(null);
|
|
143
|
+
setSideQError(null);
|
|
144
|
+
setSideQQuestion(question);
|
|
145
|
+
const ctx = cray.toolContext;
|
|
146
|
+
if (!ctx) {
|
|
147
|
+
setSideQError('Context not initialized');
|
|
148
|
+
setSideQActive(false);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Build cache-safe params from current context
|
|
152
|
+
const systemPrompt = ctx.systemPromptParts?.join('\n') ?? '';
|
|
153
|
+
const userContext = `Working directory: ${ctx.cwd}\nDate: ${new Date().toISOString().split('T')[0]}`;
|
|
154
|
+
const systemContext = `Platform: ${process.platform}`;
|
|
155
|
+
const cacheSafeParams = {
|
|
156
|
+
systemPrompt,
|
|
157
|
+
userContext,
|
|
158
|
+
systemContext,
|
|
159
|
+
toolUseContext: ctx,
|
|
160
|
+
forkContextMessages: [...(ctx.messages ?? [])],
|
|
161
|
+
};
|
|
162
|
+
try {
|
|
163
|
+
const result = await runSideQuestion({
|
|
164
|
+
question,
|
|
165
|
+
cacheSafeParams,
|
|
166
|
+
});
|
|
167
|
+
setSideQResponse(result.response ?? '(No response received)');
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
setSideQError(err.message || 'Side question failed');
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
setSideQActive(false);
|
|
174
|
+
}
|
|
175
|
+
}, [cray, sideQActive]);
|
|
176
|
+
// Dismiss side question result
|
|
177
|
+
const dismissSideQuestion = useCallback(() => {
|
|
178
|
+
setSideQResponse(null);
|
|
179
|
+
setSideQError(null);
|
|
180
|
+
setSideQQuestion('');
|
|
181
|
+
}, []);
|
|
182
|
+
const sendMessage = useCallback(async (userInput) => {
|
|
183
|
+
if (loadingRef.current || !userInput.trim())
|
|
184
|
+
return;
|
|
185
|
+
push(userInput);
|
|
186
|
+
setShowWelcome(false);
|
|
187
|
+
loadingRef.current = true;
|
|
188
|
+
thinkingFirstRef.current = '';
|
|
189
|
+
setLoading(true);
|
|
190
|
+
setCancelling(false);
|
|
191
|
+
setError(null);
|
|
192
|
+
setThinkingText('');
|
|
193
|
+
setThinkingFirstLine('');
|
|
194
|
+
setThinkingActive(false);
|
|
195
|
+
setThinkingDone(false);
|
|
196
|
+
setLiveText('');
|
|
197
|
+
setLiveTools([]);
|
|
198
|
+
const userMsg = { role: 'user', id: `u-${Date.now()}`, timestamp: Date.now(), content: userInput };
|
|
199
|
+
setMessages((prev) => [...prev, userMsg]);
|
|
200
|
+
// Persist user message immediately
|
|
201
|
+
const sId = getCurrentSessionId();
|
|
202
|
+
if (sId) {
|
|
203
|
+
recordMessages(cray.toolContext?.cwd ?? process.cwd(), sId, [userMsg]);
|
|
204
|
+
}
|
|
205
|
+
const ctx = cray.toolContext;
|
|
206
|
+
if (!ctx) {
|
|
207
|
+
setError('Context not initialized');
|
|
208
|
+
setLoading(false);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Set context.messages to ALL previous history (without the current userMsg,
|
|
212
|
+
// which historyToConversation will append as the prompt).
|
|
213
|
+
ctx.messages = [...messagesRef.current];
|
|
214
|
+
ctx.permissionContext.mode = permissionMode;
|
|
215
|
+
const ac = new AbortController();
|
|
216
|
+
abortRef.current = ac;
|
|
217
|
+
ctx.abortSignal = ac.signal;
|
|
218
|
+
// Wire up permission resolver (called from inside queryStream)
|
|
219
|
+
setPermissionResolver(async (toolName, message) => {
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
setAskState({ toolName, message, resolve });
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
// Streaming callbacks (real-time text/thinking)
|
|
225
|
+
const cbs = {
|
|
226
|
+
onThinking(text, _turn) {
|
|
227
|
+
if (ac.signal.aborted)
|
|
228
|
+
return;
|
|
229
|
+
setThinkingActive(true);
|
|
230
|
+
setThinkingDone(false);
|
|
231
|
+
setThinkingText((p) => {
|
|
232
|
+
const next = p + text;
|
|
233
|
+
if (!thinkingFirstRef.current) {
|
|
234
|
+
const first = next.split('\n').find((l) => l.trim());
|
|
235
|
+
if (first) {
|
|
236
|
+
thinkingFirstRef.current = first.trim();
|
|
237
|
+
setThinkingFirstLine(first.trim());
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return next;
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
onText(text, _turn) {
|
|
244
|
+
if (ac.signal.aborted)
|
|
245
|
+
return;
|
|
246
|
+
freezeThinking();
|
|
247
|
+
setLiveText((p) => p + text);
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
try {
|
|
251
|
+
// ─── Async generator: for-await event loop (Claude Code pattern) ──
|
|
252
|
+
for await (const event of queryStream({ prompt: userInput, context: ctx, maxTurns: 25 }, cbs)) {
|
|
253
|
+
if (ac.signal.aborted)
|
|
254
|
+
break;
|
|
255
|
+
onQueryEvent(event);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
if (err.name === 'AbortError') {
|
|
260
|
+
setLiveTools((prev) => prev.map((t) => t.status === 'running' ? { ...t, status: 'cancelled' } : t));
|
|
261
|
+
setError('Operation cancelled.');
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
setError(err.message);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
freezeThinking();
|
|
269
|
+
loadingRef.current = false;
|
|
270
|
+
setLoading(false);
|
|
271
|
+
setCancelling(false);
|
|
272
|
+
setThinkingText('');
|
|
273
|
+
setThinkingFirstLine('');
|
|
274
|
+
setThinkingActive(false);
|
|
275
|
+
setThinkingDone(false);
|
|
276
|
+
setLiveText('');
|
|
277
|
+
setLiveTools([]);
|
|
278
|
+
abortRef.current = null;
|
|
279
|
+
setPermissionResolver(null);
|
|
280
|
+
}
|
|
281
|
+
}, [cray, freezeThinking, push, permissionMode]);
|
|
282
|
+
// ─── onQueryEvent: dispatch typed events to React state (Claude Code pattern) ──
|
|
283
|
+
const onQueryEvent = useCallback((event) => {
|
|
284
|
+
switch (event.type) {
|
|
285
|
+
case 'thinking_done':
|
|
286
|
+
freezeThinking();
|
|
287
|
+
break;
|
|
288
|
+
case 'tool_start':
|
|
289
|
+
freezeThinking();
|
|
290
|
+
setLiveTools((prev) => [...prev, {
|
|
291
|
+
id: event.toolId,
|
|
292
|
+
name: event.toolName,
|
|
293
|
+
input: event.input,
|
|
294
|
+
status: 'running',
|
|
295
|
+
}]);
|
|
296
|
+
break;
|
|
297
|
+
case 'tool_end':
|
|
298
|
+
setLiveTools((prev) => prev.map((t) => t.id === event.toolId && t.status === 'running'
|
|
299
|
+
? { ...t, status: event.isError ? 'error' : 'done', result: event.result }
|
|
300
|
+
: t));
|
|
301
|
+
break;
|
|
302
|
+
case 'turn_end':
|
|
303
|
+
setTurns(event.turn);
|
|
304
|
+
break;
|
|
305
|
+
case 'request_start':
|
|
306
|
+
break;
|
|
307
|
+
case 'error':
|
|
308
|
+
setError(event.message);
|
|
309
|
+
break;
|
|
310
|
+
case 'cancelled':
|
|
311
|
+
setLiveTools((prev) => prev.map((t) => t.status === 'running' ? { ...t, status: 'cancelled' } : t));
|
|
312
|
+
break;
|
|
313
|
+
case 'done': {
|
|
314
|
+
const newMsgs = event.messages;
|
|
315
|
+
setMessages((prev) => {
|
|
316
|
+
const ids = new Set(prev.map((m) => m.id));
|
|
317
|
+
return [...prev, ...newMsgs.filter((m) => !ids.has(m.id))];
|
|
318
|
+
});
|
|
319
|
+
setTurns(event.totalTurns);
|
|
320
|
+
// ─── Update context.messages for next turn's context persistence ───
|
|
321
|
+
if (newMsgs.length > 0) {
|
|
322
|
+
const ctx = cray.toolContext;
|
|
323
|
+
if (ctx) {
|
|
324
|
+
const existingIds = new Set(ctx.messages.map((m) => m.id));
|
|
325
|
+
ctx.messages.push(...newMsgs.filter((m) => !existingIds.has(m.id)));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// ─── Persist new messages to JSONL session file ───
|
|
329
|
+
const sessionId = getCurrentSessionId();
|
|
330
|
+
if (sessionId && newMsgs.length > 0) {
|
|
331
|
+
const cwd = cray.toolContext?.cwd ?? process.cwd();
|
|
332
|
+
recordMessages(cwd, sessionId, newMsgs);
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
default:
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}, [freezeThinking, cray]);
|
|
340
|
+
// ─── Keyboard Input ──────────────────────────────────────────────
|
|
341
|
+
useInput((input, key) => {
|
|
342
|
+
// Don't process main-screen input while trust is pending
|
|
343
|
+
if (!trustConfirmed)
|
|
344
|
+
return;
|
|
345
|
+
// Plugin manager overlay takes all interaction
|
|
346
|
+
if (showPluginManager)
|
|
347
|
+
return;
|
|
348
|
+
const curAsk = askStateRef.current;
|
|
349
|
+
// ─── Permission prompt active — intercept Y/A/N/Esc ─────────────
|
|
350
|
+
if (curAsk) {
|
|
351
|
+
if (key.escape) {
|
|
352
|
+
const r = curAsk.resolve;
|
|
353
|
+
setAskState(null);
|
|
354
|
+
r('deny');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const ch = input.toLowerCase();
|
|
358
|
+
if (ch === 'y') {
|
|
359
|
+
const r = curAsk.resolve;
|
|
360
|
+
setAskState(null);
|
|
361
|
+
r('allow');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (ch === 'a') {
|
|
365
|
+
const r = curAsk.resolve;
|
|
366
|
+
setAskState(null);
|
|
367
|
+
r('allowAll');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (ch === 'n') {
|
|
371
|
+
const r = curAsk.resolve;
|
|
372
|
+
setAskState(null);
|
|
373
|
+
r('deny');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
return; // swallow all other input while prompt is up
|
|
377
|
+
}
|
|
378
|
+
const loading = loadingRef.current;
|
|
379
|
+
// ─── Always-available keys ────────────────────────────────────
|
|
380
|
+
// Escape — cancel during loading, clear input otherwise
|
|
381
|
+
if (key.escape) {
|
|
382
|
+
if (loading) {
|
|
383
|
+
handleCancel();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
setInputValue('');
|
|
387
|
+
setHistIdx(-1);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
// Ctrl+C — cancel during loading, exit otherwise
|
|
391
|
+
if (input === '\x03') {
|
|
392
|
+
if (loading) {
|
|
393
|
+
handleCancel();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
cray.shutdown().then(() => exit());
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
// Shift+Tab — cycle permission mode
|
|
400
|
+
if (key.shift && key.tab) {
|
|
401
|
+
const next = nextPermissionMode(permissionMode);
|
|
402
|
+
setPermissionMode(next);
|
|
403
|
+
const ctx = cray.toolContext;
|
|
404
|
+
if (ctx)
|
|
405
|
+
ctx.permissionContext.mode = next;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
// ─── Up/Down arrow — history navigation (works even during loading) ──
|
|
409
|
+
if (key.upArrow) {
|
|
410
|
+
if (history.length === 0)
|
|
411
|
+
return;
|
|
412
|
+
if (histIdx === -1)
|
|
413
|
+
setDraftBeforeNav(inputValue);
|
|
414
|
+
const nextIdx = Math.min(histIdx + 1, history.length - 1);
|
|
415
|
+
setHistIdx(nextIdx);
|
|
416
|
+
setInputValue(history[nextIdx] ?? '');
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (key.downArrow) {
|
|
420
|
+
const nextIdx = histIdx - 1;
|
|
421
|
+
if (nextIdx < -1)
|
|
422
|
+
return;
|
|
423
|
+
setHistIdx(nextIdx);
|
|
424
|
+
setInputValue(nextIdx === -1 ? draftBeforeNav : (history[nextIdx] ?? ''));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
// ─── During loading, allow /btw input and Esc to dismiss side Q ──
|
|
428
|
+
if (loading) {
|
|
429
|
+
// Esc — during loading with side Q visible, dismiss it
|
|
430
|
+
if (key.escape) {
|
|
431
|
+
if (sideQResponse || sideQError) {
|
|
432
|
+
dismissSideQuestion();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
handleCancel();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
// Enter — during loading, only /btw is allowed
|
|
439
|
+
if (key.return) {
|
|
440
|
+
const val = inputValue.trim();
|
|
441
|
+
if (isBtwInput(val)) {
|
|
442
|
+
const question = extractBtwQuestion(val);
|
|
443
|
+
if (question) {
|
|
444
|
+
push(val);
|
|
445
|
+
setInputValue('');
|
|
446
|
+
setHistIdx(-1);
|
|
447
|
+
submitSideQuestion(question);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// Backspace / Delete during loading
|
|
453
|
+
if (key.backspace || key.delete) {
|
|
454
|
+
setInputValue((p) => p.slice(0, -1));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// Normal text input during loading — only allow /-prefixed input
|
|
458
|
+
if (input && !key.ctrl && !key.meta) {
|
|
459
|
+
const candidate = inputValue + input;
|
|
460
|
+
// Only accept input that could be /btw or similar
|
|
461
|
+
if (candidate.startsWith('/')) {
|
|
462
|
+
setInputValue(candidate);
|
|
463
|
+
}
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// ─── Tab — autocomplete command names ───────────────────────────
|
|
469
|
+
if (key.tab) {
|
|
470
|
+
const val = inputValue;
|
|
471
|
+
if (val.startsWith('/')) {
|
|
472
|
+
const partial = val.replace(/^\//, '').split(/\s+/)[0];
|
|
473
|
+
const matches = getAllCommands().filter((c) => c.name.startsWith(partial) && c.userInvocable);
|
|
474
|
+
if (matches.length === 1) {
|
|
475
|
+
setInputValue(`/${matches[0].name} `);
|
|
476
|
+
}
|
|
477
|
+
else if (matches.length > 1) {
|
|
478
|
+
const common = matches.reduce((prefix, m) => {
|
|
479
|
+
while (prefix && !m.name.startsWith(prefix))
|
|
480
|
+
prefix = prefix.slice(0, -1);
|
|
481
|
+
return prefix;
|
|
482
|
+
}, partial);
|
|
483
|
+
if (common && common !== partial)
|
|
484
|
+
setInputValue(`/${common}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
// ─── Enter — submit ────────────────────────────────────────────
|
|
490
|
+
if (key.return) {
|
|
491
|
+
const val = inputValue.trim();
|
|
492
|
+
if (!val)
|
|
493
|
+
return;
|
|
494
|
+
// ─── /compact: run real compaction on the live message store ──
|
|
495
|
+
if (val === '/compact' || val.startsWith('/compact ')) {
|
|
496
|
+
const keepArg = val.split(/\s+/)[1];
|
|
497
|
+
const keepTurns = keepArg ? parseInt(keepArg, 10) || 3 : 3;
|
|
498
|
+
const result = cray.compact({ keepLastTurns: keepTurns });
|
|
499
|
+
setMessages(result.messages);
|
|
500
|
+
const msg = `Context compacted: ${result.removedCount} messages summarized → ${result.keptCount} kept (saved ~${result.estimatedTokensSaved} tokens). Keeping last ${keepTurns} turns intact.`;
|
|
501
|
+
setMessages((p) => [...p, { role: 'system', id: `sys-${Date.now()}`, timestamp: Date.now(), content: msg }]);
|
|
502
|
+
setInputValue('');
|
|
503
|
+
setHistIdx(-1);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
// ─── /btw — side question (agent is not running) ──
|
|
507
|
+
if (isBtwInput(val)) {
|
|
508
|
+
const question = extractBtwQuestion(val);
|
|
509
|
+
if (!question) {
|
|
510
|
+
setMessages((p) => [...p, { role: 'system', id: `sys-${Date.now()}`, timestamp: Date.now(), content: 'Usage: /btw <question> — ask a side question without interrupting the conversation.' }]);
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
// When agent is idle, /btw runs immediately and shows answer inline
|
|
514
|
+
push(val);
|
|
515
|
+
submitSideQuestion(question);
|
|
516
|
+
}
|
|
517
|
+
setInputValue('');
|
|
518
|
+
setHistIdx(-1);
|
|
519
|
+
setShowWelcome(false);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// ─── /plugin /plugins /marketplace — open interactive manager ──
|
|
523
|
+
if (val === '/plugin' || val === '/plugins' || val === '/marketplace') {
|
|
524
|
+
setInputValue('');
|
|
525
|
+
setShowWelcome(false);
|
|
526
|
+
setShowPluginManager(true);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
// Slash command
|
|
530
|
+
if (val.startsWith('/')) {
|
|
531
|
+
// /plugin subcommands — still go through command execution
|
|
532
|
+
if (val.startsWith('/plugin ')) {
|
|
533
|
+
cray.executeCommand(val).then((r) => {
|
|
534
|
+
setInputValue('');
|
|
535
|
+
setShowWelcome(false);
|
|
536
|
+
if (r.message) {
|
|
537
|
+
setMessages((p) => [...p, { role: 'system', id: `sys-${Date.now()}`, timestamp: Date.now(), content: r.message }]);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
cray.executeCommand(val).then((r) => {
|
|
543
|
+
if (val === '/exit' || val === '/quit' || val === '/q')
|
|
544
|
+
cray.shutdown().then(() => exit());
|
|
545
|
+
setInputValue('');
|
|
546
|
+
setShowWelcome(false);
|
|
547
|
+
if (r.message) {
|
|
548
|
+
setMessages((p) => [...p, { role: 'system', id: `sys-${Date.now()}`, timestamp: Date.now(), content: r.message }]);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
setInputValue('');
|
|
552
|
+
setHistIdx(-1);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
sendMessage(val);
|
|
556
|
+
setInputValue('');
|
|
557
|
+
setHistIdx(-1);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
// ─── Backspace / Delete ─────────────────────────────────────────
|
|
561
|
+
if (key.backspace || key.delete) {
|
|
562
|
+
setInputValue((p) => p.slice(0, -1));
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
// ─── Normal text input (includes pasted text) ───────────────────
|
|
566
|
+
// Ink sends clipboard paste as a single `input` containing all chars.
|
|
567
|
+
// Multi-line pastes are flattened. Image pastes come as base64 strings
|
|
568
|
+
// with OSC sequences — we detect and handle those.
|
|
569
|
+
if (input && !key.ctrl && !key.meta) {
|
|
570
|
+
// Detect image paste (base64 data URI or OSC-52 clipboard)
|
|
571
|
+
if (input.startsWith('data:image/') || input.includes('\x1b]52;')) {
|
|
572
|
+
const imgPath = extractPastedImage(input);
|
|
573
|
+
if (imgPath) {
|
|
574
|
+
setInputValue((p) => p + ` ${imgPath} `);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// Normal text paste: append all at once
|
|
579
|
+
setInputValue((p) => p + input);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
// Auto-send initial prompt
|
|
583
|
+
useEffect(() => {
|
|
584
|
+
if (initialPrompt && !loading && messages.length === 0) {
|
|
585
|
+
const t = setTimeout(() => sendMessage(initialPrompt), 100);
|
|
586
|
+
return () => clearTimeout(t);
|
|
587
|
+
}
|
|
588
|
+
}, [initialPrompt]);
|
|
589
|
+
// ─── Render ──────────────────────────────────────────────────────
|
|
590
|
+
const thinkingBlock = (thinkingText || thinkingDone) ? (_jsx(ThinkingBlock, { lastLine: thinkingText, firstLine: thinkingFirstLine, elapsed: elapsed, started: thinkingActive || thinkingDone, finished: thinkingDone, completeElapsed: thinkingFinalElapsed })) : null;
|
|
591
|
+
// ─── Trust guard: render ONLY the dialog until confirmed ──────
|
|
592
|
+
if (!trustConfirmed) {
|
|
593
|
+
// Brief loading state while checking disk
|
|
594
|
+
if (trustChecking) {
|
|
595
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { color: "#6C5CE7", children: CRAY_LOGO }), _jsxs(Text, { bold: true, color: "#A29BFE", children: [" ", CRAY_TAGLINE] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: " Initializing..." }) })] }));
|
|
596
|
+
}
|
|
597
|
+
return (_jsx(TrustDialog, { cwd: cwd, onAccept: () => {
|
|
598
|
+
trustDirectory(cwd);
|
|
599
|
+
setTrustConfirmed(true);
|
|
600
|
+
}, onReject: () => {
|
|
601
|
+
cray.shutdown().then(() => exit());
|
|
602
|
+
} }));
|
|
603
|
+
}
|
|
604
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [showPluginManager && (_jsx(Box, { flexDirection: "column", children: _jsx(PluginManager, { onDone: () => setShowPluginManager(false) }) })), (sideQResponse || sideQError) && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "single", borderColor: "#FDCB6E", paddingLeft: 1, paddingRight: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "#FDCB6E", children: "/btw " }), _jsx(Text, { dimColor: true, children: sideQQuestion })] }), sideQError ? (_jsx(Box, { children: _jsxs(Text, { color: "#E17055", children: ["Error: ", sideQError] }) })) : (_jsx(Box, { children: _jsx(Text, { color: "#00B894", children: sideQResponse }) })), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Esc to dismiss" }) })] })), sideQActive && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "#FDCB6E", children: [" /btw", sideQQuestion ? ` ${sideQQuestion}` : '', " \u2014 answering..."] }) })), showWelcome && messages.length === 0 && !loading && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: "#6C5CE7", children: CRAY_LOGO }), _jsxs(Text, { bold: true, color: "#A29BFE", children: [" ", CRAY_TAGLINE] }), _jsx(Newline, {}), _jsx(Text, { dimColor: true, children: " \u2191\u2193 history | Tab complete | Ctrl+V paste | Esc cancel" }), _jsx(Newline, {})] })), messages.map((msg) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [msg.role === 'user' && (_jsx(Box, { children: _jsx(Text, { bold: true, color: "#A29BFE", children: typeof msg.content === 'string'
|
|
605
|
+
? msg.content
|
|
606
|
+
: msg.content.filter((c) => c.type === 'text').map((c) => c.text).join('') }) })), msg.role === 'assistant' &&
|
|
607
|
+
renderAssistantContent(msg.content), msg.role === 'system' && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: typeof msg.content === 'string' ? msg.content : '[system]' }) }))] }, msg.id))), thinkingBlock, liveText && _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: liveText }) }), loading && liveTools.length > 0 && !liveText && !thinkingActive && !thinkingDone && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: " ..." }) })), cancelling && _jsx(Box, { marginY: 1, children: _jsx(Text, { color: "#FDCB6E", children: "Cancelling..." }) }), askState && (_jsx(PermissionPrompt, { toolName: askState.toolName, message: askState.message })), error && _jsx(Box, { marginY: 1, children: _jsxs(Text, { color: "#E17055", bold: true, children: ["Error: ", error] }) }), _jsx(Newline, {}), loading && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: cancelling ? ' Cancelling...' : ' Processing... (Esc to cancel, /btw to ask side Q)' }) })), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '─'.repeat(80) }) }), _jsxs(Box, { children: [_jsx(Text, { color: "#6C5CE7", bold: true, children: "Cray> " }), _jsx(Text, { children: inputValue }), _jsx(Text, { children: '█' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '─'.repeat(80) }) })] }));
|
|
608
|
+
};
|
|
609
|
+
// ─── Assistant content renderer (Claude Code-style concise output) ─────
|
|
610
|
+
/** Tools worth showing in output — the rest are internal housekeeping */
|
|
611
|
+
const VISIBLE_TOOLS = new Set([
|
|
612
|
+
'read', 'view', 'cat',
|
|
613
|
+
'write', 'create',
|
|
614
|
+
'edit', 'modify', 'replace',
|
|
615
|
+
'bash', 'shell', 'exec',
|
|
616
|
+
'glob', 'ls', 'find',
|
|
617
|
+
'grep', 'search', 'rg',
|
|
618
|
+
'web_fetch', 'web_search',
|
|
619
|
+
'notebook_read', 'nb_read', 'read_notebook',
|
|
620
|
+
'notebook_edit', 'nb_edit', 'edit_notebook',
|
|
621
|
+
]);
|
|
622
|
+
function renderAssistantContent(content) {
|
|
623
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
624
|
+
const nodes = [];
|
|
625
|
+
// Pair tool_use with following tool_result, collect standalone blocks
|
|
626
|
+
let i = 0;
|
|
627
|
+
while (i < blocks.length) {
|
|
628
|
+
const b = blocks[i];
|
|
629
|
+
if (b.type === 'thinking') {
|
|
630
|
+
nodes.push(_jsx(ThinkingBlock, { lastLine: b.thinking ?? '', firstLine: (b.thinking ?? '').split('\n').find((l) => l.trim())?.trim() ?? '', elapsed: 0, started: true, finished: true, completeElapsed: 0 }, `t-${i}`));
|
|
631
|
+
i++;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (b.type === 'text') {
|
|
635
|
+
nodes.push(_jsx(Box, { marginTop: nodes.length === 0 ? 0 : 1, children: _jsx(Text, { children: b.text }) }, `x-${i}`));
|
|
636
|
+
i++;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (b.type === 'tool_use') {
|
|
640
|
+
const next = blocks[i + 1];
|
|
641
|
+
const hasResult = next?.type === 'tool_result';
|
|
642
|
+
// Only show visible tools
|
|
643
|
+
if (VISIBLE_TOOLS.has(b.name)) {
|
|
644
|
+
const label = toolOneLiner(b.name, b.input, hasResult ? String(next.content ?? '') : undefined, next?.is_error);
|
|
645
|
+
const isFileOp = ['read', 'write', 'edit', 'view', 'cat', 'create', 'modify', 'replace'].includes(b.name);
|
|
646
|
+
nodes.push(isFileOp
|
|
647
|
+
? _jsx(Box, { borderStyle: "round", borderColor: "#636E72", paddingLeft: 1, paddingRight: 1, children: _jsx(Text, { color: "#00B894", children: label }) }, `tu-${i}`)
|
|
648
|
+
: _jsx(Box, { children: _jsxs(Text, { color: "#00B894", children: [" ", label] }) }, `tu-${i}`));
|
|
649
|
+
}
|
|
650
|
+
i += hasResult ? 2 : 1; // skip the tool_result we consumed
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
// Standalone tool_result (shouldn't normally happen)
|
|
654
|
+
if (b.type === 'tool_result') {
|
|
655
|
+
i++;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
i++;
|
|
659
|
+
}
|
|
660
|
+
return nodes;
|
|
661
|
+
}
|
|
662
|
+
function toolOneLiner(name, input, result, isError) {
|
|
663
|
+
switch (name) {
|
|
664
|
+
case 'read':
|
|
665
|
+
case 'view':
|
|
666
|
+
case 'cat': {
|
|
667
|
+
const fp = String(input.file_path ?? '');
|
|
668
|
+
const fn = fp.split(/[/\\]/).pop() || fp;
|
|
669
|
+
const lines = result ? result.split('\n').length : 0;
|
|
670
|
+
return `Read ${fn}${lines ? ` (${lines} lines)` : ''}`;
|
|
671
|
+
}
|
|
672
|
+
case 'write':
|
|
673
|
+
case 'create': {
|
|
674
|
+
const fp = String(input.file_path ?? '');
|
|
675
|
+
const fn = fp.split(/[/\\]/).pop() || fp;
|
|
676
|
+
return `Write ${fn}`;
|
|
677
|
+
}
|
|
678
|
+
case 'edit':
|
|
679
|
+
case 'modify':
|
|
680
|
+
case 'replace': {
|
|
681
|
+
const fp = String(input.file_path ?? '');
|
|
682
|
+
const fn = fp.split(/[/\\]/).pop() || fp;
|
|
683
|
+
const old = String(input.old_string ?? '').replace(/\n/g, '↵').slice(0, 60);
|
|
684
|
+
return `Edit ${fn} — "${old}"`;
|
|
685
|
+
}
|
|
686
|
+
case 'bash':
|
|
687
|
+
case 'shell':
|
|
688
|
+
case 'exec': {
|
|
689
|
+
const cmd = String(input.command ?? '').replace(/\n/g, '; ').slice(0, 80);
|
|
690
|
+
const status = isError ? ' (failed)' : '';
|
|
691
|
+
return `$ ${cmd}${status}`;
|
|
692
|
+
}
|
|
693
|
+
case 'glob':
|
|
694
|
+
case 'ls':
|
|
695
|
+
case 'find': {
|
|
696
|
+
const p = String(input.pattern ?? '');
|
|
697
|
+
const count = result ? result.split('\n').filter(Boolean).length : 0;
|
|
698
|
+
return `Glob ${p}${count ? ` → ${count} files` : ''}`;
|
|
699
|
+
}
|
|
700
|
+
case 'grep':
|
|
701
|
+
case 'search':
|
|
702
|
+
case 'rg': {
|
|
703
|
+
const p = String(input.pattern ?? '');
|
|
704
|
+
const count = result ? result.split('\n').filter(Boolean).length : 0;
|
|
705
|
+
return `Grep "${p.slice(0, 60)}"${count ? ` → ${count} matches` : ''}`;
|
|
706
|
+
}
|
|
707
|
+
case 'web_fetch':
|
|
708
|
+
case 'fetch': {
|
|
709
|
+
const u = String(input.url ?? '');
|
|
710
|
+
return `Fetch ${u.slice(0, 60)}`;
|
|
711
|
+
}
|
|
712
|
+
case 'web_search':
|
|
713
|
+
case 'search_web': {
|
|
714
|
+
const q = String(input.query ?? '');
|
|
715
|
+
return `Search "${q.slice(0, 50)}"`;
|
|
716
|
+
}
|
|
717
|
+
case 'notebook_read':
|
|
718
|
+
case 'nb_read':
|
|
719
|
+
case 'read_notebook': {
|
|
720
|
+
const fp = String(input.file_path ?? '');
|
|
721
|
+
const fn = fp.split(/[/\\]/).pop() || fp;
|
|
722
|
+
const pages = input.pages ? ` cells ${input.pages}` : '';
|
|
723
|
+
return `Notebook ${fn}${pages}`;
|
|
724
|
+
}
|
|
725
|
+
case 'notebook_edit':
|
|
726
|
+
case 'nb_edit':
|
|
727
|
+
case 'edit_notebook': {
|
|
728
|
+
const fp = String(input.notebook_path ?? '');
|
|
729
|
+
const fn = fp.split(/[/\\]/).pop() || fp;
|
|
730
|
+
const mode = input.edit_mode === 'delete' ? 'Delete cell' : input.edit_mode === 'insert' ? 'Insert cell' : 'Edit cell';
|
|
731
|
+
return `${mode} in ${fn}`;
|
|
732
|
+
}
|
|
733
|
+
default:
|
|
734
|
+
return `${name}`;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
// ─── Extract image path from clipboard paste data ────────────────────
|
|
738
|
+
function extractPastedImage(input) {
|
|
739
|
+
// data:image/png;base64,... → save to temp dir, return path
|
|
740
|
+
const dataUriMatch = input.match(/^data:(image\/\w+);base64,(.+)$/);
|
|
741
|
+
if (dataUriMatch) {
|
|
742
|
+
try {
|
|
743
|
+
const ext = dataUriMatch[1].split('/')[1] || 'png';
|
|
744
|
+
const tmpDir = join(homedir(), '.cray', 'pasted');
|
|
745
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
746
|
+
const filename = `paste-${Date.now()}.${ext}`;
|
|
747
|
+
const filepath = join(tmpDir, filename);
|
|
748
|
+
const buffer = Buffer.from(dataUriMatch[2], 'base64');
|
|
749
|
+
writeFileSync(filepath, buffer);
|
|
750
|
+
return filepath;
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
export default ReplScreen;
|
|
759
|
+
export function mountReplScreen(cray, initialPrompt) {
|
|
760
|
+
const { unmount } = render(React.createElement(ReplScreen, { cray, initialPrompt }), { exitOnCtrlC: false });
|
|
761
|
+
return { unmount };
|
|
762
|
+
}
|
|
763
|
+
//# sourceMappingURL=ReplScreen.js.map
|