@kirosnn/mosaic 0.0.7

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 (154) hide show
  1. package/.mosaic/mosaic.local.jsonc +0 -0
  2. package/MOSAIC.md +188 -0
  3. package/README.md +127 -0
  4. package/docs/mosaic.png +0 -0
  5. package/package.json +42 -0
  6. package/src/agent/Agent.ts +131 -0
  7. package/src/agent/context.ts +96 -0
  8. package/src/agent/index.ts +2 -0
  9. package/src/agent/prompts/systemPrompt.ts +138 -0
  10. package/src/agent/prompts/toolsPrompt.ts +139 -0
  11. package/src/agent/provider/anthropic.ts +122 -0
  12. package/src/agent/provider/google.ts +124 -0
  13. package/src/agent/provider/mistral.ts +117 -0
  14. package/src/agent/provider/ollama.ts +531 -0
  15. package/src/agent/provider/openai.ts +220 -0
  16. package/src/agent/provider/xai.ts +122 -0
  17. package/src/agent/tools/bash.ts +20 -0
  18. package/src/agent/tools/definitions.ts +27 -0
  19. package/src/agent/tools/edit.ts +23 -0
  20. package/src/agent/tools/executor.ts +751 -0
  21. package/src/agent/tools/explore.ts +18 -0
  22. package/src/agent/tools/exploreExecutor.ts +320 -0
  23. package/src/agent/tools/glob.ts +16 -0
  24. package/src/agent/tools/grep.ts +19 -0
  25. package/src/agent/tools/index.ts +4 -0
  26. package/src/agent/tools/list.ts +20 -0
  27. package/src/agent/tools/question.ts +20 -0
  28. package/src/agent/tools/read.ts +15 -0
  29. package/src/agent/tools/write.ts +21 -0
  30. package/src/agent/types.ts +155 -0
  31. package/src/components/App.tsx +174 -0
  32. package/src/components/CommandsModal.tsx +77 -0
  33. package/src/components/CustomInput.tsx +328 -0
  34. package/src/components/Main.tsx +1112 -0
  35. package/src/components/Notification.tsx +91 -0
  36. package/src/components/SelectList.tsx +47 -0
  37. package/src/components/Setup.tsx +528 -0
  38. package/src/components/ShortcutsModal.tsx +67 -0
  39. package/src/components/Welcome.tsx +39 -0
  40. package/src/components/main/ApprovalPanel.tsx +134 -0
  41. package/src/components/main/ChatPage.tsx +516 -0
  42. package/src/components/main/HomePage.tsx +111 -0
  43. package/src/components/main/QuestionPanel.tsx +85 -0
  44. package/src/components/main/ThinkingIndicator.tsx +101 -0
  45. package/src/components/main/types.ts +55 -0
  46. package/src/components/main/wrapText.ts +41 -0
  47. package/src/index.tsx +212 -0
  48. package/src/utils/approvalBridge.ts +129 -0
  49. package/src/utils/commands/echo.ts +22 -0
  50. package/src/utils/commands/help.ts +25 -0
  51. package/src/utils/commands/index.ts +68 -0
  52. package/src/utils/commands/init.ts +68 -0
  53. package/src/utils/commands/redo.ts +74 -0
  54. package/src/utils/commands/registry.ts +29 -0
  55. package/src/utils/commands/sessions.ts +129 -0
  56. package/src/utils/commands/types.ts +20 -0
  57. package/src/utils/commands/undo.ts +75 -0
  58. package/src/utils/commands/web.ts +77 -0
  59. package/src/utils/config.ts +357 -0
  60. package/src/utils/diff.ts +201 -0
  61. package/src/utils/diffRendering.tsx +62 -0
  62. package/src/utils/exploreBridge.ts +87 -0
  63. package/src/utils/fileChangeTracker.ts +98 -0
  64. package/src/utils/fileChangesBridge.ts +18 -0
  65. package/src/utils/history.ts +106 -0
  66. package/src/utils/markdown.tsx +232 -0
  67. package/src/utils/models.ts +304 -0
  68. package/src/utils/questionBridge.ts +122 -0
  69. package/src/utils/terminalUtils.ts +25 -0
  70. package/src/utils/toolFormatting.ts +384 -0
  71. package/src/utils/undoRedo.ts +429 -0
  72. package/src/utils/undoRedoBridge.ts +45 -0
  73. package/src/utils/undoRedoDb.ts +338 -0
  74. package/src/utils/uninstall.ts +45 -0
  75. package/src/utils/version.ts +3 -0
  76. package/src/web/app.tsx +606 -0
  77. package/src/web/assets/css/ChatPage.css +212 -0
  78. package/src/web/assets/css/FileExplorer.css +202 -0
  79. package/src/web/assets/css/HomePage.css +119 -0
  80. package/src/web/assets/css/Markdown.css +178 -0
  81. package/src/web/assets/css/MessageItem.css +160 -0
  82. package/src/web/assets/css/Sidebar.css +208 -0
  83. package/src/web/assets/css/SidebarModal.css +137 -0
  84. package/src/web/assets/css/ThinkingIndicator.css +47 -0
  85. package/src/web/assets/css/ToolMessage.css +148 -0
  86. package/src/web/assets/css/global.css +226 -0
  87. package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
  88. package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
  89. package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
  90. package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
  91. package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
  92. package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
  93. package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
  94. package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
  95. package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
  96. package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
  97. package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
  98. package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
  99. package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
  100. package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
  101. package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
  102. package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
  103. package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
  104. package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
  105. package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
  106. package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
  107. package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
  108. package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
  109. package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
  110. package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
  111. package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
  112. package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
  113. package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
  114. package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
  115. package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
  116. package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
  117. package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
  118. package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
  119. package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
  120. package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
  121. package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
  122. package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
  123. package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
  124. package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
  125. package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
  126. package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
  127. package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
  128. package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
  129. package/src/web/assets/images/favicon-v2.svg +6 -0
  130. package/src/web/assets/images/favicon.png +0 -0
  131. package/src/web/assets/images/foruse.svg +5 -0
  132. package/src/web/assets/images/logo_black.svg +5 -0
  133. package/src/web/assets/images/logo_white.svg +5 -0
  134. package/src/web/assets/images/logoblack.png +0 -0
  135. package/src/web/assets/images/logowhite.png +0 -0
  136. package/src/web/build.ts +23 -0
  137. package/src/web/components/ApprovalPanel.tsx +191 -0
  138. package/src/web/components/ChatPage.tsx +273 -0
  139. package/src/web/components/FileExplorer.tsx +162 -0
  140. package/src/web/components/HomePage.tsx +121 -0
  141. package/src/web/components/MessageItem.tsx +178 -0
  142. package/src/web/components/Modal.tsx +30 -0
  143. package/src/web/components/QuestionPanel.tsx +149 -0
  144. package/src/web/components/Setup.tsx +211 -0
  145. package/src/web/components/Sidebar.tsx +292 -0
  146. package/src/web/components/ThinkingIndicator.tsx +85 -0
  147. package/src/web/logo_black.svg +5 -0
  148. package/src/web/logo_white.svg +5 -0
  149. package/src/web/router.ts +46 -0
  150. package/src/web/server.tsx +662 -0
  151. package/src/web/storage.ts +92 -0
  152. package/src/web/types.ts +17 -0
  153. package/src/web/utils.ts +61 -0
  154. package/tsconfig.json +33 -0
