@kirosnn/mosaic 0.0.91 → 0.71.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 -2
- package/package.json +52 -47
- package/src/agent/prompts/systemPrompt.ts +198 -68
- package/src/agent/prompts/toolsPrompt.ts +217 -135
- package/src/agent/provider/anthropic.ts +19 -15
- package/src/agent/provider/google.ts +21 -17
- package/src/agent/provider/ollama.ts +80 -41
- package/src/agent/provider/openai.ts +107 -67
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +19 -15
- package/src/agent/tools/definitions.ts +9 -5
- package/src/agent/tools/executor.ts +655 -46
- package/src/agent/tools/exploreExecutor.ts +12 -12
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +62 -8
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +6 -6
- package/src/components/App.tsx +67 -25
- package/src/components/CustomInput.tsx +274 -68
- package/src/components/Main.tsx +323 -168
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/main/ChatPage.tsx +217 -58
- package/src/components/main/HomePage.tsx +5 -1
- package/src/components/main/ThinkingIndicator.tsx +11 -1
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +3 -5
- package/src/utils/approvalBridge.ts +29 -8
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +5 -1
- package/src/utils/diffRendering.tsx +13 -14
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/models.ts +0 -7
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/toolFormatting.ts +162 -43
- package/src/web/app.tsx +94 -34
- 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 +6 -6
- package/src/web/components/MessageItem.tsx +88 -89
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -1
- package/src/web/components/ThinkingIndicator.tsx +40 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +187 -39
- package/src/web/storage.ts +23 -1
- package/src/web/types.ts +7 -6
|
@@ -9,12 +9,13 @@ const TOOL_DISPLAY_NAMES: Record<string, string> = {
|
|
|
9
9
|
write: 'Write',
|
|
10
10
|
edit: 'Edit',
|
|
11
11
|
list: 'List',
|
|
12
|
-
create_directory: 'Mkdir',
|
|
13
12
|
glob: 'Glob',
|
|
14
13
|
grep: 'Grep',
|
|
15
14
|
bash: 'Command',
|
|
16
15
|
question: 'Question',
|
|
17
16
|
explore: 'Explore',
|
|
17
|
+
fetch: 'Fetch',
|
|
18
|
+
plan: 'Plan',
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
function getToolDisplayName(toolName: string): string {
|
|
@@ -48,11 +49,15 @@ function formatKnownToolArgs(toolName: string, args: Record<string, unknown>): s
|
|
|
48
49
|
case 'write':
|
|
49
50
|
case 'edit':
|
|
50
51
|
case 'list':
|
|
51
|
-
case 'create_directory':
|
|
52
52
|
case 'glob':
|
|
53
53
|
case 'grep':
|
|
54
54
|
case 'bash':
|
|
55
|
-
case 'explore':
|
|
55
|
+
case 'explore':
|
|
56
|
+
case 'fetch': {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case 'plan': {
|
|
56
61
|
return null;
|
|
57
62
|
}
|
|
58
63
|
|
|
@@ -89,37 +94,83 @@ export function isToolSuccess(result: unknown): boolean {
|
|
|
89
94
|
|
|
90
95
|
function formatToolHeader(toolName: string, args: Record<string, unknown>): string {
|
|
91
96
|
const displayName = getToolDisplayName(toolName);
|
|
92
|
-
const path = typeof args.path === 'string' ? args.path : '';
|
|
93
97
|
|
|
94
98
|
switch (toolName) {
|
|
95
99
|
case 'read':
|
|
100
|
+
const readPath = typeof args.path === 'string' ? args.path : '';
|
|
101
|
+
return readPath ? `${displayName} (${readPath})` : displayName;
|
|
96
102
|
case 'write':
|
|
103
|
+
const writePath = typeof args.path === 'string' ? args.path : '';
|
|
104
|
+
return writePath ? `${displayName} (${writePath})` : displayName;
|
|
97
105
|
case 'edit':
|
|
106
|
+
const editPath = typeof args.path === 'string' ? args.path : '';
|
|
107
|
+
return editPath ? `${displayName} (${editPath})` : displayName;
|
|
98
108
|
case 'list':
|
|
99
|
-
|
|
100
|
-
return
|
|
109
|
+
const listPath = typeof args.path === 'string' ? args.path : '';
|
|
110
|
+
return listPath ? `${displayName} (${listPath})` : displayName;
|
|
101
111
|
case 'glob': {
|
|
102
112
|
const pattern = typeof args.pattern === 'string' ? args.pattern : '';
|
|
103
113
|
return pattern ? `${displayName} (${pattern})` : displayName;
|
|
104
114
|
}
|
|
105
115
|
case 'grep': {
|
|
106
|
-
const
|
|
107
|
-
|
|
116
|
+
const query = typeof args.query === 'string' ? args.query : '';
|
|
117
|
+
const fileType = typeof args.file_type === 'string' ? args.file_type : '';
|
|
118
|
+
const pattern = typeof args.pattern === 'string' ? args.pattern : '';
|
|
119
|
+
const info = fileType ? `*.${fileType}` : pattern;
|
|
120
|
+
const cleanQuery = query.replace(/[\r\n]+/g, ' ').trim();
|
|
121
|
+
const queryShort = cleanQuery.length > 30 ? cleanQuery.slice(0, 30) + '...' : cleanQuery;
|
|
122
|
+
return info ? `${displayName} ("${queryShort}" in ${info})` : `${displayName} ("${queryShort}")`;
|
|
108
123
|
}
|
|
109
124
|
case 'bash': {
|
|
110
125
|
const command = typeof args.command === 'string' ? args.command : '';
|
|
111
|
-
const cleanCommand = command.replace(/\s+--timeout\s+\d+$/, '');
|
|
126
|
+
const cleanCommand = command.replace(/[\r\n]+/g, ' ').trim().replace(/\s+--timeout\s+\d+$/, '');
|
|
112
127
|
return cleanCommand ? `${displayName} (${cleanCommand})` : displayName;
|
|
113
128
|
}
|
|
114
129
|
case 'explore': {
|
|
115
130
|
const purpose = typeof args.purpose === 'string' ? args.purpose : '';
|
|
116
|
-
|
|
131
|
+
const cleanPurpose = purpose.replace(/[\r\n]+/g, ' ').trim();
|
|
132
|
+
return cleanPurpose ? `${displayName} (${cleanPurpose})` : displayName;
|
|
117
133
|
}
|
|
134
|
+
case 'fetch': {
|
|
135
|
+
const url = typeof args.url === 'string' ? args.url : '';
|
|
136
|
+
try {
|
|
137
|
+
const urlObj = new URL(url);
|
|
138
|
+
const shortUrl = urlObj.hostname + (urlObj.pathname !== '/' ? urlObj.pathname : '');
|
|
139
|
+
return shortUrl ? `${displayName} (${shortUrl})` : displayName;
|
|
140
|
+
} catch {
|
|
141
|
+
return url ? `${displayName} (${url})` : displayName;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
case 'plan':
|
|
145
|
+
return displayName;
|
|
118
146
|
default:
|
|
119
147
|
return displayName;
|
|
120
148
|
}
|
|
121
149
|
}
|
|
122
150
|
|
|
151
|
+
function formatPlanHeader(result: unknown): string {
|
|
152
|
+
const displayName = getToolDisplayName('plan');
|
|
153
|
+
if (!result || typeof result !== 'object') return displayName;
|
|
154
|
+
const obj = result as Record<string, unknown>;
|
|
155
|
+
const planItems = Array.isArray(obj.plan) ? obj.plan : [];
|
|
156
|
+
const total = planItems.length;
|
|
157
|
+
if (total === 0) return displayName;
|
|
158
|
+
|
|
159
|
+
let completed = 0;
|
|
160
|
+
let inProgress = 0;
|
|
161
|
+
|
|
162
|
+
for (const item of planItems) {
|
|
163
|
+
if (!item || typeof item !== 'object') continue;
|
|
164
|
+
const status = typeof (item as Record<string, unknown>).status === 'string'
|
|
165
|
+
? (item as Record<string, unknown>).status
|
|
166
|
+
: 'pending';
|
|
167
|
+
if (status === 'completed') completed += 1;
|
|
168
|
+
if (status === 'in_progress') inProgress += 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return `${displayName} (${completed}/${total} done, ${inProgress} in progress)`;
|
|
172
|
+
}
|
|
173
|
+
|
|
123
174
|
export function parseToolHeader(toolName: string, args: Record<string, unknown>): { name: string; info: string | null } {
|
|
124
175
|
const displayName = getToolDisplayName(toolName);
|
|
125
176
|
const path = typeof args.path === 'string' ? args.path : '';
|
|
@@ -136,18 +187,36 @@ export function parseToolHeader(toolName: string, args: Record<string, unknown>)
|
|
|
136
187
|
return { name: displayName, info: pattern || null };
|
|
137
188
|
}
|
|
138
189
|
case 'grep': {
|
|
139
|
-
const
|
|
140
|
-
|
|
190
|
+
const query = typeof args.query === 'string' ? args.query : '';
|
|
191
|
+
const fileType = typeof args.file_type === 'string' ? args.file_type : '';
|
|
192
|
+
const pattern = typeof args.pattern === 'string' ? args.pattern : '';
|
|
193
|
+
const fileInfo = fileType ? `*.${fileType}` : pattern;
|
|
194
|
+
const cleanQuery = query.replace(/[\r\n]+/g, ' ').trim();
|
|
195
|
+
const queryShort = cleanQuery.length > 30 ? cleanQuery.slice(0, 30) + '...' : cleanQuery;
|
|
196
|
+
const info = fileInfo ? `"${queryShort}" in ${fileInfo}` : `"${queryShort}"`;
|
|
197
|
+
return { name: displayName, info };
|
|
141
198
|
}
|
|
142
199
|
case 'bash': {
|
|
143
200
|
const command = typeof args.command === 'string' ? args.command : '';
|
|
144
|
-
const cleanCommand = command.replace(/\s+--timeout\s+\d+$/, '');
|
|
201
|
+
const cleanCommand = command.replace(/[\r\n]+/g, ' ').trim().replace(/\s+--timeout\s+\d+$/, '');
|
|
145
202
|
return { name: displayName, info: cleanCommand || null };
|
|
146
203
|
}
|
|
147
204
|
case 'explore': {
|
|
148
205
|
const purpose = typeof args.purpose === 'string' ? args.purpose : '';
|
|
149
206
|
return { name: displayName, info: purpose || null };
|
|
150
207
|
}
|
|
208
|
+
case 'fetch': {
|
|
209
|
+
const url = typeof args.url === 'string' ? args.url : '';
|
|
210
|
+
try {
|
|
211
|
+
const urlObj = new URL(url);
|
|
212
|
+
const shortUrl = urlObj.hostname + (urlObj.pathname !== '/' ? urlObj.pathname : '');
|
|
213
|
+
return { name: displayName, info: shortUrl || null };
|
|
214
|
+
} catch {
|
|
215
|
+
return { name: displayName, info: url || null };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
case 'plan':
|
|
219
|
+
return { name: displayName, info: null };
|
|
151
220
|
default:
|
|
152
221
|
return { name: displayName, info: null };
|
|
153
222
|
}
|
|
@@ -188,38 +257,18 @@ function formatListTree(result: unknown): string[] {
|
|
|
188
257
|
}
|
|
189
258
|
|
|
190
259
|
function formatGrepResult(result: unknown): string[] {
|
|
191
|
-
if (typeof result !== 'string') return [];
|
|
260
|
+
if (typeof result !== 'string') return ['No results returned'];
|
|
261
|
+
|
|
192
262
|
try {
|
|
193
263
|
const parsed = JSON.parse(result);
|
|
194
264
|
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (typeof parsed[0] === 'string') {
|
|
199
|
-
return parsed.map(file => ` ${file}`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (typeof parsed[0] === 'object' && parsed[0] !== null) {
|
|
203
|
-
const lines: string[] = [];
|
|
204
|
-
for (const item of parsed) {
|
|
205
|
-
if (item.file) {
|
|
206
|
-
lines.push(`${item.file}:`);
|
|
207
|
-
if (Array.isArray(item.matches)) {
|
|
208
|
-
for (const match of item.matches) {
|
|
209
|
-
if (match.line && match.content) {
|
|
210
|
-
lines.push(` ${match.line}: ${match.content.trim()}`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return lines.length > 0 ? lines : ['No matches'];
|
|
217
|
-
}
|
|
265
|
+
if (typeof parsed.total_matches === 'number' && typeof parsed.files_with_matches === 'number') {
|
|
266
|
+
return [`${parsed.total_matches} matches in ${parsed.files_with_matches} files`];
|
|
218
267
|
}
|
|
219
268
|
|
|
220
|
-
return [
|
|
269
|
+
return ['No results returned'];
|
|
221
270
|
} catch {
|
|
222
|
-
return
|
|
271
|
+
return ['No results returned'];
|
|
223
272
|
}
|
|
224
273
|
}
|
|
225
274
|
|
|
@@ -238,7 +287,15 @@ function getToolErrorText(result: unknown): string | null {
|
|
|
238
287
|
|
|
239
288
|
function formatToolBodyLines(toolName: string, args: Record<string, unknown>, result: unknown): string[] {
|
|
240
289
|
const errorText = getToolErrorText(result);
|
|
241
|
-
if (errorText)
|
|
290
|
+
if (errorText) {
|
|
291
|
+
if (toolName === 'fetch') {
|
|
292
|
+
const statusMatch = errorText.match(/HTTP (\d+(?: [a-zA-Z ]+)?)/);
|
|
293
|
+
if (statusMatch) {
|
|
294
|
+
return [`${statusMatch[1]} - Failed to fetch`];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return [`Tool error: ${errorText}`];
|
|
298
|
+
}
|
|
242
299
|
|
|
243
300
|
switch (toolName) {
|
|
244
301
|
case 'read': {
|
|
@@ -318,6 +375,67 @@ function formatToolBodyLines(toolName: string, args: Record<string, unknown>, re
|
|
|
318
375
|
return ['Exploration completed'];
|
|
319
376
|
}
|
|
320
377
|
|
|
378
|
+
case 'fetch': {
|
|
379
|
+
if (typeof result === 'string') {
|
|
380
|
+
const url = typeof args.url === 'string' ? args.url : 'URL';
|
|
381
|
+
const statusMatch = result.match(/\*\*Status:\*\* (\d+(?: [a-zA-Z ]+)?)/);
|
|
382
|
+
if (statusMatch) {
|
|
383
|
+
return [`${statusMatch[1]} - Fetched ${url}`];
|
|
384
|
+
}
|
|
385
|
+
return [`Fetched ${url}`];
|
|
386
|
+
}
|
|
387
|
+
return ['Fetch completed'];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
case 'plan': {
|
|
391
|
+
if (result && typeof result === 'object') {
|
|
392
|
+
const obj = result as Record<string, unknown>;
|
|
393
|
+
const explanation = typeof obj.explanation === 'string' ? obj.explanation.trim() : '';
|
|
394
|
+
const planItems = Array.isArray(obj.plan) ? obj.plan : [];
|
|
395
|
+
const lines: string[] = [];
|
|
396
|
+
|
|
397
|
+
if (explanation) {
|
|
398
|
+
lines.push(explanation);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const normalized = planItems
|
|
402
|
+
.map((item) => {
|
|
403
|
+
if (!item || typeof item !== 'object') return null;
|
|
404
|
+
const entry = item as Record<string, unknown>;
|
|
405
|
+
const step = typeof entry.step === 'string' ? entry.step : '';
|
|
406
|
+
const status = typeof entry.status === 'string' ? entry.status : 'pending';
|
|
407
|
+
if (!step.trim()) return null;
|
|
408
|
+
return { step: step.trim(), status };
|
|
409
|
+
})
|
|
410
|
+
.filter((item): item is { step: string; status: string } => !!item);
|
|
411
|
+
|
|
412
|
+
const inProgressItems = normalized.filter(item => item.status === 'in_progress');
|
|
413
|
+
const pendingItems = normalized.filter(item => item.status === 'pending');
|
|
414
|
+
const completedItems = normalized.filter(item => item.status === 'completed');
|
|
415
|
+
|
|
416
|
+
const sectionPrefix = ' ';
|
|
417
|
+
const itemPrefix = ' ';
|
|
418
|
+
const arrowPrefix = '> ';
|
|
419
|
+
|
|
420
|
+
const addSection = (label: string, items: Array<{ step: string; status: string }>, activeStep: string | null, marker: string) => {
|
|
421
|
+
if (items.length === 0) return;
|
|
422
|
+
lines.push(`${sectionPrefix}${label} (${items.length})`);
|
|
423
|
+
for (const item of items) {
|
|
424
|
+
const isActive = activeStep !== null && item.step === activeStep;
|
|
425
|
+
const prefix = isActive ? arrowPrefix : arrowPrefix;
|
|
426
|
+
lines.push(`${itemPrefix}${prefix}${marker} ${item.step}`);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
addSection('In progress', inProgressItems, inProgressItems[0]?.step ?? null, '[~]');
|
|
431
|
+
addSection('Todo', pendingItems, null, '[ ]');
|
|
432
|
+
addSection('Completed', completedItems, null, '[✓]');
|
|
433
|
+
|
|
434
|
+
return lines.length > 0 ? lines : ['(no steps)'];
|
|
435
|
+
}
|
|
436
|
+
return ['(no steps)'];
|
|
437
|
+
}
|
|
438
|
+
|
|
321
439
|
default: {
|
|
322
440
|
const toolResultText = formatToolResult(result);
|
|
323
441
|
if (!toolResultText) return [];
|
|
@@ -334,7 +452,8 @@ export function formatToolContent(
|
|
|
334
452
|
maxLines?: number;
|
|
335
453
|
}
|
|
336
454
|
): string {
|
|
337
|
-
const
|
|
455
|
+
const header = toolName === 'plan' ? formatPlanHeader(result) : formatToolHeader(toolName, args);
|
|
456
|
+
const lines: string[] = [header];
|
|
338
457
|
|
|
339
458
|
const argsLine = formatKnownToolArgs(toolName, args);
|
|
340
459
|
if (argsLine) lines.push(argsLine);
|
|
@@ -342,7 +461,7 @@ export function formatToolContent(
|
|
|
342
461
|
const bodyLines = formatToolBodyLines(toolName, args, result);
|
|
343
462
|
for (const line of bodyLines) lines.push(line);
|
|
344
463
|
|
|
345
|
-
const skipTruncate = toolName === 'write' || toolName === 'edit';
|
|
464
|
+
const skipTruncate = toolName === 'write' || toolName === 'edit' || toolName === 'plan';
|
|
346
465
|
if (skipTruncate) {
|
|
347
466
|
return lines.join('\n');
|
|
348
467
|
}
|
|
@@ -381,4 +500,4 @@ export function getToolWrapWidth(maxWidth: number, paragraphIndex: number): numb
|
|
|
381
500
|
|
|
382
501
|
export function formatErrorMessage(errorType: 'API' | 'Mosaic' | 'Tool', errorMessage: string): string {
|
|
383
502
|
return `${errorType} Error\n${errorMessage}`;
|
|
384
|
-
}
|
|
503
|
+
}
|
package/src/web/app.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { HomePage } from './components/HomePage';
|
|
|
5
5
|
import { ChatPage } from './components/ChatPage';
|
|
6
6
|
import { Message } from './types';
|
|
7
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';
|
|
8
|
+
import { Conversation, getAllConversations, getConversation, saveConversation, deleteConversation, createNewConversation, mergeConversations } from './storage';
|
|
9
9
|
import { QuestionRequest } from '../utils/questionBridge';
|
|
10
10
|
import { ApprovalRequest } from '../utils/approvalBridge';
|
|
11
11
|
import { parseRoute, navigateTo, replaceTo, Route } from './router';
|
|
@@ -42,8 +42,9 @@ function App() {
|
|
|
42
42
|
const [currentConversation, setCurrentConversation] = useState<Conversation | null>(null);
|
|
43
43
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
|
44
44
|
const [workspace, setWorkspace] = useState<string | null>(null);
|
|
45
|
-
const [questionRequest, setQuestionRequest] = useState<QuestionRequest | null>(null);
|
|
46
|
-
const [approvalRequest, setApprovalRequest] = useState<ApprovalRequest | null>(null);
|
|
45
|
+
const [questionRequest, setQuestionRequest] = useState<QuestionRequest | null>(null);
|
|
46
|
+
const [approvalRequest, setApprovalRequest] = useState<ApprovalRequest | null>(null);
|
|
47
|
+
const [requireApprovals, setRequireApprovals] = useState(true);
|
|
47
48
|
|
|
48
49
|
const refreshConversations = useCallback(() => {
|
|
49
50
|
setConversations(getAllConversations());
|
|
@@ -77,7 +78,28 @@ function App() {
|
|
|
77
78
|
.then(res => res.json())
|
|
78
79
|
.then(data => setWorkspace(data.workspace))
|
|
79
80
|
.catch(() => { });
|
|
80
|
-
|
|
81
|
+
|
|
82
|
+
fetch('/api/tui-conversations')
|
|
83
|
+
.then(res => res.ok ? res.json() : [])
|
|
84
|
+
.then((data: Conversation[]) => {
|
|
85
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
86
|
+
const changed = mergeConversations(data);
|
|
87
|
+
if (changed) {
|
|
88
|
+
refreshConversations();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
.catch(() => { });
|
|
93
|
+
|
|
94
|
+
fetch('/api/approvals')
|
|
95
|
+
.then(res => res.ok ? res.json() : null)
|
|
96
|
+
.then((data) => {
|
|
97
|
+
if (data && typeof data.requireApprovals === 'boolean') {
|
|
98
|
+
setRequireApprovals(data.requireApprovals);
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.catch(() => { });
|
|
102
|
+
}, [refreshConversations]);
|
|
81
103
|
|
|
82
104
|
useEffect(() => {
|
|
83
105
|
if (route.page === 'chat' && route.conversationId) {
|
|
@@ -135,14 +157,15 @@ function App() {
|
|
|
135
157
|
}
|
|
136
158
|
}, [messages, currentTitle]);
|
|
137
159
|
|
|
138
|
-
const handleSendMessage = async (content: string) => {
|
|
139
|
-
if (!content.trim() || isProcessing) return;
|
|
160
|
+
const handleSendMessage = async (content: string, images: Message['images'] = []) => {
|
|
161
|
+
if ((!content.trim() && images.length === 0) || isProcessing) return;
|
|
140
162
|
|
|
141
|
-
const userMessage: Message = {
|
|
142
|
-
id: createId(),
|
|
143
|
-
role: 'user',
|
|
144
|
-
content: content,
|
|
145
|
-
|
|
163
|
+
const userMessage: Message = {
|
|
164
|
+
id: createId(),
|
|
165
|
+
role: 'user',
|
|
166
|
+
content: content,
|
|
167
|
+
images: images.length > 0 ? images : undefined,
|
|
168
|
+
};
|
|
146
169
|
|
|
147
170
|
let conversation = currentConversation;
|
|
148
171
|
if (!conversation) {
|
|
@@ -167,12 +190,13 @@ function App() {
|
|
|
167
190
|
method: 'POST',
|
|
168
191
|
headers: { 'Content-Type': 'application/json' },
|
|
169
192
|
body: JSON.stringify({
|
|
170
|
-
message: userMessage.content,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
message: userMessage.content,
|
|
194
|
+
images: userMessage.images || [],
|
|
195
|
+
history: messages
|
|
196
|
+
.filter((m) => m.role === 'user' || m.role === 'assistant')
|
|
197
|
+
.map((m) => ({ role: m.role, content: m.content, images: m.images || [] })),
|
|
198
|
+
}),
|
|
199
|
+
});
|
|
176
200
|
|
|
177
201
|
if (!response.ok) {
|
|
178
202
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
@@ -470,6 +494,13 @@ function App() {
|
|
|
470
494
|
};
|
|
471
495
|
|
|
472
496
|
const handleDeleteConversation = (conversationId: string) => {
|
|
497
|
+
if (conversationId.startsWith('tui_')) {
|
|
498
|
+
fetch('/api/tui-conversation/delete', {
|
|
499
|
+
method: 'POST',
|
|
500
|
+
headers: { 'Content-Type': 'application/json' },
|
|
501
|
+
body: JSON.stringify({ id: conversationId }),
|
|
502
|
+
}).catch(() => { });
|
|
503
|
+
}
|
|
473
504
|
deleteConversation(conversationId);
|
|
474
505
|
refreshConversations();
|
|
475
506
|
|
|
@@ -478,7 +509,7 @@ function App() {
|
|
|
478
509
|
}
|
|
479
510
|
};
|
|
480
511
|
|
|
481
|
-
const handleRenameConversation = (conversationId: string, newTitle: string) => {
|
|
512
|
+
const handleRenameConversation = (conversationId: string, newTitle: string) => {
|
|
482
513
|
const conversation = getConversation(conversationId);
|
|
483
514
|
if (conversation) {
|
|
484
515
|
const updated: Conversation = {
|
|
@@ -494,8 +525,35 @@ function App() {
|
|
|
494
525
|
setCurrentTitle(newTitle);
|
|
495
526
|
setDocumentTitle(newTitle);
|
|
496
527
|
}
|
|
528
|
+
|
|
529
|
+
if (conversationId.startsWith('tui_')) {
|
|
530
|
+
fetch('/api/tui-conversation/rename', {
|
|
531
|
+
method: 'POST',
|
|
532
|
+
headers: { 'Content-Type': 'application/json' },
|
|
533
|
+
body: JSON.stringify({ id: conversationId, title: newTitle }),
|
|
534
|
+
}).catch(() => { });
|
|
535
|
+
}
|
|
497
536
|
}
|
|
498
|
-
};
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const handleToggleApprovals = async () => {
|
|
540
|
+
try {
|
|
541
|
+
const next = !requireApprovals;
|
|
542
|
+
const res = await fetch('/api/approvals', {
|
|
543
|
+
method: 'POST',
|
|
544
|
+
headers: { 'Content-Type': 'application/json' },
|
|
545
|
+
body: JSON.stringify({ requireApprovals: next }),
|
|
546
|
+
});
|
|
547
|
+
if (res.ok) {
|
|
548
|
+
setRequireApprovals(next);
|
|
549
|
+
if (!next) {
|
|
550
|
+
setApprovalRequest(null);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} catch (error) {
|
|
554
|
+
console.error('Failed to toggle approvals:', error);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
499
557
|
|
|
500
558
|
const handleNavigateHome = () => {
|
|
501
559
|
navigateTo({ page: 'home' });
|
|
@@ -547,20 +605,22 @@ function App() {
|
|
|
547
605
|
sidebarProps={sidebarProps}
|
|
548
606
|
/>
|
|
549
607
|
) : (
|
|
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
|
-
|
|
608
|
+
<ChatPage
|
|
609
|
+
messages={messages}
|
|
610
|
+
isProcessing={isProcessing}
|
|
611
|
+
processingStartTime={processingStartTime}
|
|
612
|
+
currentTokens={currentTokens}
|
|
613
|
+
onSendMessage={handleSendMessage}
|
|
614
|
+
onStopAgent={handleStopAgent}
|
|
615
|
+
sidebarProps={sidebarProps}
|
|
616
|
+
currentTitle={currentTitle}
|
|
617
|
+
workspace={workspace}
|
|
618
|
+
questionRequest={questionRequest}
|
|
619
|
+
approvalRequest={approvalRequest}
|
|
620
|
+
requireApprovals={requireApprovals}
|
|
621
|
+
onToggleApprovals={handleToggleApprovals}
|
|
622
|
+
/>
|
|
623
|
+
)}
|
|
564
624
|
|
|
565
625
|
<Modal
|
|
566
626
|
isOpen={activeModal === 'settings'}
|
|
@@ -603,4 +663,4 @@ function App() {
|
|
|
603
663
|
|
|
604
664
|
|
|
605
665
|
const root = ReactDOM.createRoot(document.getElementById('root')!);
|
|
606
|
-
root.render(<App />);
|
|
666
|
+
root.render(<App />);
|
|
@@ -7,17 +7,18 @@
|
|
|
7
7
|
overflow: hidden;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
.chat-title-bar {
|
|
11
|
-
display: flex;
|
|
12
|
-
align-items: center;
|
|
13
|
-
padding: 0.75rem 1.5rem;
|
|
14
|
-
flex-shrink: 0;
|
|
15
|
-
width: 100%;
|
|
16
|
-
left: 0;
|
|
17
|
-
position: sticky;
|
|
18
|
-
top: 0;
|
|
19
|
-
z-index: 10;
|
|
20
|
-
|
|
10
|
+
.chat-title-bar {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
padding: 0.75rem 1.5rem;
|
|
14
|
+
flex-shrink: 0;
|
|
15
|
+
width: 100%;
|
|
16
|
+
left: 0;
|
|
17
|
+
position: sticky;
|
|
18
|
+
top: 0;
|
|
19
|
+
z-index: 10;
|
|
20
|
+
gap: 0.75rem;
|
|
21
|
+
}
|
|
21
22
|
|
|
22
23
|
.chat-title {
|
|
23
24
|
font-size: 0.9rem;
|
|
@@ -27,13 +28,42 @@
|
|
|
27
28
|
cursor: default;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
.chat-workspace {
|
|
31
|
-
|
|
32
|
-
font-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
.chat-workspace {
|
|
32
|
+
font-size: 0.9rem;
|
|
33
|
+
font-family: 'Geist Medium Monospace', monospace;
|
|
34
|
+
color: var(--text-secondary);
|
|
35
|
+
cursor: default;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.chat-title-actions {
|
|
39
|
+
margin-left: auto;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 0.75rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.approval-toggle {
|
|
46
|
+
border: 1px solid var(--border-subtle);
|
|
47
|
+
background: transparent;
|
|
48
|
+
color: var(--text-secondary);
|
|
49
|
+
font-size: 0.8rem;
|
|
50
|
+
padding: 0.35rem 0.65rem;
|
|
51
|
+
border-radius: 999px;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
transition: all 0.15s ease;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.approval-toggle:hover {
|
|
57
|
+
background: var(--bg-hover);
|
|
58
|
+
color: var(--text-primary);
|
|
59
|
+
border-color: var(--text-muted);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.approval-toggle.active {
|
|
63
|
+
background: var(--accent-color);
|
|
64
|
+
border-color: var(--accent-color);
|
|
65
|
+
color: white;
|
|
66
|
+
}
|
|
37
67
|
|
|
38
68
|
.chat-container {
|
|
39
69
|
display: flex;
|
|
@@ -73,11 +103,11 @@
|
|
|
73
103
|
gap: 0;
|
|
74
104
|
}
|
|
75
105
|
|
|
76
|
-
.input-area textarea {
|
|
77
|
-
width: 100%;
|
|
78
|
-
background: transparent;
|
|
79
|
-
border: none;
|
|
80
|
-
color: var(--text-primary);
|
|
106
|
+
.input-area textarea {
|
|
107
|
+
width: 100%;
|
|
108
|
+
background: transparent;
|
|
109
|
+
border: none;
|
|
110
|
+
color: var(--text-primary);
|
|
81
111
|
font-size: 1rem;
|
|
82
112
|
font-family: inherit;
|
|
83
113
|
resize: none;
|
|
@@ -85,12 +115,54 @@
|
|
|
85
115
|
min-height: 60px;
|
|
86
116
|
max-height: 200px;
|
|
87
117
|
padding: 0;
|
|
88
|
-
line-height: 1.5;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.input-area textarea::placeholder {
|
|
92
|
-
color: var(--text-muted);
|
|
93
|
-
}
|
|
118
|
+
line-height: 1.5;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.input-area textarea::placeholder {
|
|
122
|
+
color: var(--text-muted);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.attachment-strip {
|
|
126
|
+
display: flex;
|
|
127
|
+
gap: 0.5rem;
|
|
128
|
+
flex-wrap: wrap;
|
|
129
|
+
padding-bottom: 0.5rem;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.attachment-item {
|
|
133
|
+
position: relative;
|
|
134
|
+
width: 64px;
|
|
135
|
+
height: 64px;
|
|
136
|
+
border-radius: 10px;
|
|
137
|
+
overflow: hidden;
|
|
138
|
+
border: 1px solid var(--border-code);
|
|
139
|
+
background: var(--bg-code);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.attachment-item img {
|
|
143
|
+
width: 100%;
|
|
144
|
+
height: 100%;
|
|
145
|
+
object-fit: cover;
|
|
146
|
+
display: block;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.attachment-item button {
|
|
150
|
+
position: absolute;
|
|
151
|
+
top: 4px;
|
|
152
|
+
right: 4px;
|
|
153
|
+
width: 18px;
|
|
154
|
+
height: 18px;
|
|
155
|
+
border-radius: 50%;
|
|
156
|
+
border: none;
|
|
157
|
+
background: rgba(0, 0, 0, 0.6);
|
|
158
|
+
color: white;
|
|
159
|
+
font-size: 12px;
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.attachment-item button:hover {
|
|
164
|
+
background: rgba(0, 0, 0, 0.8);
|
|
165
|
+
}
|
|
94
166
|
|
|
95
167
|
.input-area textarea:disabled {
|
|
96
168
|
opacity: 0.5;
|
|
@@ -209,4 +281,4 @@
|
|
|
209
281
|
|
|
210
282
|
.send-btn.stop:hover {
|
|
211
283
|
background: #ff3333;
|
|
212
|
-
}
|
|
284
|
+
}
|