@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,606 @@
1
+ /** @jsxImportSource react */
2
+ import { useState, useEffect, useRef, useCallback } from 'react';
3
+ import ReactDOM from 'react-dom/client';
4
+ import { HomePage } from './components/HomePage';
5
+ import { ChatPage } from './components/ChatPage';
6
+ import { Message } from './types';
7
+ import { createId, extractTitle, setDocumentTitle, formatToolMessage, parseToolHeader, formatErrorMessage, DEFAULT_MAX_TOOL_LINES, getRandomBlendWord } from './utils';
8
+ import { Conversation, getAllConversations, getConversation, saveConversation, deleteConversation, createNewConversation } from './storage';
9
+ import { QuestionRequest } from '../utils/questionBridge';
10
+ import { ApprovalRequest } from '../utils/approvalBridge';
11
+ import { parseRoute, navigateTo, replaceTo, Route } from './router';
12
+ import './assets/css/global.css'
13
+
14
+ import { Modal } from './components/Modal';
15
+
16
+ function useRouter() {
17
+ const [route, setRoute] = useState<Route>(() => parseRoute(window.location.pathname));
18
+
19
+ useEffect(() => {
20
+ const handlePopState = () => {
21
+ setRoute(parseRoute(window.location.pathname));
22
+ };
23
+
24
+ window.addEventListener('popstate', handlePopState);
25
+ return () => window.removeEventListener('popstate', handlePopState);
26
+ }, []);
27
+
28
+ return route;
29
+ }
30
+
31
+ function App() {
32
+ const route = useRouter();
33
+ const currentPage = route.page;
34
+ const [messages, setMessages] = useState<Message[]>([]);
35
+ const [isProcessing, setIsProcessing] = useState(false);
36
+ const [processingStartTime, setProcessingStartTime] = useState<number | undefined>(undefined);
37
+ const [currentTokens, setCurrentTokens] = useState(0);
38
+ const [, setTimerTick] = useState(0);
39
+ const [isSidebarExpanded, setIsSidebarExpanded] = useState(false);
40
+ const [activeModal, setActiveModal] = useState<'none' | 'settings' | 'help'>('none');
41
+ const [currentTitle, setCurrentTitle] = useState<string | null>(null);
42
+ const [currentConversation, setCurrentConversation] = useState<Conversation | null>(null);
43
+ const [conversations, setConversations] = useState<Conversation[]>([]);
44
+ const [workspace, setWorkspace] = useState<string | null>(null);
45
+ const [questionRequest, setQuestionRequest] = useState<QuestionRequest | null>(null);
46
+ const [approvalRequest, setApprovalRequest] = useState<ApprovalRequest | null>(null);
47
+
48
+ const refreshConversations = useCallback(() => {
49
+ setConversations(getAllConversations());
50
+ }, []);
51
+
52
+ const handleStopAgent = useCallback(async () => {
53
+ try {
54
+ await fetch('/api/stop', { method: 'POST' });
55
+ setIsProcessing(false);
56
+ setProcessingStartTime(undefined);
57
+ setQuestionRequest(null);
58
+ setApprovalRequest(null);
59
+ setMessages((prev) => [
60
+ ...prev,
61
+ {
62
+ id: createId(),
63
+ role: 'tool',
64
+ content: "Generation aborted. What should Mosaic do instead?",
65
+ toolName: 'stop',
66
+ success: false,
67
+ },
68
+ ]);
69
+ } catch (error) {
70
+ console.error('Failed to stop agent:', error);
71
+ }
72
+ }, []);
73
+
74
+ useEffect(() => {
75
+ refreshConversations();
76
+ fetch('/api/workspace')
77
+ .then(res => res.json())
78
+ .then(data => setWorkspace(data.workspace))
79
+ .catch(() => { });
80
+ }, [refreshConversations]);
81
+
82
+ useEffect(() => {
83
+ if (route.page === 'chat' && route.conversationId) {
84
+ const conversation = getConversation(route.conversationId);
85
+ if (conversation) {
86
+ setCurrentConversation(conversation);
87
+ setMessages(conversation.messages);
88
+ setCurrentTitle(conversation.title);
89
+ if (conversation.title) {
90
+ setDocumentTitle(conversation.title);
91
+ }
92
+ if (conversation.workspace) {
93
+ setWorkspace(conversation.workspace);
94
+ fetch('/api/workspace', {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({ path: conversation.workspace }),
98
+ }).catch(() => { });
99
+ }
100
+ } else {
101
+ navigateTo({ page: 'home' });
102
+ }
103
+ } else if (route.page === 'chat' && !route.conversationId) {
104
+ setCurrentConversation(null);
105
+ setMessages([]);
106
+ setCurrentTitle(null);
107
+ document.title = 'Mosaic';
108
+ } else if (route.page === 'home') {
109
+ setCurrentConversation(null);
110
+ setMessages([]);
111
+ setCurrentTitle(null);
112
+ document.title = 'Mosaic';
113
+ }
114
+ }, [route]);
115
+
116
+ useEffect(() => {
117
+ const timerInterval = setInterval(() => {
118
+ setTimerTick((t) => t + 1);
119
+ }, 1000);
120
+ return () => clearInterval(timerInterval);
121
+ }, []);
122
+
123
+ useEffect(() => {
124
+ if (currentConversation && messages.length > 0) {
125
+ const updatedConversation: Conversation = {
126
+ ...currentConversation,
127
+ messages,
128
+ title: currentTitle,
129
+ workspace: currentConversation.workspace || workspace,
130
+ updatedAt: Date.now(),
131
+ };
132
+ saveConversation(updatedConversation);
133
+ setCurrentConversation(updatedConversation);
134
+ refreshConversations();
135
+ }
136
+ }, [messages, currentTitle]);
137
+
138
+ const handleSendMessage = async (content: string) => {
139
+ if (!content.trim() || isProcessing) return;
140
+
141
+ const userMessage: Message = {
142
+ id: createId(),
143
+ role: 'user',
144
+ content: content,
145
+ };
146
+
147
+ let conversation = currentConversation;
148
+ if (!conversation) {
149
+ conversation = createNewConversation(workspace);
150
+ setCurrentConversation(conversation);
151
+ replaceTo({ page: 'chat', conversationId: conversation.id });
152
+ }
153
+
154
+ if (currentPage === 'home') {
155
+ navigateTo({ page: 'chat', conversationId: conversation.id });
156
+ }
157
+
158
+ setMessages((prev) => [...prev, userMessage]);
159
+ setIsProcessing(true);
160
+ setProcessingStartTime(Date.now());
161
+ setCurrentTokens(0);
162
+ let totalChars = 0;
163
+ const estimateTokens = () => Math.ceil(totalChars / 4);
164
+
165
+ try {
166
+ const response = await fetch('/api/message', {
167
+ method: 'POST',
168
+ headers: { 'Content-Type': 'application/json' },
169
+ body: JSON.stringify({
170
+ message: userMessage.content,
171
+ history: messages
172
+ .filter((m) => m.role === 'user' || m.role === 'assistant')
173
+ .map((m) => ({ role: m.role, content: m.content })),
174
+ }),
175
+ });
176
+
177
+ if (!response.ok) {
178
+ throw new Error(`HTTP error! status: ${response.status}`);
179
+ }
180
+
181
+ const reader = response.body?.getReader();
182
+ const decoder = new TextDecoder();
183
+ let assistantChunk = '';
184
+ let thinkingChunk = '';
185
+ let assistantMessageId: string | null = null;
186
+ let assistantStartTime: number | null = null;
187
+ let titleExtracted = false;
188
+ const pendingToolCalls = new Map<string, { toolName: string; args: Record<string, unknown>; messageId: string }>();
189
+ let exploreMessageId: string | null = null;
190
+ let exploreTools: Array<{ tool: string; info: string; success: boolean }> = [];
191
+ let explorePurpose = '';
192
+
193
+ while (reader) {
194
+ const { done, value } = await reader.read();
195
+ if (done) break;
196
+
197
+ const chunk = decoder.decode(value);
198
+ const lines = chunk.split('\n').filter((line) => line.trim());
199
+
200
+ for (const line of lines) {
201
+ try {
202
+ const event = JSON.parse(line);
203
+
204
+ if (event.type === 'reasoning-delta') {
205
+ thinkingChunk += event.content;
206
+ totalChars += event.content.length;
207
+ setCurrentTokens(estimateTokens());
208
+
209
+ if (assistantMessageId === null) {
210
+ assistantMessageId = createId();
211
+ assistantStartTime = Date.now();
212
+ }
213
+
214
+ setMessages((prev) => {
215
+ const newMessages = [...prev];
216
+ const messageIndex = newMessages.findIndex((m) => m.id === assistantMessageId);
217
+
218
+ if (messageIndex === -1) {
219
+ newMessages.push({
220
+ id: assistantMessageId!,
221
+ role: 'assistant',
222
+ content: assistantChunk,
223
+ thinkingContent: thinkingChunk,
224
+ });
225
+ } else {
226
+ newMessages[messageIndex] = {
227
+ ...newMessages[messageIndex]!,
228
+ thinkingContent: thinkingChunk,
229
+ };
230
+ }
231
+ return newMessages;
232
+ });
233
+ } else if (event.type === 'text-delta') {
234
+ assistantChunk += event.content;
235
+ totalChars += event.content.length;
236
+ setCurrentTokens(estimateTokens());
237
+
238
+ const { title, cleanContent, isPending, noTitle } = extractTitle(assistantChunk, titleExtracted);
239
+
240
+ if (title) {
241
+ titleExtracted = true;
242
+ setCurrentTitle(title);
243
+ setDocumentTitle(title);
244
+ } else if (noTitle) {
245
+ titleExtracted = true;
246
+ }
247
+
248
+ if (isPending) continue;
249
+
250
+ if (assistantMessageId === null) {
251
+ assistantMessageId = createId();
252
+ assistantStartTime = Date.now();
253
+ }
254
+
255
+ setMessages((prev) => {
256
+ const newMessages = [...prev];
257
+ const messageIndex = newMessages.findIndex((m) => m.id === assistantMessageId);
258
+
259
+ if (messageIndex === -1) {
260
+ newMessages.push({
261
+ id: assistantMessageId!,
262
+ role: 'assistant',
263
+ content: cleanContent,
264
+ thinkingContent: thinkingChunk,
265
+ });
266
+ } else {
267
+ newMessages[messageIndex] = {
268
+ ...newMessages[messageIndex]!,
269
+ content: cleanContent,
270
+ };
271
+ }
272
+ return newMessages;
273
+ });
274
+ } else if (event.type === 'tool-call-end') {
275
+ totalChars += JSON.stringify(event.args || {}).length;
276
+ setCurrentTokens(estimateTokens());
277
+
278
+ const toolCallMessageId = createId();
279
+ const toolName = event.toolName;
280
+ const args = event.args || {};
281
+
282
+ const needsApproval = toolName === 'write' || toolName === 'edit' || toolName === 'bash';
283
+ const isBashTool = toolName === 'bash';
284
+ const isExploreTool = toolName === 'explore';
285
+
286
+ const { name: toolDisplayName, info: toolInfo } = parseToolHeader(toolName, args);
287
+ const runningContent = toolInfo ? `${toolDisplayName} (${toolInfo})` : toolDisplayName;
288
+
289
+ pendingToolCalls.set(event.toolCallId, {
290
+ toolName,
291
+ args,
292
+ messageId: toolCallMessageId,
293
+ });
294
+
295
+ if (isExploreTool) {
296
+ exploreMessageId = toolCallMessageId;
297
+ exploreTools = [];
298
+ explorePurpose = (args.purpose as string) || 'exploring...';
299
+ console.log('[CLIENT] Set exploreMessageId:', exploreMessageId);
300
+ }
301
+
302
+ if (!needsApproval) {
303
+ setMessages((prev) => [
304
+ ...prev,
305
+ {
306
+ id: toolCallMessageId,
307
+ role: 'tool',
308
+ content: runningContent,
309
+ toolName,
310
+ toolArgs: args,
311
+ success: true,
312
+ isRunning: isBashTool || isExploreTool,
313
+ runningStartTime: (isBashTool || isExploreTool) ? Date.now() : undefined,
314
+ },
315
+ ]);
316
+ }
317
+ } else if (event.type === 'explore-tool') {
318
+ console.log('[CLIENT] explore-tool received:', event.toolName, 'exploreMessageId:', exploreMessageId);
319
+ const info = (event.args?.path || event.args?.pattern || event.args?.query || '') as string;
320
+ const shortInfo = info.length > 40 ? info.substring(0, 37) + '...' : info;
321
+ exploreTools.push({ tool: event.toolName, info: shortInfo, success: event.success });
322
+
323
+ totalChars += event.tokenEstimate * 4;
324
+ setCurrentTokens(estimateTokens());
325
+
326
+ if (exploreMessageId) {
327
+ console.log('[CLIENT] Updating message:', exploreMessageId);
328
+ setMessages((prev) => {
329
+ const newMessages = [...prev];
330
+ const idx = newMessages.findIndex(m => m.id === exploreMessageId);
331
+ console.log('[CLIENT] Found message at index:', idx);
332
+ if (idx !== -1) {
333
+ const toolLines = exploreTools.map(t => {
334
+ const icon = t.success ? '+' : '-';
335
+ return ` ${icon} ${t.tool}(${t.info})`;
336
+ });
337
+ const newContent = `Explore (${explorePurpose})\n${toolLines.join('\n')}`;
338
+ newMessages[idx] = { ...newMessages[idx]!, content: newContent };
339
+ }
340
+ return newMessages;
341
+ });
342
+ } else {
343
+ console.log('[CLIENT] exploreMessageId is null!');
344
+ }
345
+ } else if (event.type === 'tool-result') {
346
+ const toolResultStr = typeof event.result === 'string' ? event.result : JSON.stringify(event.result);
347
+ totalChars += toolResultStr.length;
348
+ setCurrentTokens(estimateTokens());
349
+
350
+ const pending = pendingToolCalls.get(event.toolCallId);
351
+ const toolName = pending?.toolName ?? event.toolName;
352
+ const toolArgs = pending?.args ?? {};
353
+ const runningMessageId = pending?.messageId;
354
+ pendingToolCalls.delete(event.toolCallId);
355
+
356
+ if (toolName === 'explore') {
357
+ exploreMessageId = null;
358
+ }
359
+
360
+ const { content: toolContent, success } = formatToolMessage(
361
+ toolName,
362
+ toolArgs,
363
+ event.result,
364
+ { maxLines: DEFAULT_MAX_TOOL_LINES }
365
+ );
366
+
367
+ setMessages((prev) => {
368
+ const newMessages = [...prev];
369
+
370
+ let runningIndex = -1;
371
+ if (runningMessageId) {
372
+ runningIndex = newMessages.findIndex((m) => m.id === runningMessageId);
373
+ } else if (toolName === 'bash' || toolName === 'explore') {
374
+ runningIndex = newMessages.findIndex((m) => m.toolName === toolName && m.isRunning === true);
375
+ }
376
+
377
+ if (runningIndex !== -1) {
378
+ newMessages[runningIndex] = {
379
+ ...newMessages[runningIndex]!,
380
+ content: toolContent,
381
+ toolArgs: toolArgs,
382
+ toolResult: event.result,
383
+ success,
384
+ isRunning: false,
385
+ runningStartTime: undefined,
386
+ };
387
+ return newMessages;
388
+ }
389
+
390
+ newMessages.push({
391
+ id: createId(),
392
+ role: 'tool',
393
+ content: toolContent,
394
+ toolName,
395
+ toolArgs,
396
+ toolResult: event.result,
397
+ success,
398
+ });
399
+ return newMessages;
400
+ });
401
+
402
+ assistantChunk = '';
403
+ thinkingChunk = '';
404
+ assistantMessageId = null;
405
+ assistantStartTime = null;
406
+ } else if (event.type === 'question') {
407
+ setQuestionRequest(event.request);
408
+ } else if (event.type === 'approval') {
409
+ setApprovalRequest(event.request);
410
+ } else if (event.type === 'finish' || event.type === 'step-finish') {
411
+ if (assistantMessageId && assistantStartTime) {
412
+ const responseDuration = Date.now() - assistantStartTime;
413
+ if (responseDuration > 60000) {
414
+ const blendWord = getRandomBlendWord();
415
+ setMessages((prev) => {
416
+ const newMessages = [...prev];
417
+ const idx = newMessages.findIndex(m => m.id === assistantMessageId);
418
+ if (idx !== -1) {
419
+ newMessages[idx] = {
420
+ ...newMessages[idx]!,
421
+ responseDuration,
422
+ blendWord,
423
+ };
424
+ }
425
+ return newMessages;
426
+ });
427
+ }
428
+ }
429
+ break;
430
+ } else if (event.type === 'stopped') {
431
+ break;
432
+ } else if (event.type === 'error') {
433
+ const errorContent = formatErrorMessage('API', event.error);
434
+ setMessages((prev) => [
435
+ ...prev,
436
+ {
437
+ id: createId(),
438
+ role: 'assistant',
439
+ content: errorContent,
440
+ isError: true,
441
+ },
442
+ ]);
443
+ break;
444
+ }
445
+ } catch (parseError) {
446
+ console.error('Failed to parse event:', parseError);
447
+ }
448
+ }
449
+ }
450
+ } catch (error) {
451
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
452
+ const errorContent = formatErrorMessage('Mosaic', errorMessage);
453
+ setMessages((prev) => [
454
+ ...prev,
455
+ {
456
+ id: createId(),
457
+ role: 'assistant',
458
+ content: errorContent,
459
+ isError: true,
460
+ },
461
+ ]);
462
+ } finally {
463
+ setIsProcessing(false);
464
+ setProcessingStartTime(undefined);
465
+ }
466
+ };
467
+
468
+ const handleLoadConversation = (conversationId: string) => {
469
+ navigateTo({ page: 'chat', conversationId });
470
+ };
471
+
472
+ const handleDeleteConversation = (conversationId: string) => {
473
+ deleteConversation(conversationId);
474
+ refreshConversations();
475
+
476
+ if (currentConversation?.id === conversationId) {
477
+ navigateTo({ page: 'home' });
478
+ }
479
+ };
480
+
481
+ const handleRenameConversation = (conversationId: string, newTitle: string) => {
482
+ const conversation = getConversation(conversationId);
483
+ if (conversation) {
484
+ const updated: Conversation = {
485
+ ...conversation,
486
+ title: newTitle,
487
+ updatedAt: Date.now(),
488
+ };
489
+ saveConversation(updated);
490
+ refreshConversations();
491
+
492
+ if (currentConversation?.id === conversationId) {
493
+ setCurrentConversation(updated);
494
+ setCurrentTitle(newTitle);
495
+ setDocumentTitle(newTitle);
496
+ }
497
+ }
498
+ };
499
+
500
+ const handleNavigateHome = () => {
501
+ navigateTo({ page: 'home' });
502
+ };
503
+
504
+ const sidebarProps = {
505
+ isExpanded: isSidebarExpanded,
506
+ onToggleExpand: () => setIsSidebarExpanded(!isSidebarExpanded),
507
+ onNavigateToNewChat: () => {
508
+ navigateTo({ page: 'chat', conversationId: null });
509
+ },
510
+ onNavigateHome: handleNavigateHome,
511
+ onOpenSettings: () => setActiveModal('settings'),
512
+ onOpenHelp: () => setActiveModal('help'),
513
+ conversations,
514
+ currentConversationId: currentConversation?.id || null,
515
+ onLoadConversation: handleLoadConversation,
516
+ onDeleteConversation: handleDeleteConversation,
517
+ onRenameConversation: handleRenameConversation,
518
+ };
519
+
520
+ return (
521
+ <>
522
+ {currentPage === 'home' ? (
523
+ <HomePage
524
+ onStartChat={handleSendMessage}
525
+ onOpenProject={async (path) => {
526
+ try {
527
+ const res = await fetch('/api/workspace', {
528
+ method: 'POST',
529
+ headers: { 'Content-Type': 'application/json' },
530
+ body: JSON.stringify({ path }),
531
+ });
532
+
533
+ if (res.ok) {
534
+ setWorkspace(path);
535
+ navigateTo({ page: 'chat', conversationId: null });
536
+
537
+ await fetch('/api/add-recent-project', {
538
+ method: 'POST',
539
+ headers: { 'Content-Type': 'application/json' },
540
+ body: JSON.stringify({ path }),
541
+ });
542
+ }
543
+ } catch (error) {
544
+ console.error('Failed to open project:', error);
545
+ }
546
+ }}
547
+ sidebarProps={sidebarProps}
548
+ />
549
+ ) : (
550
+ <ChatPage
551
+ messages={messages}
552
+ isProcessing={isProcessing}
553
+ processingStartTime={processingStartTime}
554
+ currentTokens={currentTokens}
555
+ onSendMessage={handleSendMessage}
556
+ onStopAgent={handleStopAgent}
557
+ sidebarProps={sidebarProps}
558
+ currentTitle={currentTitle}
559
+ workspace={workspace}
560
+ questionRequest={questionRequest}
561
+ approvalRequest={approvalRequest}
562
+ />
563
+ )}
564
+
565
+ <Modal
566
+ isOpen={activeModal === 'settings'}
567
+ onClose={() => setActiveModal('none')}
568
+ title="Settings"
569
+ >
570
+ <div>
571
+ <h3 style={{ marginBottom: '0.5rem', color: 'var(--text-primary)' }}>Application Settings</h3>
572
+ <p>Customize your Mosaic experience here.</p>
573
+ <div style={{ marginTop: '1rem' }}>
574
+ <div style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-subtle)' }}>
575
+ <label style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
576
+ <span>Dark Mode</span>
577
+ <input type="checkbox" checked readOnly />
578
+ </label>
579
+ </div>
580
+ </div>
581
+ </div>
582
+ </Modal>
583
+
584
+ <Modal
585
+ isOpen={activeModal === 'help'}
586
+ onClose={() => setActiveModal('none')}
587
+ title="Help & Support"
588
+ >
589
+ <div>
590
+ <h3 style={{ marginBottom: '0.5rem', color: 'var(--text-primary)' }}>Getting Started</h3>
591
+ <p>Welcome to Mosaic! Use the sidebar to navigate between your projects and chat history.</p>
592
+ <ul style={{ marginTop: '1rem', paddingLeft: '1.5rem' }}>
593
+ <li>Use <strong>+</strong> to start a new chat.</li>
594
+ <li>Use the layout button to expand/collapse the sidebar.</li>
595
+ <li>Type your message in the chat input to interact with the AI.</li>
596
+ </ul>
597
+ </div>
598
+ </Modal>
599
+
600
+ </>
601
+ );
602
+ }
603
+
604
+
605
+ const root = ReactDOM.createRoot(document.getElementById('root')!);
606
+ root.render(<App />);