@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,273 @@
1
+ /** @jsxImportSource react */
2
+ import React, { useState, useEffect, useRef } from 'react';
3
+ import { Message } from '../types';
4
+ import { MessageItem } from './MessageItem';
5
+ import { Sidebar, SidebarProps } from './Sidebar';
6
+ import { QuestionRequest } from '../../utils/questionBridge';
7
+ import { ApprovalRequest } from '../../utils/approvalBridge';
8
+ import { QuestionPanel } from './QuestionPanel';
9
+ import { ApprovalPanel } from './ApprovalPanel';
10
+ import { ThinkingIndicator } from './ThinkingIndicator';
11
+ import { findModelsDevModelById, modelAcceptsImages } from '../../utils/models';
12
+ import '../assets/css/global.css'
13
+
14
+ interface ChatPageProps {
15
+ messages: Message[];
16
+ isProcessing: boolean;
17
+ processingStartTime?: number;
18
+ currentTokens?: number;
19
+ onSendMessage: (message: string) => void;
20
+ onStopAgent?: () => void;
21
+ sidebarProps: SidebarProps;
22
+ currentTitle?: string | null;
23
+ workspace?: string | null;
24
+ questionRequest?: QuestionRequest | null;
25
+ approvalRequest?: ApprovalRequest | null;
26
+ }
27
+
28
+ function formatWorkspace(path: string | null | undefined): string {
29
+ if (!path) return '';
30
+
31
+ let normalized = path.replace(/\\/g, '/');
32
+
33
+ const homePatterns = [
34
+ /^\/Users\/[^/]+/,
35
+ /^\/home\/[^/]+/,
36
+ /^[A-Z]:\/Users\/[^/]+/i,
37
+ ];
38
+
39
+ for (const pattern of homePatterns) {
40
+ if (pattern.test(normalized)) {
41
+ normalized = normalized.replace(pattern, '~');
42
+ break;
43
+ }
44
+ }
45
+
46
+ const parts = normalized.split('/').filter(Boolean);
47
+ const maxLength = 35;
48
+
49
+ if (normalized.length > maxLength && parts.length > 3) {
50
+ const isHome = normalized.startsWith('~');
51
+ const lastParts = parts.slice(-2).join('/');
52
+ return isHome ? `~/.../` + lastParts : '.../' + lastParts;
53
+ }
54
+
55
+ return normalized;
56
+ }
57
+
58
+ export function ChatPage({ messages, isProcessing, processingStartTime, currentTokens, onSendMessage, onStopAgent, sidebarProps, currentTitle, workspace, questionRequest, approvalRequest }: ChatPageProps) {
59
+ const [inputValue, setInputValue] = useState('');
60
+ const [showAttachButton, setShowAttachButton] = useState(false);
61
+ const messagesEndRef = useRef<HTMLDivElement>(null);
62
+ const inputRef = useRef<HTMLTextAreaElement>(null);
63
+
64
+ useEffect(() => {
65
+ if (messagesEndRef.current) {
66
+ messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
67
+ }
68
+ }, [messages, questionRequest, approvalRequest]);
69
+
70
+ useEffect(() => {
71
+ if (inputRef.current && !isProcessing && !questionRequest && !approvalRequest) {
72
+ inputRef.current.focus();
73
+ }
74
+ }, [isProcessing, questionRequest, approvalRequest]);
75
+
76
+ useEffect(() => {
77
+ if (inputRef.current) inputRef.current.focus();
78
+ }, []);
79
+
80
+ useEffect(() => {
81
+ const handleKeyDown = (e: KeyboardEvent) => {
82
+ if (e.key === 'Escape' && isProcessing && onStopAgent) {
83
+ e.preventDefault();
84
+ onStopAgent();
85
+ }
86
+ };
87
+
88
+ document.addEventListener('keydown', handleKeyDown);
89
+ return () => document.removeEventListener('keydown', handleKeyDown);
90
+ }, [isProcessing, onStopAgent]);
91
+
92
+ useEffect(() => {
93
+ const checkModelSupport = async () => {
94
+ try {
95
+ const configRes = await fetch('/api/config');
96
+ if (!configRes.ok) return;
97
+
98
+ const { model } = await configRes.json();
99
+
100
+ if (model) {
101
+ // Try to find the model using the enhanced fuzzy search
102
+ const result = await findModelsDevModelById(model);
103
+
104
+ if (result && result.model) {
105
+ setShowAttachButton(modelAcceptsImages(result.model));
106
+ } else {
107
+ // Very basic fallback if even fuzzy search fails
108
+ const lowerId = model.toLowerCase();
109
+ const likelySupportsImages =
110
+ lowerId.includes('gpt-4') ||
111
+ lowerId.includes('gpt-5') ||
112
+ lowerId.includes('claude-3') ||
113
+ lowerId.includes('gemini') ||
114
+ lowerId.includes('vision');
115
+ setShowAttachButton(likelySupportsImages);
116
+ }
117
+ }
118
+ } catch (err) {
119
+ console.error('Failed to check model support:', err);
120
+ }
121
+ };
122
+
123
+ checkModelSupport();
124
+ }, []);
125
+
126
+ const handleSubmit = (e?: React.FormEvent) => {
127
+ if (e) e.preventDefault();
128
+ if (!inputValue.trim() || isProcessing) return;
129
+ onSendMessage(inputValue);
130
+ setInputValue('');
131
+ };
132
+
133
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
134
+ if (e.key === 'Enter' && !e.shiftKey) {
135
+ e.preventDefault();
136
+ handleSubmit();
137
+ }
138
+ };
139
+
140
+ const handleQuestionAnswer = async (index: number, customText?: string) => {
141
+ try {
142
+ await fetch('/api/question/answer', {
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json' },
145
+ body: JSON.stringify({ index, customText })
146
+ });
147
+ } catch (err) {
148
+ console.error(err);
149
+ }
150
+ };
151
+
152
+ const handleApprovalResponse = async (approved: boolean, customResponse?: string) => {
153
+ try {
154
+ await fetch('/api/approval/respond', {
155
+ method: 'POST',
156
+ headers: { 'Content-Type': 'application/json' },
157
+ body: JSON.stringify({ approved, customResponse })
158
+ });
159
+ } catch (err) {
160
+ console.error(err);
161
+ }
162
+ };
163
+
164
+ const formattedWorkspace = formatWorkspace(workspace);
165
+
166
+ return (
167
+ <div className="home-page">
168
+ <Sidebar {...sidebarProps} />
169
+
170
+ <div className="main-content" style={{ padding: 0 }}>
171
+ <div className="chat-page">
172
+ {(currentTitle || workspace) && (
173
+ <div className="chat-title-bar">
174
+ <span className="chat-title">{currentTitle || ''}</span>
175
+ {formattedWorkspace && (
176
+ <span className="chat-workspace" title={workspace || ''}>
177
+ {formattedWorkspace}
178
+ </span>
179
+ )}
180
+ </div>
181
+ )}
182
+ <div className="chat-container">
183
+ <div className="messages">
184
+ {messages.map((msg) => (
185
+ <MessageItem key={msg.id} message={msg} />
186
+ ))}
187
+ {isProcessing && !questionRequest && !approvalRequest && (
188
+ <div className="message assistant">
189
+ <div className="message-content">
190
+ <ThinkingIndicator startTime={processingStartTime} tokens={currentTokens} />
191
+ </div>
192
+ </div>
193
+ )}
194
+
195
+ {questionRequest && (
196
+ <div className="message assistant">
197
+ <div className="message-content" style={{ width: '100%', maxWidth: '100%' }}>
198
+ <QuestionPanel
199
+ request={questionRequest}
200
+ onAnswer={handleQuestionAnswer}
201
+ />
202
+ </div>
203
+ </div>
204
+ )}
205
+
206
+ {approvalRequest && (
207
+ <div className="message assistant">
208
+ <div className="message-content" style={{ width: '100%', maxWidth: '100%' }}>
209
+ <ApprovalPanel
210
+ request={approvalRequest}
211
+ onRespond={handleApprovalResponse}
212
+ />
213
+ </div>
214
+ </div>
215
+ )}
216
+
217
+ <div ref={messagesEndRef} />
218
+ </div>
219
+
220
+ <form onSubmit={handleSubmit} className="input-area">
221
+ <textarea
222
+ ref={inputRef}
223
+ value={inputValue}
224
+ onChange={(e) => setInputValue(e.target.value)}
225
+ onKeyDown={handleKeyDown}
226
+ placeholder="Type your message..."
227
+ rows={2}
228
+ disabled={isProcessing || !!questionRequest || !!approvalRequest}
229
+ />
230
+ <div className="input-actions">
231
+ <div className="input-actions-left">
232
+ {showAttachButton && (
233
+ <button type="button" className="send-btn" disabled={isProcessing} title="Attach file">
234
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ transform: 'rotate(-45deg)' }}>
235
+ <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
236
+ </svg>
237
+ </button>
238
+ )}
239
+ </div>
240
+ <div className="input-actions-right">
241
+ {isProcessing ? (
242
+ <button
243
+ type="button"
244
+ className="send-btn stop"
245
+ onClick={onStopAgent}
246
+ title="Stop (Esc)"
247
+ >
248
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
249
+ <rect x="6" y="6" width="12" height="12" rx="1" />
250
+ </svg>
251
+ </button>
252
+ ) : (
253
+ <button
254
+ type="submit"
255
+ className="send-btn"
256
+ disabled={!inputValue.trim() || !!questionRequest || !!approvalRequest}
257
+ title="Send"
258
+ >
259
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
260
+ <line x1="12" y1="19" x2="12" y2="5"></line>
261
+ <polyline points="5 12 12 5 19 12"></polyline>
262
+ </svg>
263
+ </button>
264
+ )}
265
+ </div>
266
+ </div>
267
+ </form>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ );
273
+ }
@@ -0,0 +1,162 @@
1
+ /** @jsxImportSource react */
2
+ import { useState, useEffect } from 'react';
3
+ import '../assets/css/FileExplorer.css';
4
+
5
+ interface FileInfo {
6
+ name: string;
7
+ isDirectory: boolean;
8
+ path: string;
9
+ }
10
+
11
+ interface FileExplorerProps {
12
+ onSelect: (path: string) => void;
13
+ onCancel: () => void;
14
+ initialPath?: string;
15
+ }
16
+
17
+ export function FileExplorer({ onSelect, onCancel, initialPath }: FileExplorerProps) {
18
+ const [currentPath, setCurrentPath] = useState<string>(initialPath || '');
19
+ const [files, setFiles] = useState<FileInfo[]>([]);
20
+ const [selectedFile, setSelectedFile] = useState<FileInfo | null>(null);
21
+ const [isLoading, setIsLoading] = useState(false);
22
+
23
+ useEffect(() => {
24
+ if (!currentPath) {
25
+ fetch('/api/workspace')
26
+ .then(res => res.json())
27
+ .then(data => setCurrentPath(data.workspace))
28
+ .catch(() => { });
29
+ }
30
+ }, [initialPath]);
31
+
32
+ useEffect(() => {
33
+ if (!currentPath) return;
34
+
35
+ async function fetchFiles() {
36
+ setIsLoading(true);
37
+ try {
38
+ const encodedPath = encodeURIComponent(currentPath);
39
+ const response = await fetch(`/api/files?path=${encodedPath}`);
40
+ if (response.ok) {
41
+ const data = await response.json();
42
+ setFiles(data.files);
43
+ if (data.path) setCurrentPath(data.path);
44
+ }
45
+ } catch (error) {
46
+ console.error("Failed to load files", error);
47
+ } finally {
48
+ setIsLoading(false);
49
+ }
50
+ }
51
+
52
+ fetchFiles();
53
+ fetchFiles();
54
+ }, [currentPath]);
55
+
56
+ useEffect(() => {
57
+ setSelectedFile(null);
58
+ }, [currentPath]);
59
+
60
+ const handleNavigate = (path: string) => {
61
+ setCurrentPath(path);
62
+ };
63
+
64
+ const handleFileClick = (file: FileInfo) => {
65
+ setSelectedFile(file);
66
+ };
67
+
68
+ const handleFileDoubleClick = (file: FileInfo) => {
69
+ if (file.isDirectory) {
70
+ handleNavigate(file.path);
71
+ }
72
+ };
73
+
74
+ const handleConfirm = () => {
75
+ if (selectedFile && selectedFile.isDirectory) {
76
+ onSelect(selectedFile.path);
77
+ } else {
78
+ onSelect(currentPath);
79
+ }
80
+ };
81
+
82
+ const handleUp = () => {
83
+ const separator = currentPath.includes('\\') ? '\\' : '/';
84
+ let cleanPath = currentPath;
85
+ if (cleanPath.endsWith(separator) && cleanPath.length > 1) {
86
+ cleanPath = cleanPath.slice(0, -1);
87
+ }
88
+
89
+ const lastIndex = cleanPath.lastIndexOf(separator);
90
+ if (lastIndex > 0) {
91
+ setCurrentPath(cleanPath.substring(0, lastIndex));
92
+ } else if (lastIndex === 0) {
93
+ setCurrentPath(separator);
94
+ } else if (lastIndex === -1 && separator === '\\' && cleanPath.length > 2) {
95
+ // C: case? simplified
96
+ }
97
+ };
98
+
99
+ const FolderIcon = () => (
100
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="file-icon">
101
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
102
+ </svg>
103
+ );
104
+
105
+ const FileIcon = () => (
106
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="file-icon">
107
+ <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
108
+ <polyline points="13 2 13 9 20 9"></polyline>
109
+ </svg>
110
+ );
111
+
112
+ const UpIcon = () => (
113
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
114
+ <polyline points="18 15 12 9 6 15"></polyline>
115
+ </svg>
116
+ );
117
+
118
+ return (
119
+ <div className="file-explorer-container">
120
+ <div className="file-explorer-header">
121
+ <button className="explorer-btn cancel" onClick={handleUp} title="Go Up">
122
+ <UpIcon />
123
+ </button>
124
+ <div className="file-explorer-path" title={currentPath}>
125
+ {currentPath}
126
+ </div>
127
+ </div>
128
+
129
+ <div className="file-list">
130
+ {isLoading ? (
131
+ <div className="loading-indicator">Loading...</div>
132
+ ) : files.length === 0 ? (
133
+ <div className="empty-message">Empty directory</div>
134
+ ) : (
135
+ files.map((file, i) => (
136
+ <div
137
+ key={i}
138
+ className={`file-item ${file.isDirectory ? 'is-directory' : ''} ${selectedFile?.path === file.path ? 'selected' : ''}`}
139
+ onClick={() => handleFileClick(file)}
140
+ onDoubleClick={() => handleFileDoubleClick(file)}
141
+ >
142
+ {file.isDirectory ? <FolderIcon /> : <FileIcon />}
143
+ <span className="file-name">{file.name}</span>
144
+ </div>
145
+ ))
146
+ )}
147
+ </div>
148
+
149
+ <div className="file-explorer-footer">
150
+ <div className="selected-path-display">
151
+ {selectedFile ? selectedFile.name : ''}
152
+ </div>
153
+ <div className="footer-actions">
154
+ <button className="explorer-btn cancel" onClick={onCancel}>Cancel</button>
155
+ <button className="explorer-btn confirm" onClick={handleConfirm}>
156
+ {selectedFile?.isDirectory ? 'Open Selected' : 'Open Current Folder'}
157
+ </button>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ );
162
+ }
@@ -0,0 +1,121 @@
1
+ /** @jsxImportSource react */
2
+ import { useState, useEffect } from 'react';
3
+ import { Sidebar, SidebarProps } from './Sidebar';
4
+ import { Modal } from './Modal';
5
+ import { FileExplorer } from './FileExplorer';
6
+ import '../assets/css/global.css'
7
+ import '../assets/css/HomePage.css'
8
+
9
+ interface RecentProject {
10
+ path: string;
11
+ lastOpened: number;
12
+ }
13
+
14
+ interface HomePageProps {
15
+ onStartChat: (message: string) => void;
16
+ onOpenProject: (path: string) => void;
17
+ sidebarProps: SidebarProps;
18
+ }
19
+
20
+ function formatRelativeTime(timestamp: number): string {
21
+ const now = Date.now();
22
+ const diff = now - timestamp;
23
+ const seconds = Math.floor(diff / 1000);
24
+ const minutes = Math.floor(seconds / 60);
25
+ const hours = Math.floor(minutes / 60);
26
+ const days = Math.floor(hours / 24);
27
+
28
+ if (seconds < 60) return `${seconds} second${seconds !== 1 ? 's' : ''} ago`;
29
+ if (minutes < 60) return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
30
+ if (hours < 24) return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
31
+ return `${days} day${days !== 1 ? 's' : ''} ago`;
32
+ }
33
+
34
+ export function HomePage({ onStartChat, onOpenProject, sidebarProps }: HomePageProps) {
35
+ const [recentProjects, setRecentProjects] = useState<RecentProject[]>([]);
36
+ const [isLoading, setIsLoading] = useState(true);
37
+ const [showFileExplorer, setShowFileExplorer] = useState(false);
38
+
39
+ useEffect(() => {
40
+ async function fetchRecentProjects() {
41
+ try {
42
+ const response = await fetch('/api/recent-projects');
43
+ if (response.ok) {
44
+ const projects = await response.json();
45
+ setRecentProjects(projects.slice(0, 3));
46
+ }
47
+ } catch (error) {
48
+ console.error('Failed to fetch recent projects:', error);
49
+ } finally {
50
+ setIsLoading(false);
51
+ }
52
+ }
53
+ fetchRecentProjects();
54
+
55
+ const interval = setInterval(fetchRecentProjects, 5000);
56
+ return () => clearInterval(interval);
57
+ }, []);
58
+
59
+ const handleProjectClick = async (path: string) => {
60
+ onOpenProject(path);
61
+ };
62
+ return (
63
+ <div className="home-page">
64
+ <Sidebar {...sidebarProps} />
65
+
66
+ <div className="main-content">
67
+ <div className="branding">
68
+ <picture className="logo-container">
69
+ <source srcSet="/logo_black.svg" media="(prefers-color-scheme: light)" />
70
+ <img src="/logo_white.svg" alt="Mosaic Logo" className="logo-img" />
71
+ </picture>
72
+ </div>
73
+
74
+ <div className="projects-section">
75
+ <div className="section-header">
76
+ <h2>Recents projects</h2>
77
+ <button className="open-project-btn" onClick={() => setShowFileExplorer(true)}>
78
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: '8px' }}><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
79
+ Open project
80
+ </button>
81
+ </div>
82
+
83
+ <div className="project-list">
84
+ {isLoading ? (
85
+ <div className="project-item">
86
+ <span className="project-path">Loading...</span>
87
+ </div>
88
+ ) : recentProjects.length === 0 ? (
89
+ <div className="project-item">
90
+ <span className="project-path" style={{ color: 'var(--text-secondary)' }}>No recent projects</span>
91
+ </div>
92
+ ) : (
93
+ recentProjects.map((proj, i) => (
94
+ <div key={i} className="project-item" onClick={() => handleProjectClick(proj.path)}>
95
+ <span className="project-path">{proj.path}</span>
96
+ <span className="project-time">{formatRelativeTime(proj.lastOpened)}</span>
97
+ </div>
98
+ ))
99
+ )}
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+
105
+ <Modal
106
+ isOpen={showFileExplorer}
107
+ onClose={() => setShowFileExplorer(false)}
108
+ title="Open project"
109
+ className="file-explorer-modal"
110
+ >
111
+ <FileExplorer
112
+ onSelect={(path) => {
113
+ setShowFileExplorer(false);
114
+ onOpenProject(path);
115
+ }}
116
+ onCancel={() => setShowFileExplorer(false)}
117
+ />
118
+ </Modal>
119
+ </div>
120
+ );
121
+ }