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.
Files changed (252) hide show
  1. package/README.md +316 -0
  2. package/dist/Tool.d.ts +217 -0
  3. package/dist/Tool.d.ts.map +1 -0
  4. package/dist/Tool.js +89 -0
  5. package/dist/Tool.js.map +1 -0
  6. package/dist/branding/logo.d.ts +8 -0
  7. package/dist/branding/logo.d.ts.map +1 -0
  8. package/dist/branding/logo.js +26 -0
  9. package/dist/branding/logo.js.map +1 -0
  10. package/dist/branding/theme.d.ts +27 -0
  11. package/dist/branding/theme.d.ts.map +1 -0
  12. package/dist/branding/theme.js +28 -0
  13. package/dist/branding/theme.js.map +1 -0
  14. package/dist/commands/registry.d.ts +32 -0
  15. package/dist/commands/registry.d.ts.map +1 -0
  16. package/dist/commands/registry.js +759 -0
  17. package/dist/commands/registry.js.map +1 -0
  18. package/dist/components/MessageView.d.ts +12 -0
  19. package/dist/components/MessageView.d.ts.map +1 -0
  20. package/dist/components/MessageView.js +35 -0
  21. package/dist/components/MessageView.js.map +1 -0
  22. package/dist/components/PermissionPrompt.d.ts +11 -0
  23. package/dist/components/PermissionPrompt.d.ts.map +1 -0
  24. package/dist/components/PermissionPrompt.js +6 -0
  25. package/dist/components/PermissionPrompt.js.map +1 -0
  26. package/dist/components/PluginManager.d.ts +27 -0
  27. package/dist/components/PluginManager.d.ts.map +1 -0
  28. package/dist/components/PluginManager.js +391 -0
  29. package/dist/components/PluginManager.js.map +1 -0
  30. package/dist/components/ThinkingBlock.d.ts +27 -0
  31. package/dist/components/ThinkingBlock.d.ts.map +1 -0
  32. package/dist/components/ThinkingBlock.js +29 -0
  33. package/dist/components/ThinkingBlock.js.map +1 -0
  34. package/dist/components/ToolCallBlock.d.ts +14 -0
  35. package/dist/components/ToolCallBlock.d.ts.map +1 -0
  36. package/dist/components/ToolCallBlock.js +83 -0
  37. package/dist/components/ToolCallBlock.js.map +1 -0
  38. package/dist/components/TrustDialog.d.ts +20 -0
  39. package/dist/components/TrustDialog.d.ts.map +1 -0
  40. package/dist/components/TrustDialog.js +80 -0
  41. package/dist/components/TrustDialog.js.map +1 -0
  42. package/dist/context.d.ts +25 -0
  43. package/dist/context.d.ts.map +1 -0
  44. package/dist/context.js +268 -0
  45. package/dist/context.js.map +1 -0
  46. package/dist/cray.d.ts +114 -0
  47. package/dist/cray.d.ts.map +1 -0
  48. package/dist/cray.js +338 -0
  49. package/dist/cray.js.map +1 -0
  50. package/dist/index.d.ts +16 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +122 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/plugins/registry.d.ts +106 -0
  55. package/dist/plugins/registry.d.ts.map +1 -0
  56. package/dist/plugins/registry.js +695 -0
  57. package/dist/plugins/registry.js.map +1 -0
  58. package/dist/query.d.ts +31 -0
  59. package/dist/query.d.ts.map +1 -0
  60. package/dist/query.js +637 -0
  61. package/dist/query.js.map +1 -0
  62. package/dist/queryStream.d.ts +36 -0
  63. package/dist/queryStream.d.ts.map +1 -0
  64. package/dist/queryStream.js +704 -0
  65. package/dist/queryStream.js.map +1 -0
  66. package/dist/screens/ReplScreen.d.ts +22 -0
  67. package/dist/screens/ReplScreen.d.ts.map +1 -0
  68. package/dist/screens/ReplScreen.js +763 -0
  69. package/dist/screens/ReplScreen.js.map +1 -0
  70. package/dist/services/agentRunner.d.ts +39 -0
  71. package/dist/services/agentRunner.d.ts.map +1 -0
  72. package/dist/services/agentRunner.js +115 -0
  73. package/dist/services/agentRunner.js.map +1 -0
  74. package/dist/services/compact.d.ts +34 -0
  75. package/dist/services/compact.d.ts.map +1 -0
  76. package/dist/services/compact.js +179 -0
  77. package/dist/services/compact.js.map +1 -0
  78. package/dist/services/loadPluginCommands.d.ts +55 -0
  79. package/dist/services/loadPluginCommands.d.ts.map +1 -0
  80. package/dist/services/loadPluginCommands.js +219 -0
  81. package/dist/services/loadPluginCommands.js.map +1 -0
  82. package/dist/services/mcp/index.d.ts +3 -0
  83. package/dist/services/mcp/index.d.ts.map +1 -0
  84. package/dist/services/mcp/index.js +3 -0
  85. package/dist/services/mcp/index.js.map +1 -0
  86. package/dist/services/mcp/manager.d.ts +24 -0
  87. package/dist/services/mcp/manager.d.ts.map +1 -0
  88. package/dist/services/mcp/manager.js +138 -0
  89. package/dist/services/mcp/manager.js.map +1 -0
  90. package/dist/services/mcp/types.d.ts +52 -0
  91. package/dist/services/mcp/types.d.ts.map +1 -0
  92. package/dist/services/mcp/types.js +5 -0
  93. package/dist/services/mcp/types.js.map +1 -0
  94. package/dist/services/memory.d.ts +38 -0
  95. package/dist/services/memory.d.ts.map +1 -0
  96. package/dist/services/memory.js +181 -0
  97. package/dist/services/memory.js.map +1 -0
  98. package/dist/services/permissionPrompt.d.ts +38 -0
  99. package/dist/services/permissionPrompt.d.ts.map +1 -0
  100. package/dist/services/permissionPrompt.js +83 -0
  101. package/dist/services/permissionPrompt.js.map +1 -0
  102. package/dist/services/permissions.d.ts +15 -0
  103. package/dist/services/permissions.d.ts.map +1 -0
  104. package/dist/services/permissions.js +237 -0
  105. package/dist/services/permissions.js.map +1 -0
  106. package/dist/services/sessionStorage.d.ts +51 -0
  107. package/dist/services/sessionStorage.d.ts.map +1 -0
  108. package/dist/services/sessionStorage.js +266 -0
  109. package/dist/services/sessionStorage.js.map +1 -0
  110. package/dist/setup.d.ts +22 -0
  111. package/dist/setup.d.ts.map +1 -0
  112. package/dist/setup.js +160 -0
  113. package/dist/setup.js.map +1 -0
  114. package/dist/skills/bundledSkills.d.ts +18 -0
  115. package/dist/skills/bundledSkills.d.ts.map +1 -0
  116. package/dist/skills/bundledSkills.js +277 -0
  117. package/dist/skills/bundledSkills.js.map +1 -0
  118. package/dist/skills/index.d.ts +4 -0
  119. package/dist/skills/index.d.ts.map +1 -0
  120. package/dist/skills/index.js +3 -0
  121. package/dist/skills/index.js.map +1 -0
  122. package/dist/skills/loadSkillsDir.d.ts +45 -0
  123. package/dist/skills/loadSkillsDir.d.ts.map +1 -0
  124. package/dist/skills/loadSkillsDir.js +233 -0
  125. package/dist/skills/loadSkillsDir.js.map +1 -0
  126. package/dist/state/AppState.d.ts +70 -0
  127. package/dist/state/AppState.d.ts.map +1 -0
  128. package/dist/state/AppState.js +106 -0
  129. package/dist/state/AppState.js.map +1 -0
  130. package/dist/tools/AgentTool.d.ts +62 -0
  131. package/dist/tools/AgentTool.d.ts.map +1 -0
  132. package/dist/tools/AgentTool.js +133 -0
  133. package/dist/tools/AgentTool.js.map +1 -0
  134. package/dist/tools/AskUserQuestionTool.d.ts +60 -0
  135. package/dist/tools/AskUserQuestionTool.d.ts.map +1 -0
  136. package/dist/tools/AskUserQuestionTool.js +52 -0
  137. package/dist/tools/AskUserQuestionTool.js.map +1 -0
  138. package/dist/tools/BashTool.d.ts +33 -0
  139. package/dist/tools/BashTool.d.ts.map +1 -0
  140. package/dist/tools/BashTool.js +211 -0
  141. package/dist/tools/BashTool.js.map +1 -0
  142. package/dist/tools/EditTool.d.ts +24 -0
  143. package/dist/tools/EditTool.d.ts.map +1 -0
  144. package/dist/tools/EditTool.js +102 -0
  145. package/dist/tools/EditTool.js.map +1 -0
  146. package/dist/tools/GlobTool.d.ts +17 -0
  147. package/dist/tools/GlobTool.d.ts.map +1 -0
  148. package/dist/tools/GlobTool.js +65 -0
  149. package/dist/tools/GlobTool.js.map +1 -0
  150. package/dist/tools/GrepTool.d.ts +30 -0
  151. package/dist/tools/GrepTool.d.ts.map +1 -0
  152. package/dist/tools/GrepTool.js +140 -0
  153. package/dist/tools/GrepTool.js.map +1 -0
  154. package/dist/tools/MCPTool.d.ts +24 -0
  155. package/dist/tools/MCPTool.d.ts.map +1 -0
  156. package/dist/tools/MCPTool.js +67 -0
  157. package/dist/tools/MCPTool.js.map +1 -0
  158. package/dist/tools/NotebookEditTool.d.ts +28 -0
  159. package/dist/tools/NotebookEditTool.d.ts.map +1 -0
  160. package/dist/tools/NotebookEditTool.js +213 -0
  161. package/dist/tools/NotebookEditTool.js.map +1 -0
  162. package/dist/tools/NotebookReadTool.d.ts +19 -0
  163. package/dist/tools/NotebookReadTool.d.ts.map +1 -0
  164. package/dist/tools/NotebookReadTool.js +191 -0
  165. package/dist/tools/NotebookReadTool.js.map +1 -0
  166. package/dist/tools/PlanTools.d.ts +17 -0
  167. package/dist/tools/PlanTools.d.ts.map +1 -0
  168. package/dist/tools/PlanTools.js +65 -0
  169. package/dist/tools/PlanTools.js.map +1 -0
  170. package/dist/tools/ReadTool.d.ts +21 -0
  171. package/dist/tools/ReadTool.d.ts.map +1 -0
  172. package/dist/tools/ReadTool.js +202 -0
  173. package/dist/tools/ReadTool.js.map +1 -0
  174. package/dist/tools/SkillTool.d.ts +32 -0
  175. package/dist/tools/SkillTool.d.ts.map +1 -0
  176. package/dist/tools/SkillTool.js +217 -0
  177. package/dist/tools/SkillTool.js.map +1 -0
  178. package/dist/tools/TodoWriteTool.d.ts +35 -0
  179. package/dist/tools/TodoWriteTool.d.ts.map +1 -0
  180. package/dist/tools/TodoWriteTool.js +58 -0
  181. package/dist/tools/TodoWriteTool.js.map +1 -0
  182. package/dist/tools/WebFetchTool.d.ts +17 -0
  183. package/dist/tools/WebFetchTool.d.ts.map +1 -0
  184. package/dist/tools/WebFetchTool.js +97 -0
  185. package/dist/tools/WebFetchTool.js.map +1 -0
  186. package/dist/tools/WebSearchTool.d.ts +18 -0
  187. package/dist/tools/WebSearchTool.d.ts.map +1 -0
  188. package/dist/tools/WebSearchTool.js +76 -0
  189. package/dist/tools/WebSearchTool.js.map +1 -0
  190. package/dist/tools/WriteTool.d.ts +17 -0
  191. package/dist/tools/WriteTool.d.ts.map +1 -0
  192. package/dist/tools/WriteTool.js +84 -0
  193. package/dist/tools/WriteTool.js.map +1 -0
  194. package/dist/tools/index.d.ts +21 -0
  195. package/dist/tools/index.d.ts.map +1 -0
  196. package/dist/tools/index.js +20 -0
  197. package/dist/tools/index.js.map +1 -0
  198. package/dist/tools.d.ts +34 -0
  199. package/dist/tools.d.ts.map +1 -0
  200. package/dist/tools.js +102 -0
  201. package/dist/tools.js.map +1 -0
  202. package/dist/types/events.d.ts +85 -0
  203. package/dist/types/events.d.ts.map +1 -0
  204. package/dist/types/events.js +12 -0
  205. package/dist/types/events.js.map +1 -0
  206. package/dist/types/index.d.ts +4 -0
  207. package/dist/types/index.d.ts.map +1 -0
  208. package/dist/types/index.js +4 -0
  209. package/dist/types/index.js.map +1 -0
  210. package/dist/types/message.d.ts +71 -0
  211. package/dist/types/message.d.ts.map +1 -0
  212. package/dist/types/message.js +5 -0
  213. package/dist/types/message.js.map +1 -0
  214. package/dist/types/permission.d.ts +56 -0
  215. package/dist/types/permission.d.ts.map +1 -0
  216. package/dist/types/permission.js +46 -0
  217. package/dist/types/permission.js.map +1 -0
  218. package/dist/types/processUserInput.d.ts +18 -0
  219. package/dist/types/processUserInput.d.ts.map +1 -0
  220. package/dist/types/processUserInput.js +8 -0
  221. package/dist/types/processUserInput.js.map +1 -0
  222. package/dist/types/tool.d.ts +32 -0
  223. package/dist/types/tool.d.ts.map +1 -0
  224. package/dist/types/tool.js +5 -0
  225. package/dist/types/tool.js.map +1 -0
  226. package/dist/utils/compactBoundary.d.ts +11 -0
  227. package/dist/utils/compactBoundary.d.ts.map +1 -0
  228. package/dist/utils/compactBoundary.js +26 -0
  229. package/dist/utils/compactBoundary.js.map +1 -0
  230. package/dist/utils/configStore.d.ts +41 -0
  231. package/dist/utils/configStore.d.ts.map +1 -0
  232. package/dist/utils/configStore.js +111 -0
  233. package/dist/utils/configStore.js.map +1 -0
  234. package/dist/utils/forkedAgent.d.ts +40 -0
  235. package/dist/utils/forkedAgent.d.ts.map +1 -0
  236. package/dist/utils/forkedAgent.js +231 -0
  237. package/dist/utils/forkedAgent.js.map +1 -0
  238. package/dist/utils/messages.d.ts +14 -0
  239. package/dist/utils/messages.d.ts.map +1 -0
  240. package/dist/utils/messages.js +29 -0
  241. package/dist/utils/messages.js.map +1 -0
  242. package/dist/utils/sandbox.d.ts +22 -0
  243. package/dist/utils/sandbox.d.ts.map +1 -0
  244. package/dist/utils/sandbox.js +59 -0
  245. package/dist/utils/sandbox.js.map +1 -0
  246. package/dist/utils/sideQuestion.d.ts +29 -0
  247. package/dist/utils/sideQuestion.d.ts.map +1 -0
  248. package/dist/utils/sideQuestion.js +81 -0
  249. package/dist/utils/sideQuestion.js.map +1 -0
  250. package/install.ps1 +86 -0
  251. package/install.sh +92 -0
  252. 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