@kirosnn/mosaic 0.0.91 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -6
- package/package.json +55 -48
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +209 -70
- package/src/agent/prompts/toolsPrompt.ts +285 -138
- package/src/agent/provider/anthropic.ts +109 -105
- package/src/agent/provider/google.ts +111 -107
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +73 -17
- package/src/agent/provider/openai.ts +146 -102
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +108 -104
- package/src/agent/tools/definitions.ts +15 -1
- package/src/agent/tools/executor.ts +717 -98
- package/src/agent/tools/exploreExecutor.ts +20 -22
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +64 -9
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +15 -14
- package/src/components/App.tsx +50 -8
- package/src/components/CustomInput.tsx +461 -77
- package/src/components/Main.tsx +1459 -1112
- package/src/components/Setup.tsx +1 -1
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -516
- package/src/components/main/HomePage.tsx +58 -39
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +13 -2
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +53 -25
- package/src/mcp/approvalPolicy.ts +148 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +77 -0
- package/src/mcp/cli/index.ts +85 -0
- package/src/mcp/cli/list.ts +50 -0
- package/src/mcp/cli/logs.ts +24 -0
- package/src/mcp/cli/manage.ts +99 -0
- package/src/mcp/cli/show.ts +53 -0
- package/src/mcp/cli/tools.ts +77 -0
- package/src/mcp/config.ts +223 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +299 -0
- package/src/mcp/rateLimiter.ts +50 -0
- package/src/mcp/registry.ts +151 -0
- package/src/mcp/schemaConverter.ts +100 -0
- package/src/mcp/servers/navigation.ts +854 -0
- package/src/mcp/toolCatalog.ts +169 -0
- package/src/mcp/types.ts +95 -0
- package/src/utils/approvalBridge.ts +45 -12
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +9 -7
- package/src/utils/commands/new.ts +15 -0
- package/src/utils/commands/types.ts +3 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/diffRendering.tsx +13 -16
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/markdown.tsx +163 -99
- package/src/utils/models.ts +31 -16
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +428 -48
- package/src/web/app.tsx +65 -5
- package/src/web/assets/css/ChatPage.css +102 -30
- package/src/web/assets/css/MessageItem.css +26 -29
- package/src/web/assets/css/ThinkingIndicator.css +44 -6
- package/src/web/assets/css/ToolMessage.css +36 -14
- package/src/web/components/ChatPage.tsx +228 -105
- package/src/web/components/HomePage.tsx +3 -3
- package/src/web/components/MessageItem.tsx +80 -81
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -3
- package/src/web/components/ThinkingIndicator.tsx +41 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +894 -662
- package/src/web/storage.ts +23 -1
- package/src/web/types.ts +7 -6
- package/src/utils/commands/redo.ts +0 -74
- package/src/utils/commands/sessions.ts +0 -129
- package/src/utils/commands/undo.ts +0 -75
- package/src/utils/undoRedo.ts +0 -429
- package/src/utils/undoRedoBridge.ts +0 -45
- package/src/utils/undoRedoDb.ts +0 -338
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
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 '
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 type { ImageAttachment } from '../../utils/images';
|
|
13
|
+
import { guessImageMimeType, toDataUrl } from '../../utils/images';
|
|
14
|
+
import '../assets/css/global.css'
|
|
15
|
+
|
|
16
|
+
interface ChatPageProps {
|
|
17
|
+
messages: Message[];
|
|
18
|
+
isProcessing: boolean;
|
|
19
|
+
processingStartTime?: number;
|
|
20
|
+
currentTokens?: number;
|
|
21
|
+
onSendMessage: (message: string, images?: ImageAttachment[]) => void;
|
|
22
|
+
onStopAgent?: () => void;
|
|
23
|
+
sidebarProps: SidebarProps;
|
|
24
|
+
currentTitle?: string | null;
|
|
25
|
+
workspace?: string | null;
|
|
26
|
+
questionRequest?: QuestionRequest | null;
|
|
27
|
+
approvalRequest?: ApprovalRequest | null;
|
|
28
|
+
requireApprovals: boolean;
|
|
29
|
+
onToggleApprovals: () => void;
|
|
30
|
+
}
|
|
27
31
|
|
|
28
32
|
function formatWorkspace(path: string | null | undefined): string {
|
|
29
33
|
if (!path) return '';
|
|
@@ -55,11 +59,54 @@ function formatWorkspace(path: string | null | undefined): string {
|
|
|
55
59
|
return normalized;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
function getPlanProgress(messages: Message[]): { inProgressStep?: string; nextStep?: string } {
|
|
63
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
64
|
+
const message = messages[i];
|
|
65
|
+
if (!message || message.role !== 'tool' || message.toolName !== 'plan') continue;
|
|
66
|
+
const result = message.toolResult;
|
|
67
|
+
if (!result || typeof result !== 'object') continue;
|
|
68
|
+
const obj = result as Record<string, unknown>;
|
|
69
|
+
const planItems = Array.isArray(obj.plan) ? obj.plan : [];
|
|
70
|
+
const normalized = planItems
|
|
71
|
+
.map((item) => {
|
|
72
|
+
if (!item || typeof item !== 'object') return null;
|
|
73
|
+
const entry = item as Record<string, unknown>;
|
|
74
|
+
const step = typeof entry.step === 'string' ? entry.step.trim() : '';
|
|
75
|
+
const status = typeof entry.status === 'string' ? entry.status : 'pending';
|
|
76
|
+
if (!step) return null;
|
|
77
|
+
return { step, status };
|
|
78
|
+
})
|
|
79
|
+
.filter((item): item is { step: string; status: string } => !!item);
|
|
80
|
+
|
|
81
|
+
if (normalized.length === 0) return {};
|
|
82
|
+
|
|
83
|
+
const inProgressIndex = normalized.findIndex(item => item.status === 'in_progress');
|
|
84
|
+
const inProgressStep = inProgressIndex >= 0 ? normalized[inProgressIndex]?.step : undefined;
|
|
85
|
+
let nextStep: string | undefined;
|
|
86
|
+
|
|
87
|
+
if (inProgressIndex >= 0) {
|
|
88
|
+
const after = normalized.slice(inProgressIndex + 1).find(item => item.status === 'pending');
|
|
89
|
+
nextStep = after?.step;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!nextStep) {
|
|
93
|
+
nextStep = normalized.find(item => item.status === 'pending')?.step;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { inProgressStep, nextStep };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function ChatPage({ messages, isProcessing, processingStartTime, currentTokens, onSendMessage, onStopAgent, sidebarProps, currentTitle, workspace, questionRequest, approvalRequest, requireApprovals, onToggleApprovals }: ChatPageProps) {
|
|
103
|
+
const [inputValue, setInputValue] = useState('');
|
|
104
|
+
const [showAttachButton, setShowAttachButton] = useState(false);
|
|
105
|
+
const [pendingImages, setPendingImages] = useState<ImageAttachment[]>([]);
|
|
106
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
107
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
108
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
109
|
+
const planProgress = getPlanProgress(messages);
|
|
63
110
|
|
|
64
111
|
useEffect(() => {
|
|
65
112
|
if (messagesEndRef.current) {
|
|
@@ -77,17 +124,23 @@ export function ChatPage({ messages, isProcessing, processingStartTime, currentT
|
|
|
77
124
|
if (inputRef.current) inputRef.current.focus();
|
|
78
125
|
}, []);
|
|
79
126
|
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
82
|
-
if (e.key === 'Escape' && isProcessing && onStopAgent) {
|
|
83
|
-
e.preventDefault();
|
|
84
|
-
onStopAgent();
|
|
85
|
-
}
|
|
86
|
-
};
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
129
|
+
if (e.key === 'Escape' && isProcessing && onStopAgent) {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
onStopAgent();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
87
134
|
|
|
88
135
|
document.addEventListener('keydown', handleKeyDown);
|
|
89
|
-
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
90
|
-
}, [isProcessing, onStopAgent]);
|
|
136
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
137
|
+
}, [isProcessing, onStopAgent]);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!showAttachButton) {
|
|
141
|
+
setPendingImages([]);
|
|
142
|
+
}
|
|
143
|
+
}, [showAttachButton]);
|
|
91
144
|
|
|
92
145
|
useEffect(() => {
|
|
93
146
|
const checkModelSupport = async () => {
|
|
@@ -98,44 +151,82 @@ export function ChatPage({ messages, isProcessing, processingStartTime, currentT
|
|
|
98
151
|
const { model } = await configRes.json();
|
|
99
152
|
|
|
100
153
|
if (model) {
|
|
101
|
-
// Try to find the model using the enhanced fuzzy search
|
|
102
154
|
const result = await findModelsDevModelById(model);
|
|
103
155
|
|
|
104
|
-
if (result && result.model) {
|
|
105
|
-
setShowAttachButton(modelAcceptsImages(result.model));
|
|
106
|
-
} else {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
lowerId.includes('vision');
|
|
115
|
-
setShowAttachButton(likelySupportsImages);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
} catch (err) {
|
|
119
|
-
console.error('Failed to check model support:', err);
|
|
120
|
-
}
|
|
121
|
-
};
|
|
156
|
+
if (result && result.model) {
|
|
157
|
+
setShowAttachButton(modelAcceptsImages(result.model));
|
|
158
|
+
} else {
|
|
159
|
+
setShowAttachButton(false);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error('Failed to check model support:', err);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
122
166
|
|
|
123
167
|
checkModelSupport();
|
|
124
168
|
}, []);
|
|
125
169
|
|
|
126
|
-
const handleSubmit = (e?: React.FormEvent) => {
|
|
127
|
-
if (e) e.preventDefault();
|
|
128
|
-
if (!inputValue.trim() || isProcessing) return;
|
|
129
|
-
onSendMessage(inputValue);
|
|
130
|
-
setInputValue('');
|
|
131
|
-
|
|
170
|
+
const handleSubmit = (e?: React.FormEvent) => {
|
|
171
|
+
if (e) e.preventDefault();
|
|
172
|
+
if ((!inputValue.trim() && pendingImages.length === 0) || isProcessing) return;
|
|
173
|
+
onSendMessage(inputValue, showAttachButton ? pendingImages : []);
|
|
174
|
+
setInputValue('');
|
|
175
|
+
setPendingImages([]);
|
|
176
|
+
};
|
|
132
177
|
|
|
133
|
-
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
134
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
135
|
-
e.preventDefault();
|
|
136
|
-
handleSubmit();
|
|
137
|
-
}
|
|
138
|
-
};
|
|
178
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
179
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
handleSubmit();
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const toBase64 = async (file: File): Promise<string> => {
|
|
186
|
+
const buffer = await file.arrayBuffer();
|
|
187
|
+
const bytes = new Uint8Array(buffer);
|
|
188
|
+
let binary = '';
|
|
189
|
+
const chunkSize = 0x8000;
|
|
190
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
191
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
192
|
+
binary += String.fromCharCode(...Array.from(chunk));
|
|
193
|
+
}
|
|
194
|
+
return btoa(binary);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleAttachClick = () => {
|
|
198
|
+
fileInputRef.current?.click();
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const handleFilesSelected = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
202
|
+
if (!showAttachButton) {
|
|
203
|
+
e.target.value = '';
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const files = Array.from(e.target.files || []);
|
|
207
|
+
if (files.length === 0) return;
|
|
208
|
+
const attachments: ImageAttachment[] = [];
|
|
209
|
+
for (const file of files) {
|
|
210
|
+
const mimeType = file.type || guessImageMimeType(file.name);
|
|
211
|
+
if (!mimeType.startsWith('image/')) continue;
|
|
212
|
+
const data = await toBase64(file);
|
|
213
|
+
attachments.push({
|
|
214
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
215
|
+
name: file.name,
|
|
216
|
+
mimeType,
|
|
217
|
+
data,
|
|
218
|
+
size: file.size
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (attachments.length > 0) {
|
|
222
|
+
setPendingImages((prev) => [...prev, ...attachments]);
|
|
223
|
+
}
|
|
224
|
+
e.target.value = '';
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const handleRemovePendingImage = (id: string) => {
|
|
228
|
+
setPendingImages((prev) => prev.filter((img) => img.id !== id));
|
|
229
|
+
};
|
|
139
230
|
|
|
140
231
|
const handleQuestionAnswer = async (index: number, customText?: string) => {
|
|
141
232
|
try {
|
|
@@ -169,16 +260,24 @@ export function ChatPage({ messages, isProcessing, processingStartTime, currentT
|
|
|
169
260
|
|
|
170
261
|
<div className="main-content" style={{ padding: 0 }}>
|
|
171
262
|
<div className="chat-page">
|
|
172
|
-
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
{formattedWorkspace && (
|
|
176
|
-
<span className="chat-workspace" title={workspace || ''}>
|
|
177
|
-
{formattedWorkspace}
|
|
178
|
-
</span>
|
|
179
|
-
)}
|
|
180
|
-
|
|
181
|
-
|
|
263
|
+
<div className="chat-title-bar">
|
|
264
|
+
<span className="chat-title">{currentTitle || ''}</span>
|
|
265
|
+
<div className="chat-title-actions">
|
|
266
|
+
{formattedWorkspace && (
|
|
267
|
+
<span className="chat-workspace" title={workspace || ''}>
|
|
268
|
+
{formattedWorkspace}
|
|
269
|
+
</span>
|
|
270
|
+
)}
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
className={`approval-toggle ${requireApprovals ? '' : 'active'}`}
|
|
274
|
+
onClick={onToggleApprovals}
|
|
275
|
+
title={requireApprovals ? 'Enable auto-approve' : 'Disable auto-approve'}
|
|
276
|
+
>
|
|
277
|
+
{requireApprovals ? 'Approvals on' : 'Auto-approve'}
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
182
281
|
<div className="chat-container">
|
|
183
282
|
<div className="messages">
|
|
184
283
|
{messages.map((msg) => (
|
|
@@ -187,7 +286,12 @@ export function ChatPage({ messages, isProcessing, processingStartTime, currentT
|
|
|
187
286
|
{isProcessing && !questionRequest && !approvalRequest && (
|
|
188
287
|
<div className="message assistant">
|
|
189
288
|
<div className="message-content">
|
|
190
|
-
<ThinkingIndicator
|
|
289
|
+
<ThinkingIndicator
|
|
290
|
+
startTime={processingStartTime}
|
|
291
|
+
tokens={currentTokens}
|
|
292
|
+
inProgressStep={planProgress.inProgressStep}
|
|
293
|
+
nextStep={planProgress.nextStep}
|
|
294
|
+
/>
|
|
191
295
|
</div>
|
|
192
296
|
</div>
|
|
193
297
|
)}
|
|
@@ -217,25 +321,44 @@ export function ChatPage({ messages, isProcessing, processingStartTime, currentT
|
|
|
217
321
|
<div ref={messagesEndRef} />
|
|
218
322
|
</div>
|
|
219
323
|
|
|
220
|
-
<form onSubmit={handleSubmit} className="input-area">
|
|
221
|
-
<
|
|
222
|
-
ref={
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
</
|
|
237
|
-
</
|
|
238
|
-
)}
|
|
324
|
+
<form onSubmit={handleSubmit} className="input-area">
|
|
325
|
+
<input
|
|
326
|
+
ref={fileInputRef}
|
|
327
|
+
type="file"
|
|
328
|
+
accept="image/*"
|
|
329
|
+
multiple
|
|
330
|
+
style={{ display: 'none' }}
|
|
331
|
+
onChange={handleFilesSelected}
|
|
332
|
+
/>
|
|
333
|
+
{pendingImages.length > 0 && (
|
|
334
|
+
<div className="attachment-strip">
|
|
335
|
+
{pendingImages.map((img) => (
|
|
336
|
+
<div key={img.id} className="attachment-item">
|
|
337
|
+
<img src={toDataUrl(img)} alt={img.name} />
|
|
338
|
+
<button type="button" onClick={() => handleRemovePendingImage(img.id)} title="Remove">
|
|
339
|
+
x
|
|
340
|
+
</button>
|
|
341
|
+
</div>
|
|
342
|
+
))}
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
<textarea
|
|
346
|
+
ref={inputRef}
|
|
347
|
+
value={inputValue}
|
|
348
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
349
|
+
onKeyDown={handleKeyDown}
|
|
350
|
+
placeholder="Type your message..."
|
|
351
|
+
rows={2}
|
|
352
|
+
/>
|
|
353
|
+
<div className="input-actions">
|
|
354
|
+
<div className="input-actions-left">
|
|
355
|
+
{showAttachButton && (
|
|
356
|
+
<button type="button" className="send-btn" title="Attach image" onClick={handleAttachClick}>
|
|
357
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ transform: 'rotate(-45deg)' }}>
|
|
358
|
+
<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>
|
|
359
|
+
</svg>
|
|
360
|
+
</button>
|
|
361
|
+
)}
|
|
239
362
|
</div>
|
|
240
363
|
<div className="input-actions-right">
|
|
241
364
|
{isProcessing ? (
|
|
@@ -250,12 +373,12 @@ export function ChatPage({ messages, isProcessing, processingStartTime, currentT
|
|
|
250
373
|
</svg>
|
|
251
374
|
</button>
|
|
252
375
|
) : (
|
|
253
|
-
<button
|
|
254
|
-
type="submit"
|
|
255
|
-
className="send-btn"
|
|
256
|
-
disabled={!inputValue.trim() || !!questionRequest || !!approvalRequest}
|
|
257
|
-
title="Send"
|
|
258
|
-
>
|
|
376
|
+
<button
|
|
377
|
+
type="submit"
|
|
378
|
+
className="send-btn"
|
|
379
|
+
disabled={(!inputValue.trim() && pendingImages.length === 0) || !!questionRequest || !!approvalRequest}
|
|
380
|
+
title="Send"
|
|
381
|
+
>
|
|
259
382
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
260
383
|
<line x1="12" y1="19" x2="12" y2="5"></line>
|
|
261
384
|
<polyline points="5 12 12 5 19 12"></polyline>
|
|
@@ -12,7 +12,7 @@ interface RecentProject {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
interface HomePageProps {
|
|
15
|
-
onStartChat: (message: string) => void;
|
|
15
|
+
onStartChat: (message: string, images?: import("../../utils/images").ImageAttachment[]) => void;
|
|
16
16
|
onOpenProject: (path: string) => void;
|
|
17
17
|
sidebarProps: SidebarProps;
|
|
18
18
|
}
|
|
@@ -31,7 +31,7 @@ function formatRelativeTime(timestamp: number): string {
|
|
|
31
31
|
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function HomePage({ onStartChat, onOpenProject, sidebarProps }: HomePageProps) {
|
|
34
|
+
export function HomePage({ onStartChat: _onStartChat, onOpenProject, sidebarProps }: HomePageProps) {
|
|
35
35
|
const [recentProjects, setRecentProjects] = useState<RecentProject[]>([]);
|
|
36
36
|
const [isLoading, setIsLoading] = useState(true);
|
|
37
37
|
const [showFileExplorer, setShowFileExplorer] = useState(false);
|
|
@@ -118,4 +118,4 @@ export function HomePage({ onStartChat, onOpenProject, sidebarProps }: HomePageP
|
|
|
118
118
|
</Modal>
|
|
119
119
|
</div>
|
|
120
120
|
);
|
|
121
|
-
}
|
|
121
|
+
}
|
|
@@ -5,38 +5,10 @@ import remarkGfm from 'remark-gfm';
|
|
|
5
5
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
6
6
|
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
7
7
|
import { Message } from '../types';
|
|
8
|
+
import { toDataUrl } from '../../utils/images';
|
|
8
9
|
import { parseDiffLine, getDiffLineColors } from '../utils';
|
|
9
10
|
import '../assets/css/global.css'
|
|
10
11
|
|
|
11
|
-
function BlendIcon() {
|
|
12
|
-
return (
|
|
13
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="blend-icon">
|
|
14
|
-
<circle cx="12" cy="6.5" r="1.4" />
|
|
15
|
-
<circle cx="17.5" cy="12" r="1.4" />
|
|
16
|
-
<circle cx="12" cy="17.5" r="1.4" />
|
|
17
|
-
<circle cx="6.5" cy="12" r="1.4" />
|
|
18
|
-
</svg>
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function formatBlendTime(ms: number): string {
|
|
23
|
-
const totalSeconds = Math.floor(ms / 1000);
|
|
24
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
25
|
-
const seconds = totalSeconds % 60;
|
|
26
|
-
|
|
27
|
-
if (minutes >= 60) {
|
|
28
|
-
const hours = Math.floor(minutes / 60);
|
|
29
|
-
const remainingMinutes = minutes % 60;
|
|
30
|
-
return `${hours}h ${remainingMinutes}m`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (minutes > 0) {
|
|
34
|
-
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return `${seconds}s`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
12
|
interface MessageItemProps {
|
|
41
13
|
message: Message;
|
|
42
14
|
}
|
|
@@ -72,6 +44,35 @@ function renderDiffLine(line: string, index: number): React.ReactElement {
|
|
|
72
44
|
);
|
|
73
45
|
}
|
|
74
46
|
|
|
47
|
+
function renderToolLine(line: string, index: number): React.ReactElement {
|
|
48
|
+
const parsed = parseDiffLine(line);
|
|
49
|
+
if (parsed.isDiffLine) {
|
|
50
|
+
return renderDiffLine(line, index);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const planMatch = line.match(/^(\s*)>\s*(\[[~x ]\])?\s*(.*)$/);
|
|
54
|
+
if (planMatch) {
|
|
55
|
+
const [, leading, bracket, rest] = planMatch;
|
|
56
|
+
const isActive = bracket === '[~]';
|
|
57
|
+
return (
|
|
58
|
+
<div key={index} className="tool-line plan-line">
|
|
59
|
+
<span className="plan-indent">{leading || ''}</span>
|
|
60
|
+
<span className="plan-prefix">{'>'}</span>
|
|
61
|
+
<span> </span>
|
|
62
|
+
{bracket && <span className={`plan-bracket${isActive ? ' active' : ''}`}>{bracket}</span>}
|
|
63
|
+
{bracket && <span> </span>}
|
|
64
|
+
<span className="plan-step">{rest}</span>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div key={index} className="tool-line">
|
|
71
|
+
{line}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
75
76
|
function parseToolHeader(content: string): { name: string; info: string | null; bodyLines: string[] } {
|
|
76
77
|
const lines = content.split('\n');
|
|
77
78
|
const firstLine = lines[0] || '';
|
|
@@ -88,11 +89,12 @@ function parseToolHeader(content: string): { name: string; info: string | null;
|
|
|
88
89
|
export function MessageItem({ message }: MessageItemProps) {
|
|
89
90
|
if (message.role === 'tool') {
|
|
90
91
|
const statusClass = message.success === false ? 'error' : message.isRunning ? 'running' : 'success';
|
|
92
|
+
const planClass = message.toolName === 'plan' ? 'plan-tool' : '';
|
|
91
93
|
|
|
92
94
|
const { name, info, bodyLines } = parseToolHeader(message.content);
|
|
93
95
|
|
|
94
96
|
return (
|
|
95
|
-
<div className={`message tool ${statusClass}`}>
|
|
97
|
+
<div className={`message tool ${statusClass} ${planClass}`}>
|
|
96
98
|
<div className="message-content">
|
|
97
99
|
<div className="tool-header">
|
|
98
100
|
<span className={`tool-name ${message.toolName === 'stop' ? 'no-bold' : ''}`}>{name}</span>
|
|
@@ -105,7 +107,7 @@ export function MessageItem({ message }: MessageItemProps) {
|
|
|
105
107
|
</div>
|
|
106
108
|
{bodyLines.length > 0 && (
|
|
107
109
|
<div className="tool-output">
|
|
108
|
-
{bodyLines.map((line, index) =>
|
|
110
|
+
{bodyLines.map((line, index) => renderToolLine(line, index))}
|
|
109
111
|
</div>
|
|
110
112
|
)}
|
|
111
113
|
</div>
|
|
@@ -114,65 +116,62 @@ export function MessageItem({ message }: MessageItemProps) {
|
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
if (message.role === 'assistant') {
|
|
117
|
-
const showBlend = message.responseDuration && message.responseDuration > 60000;
|
|
118
|
-
|
|
119
119
|
return (
|
|
120
|
-
|
|
121
|
-
<div className="message
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
</ReactMarkdown>
|
|
155
|
-
</div>
|
|
120
|
+
<div className="message assistant">
|
|
121
|
+
<div className="message-content">
|
|
122
|
+
{message.thinkingContent && (
|
|
123
|
+
<details className="thinking-section">
|
|
124
|
+
<summary>Thinking...</summary>
|
|
125
|
+
<pre className="thinking-content">{message.thinkingContent}</pre>
|
|
126
|
+
</details>
|
|
127
|
+
)}
|
|
128
|
+
<div className="markdown-content">
|
|
129
|
+
<ReactMarkdown
|
|
130
|
+
remarkPlugins={[remarkGfm]}
|
|
131
|
+
components={{
|
|
132
|
+
code({ node, className, children, ...props }) {
|
|
133
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
134
|
+
const { ref, ...rest } = props as any;
|
|
135
|
+
return match ? (
|
|
136
|
+
<SyntaxHighlighter
|
|
137
|
+
style={vscDarkPlus as any}
|
|
138
|
+
language={match[1]}
|
|
139
|
+
PreTag="div"
|
|
140
|
+
{...rest}
|
|
141
|
+
>
|
|
142
|
+
{String(children).replace(/\n$/, '')}
|
|
143
|
+
</SyntaxHighlighter>
|
|
144
|
+
) : (
|
|
145
|
+
<code className={className} {...props}>
|
|
146
|
+
{children}
|
|
147
|
+
</code>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{message.content}
|
|
153
|
+
</ReactMarkdown>
|
|
156
154
|
</div>
|
|
157
155
|
</div>
|
|
158
|
-
|
|
159
|
-
<div className="message assistant blend-message">
|
|
160
|
-
<div className="message-content blend-indicator">
|
|
161
|
-
<BlendIcon />
|
|
162
|
-
<span className="blend-text">
|
|
163
|
-
{message.blendWord || 'Blended'} for {formatBlendTime(message.responseDuration!)}
|
|
164
|
-
</span>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
168
|
-
</>
|
|
156
|
+
</div>
|
|
169
157
|
);
|
|
170
158
|
}
|
|
171
159
|
|
|
160
|
+
const hasImages = Array.isArray(message.images) && message.images.length > 0;
|
|
161
|
+
|
|
172
162
|
return (
|
|
173
163
|
<div className={`message ${message.role} ${message.isError ? 'error' : ''}`}>
|
|
174
164
|
<div className="message-content">
|
|
175
|
-
{
|
|
165
|
+
{hasImages && (
|
|
166
|
+
<div className="message-images">
|
|
167
|
+
{message.images!.map((img) => (
|
|
168
|
+
<img key={img.id} src={toDataUrl(img)} alt={img.name} />
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
<div className="message-text">
|
|
173
|
+
{message.displayContent || message.content}
|
|
174
|
+
</div>
|
|
176
175
|
</div>
|
|
177
176
|
</div>
|
|
178
177
|
);
|