@@ -0,0 +1,1112 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { useKeyboard } from "@opentui/react";
3
+ import { Agent } from "../agent";
4
+ import { saveConversation, addInputToHistory, type ConversationHistory, type ConversationStep } from "../utils/history";
5
+ import { readConfig } from "../utils/config";
6
+ import { DEFAULT_MAX_TOOL_LINES, formatToolMessage, formatErrorMessage, parseToolHeader } from '../utils/toolFormatting';
7
+ import { initializeCommands, isCommand, executeCommand } from '../utils/commands';
8
+ import type { InputSubmitMeta } from './CustomInput';
9
+
10
+ import { subscribeQuestion, type QuestionRequest } from "../utils/questionBridge";
11
+ import { subscribeApprovalAccepted, type ApprovalAccepted } from "../utils/approvalBridge";
12
+ import { subscribeUndoRedo } from "../utils/undoRedoBridge";
13
+ import { setExploreAbortController, setExploreToolCallback, abortExplore } from "../utils/exploreBridge";
14
+ import { initializeSession, saveState } from "../utils/undoRedo";
15
+ import { resetFileChanges } from "../utils/fileChangeTracker";
16
+ import { getCurrentQuestion, cancelQuestion } from "../utils/questionBridge";
17
+ import { getCurrentApproval, cancelApproval } from "../utils/approvalBridge";
18
+ import { BLEND_WORDS, type MainProps, type Message } from "./main/types";
19
+ import { HomePage } from './main/HomePage';
20
+ import { ChatPage } from './main/ChatPage';
21
+
22
+ function extractTitle(content: string, alreadyResolved: boolean): { title: string | null; cleanContent: string; isPending: boolean; noTitle: boolean } {
23
+ const trimmed = content.trimStart();
24
+
25
+ const titleMatch = trimmed.match(/^<title>(.*?)<\/title>\s*/s);
26
+ if (titleMatch) {
27
+ const title = alreadyResolved ? null : (titleMatch[1]?.trim() || null);
28
+ const cleanContent = trimmed.replace(/^<title>.*?<\/title>\s*/s, '');
29
+ return { title, cleanContent, isPending: false, noTitle: false };
30
+ }
31
+
32
+ if (alreadyResolved) {
33
+ return { title: null, cleanContent: content, isPending: false, noTitle: false };
34
+ }
35
+
36
+ const partialTitlePattern = /^<(t(i(t(l(e(>.*)?)?)?)?)?)?$/i;
37
+ if (partialTitlePattern.test(trimmed) || (trimmed.startsWith('<title>') && !trimmed.includes('</title>'))) {
38
+ return { title: null, cleanContent: '', isPending: true, noTitle: false };
39
+ }
40
+
41
+ return { title: null, cleanContent: content, isPending: false, noTitle: true };
42
+ }
43
+
44
+ function setTerminalTitle(title: string) {
45
+ process.title = `⁘ ${title}`;
46
+ }
47
+
48
+ export function Main({ pasteRequestId = 0, copyRequestId = 0, onCopy, shortcutsOpen = false, commandsOpen = false, initialMessage }: MainProps) {
49
+ const [currentPage, setCurrentPage] = useState<"home" | "chat">(initialMessage ? "chat" : "home");
50
+ const [messages, setMessages] = useState<Message[]>([]);
51
+ const [isProcessing, setIsProcessing] = useState(false);
52
+ const [processingStartTime, setProcessingStartTime] = useState<number | null>(null);
53
+ const [currentTokens, setCurrentTokens] = useState(0);
54
+ const [scrollOffset, setScrollOffset] = useState(0);
55
+ const [terminalHeight, setTerminalHeight] = useState(process.stdout.rows || 24);
56
+ const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
57
+ const [questionRequest, setQuestionRequest] = useState<QuestionRequest | null>(null);
58
+ const [currentTitle, setCurrentTitle] = useState<string | null>(null);
59
+ const currentTitleRef = useRef<string | null>(null);
60
+ const titleExtractedRef = useRef(false);
61
+ const shouldAutoScroll = useRef(true);
62
+ const abortControllerRef = useRef<AbortController | null>(null);
63
+ const currentPageRef = useRef(currentPage);
64
+ const shortcutsOpenRef = useRef(shortcutsOpen);
65
+ const commandsOpenRef = useRef(commandsOpen);
66
+ const questionRequestRef = useRef<QuestionRequest | null>(questionRequest);
67
+ const initialMessageProcessed = useRef(false);
68
+ const exploreMessageIdRef = useRef<string | null>(null);
69
+ const exploreToolsRef = useRef<Array<{ tool: string; info: string; success: boolean }>>([]);
70
+ const explorePurposeRef = useRef<string>('');
71
+
72
+ const createId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
73
+
74
+ useEffect(() => {
75
+ initializeCommands();
76
+ initializeSession();
77
+ }, []);
78
+
79
+ useEffect(() => {
80
+ let lastExploreTokens = 0;
81
+ setExploreToolCallback((toolName, args, result, totalTokens) => {
82
+ const info = (args.path || args.pattern || args.query || '') as string;
83
+ const shortInfo = info.length > 40 ? info.substring(0, 37) + '...' : info;
84
+ exploreToolsRef.current.push({ tool: toolName, info: shortInfo, success: result.success });
85
+
86
+ const tokenDelta = totalTokens - lastExploreTokens;
87
+ lastExploreTokens = totalTokens;
88
+ if (tokenDelta > 0) {
89
+ setCurrentTokens(prev => prev + tokenDelta);
90
+ }
91
+
92
+ if (exploreMessageIdRef.current) {
93
+ setMessages((prev: Message[]) => {
94
+ const newMessages = [...prev];
95
+ const idx = newMessages.findIndex(m => m.id === exploreMessageIdRef.current);
96
+ if (idx !== -1) {
97
+ const toolLines = exploreToolsRef.current.map(t => {
98
+ const icon = t.success ? '+' : '-';
99
+ return ` ${icon} ${t.tool}(${t.info})`;
100
+ });
101
+ const purpose = explorePurposeRef.current;
102
+ const newContent = `Explore (${purpose})\n${toolLines.join('\n')}`;
103
+ newMessages[idx] = { ...newMessages[idx]!, content: newContent };
104
+ }
105
+ return newMessages;
106
+ });
107
+ }
108
+ });
109
+
110
+ return () => {
111
+ setExploreToolCallback(null);
112
+ };
113
+ }, []);
114
+
115
+ useEffect(() => {
116
+ return subscribeUndoRedo((state, action) => {
117
+ if (state) {
118
+ setMessages(state.messages);
119
+ resetFileChanges();
120
+ }
121
+ });
122
+ }, []);
123
+
124
+ useEffect(() => {
125
+ const handleResize = () => {
126
+ const newWidth = process.stdout.columns || 80;
127
+ const newHeight = process.stdout.rows || 24;
128
+ const oldWidth = terminalWidth;
129
+ const oldHeight = terminalHeight;
130
+
131
+ setTerminalWidth(newWidth);
132
+ setTerminalHeight(newHeight);
133
+
134
+ if (shouldAutoScroll.current) {
135
+ setScrollOffset(0);
136
+ } else if (oldHeight !== newHeight) {
137
+ const heightDiff = newHeight - oldHeight;
138
+ setScrollOffset(prev => Math.max(0, prev - heightDiff));
139
+ }
140
+ };
141
+ process.stdout.on('resize', handleResize);
142
+ return () => {
143
+ process.stdout.off('resize', handleResize);
144
+ };
145
+ }, [terminalWidth, terminalHeight]);
146
+
147
+ useEffect(() => {
148
+ return subscribeQuestion(setQuestionRequest);
149
+ }, []);
150
+
151
+ useEffect(() => {
152
+ return subscribeApprovalAccepted((accepted) => {
153
+ const isBashTool = accepted.toolName === 'bash';
154
+
155
+ if (isBashTool) {
156
+ const { name: toolDisplayName, info: toolInfo } = parseToolHeader(accepted.toolName, accepted.args);
157
+ const runningContent = toolInfo ? `${toolDisplayName} (${toolInfo})` : toolDisplayName;
158
+
159
+ setMessages((prev: Message[]) => {
160
+ const newMessages = [...prev];
161
+ newMessages.push({
162
+ id: createId(),
163
+ role: "tool",
164
+ content: runningContent,
165
+ toolName: accepted.toolName,
166
+ toolArgs: accepted.args,
167
+ success: true,
168
+ isRunning: true,
169
+ runningStartTime: Date.now()
170
+ });
171
+ return newMessages;
172
+ });
173
+ }
174
+ });
175
+ }, []);
176
+
177
+ useEffect(() => {
178
+ currentPageRef.current = currentPage;
179
+ }, [currentPage]);
180
+
181
+ useEffect(() => {
182
+ shortcutsOpenRef.current = shortcutsOpen;
183
+ }, [shortcutsOpen]);
184
+
185
+ useEffect(() => {
186
+ commandsOpenRef.current = commandsOpen;
187
+ }, [commandsOpen]);
188
+
189
+ useEffect(() => {
190
+ questionRequestRef.current = questionRequest;
191
+ }, [questionRequest]);
192
+
193
+ useEffect(() => {
194
+ if (questionRequest) {
195
+ shouldAutoScroll.current = true;
196
+ setScrollOffset(0);
197
+ }
198
+ }, [questionRequest]);
199
+
200
+ useEffect(() => {
201
+ if (currentPage !== "chat") return;
202
+
203
+ process.stdin.setRawMode(true);
204
+ process.stdout.write('\x1b[?1000h');
205
+ process.stdout.write('\x1b[?1003h');
206
+ process.stdout.write('\x1b[?1006h');
207
+
208
+ const handleData = (data: Buffer) => {
209
+ const str = data.toString();
210
+
211
+ if (str.match(/\x1b\[<(\d+);(\d+);(\d+)([mM])/)) {
212
+ const match = str.match(/\x1b\[<(\d+);(\d+);(\d+)([mM])/);
213
+ if (match) {
214
+ const button = parseInt(match[1] || '0');
215
+
216
+ if (button === 64) {
217
+ shouldAutoScroll.current = false;
218
+ setScrollOffset((prev) => prev + 1);
219
+ } else if (button === 65) {
220
+ setScrollOffset((prev) => {
221
+ const newOffset = Math.max(0, prev - 1);
222
+ if (newOffset === 0) {
223
+ shouldAutoScroll.current = true;
224
+ }
225
+ return newOffset;
226
+ });
227
+ }
228
+ }
229
+ }
230
+ };
231
+
232
+ process.stdin.on('data', handleData);
233
+
234
+ return () => {
235
+ process.stdin.off('data', handleData);
236
+ process.stdout.write('\x1b[?1000l');
237
+ process.stdout.write('\x1b[?1003l');
238
+ process.stdout.write('\x1b[?1006l');
239
+ };
240
+ }, [currentPage]);
241
+
242
+ useEffect(() => {
243
+ if (currentPage === "chat") {
244
+ setScrollOffset((prevOffset) => {
245
+ if (shouldAutoScroll.current || prevOffset < 5) {
246
+ shouldAutoScroll.current = true;
247
+ return 0;
248
+ }
249
+ return prevOffset;
250
+ });
251
+ }
252
+ }, [messages, currentPage]);
253
+
254
+ useEffect(() => {
255
+ if (copyRequestId > 0 && onCopy && messages.length > 0) {
256
+ const lastAssistantMessage = messages.slice().reverse().find(m => m.role === 'assistant');
257
+ if (lastAssistantMessage) {
258
+ onCopy(lastAssistantMessage.content);
259
+ }
260
+ }
261
+ }, [copyRequestId, onCopy, messages]);
262
+
263
+ useKeyboard((key) => {
264
+ if (key.name === 'escape') {
265
+ if (getCurrentQuestion()) {
266
+ cancelQuestion();
267
+ }
268
+ if (getCurrentApproval()) {
269
+ cancelApproval();
270
+ }
271
+ abortControllerRef.current?.abort();
272
+ return;
273
+ }
274
+ });
275
+
276
+ const handleSubmit = async (value: string, meta?: InputSubmitMeta) => {
277
+ if (isProcessing) return;
278
+
279
+ const hasPastedContent = Boolean(meta?.isPaste && meta.pastedContent);
280
+ if (!value.trim() && !hasPastedContent) return;
281
+
282
+ if (isCommand(value)) {
283
+ const result = await executeCommand(value);
284
+ if (result) {
285
+ if (result.shouldAddToHistory === true) {
286
+ addInputToHistory(value.trim());
287
+
288
+ saveState(messages);
289
+
290
+ const userMessage: Message = {
291
+ id: createId(),
292
+ role: "user",
293
+ content: result.content,
294
+ displayContent: value,
295
+ };
296
+
297
+ setMessages((prev: Message[]) => [...prev, userMessage]);
298
+ setIsProcessing(true);
299
+ const localStartTime = Date.now();
300
+ setProcessingStartTime(localStartTime);
301
+ setCurrentTokens(0);
302
+ shouldAutoScroll.current = true;
303
+
304
+ const conversationId = createId();
305
+ const conversationSteps: ConversationStep[] = [];
306
+ let totalTokens = { prompt: 0, completion: 0, total: 0 };
307
+ let stepCount = 0;
308
+ let totalChars = 0;
309
+ for (const m of messages) {
310
+ if (m.role === 'assistant') {
311
+ totalChars += m.content.length;
312
+ if (m.thinkingContent) totalChars += m.thinkingContent.length;
313
+ } else if (m.role === 'tool') {
314
+ totalChars += m.content.length;
315
+ }
316
+ }
317
+
318
+ const estimateTokens = () => Math.ceil(totalChars / 4);
319
+ setCurrentTokens(estimateTokens());
320
+ const config = readConfig();
321
+ const abortController = new AbortController();
322
+ abortControllerRef.current = abortController;
323
+ let abortNotified = false;
324
+ const notifyAbort = () => {
325
+ if (abortNotified) return;
326
+ abortNotified = true;
327
+ setMessages((prev: Message[]) => {
328
+ const newMessages = [...prev];
329
+ newMessages.push({
330
+ id: createId(),
331
+ role: "tool",
332
+ success: false,
333
+ content: "Request interrupted by user. \n↪ What should Mosaic do instead?"
334
+ });
335
+ return newMessages;
336
+ });
337
+ };
338
+
339
+ conversationSteps.push({
340
+ type: 'user',
341
+ content: result.content,
342
+ timestamp: Date.now()
343
+ });
344
+
345
+ try {
346
+ const providerStatus = await Agent.ensureProviderReady();
347
+ if (!providerStatus.ready) {
348
+ setMessages((prev: Message[]) => {
349
+ const newMessages = [...prev];
350
+ newMessages.push({
351
+ id: createId(),
352
+ role: "assistant",
353
+ content: `Ollama error: ${providerStatus.error || 'Could not start Ollama. Make sure Ollama is installed.'}`,
354
+ isError: true
355
+ });
356
+ return newMessages;
357
+ });
358
+ setIsProcessing(false);
359
+ return;
360
+ }
361
+
362
+ const agent = new Agent();
363
+ const conversationHistory = [...messages, userMessage]
364
+ .filter((m): m is Message & { role: 'user' | 'assistant' } => m.role === 'user' || m.role === 'assistant')
365
+ .map((m) => ({ role: m.role, content: m.content }));
366
+ let assistantChunk = '';
367
+ let thinkingChunk = '';
368
+ const pendingToolCalls = new Map<string, { toolName: string; args: Record<string, unknown>; messageId?: string }>();
369
+ let assistantMessageId: string | null = null;
370
+ let streamHadError = false;
371
+ titleExtractedRef.current = false;
372
+
373
+ for await (const event of agent.streamMessages(conversationHistory, { abortSignal: abortController.signal })) {
374
+ if (event.type === 'reasoning-delta') {
375
+ thinkingChunk += event.content;
376
+ totalChars += event.content.length;
377
+ setCurrentTokens(estimateTokens());
378
+
379
+ if (assistantMessageId === null) {
380
+ assistantMessageId = createId();
381
+ }
382
+
383
+ const currentMessageId = assistantMessageId;
384
+ setMessages((prev: Message[]) => {
385
+ const newMessages = [...prev];
386
+ const messageIndex = newMessages.findIndex(m => m.id === currentMessageId);
387
+
388
+ if (messageIndex === -1) {
389
+ newMessages.push({ id: currentMessageId, role: "assistant", content: '', thinkingContent: thinkingChunk });
390
+ } else {
391
+ newMessages[messageIndex] = {
392
+ ...newMessages[messageIndex]!,
393
+ thinkingContent: thinkingChunk
394
+ };
395
+ }
396
+ return newMessages;
397
+ });
398
+ } else if (event.type === 'text-delta') {
399
+ assistantChunk += event.content;
400
+ totalChars += event.content.length;
401
+ setCurrentTokens(estimateTokens());
402
+
403
+ const { title, cleanContent, isPending, noTitle } = extractTitle(assistantChunk, titleExtractedRef.current);
404
+
405
+ if (title) {
406
+ titleExtractedRef.current = true;
407
+ currentTitleRef.current = title;
408
+ setCurrentTitle(title);
409
+ setTerminalTitle(title);
410
+ } else if (noTitle) {
411
+ titleExtractedRef.current = true;
412
+ }
413
+
414
+ if (isPending) continue;
415
+
416
+ if (assistantMessageId === null) {
417
+ assistantMessageId = createId();
418
+ }
419
+
420
+ const displayContent = cleanContent;
421
+ const currentMessageId = assistantMessageId;
422
+ setMessages((prev: Message[]) => {
423
+ const newMessages = [...prev];
424
+ const messageIndex = newMessages.findIndex(m => m.id === currentMessageId);
425
+
426
+ if (messageIndex === -1) {
427
+ newMessages.push({ id: currentMessageId, role: "assistant", content: displayContent, thinkingContent: thinkingChunk });
428
+ } else {
429
+ newMessages[messageIndex] = {
430
+ ...newMessages[messageIndex]!,
431
+ content: displayContent
432
+ };
433
+ }
434
+ return newMessages;
435
+ });
436
+ } else if (event.type === 'step-start') {
437
+ stepCount++;
438
+ } else if (event.type === 'tool-call-end') {
439
+ totalChars += JSON.stringify(event.args).length;
440
+ setCurrentTokens(estimateTokens());
441
+
442
+ const needsApproval = event.toolName === 'write' || event.toolName === 'edit' || event.toolName === 'bash';
443
+ const isExploreTool = event.toolName === 'explore';
444
+ const showRunning = event.toolName === 'bash';
445
+ let runningMessageId: string | undefined;
446
+
447
+ if (isExploreTool) {
448
+ setExploreAbortController(abortController);
449
+ exploreToolsRef.current = [];
450
+ const purpose = (event.args.purpose as string) || 'exploring...';
451
+ explorePurposeRef.current = purpose;
452
+ }
453
+
454
+ if (!needsApproval) {
455
+ runningMessageId = createId();
456
+ const { name: toolDisplayName, info: toolInfo } = parseToolHeader(event.toolName, event.args);
457
+ const runningContent = toolInfo ? `${toolDisplayName} (${toolInfo})` : toolDisplayName;
458
+
459
+ if (isExploreTool) {
460
+ exploreMessageIdRef.current = runningMessageId;
461
+ }
462
+
463
+ setMessages((prev: Message[]) => {
464
+ const newMessages = [...prev];
465
+ newMessages.push({
466
+ id: runningMessageId!,
467
+ role: "tool",
468
+ content: runningContent,
469
+ toolName: event.toolName,
470
+ toolArgs: event.args,
471
+ success: true,
472
+ isRunning: showRunning || isExploreTool,
473
+ runningStartTime: (showRunning || isExploreTool) ? Date.now() : undefined
474
+ });
475
+ return newMessages;
476
+ });
477
+ }
478
+
479
+ pendingToolCalls.set(event.toolCallId, {
480
+ toolName: event.toolName,
481
+ args: event.args,
482
+ messageId: runningMessageId
483
+ });
484
+
485
+ } else if (event.type === 'tool-result') {
486
+ const pending = pendingToolCalls.get(event.toolCallId);
487
+ const toolName = pending?.toolName ?? event.toolName;
488
+ const toolArgs = pending?.args ?? {};
489
+ const runningMessageId = pending?.messageId;
490
+ pendingToolCalls.delete(event.toolCallId);
491
+
492
+ if (toolName === 'explore') {
493
+ exploreMessageIdRef.current = null;
494
+ setExploreAbortController(null);
495
+ }
496
+
497
+ const { content: toolContent, success } = formatToolMessage(
498
+ toolName,
499
+ toolArgs,
500
+ event.result,
501
+ { maxLines: DEFAULT_MAX_TOOL_LINES }
502
+ );
503
+
504
+ const toolResultStr = typeof event.result === 'string' ? event.result : JSON.stringify(event.result);
505
+ totalChars += toolResultStr.length;
506
+ setCurrentTokens(estimateTokens());
507
+
508
+ if (assistantChunk.trim()) {
509
+ conversationSteps.push({
510
+ type: 'assistant',
511
+ content: assistantChunk,
512
+ timestamp: Date.now()
513
+ });
514
+ }
515
+
516
+ conversationSteps.push({
517
+ type: 'tool',
518
+ content: toolContent,
519
+ toolName,
520
+ toolArgs,
521
+ toolResult: event.result,
522
+ timestamp: Date.now()
523
+ });
524
+
525
+ setMessages((prev: Message[]) => {
526
+ const newMessages = [...prev];
527
+
528
+ let runningIndex = -1;
529
+ if (runningMessageId) {
530
+ runningIndex = newMessages.findIndex(m => m.id === runningMessageId);
531
+ } else if (toolName === 'bash' || toolName === 'explore') {
532
+ runningIndex = newMessages.findIndex(m => m.toolName === toolName && m.isRunning === true);
533
+ }
534
+
535
+ if (runningIndex !== -1) {
536
+ newMessages[runningIndex] = {
537
+ ...newMessages[runningIndex]!,
538
+ content: toolContent,
539
+ toolArgs: toolArgs,
540
+ toolResult: event.result,
541
+ success,
542
+ isRunning: false,
543
+ runningStartTime: undefined,
544
+ timestamp: Date.now()
545
+ };
546
+ return newMessages;
547
+ }
548
+
549
+ newMessages.push({
550
+ id: createId(),
551
+ role: "tool",
552
+ content: toolContent,
553
+ toolName,
554
+ toolArgs: toolArgs,
555
+ toolResult: event.result,
556
+ success: success,
557
+ timestamp: Date.now()
558
+ });
559
+ return newMessages;
560
+ });
561
+
562
+ assistantChunk = '';
563
+ assistantMessageId = null;
564
+ } else if (event.type === 'error') {
565
+ if (abortController.signal.aborted) {
566
+ notifyAbort();
567
+ streamHadError = true;
568
+ break;
569
+ }
570
+ if (assistantChunk.trim()) {
571
+ conversationSteps.push({
572
+ type: 'assistant',
573
+ content: assistantChunk,
574
+ timestamp: Date.now()
575
+ });
576
+ }
577
+
578
+ const errorContent = formatErrorMessage('API', event.error);
579
+ conversationSteps.push({
580
+ type: 'assistant',
581
+ content: errorContent,
582
+ timestamp: Date.now()
583
+ });
584
+
585
+ setMessages((prev: Message[]) => {
586
+ const newMessages = [...prev];
587
+ newMessages.push({
588
+ id: createId(),
589
+ role: 'assistant',
590
+ content: errorContent,
591
+ isError: true,
592
+ });
593
+ return newMessages;
594
+ });
595
+
596
+ assistantChunk = '';
597
+ assistantMessageId = null;
598
+ streamHadError = true;
599
+ break;
600
+ } else if (event.type === 'finish') {
601
+ if (event.usage && event.usage.totalTokens > 0) {
602
+ totalTokens = {
603
+ prompt: event.usage.promptTokens,
604
+ completion: event.usage.completionTokens,
605
+ total: event.usage.totalTokens
606
+ };
607
+ setCurrentTokens(event.usage.totalTokens);
608
+ }
609
+ }
610
+ }
611
+
612
+ if (abortController.signal.aborted) {
613
+ notifyAbort();
614
+ return;
615
+ }
616
+
617
+ if (!streamHadError && assistantChunk.trim()) {
618
+ conversationSteps.push({
619
+ type: 'assistant',
620
+ content: assistantChunk,
621
+ timestamp: Date.now()
622
+ });
623
+ }
624
+
625
+ const conversationData: ConversationHistory = {
626
+ id: conversationId,
627
+ timestamp: Date.now(),
628
+ steps: conversationSteps,
629
+ totalSteps: stepCount,
630
+ totalTokens: totalTokens.total > 0 ? totalTokens : undefined,
631
+ model: config.model,
632
+ provider: config.provider
633
+ };
634
+
635
+ saveConversation(conversationData);
636
+
637
+ } catch (error) {
638
+ if (abortController.signal.aborted) {
639
+ notifyAbort();
640
+ return;
641
+ }
642
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
643
+ const errorContent = formatErrorMessage('Mosaic', errorMessage);
644
+ setMessages((prev: Message[]) => {
645
+ const newMessages = [...prev];
646
+ if (newMessages[newMessages.length - 1]?.role === 'assistant' && newMessages[newMessages.length - 1]?.content === '') {
647
+ newMessages[newMessages.length - 1] = {
648
+ id: newMessages[newMessages.length - 1]!.id,
649
+ role: "assistant",
650
+ content: errorContent,
651
+ isError: true
652
+ };
653
+ } else {
654
+ newMessages.push({
655
+ id: createId(),
656
+ role: "assistant",
657
+ content: errorContent,
658
+ isError: true
659
+ });
660
+ }
661
+ return newMessages;
662
+ });
663
+ } finally {
664
+ if (abortControllerRef.current === abortController) {
665
+ abortControllerRef.current = null;
666
+ }
667
+ const duration = Date.now() - localStartTime;
668
+ if (duration >= 60000) {
669
+ const blendWord = BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
670
+ setMessages((prev: Message[]) => {
671
+ const newMessages = [...prev];
672
+ for (let i = newMessages.length - 1; i >= 0; i--) {
673
+ if (newMessages[i]?.role === 'assistant') {
674
+ newMessages[i] = { ...newMessages[i]!, responseDuration: duration, blendWord };
675
+ break;
676
+ }
677
+ }
678
+ return newMessages;
679
+ });
680
+ }
681
+ setIsProcessing(false);
682
+ setProcessingStartTime(null);
683
+ }
684
+
685
+ return;
686
+ }
687
+
688
+ const commandMessage: Message = {
689
+ id: createId(),
690
+ role: "slash",
691
+ content: result.content,
692
+ isError: !result.success
693
+ };
694
+
695
+ setMessages((prev: Message[]) => [...prev, commandMessage]);
696
+
697
+ if (result.shouldAddToHistory !== false) {
698
+ addInputToHistory(value.trim());
699
+ }
700
+
701
+ return;
702
+ }
703
+ }
704
+
705
+ const composedContent = hasPastedContent
706
+ ? `${meta!.pastedContent!}${value.trim() ? `\n\n${value}` : ''}`
707
+ : value;
708
+
709
+ addInputToHistory(value.trim() || (hasPastedContent ? '[Pasted text]' : value));
710
+
711
+ saveState(messages);
712
+
713
+ const userMessage: Message = {
714
+ id: createId(),
715
+ role: "user",
716
+ content: composedContent,
717
+ displayContent: meta?.isPaste ? '[Pasted text]' : undefined,
718
+ };
719
+
720
+ setMessages((prev: Message[]) => [...prev, userMessage]);
721
+ setIsProcessing(true);
722
+ const localStartTime = Date.now();
723
+ setProcessingStartTime(localStartTime);
724
+ setCurrentTokens(0);
725
+ shouldAutoScroll.current = true;
726
+
727
+ const conversationId = createId();
728
+ const conversationSteps: ConversationStep[] = [];
729
+ let totalTokens = { prompt: 0, completion: 0, total: 0 };
730
+ let stepCount = 0;
731
+ let totalChars = 0;
732
+ for (const m of messages) {
733
+ if (m.role === 'assistant') {
734
+ totalChars += m.content.length;
735
+ if (m.thinkingContent) totalChars += m.thinkingContent.length;
736
+ } else if (m.role === 'tool') {
737
+ totalChars += m.content.length;
738
+ }
739
+ }
740
+
741
+ const estimateTokens = () => Math.ceil(totalChars / 4);
742
+ setCurrentTokens(estimateTokens());
743
+ const config = readConfig();
744
+ const abortController = new AbortController();
745
+ abortControllerRef.current = abortController;
746
+ let abortNotified = false;
747
+ const notifyAbort = () => {
748
+ if (abortNotified) return;
749
+ abortNotified = true;
750
+ setMessages((prev: Message[]) => {
751
+ const newMessages = [...prev];
752
+ newMessages.push({
753
+ id: createId(),
754
+ role: "tool",
755
+ success: false,
756
+ content: "Generation aborted. \n↪ What should Mosaic do instead?"
757
+ });
758
+ return newMessages;
759
+ });
760
+ };
761
+
762
+ conversationSteps.push({
763
+ type: 'user',
764
+ content: composedContent,
765
+ timestamp: Date.now()
766
+ });
767
+
768
+ try {
769
+ const providerStatus = await Agent.ensureProviderReady();
770
+ if (!providerStatus.ready) {
771
+ setMessages((prev: Message[]) => {
772
+ const newMessages = [...prev];
773
+ newMessages.push({
774
+ id: createId(),
775
+ role: "assistant",
776
+ content: `Ollama error: ${providerStatus.error || 'Could not start Ollama. Make sure Ollama is installed.'}`,
777
+ isError: true
778
+ });
779
+ return newMessages;
780
+ });
781
+ setIsProcessing(false);
782
+ return;
783
+ }
784
+
785
+ const agent = new Agent();
786
+ const conversationHistory = [...messages, userMessage]
787
+ .filter((m): m is Message & { role: 'user' | 'assistant' } => m.role === 'user' || m.role === 'assistant')
788
+ .map((m) => ({ role: m.role, content: m.content }));
789
+ let assistantChunk = '';
790
+ const pendingToolCalls = new Map<string, { toolName: string; args: Record<string, unknown>; messageId?: string }>();
791
+ let assistantMessageId: string | null = null;
792
+ let streamHadError = false;
793
+ titleExtractedRef.current = false;
794
+
795
+ for await (const event of agent.streamMessages(conversationHistory, { abortSignal: abortController.signal })) {
796
+ if (event.type === 'text-delta') {
797
+ assistantChunk += event.content;
798
+ totalChars += event.content.length;
799
+ setCurrentTokens(estimateTokens());
800
+
801
+ const { title, cleanContent, isPending, noTitle } = extractTitle(assistantChunk, titleExtractedRef.current);
802
+
803
+ if (title) {
804
+ titleExtractedRef.current = true;
805
+ currentTitleRef.current = title;
806
+ setCurrentTitle(title);
807
+ setTerminalTitle(title);
808
+ } else if (noTitle) {
809
+ titleExtractedRef.current = true;
810
+ }
811
+
812
+ if (isPending) continue;
813
+
814
+ if (assistantMessageId === null) {
815
+ assistantMessageId = createId();
816
+ }
817
+
818
+ const displayContent = cleanContent;
819
+ const currentMessageId = assistantMessageId;
820
+ setMessages((prev: Message[]) => {
821
+ const newMessages = [...prev];
822
+ const messageIndex = newMessages.findIndex(m => m.id === currentMessageId);
823
+
824
+ if (messageIndex === -1) {
825
+ newMessages.push({ id: currentMessageId, role: "assistant", content: displayContent });
826
+ } else {
827
+ newMessages[messageIndex] = {
828
+ ...newMessages[messageIndex]!,
829
+ content: displayContent
830
+ };
831
+ }
832
+ return newMessages;
833
+ });
834
+ } else if (event.type === 'step-start') {
835
+ stepCount++;
836
+ } else if (event.type === 'tool-call-end') {
837
+ totalChars += JSON.stringify(event.args).length;
838
+ setCurrentTokens(estimateTokens());
839
+
840
+ const isExploreTool = event.toolName === 'explore';
841
+ let runningMessageId: string | undefined;
842
+
843
+ if (isExploreTool) {
844
+ setExploreAbortController(abortController);
845
+ exploreToolsRef.current = [];
846
+ const purpose = (event.args.purpose as string) || 'exploring...';
847
+ explorePurposeRef.current = purpose;
848
+ runningMessageId = createId();
849
+ exploreMessageIdRef.current = runningMessageId;
850
+ const { name: toolDisplayName, info: toolInfo } = parseToolHeader(event.toolName, event.args);
851
+ const runningContent = toolInfo ? `${toolDisplayName} (${toolInfo})` : toolDisplayName;
852
+
853
+ setMessages((prev: Message[]) => {
854
+ const newMessages = [...prev];
855
+ newMessages.push({
856
+ id: runningMessageId!,
857
+ role: "tool",
858
+ content: runningContent,
859
+ toolName: event.toolName,
860
+ toolArgs: event.args,
861
+ success: true,
862
+ isRunning: true,
863
+ runningStartTime: Date.now()
864
+ });
865
+ return newMessages;
866
+ });
867
+ }
868
+
869
+ pendingToolCalls.set(event.toolCallId, {
870
+ toolName: event.toolName,
871
+ args: event.args,
872
+ messageId: runningMessageId
873
+ });
874
+
875
+ } else if (event.type === 'tool-result') {
876
+ const pending = pendingToolCalls.get(event.toolCallId);
877
+ const toolName = pending?.toolName ?? event.toolName;
878
+ const toolArgs = pending?.args ?? {};
879
+ const runningMessageId = pending?.messageId;
880
+ pendingToolCalls.delete(event.toolCallId);
881
+
882
+ if (toolName === 'explore') {
883
+ exploreMessageIdRef.current = null;
884
+ setExploreAbortController(null);
885
+ }
886
+
887
+ const { content: toolContent, success } = formatToolMessage(
888
+ toolName,
889
+ toolArgs,
890
+ event.result,
891
+ { maxLines: DEFAULT_MAX_TOOL_LINES }
892
+ );
893
+
894
+ const toolResultStr = typeof event.result === 'string' ? event.result : JSON.stringify(event.result);
895
+ totalChars += toolResultStr.length;
896
+ setCurrentTokens(estimateTokens());
897
+
898
+ if (assistantChunk.trim()) {
899
+ conversationSteps.push({
900
+ type: 'assistant',
901
+ content: assistantChunk,
902
+ timestamp: Date.now()
903
+ });
904
+ }
905
+
906
+ conversationSteps.push({
907
+ type: 'tool',
908
+ content: toolContent,
909
+ toolName,
910
+ toolArgs,
911
+ toolResult: event.result,
912
+ timestamp: Date.now()
913
+ });
914
+
915
+ setMessages((prev: Message[]) => {
916
+ const newMessages = [...prev];
917
+
918
+ let runningIndex = -1;
919
+ if (runningMessageId) {
920
+ runningIndex = newMessages.findIndex(m => m.id === runningMessageId);
921
+ } else if (toolName === 'bash' || toolName === 'explore') {
922
+ runningIndex = newMessages.findIndex(m => m.isRunning && m.toolName === toolName);
923
+ }
924
+
925
+ if (runningIndex !== -1) {
926
+ newMessages[runningIndex] = {
927
+ ...newMessages[runningIndex]!,
928
+ content: toolContent,
929
+ toolArgs: toolArgs,
930
+ toolResult: event.result,
931
+ success,
932
+ isRunning: false,
933
+ runningStartTime: undefined
934
+ };
935
+ return newMessages;
936
+ }
937
+
938
+ newMessages.push({
939
+ id: createId(),
940
+ role: "tool",
941
+ content: toolContent,
942
+ toolName,
943
+ toolArgs: toolArgs,
944
+ toolResult: event.result,
945
+ success: success
946
+ });
947
+ return newMessages;
948
+ });
949
+
950
+ assistantChunk = '';
951
+ assistantMessageId = null;
952
+ } else if (event.type === 'error') {
953
+ if (abortController.signal.aborted) {
954
+ notifyAbort();
955
+ streamHadError = true;
956
+ break;
957
+ }
958
+ if (assistantChunk.trim()) {
959
+ conversationSteps.push({
960
+ type: 'assistant',
961
+ content: assistantChunk,
962
+ timestamp: Date.now()
963
+ });
964
+ }
965
+
966
+ const errorContent = formatErrorMessage('API', event.error);
967
+ conversationSteps.push({
968
+ type: 'assistant',
969
+ content: errorContent,
970
+ timestamp: Date.now()
971
+ });
972
+
973
+ setMessages((prev: Message[]) => {
974
+ const newMessages = [...prev];
975
+ newMessages.push({
976
+ id: createId(),
977
+ role: 'assistant',
978
+ content: errorContent,
979
+ isError: true,
980
+ });
981
+ return newMessages;
982
+ });
983
+
984
+ assistantChunk = '';
985
+ assistantMessageId = null;
986
+ streamHadError = true;
987
+ break;
988
+ } else if (event.type === 'finish') {
989
+ if (event.usage && event.usage.totalTokens > 0) {
990
+ totalTokens = {
991
+ prompt: event.usage.promptTokens,
992
+ completion: event.usage.completionTokens,
993
+ total: event.usage.totalTokens
994
+ };
995
+ setCurrentTokens(event.usage.totalTokens);
996
+ }
997
+ }
998
+ }
999
+
1000
+ if (abortController.signal.aborted) {
1001
+ notifyAbort();
1002
+ return;
1003
+ }
1004
+
1005
+ if (!streamHadError && assistantChunk.trim()) {
1006
+ conversationSteps.push({
1007
+ type: 'assistant',
1008
+ content: assistantChunk,
1009
+ timestamp: Date.now()
1010
+ });
1011
+ }
1012
+
1013
+ const conversationData: ConversationHistory = {
1014
+ id: conversationId,
1015
+ timestamp: Date.now(),
1016
+ steps: conversationSteps,
1017
+ totalSteps: stepCount,
1018
+ totalTokens: totalTokens.total > 0 ? totalTokens : undefined,
1019
+ model: config.model,
1020
+ provider: config.provider
1021
+ };
1022
+
1023
+ saveConversation(conversationData);
1024
+
1025
+ } catch (error) {
1026
+ if (abortController.signal.aborted) {
1027
+ notifyAbort();
1028
+ return;
1029
+ }
1030
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
1031
+ const errorContent = formatErrorMessage('Mosaic', errorMessage);
1032
+ setMessages((prev: Message[]) => {
1033
+ const newMessages = [...prev];
1034
+ if (newMessages[newMessages.length - 1]?.role === 'assistant' && newMessages[newMessages.length - 1]?.content === '') {
1035
+ newMessages[newMessages.length - 1] = {
1036
+ id: newMessages[newMessages.length - 1]!.id,
1037
+ role: "assistant",
1038
+ content: errorContent,
1039
+ isError: true
1040
+ };
1041
+ } else {
1042
+ newMessages.push({
1043
+ id: createId(),
1044
+ role: "assistant",
1045
+ content: errorContent,
1046
+ isError: true
1047
+ });
1048
+ }
1049
+ return newMessages;
1050
+ });
1051
+ } finally {
1052
+ if (abortControllerRef.current === abortController) {
1053
+ abortControllerRef.current = null;
1054
+ }
1055
+ const duration = Date.now() - localStartTime;
1056
+ if (duration >= 60000) {
1057
+ const blendWord = BLEND_WORDS[Math.floor(Math.random() * BLEND_WORDS.length)];
1058
+ setMessages((prev: Message[]) => {
1059
+ const newMessages = [...prev];
1060
+ for (let i = newMessages.length - 1; i >= 0; i--) {
1061
+ if (newMessages[i]?.role === 'assistant') {
1062
+ newMessages[i] = { ...newMessages[i]!, responseDuration: duration, blendWord };
1063
+ break;
1064
+ }
1065
+ }
1066
+ return newMessages;
1067
+ });
1068
+ }
1069
+ setIsProcessing(false);
1070
+ setProcessingStartTime(null);
1071
+ }
1072
+ };
1073
+
1074
+ useEffect(() => {
1075
+ if (initialMessage && !initialMessageProcessed.current && currentPage === "chat") {
1076
+ initialMessageProcessed.current = true;
1077
+ handleSubmit(initialMessage);
1078
+ }
1079
+ }, [initialMessage, currentPage, handleSubmit]);
1080
+
1081
+ if (currentPage === "home") {
1082
+ const handleHomeSubmit = (value: string, meta?: InputSubmitMeta) => {
1083
+ const hasPastedContent = Boolean(meta?.isPaste && meta.pastedContent);
1084
+ if (!value.trim() && !hasPastedContent) return;
1085
+ setCurrentPage("chat");
1086
+ handleSubmit(value, meta);
1087
+ };
1088
+
1089
+ return (
1090
+ <HomePage
1091
+ onSubmit={handleHomeSubmit}
1092
+ pasteRequestId={pasteRequestId}
1093
+ shortcutsOpen={shortcutsOpen}
1094
+ />
1095
+ );
1096
+ }
1097
+
1098
+ return (
1099
+ <ChatPage
1100
+ messages={messages}
1101
+ isProcessing={isProcessing}
1102
+ processingStartTime={processingStartTime}
1103
+ currentTokens={currentTokens}
1104
+ scrollOffset={scrollOffset}
1105
+ terminalHeight={terminalHeight}
1106
+ terminalWidth={terminalWidth}
1107
+ pasteRequestId={pasteRequestId}
1108
+ shortcutsOpen={shortcutsOpen}
1109
+ onSubmit={handleSubmit}
1110
+ />
1111
+ );
1112
+ }