@kirosnn/mosaic 0.71.0 → 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/README.md +1 -5
- package/package.json +4 -2
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +15 -6
- package/src/agent/prompts/toolsPrompt.ts +75 -10
- package/src/agent/provider/anthropic.ts +100 -100
- package/src/agent/provider/google.ts +102 -102
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +77 -60
- package/src/agent/provider/openai.ts +42 -38
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/xai.ts +99 -99
- package/src/agent/tools/definitions.ts +19 -9
- package/src/agent/tools/executor.ts +95 -85
- package/src/agent/tools/exploreExecutor.ts +8 -10
- package/src/agent/tools/grep.ts +30 -29
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/types.ts +9 -8
- package/src/components/App.tsx +45 -45
- package/src/components/CustomInput.tsx +214 -36
- package/src/components/Main.tsx +1146 -954
- package/src/components/Setup.tsx +1 -1
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -675
- package/src/components/main/HomePage.tsx +53 -38
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +2 -1
- package/src/index.tsx +50 -20
- 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 +17 -5
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/index.ts +4 -6
- 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 +1 -3
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/markdown.tsx +163 -99
- package/src/utils/models.ts +31 -9
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +268 -7
- package/src/web/app.tsx +72 -72
- package/src/web/components/HomePage.tsx +7 -7
- package/src/web/components/MessageItem.tsx +22 -22
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Sidebar.tsx +0 -2
- package/src/web/components/ThinkingIndicator.tsx +1 -0
- package/src/web/server.tsx +767 -683
- 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
|
@@ -18,7 +18,25 @@ const TOOL_DISPLAY_NAMES: Record<string, string> = {
|
|
|
18
18
|
plan: 'Plan',
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
function parseMcpSafeId(toolName: string): { serverId: string; tool: string } | null {
|
|
22
|
+
if (!toolName.startsWith('mcp__')) return null;
|
|
23
|
+
const parts = toolName.slice(5).split('__');
|
|
24
|
+
if (parts.length < 2) return null;
|
|
25
|
+
const tool = parts.pop()!;
|
|
26
|
+
const serverId = parts.join('__');
|
|
27
|
+
return { serverId, tool };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getMcpToolDisplayName(tool: string): string {
|
|
31
|
+
const words = tool.replace(/[-_]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
32
|
+
return words.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
function getToolDisplayName(toolName: string): string {
|
|
36
|
+
const mcp = parseMcpSafeId(toolName);
|
|
37
|
+
if (mcp) {
|
|
38
|
+
return getMcpToolDisplayName(mcp.tool);
|
|
39
|
+
}
|
|
22
40
|
return TOOL_DISPLAY_NAMES[toolName] || toolName;
|
|
23
41
|
}
|
|
24
42
|
|
|
@@ -43,6 +61,50 @@ export function formatToolResult(result: unknown): string {
|
|
|
43
61
|
}
|
|
44
62
|
}
|
|
45
63
|
|
|
64
|
+
function getMcpHeaderInfo(_tool: string, args: Record<string, unknown>): string {
|
|
65
|
+
const query = args.query ?? args.search ?? args.prompt ?? args.input ?? args.text ?? args.q;
|
|
66
|
+
if (typeof query === 'string' && query.trim()) {
|
|
67
|
+
const clean = query.replace(/[\r\n]+/g, ' ').trim();
|
|
68
|
+
return clean.length > 50 ? clean.slice(0, 50) + '...' : clean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const url = args.url ?? args.uri ?? args.href;
|
|
72
|
+
if (typeof url === 'string' && url.trim()) {
|
|
73
|
+
try {
|
|
74
|
+
const u = new URL(url);
|
|
75
|
+
return u.hostname + (u.pathname !== '/' ? u.pathname : '');
|
|
76
|
+
} catch {
|
|
77
|
+
return url.length > 50 ? url.slice(0, 50) + '...' : url;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const urls = args.urls;
|
|
82
|
+
if (Array.isArray(urls) && urls.length > 0) {
|
|
83
|
+
const first = typeof urls[0] === 'string' ? urls[0] : '';
|
|
84
|
+
if (urls.length === 1) {
|
|
85
|
+
try {
|
|
86
|
+
return new URL(first).hostname;
|
|
87
|
+
} catch {
|
|
88
|
+
return first.length > 40 ? first.slice(0, 40) + '...' : first;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return `${urls.length} URLs`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const path = args.path ?? args.file ?? args.filename ?? args.filepath ?? args.name;
|
|
95
|
+
if (typeof path === 'string' && path.trim()) {
|
|
96
|
+
return path.length > 50 ? path.slice(0, 50) + '...' : path;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const command = args.command ?? args.cmd;
|
|
100
|
+
if (typeof command === 'string' && command.trim()) {
|
|
101
|
+
const clean = command.replace(/[\r\n]+/g, ' ').trim();
|
|
102
|
+
return clean.length > 50 ? clean.slice(0, 50) + '...' : clean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
|
|
46
108
|
function formatKnownToolArgs(toolName: string, args: Record<string, unknown>): string | null {
|
|
47
109
|
switch (toolName) {
|
|
48
110
|
case 'read':
|
|
@@ -67,6 +129,9 @@ function formatKnownToolArgs(toolName: string, args: Record<string, unknown>): s
|
|
|
67
129
|
}
|
|
68
130
|
|
|
69
131
|
default: {
|
|
132
|
+
if (toolName.startsWith('mcp__')) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
70
135
|
const keys = Object.keys(args);
|
|
71
136
|
if (keys.length === 0) return null;
|
|
72
137
|
try {
|
|
@@ -143,8 +208,13 @@ function formatToolHeader(toolName: string, args: Record<string, unknown>): stri
|
|
|
143
208
|
}
|
|
144
209
|
case 'plan':
|
|
145
210
|
return displayName;
|
|
146
|
-
default:
|
|
211
|
+
default: {
|
|
212
|
+
if (toolName.startsWith('mcp__')) {
|
|
213
|
+
const info = getMcpHeaderInfo('', args);
|
|
214
|
+
return info ? `${displayName} ("${info}")` : displayName;
|
|
215
|
+
}
|
|
147
216
|
return displayName;
|
|
217
|
+
}
|
|
148
218
|
}
|
|
149
219
|
}
|
|
150
220
|
|
|
@@ -217,8 +287,13 @@ export function parseToolHeader(toolName: string, args: Record<string, unknown>)
|
|
|
217
287
|
}
|
|
218
288
|
case 'plan':
|
|
219
289
|
return { name: displayName, info: null };
|
|
220
|
-
default:
|
|
290
|
+
default: {
|
|
291
|
+
if (toolName.startsWith('mcp__')) {
|
|
292
|
+
const info = getMcpHeaderInfo('', args);
|
|
293
|
+
return { name: displayName, info: info || null };
|
|
294
|
+
}
|
|
221
295
|
return { name: displayName, info: null };
|
|
296
|
+
}
|
|
222
297
|
}
|
|
223
298
|
}
|
|
224
299
|
|
|
@@ -230,10 +305,25 @@ function getLineCount(text: string): number {
|
|
|
230
305
|
function formatListTree(result: unknown): string[] {
|
|
231
306
|
if (typeof result !== 'string') return [];
|
|
232
307
|
try {
|
|
233
|
-
const parsed = JSON.parse(result)
|
|
234
|
-
if (!Array.isArray(parsed)) return [];
|
|
308
|
+
const parsed = JSON.parse(result);
|
|
235
309
|
|
|
236
|
-
|
|
310
|
+
let items: Array<{ name?: string; path?: string; type?: string }>;
|
|
311
|
+
let errors: string[] = [];
|
|
312
|
+
|
|
313
|
+
if (Array.isArray(parsed)) {
|
|
314
|
+
items = parsed;
|
|
315
|
+
} else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.files)) {
|
|
316
|
+
items = parsed.files;
|
|
317
|
+
if (Array.isArray(parsed.errors)) {
|
|
318
|
+
errors = parsed.errors
|
|
319
|
+
.map((e: unknown) => (typeof e === 'string' ? e : ''))
|
|
320
|
+
.filter((e: string) => e);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const entries = items
|
|
237
327
|
.map((e) => ({
|
|
238
328
|
name: typeof e.name === 'string' ? e.name : (typeof e.path === 'string' ? e.path : ''),
|
|
239
329
|
type: typeof e.type === 'string' ? e.type : '',
|
|
@@ -250,7 +340,16 @@ function formatListTree(result: unknown): string[] {
|
|
|
250
340
|
.map((e) => e.name)
|
|
251
341
|
.sort((a, b) => a.localeCompare(b));
|
|
252
342
|
|
|
253
|
-
|
|
343
|
+
const lines = [...dirs, ...files];
|
|
344
|
+
|
|
345
|
+
if (errors.length > 0) {
|
|
346
|
+
lines.push('', `Errors (${errors.length}):`);
|
|
347
|
+
for (const err of errors) {
|
|
348
|
+
lines.push(` ${err}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return lines;
|
|
254
353
|
} catch {
|
|
255
354
|
return [];
|
|
256
355
|
}
|
|
@@ -285,6 +384,157 @@ function getToolErrorText(result: unknown): string | null {
|
|
|
285
384
|
return typeof error === 'string' && error.trim() ? error.trim() : null;
|
|
286
385
|
}
|
|
287
386
|
|
|
387
|
+
function formatMcpResultBody(result: unknown): string[] {
|
|
388
|
+
if (typeof result !== 'string') {
|
|
389
|
+
if (result && typeof result === 'object') {
|
|
390
|
+
const obj = result as Record<string, unknown>;
|
|
391
|
+
if (typeof obj.error === 'string') {
|
|
392
|
+
return [`Error: ${obj.error}`];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return ['Completed'];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const text = result.trim();
|
|
399
|
+
if (!text) return ['(empty result)'];
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const parsed = JSON.parse(text);
|
|
403
|
+
|
|
404
|
+
if (Array.isArray(parsed)) {
|
|
405
|
+
return formatMcpArray(parsed);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
409
|
+
return formatMcpObject(parsed);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return [String(parsed)];
|
|
413
|
+
} catch {
|
|
414
|
+
const lines = text.split(/\r?\n/).filter(l => l.trim());
|
|
415
|
+
return lines.length > 0 ? lines : ['Completed'];
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function formatMcpArray(arr: unknown[]): string[] {
|
|
420
|
+
if (arr.length === 0) return ['(no results)'];
|
|
421
|
+
|
|
422
|
+
const lines: string[] = [];
|
|
423
|
+
|
|
424
|
+
for (const item of arr) {
|
|
425
|
+
if (typeof item === 'string') {
|
|
426
|
+
lines.push(` ${item}`);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (item && typeof item === 'object') {
|
|
431
|
+
const obj = item as Record<string, unknown>;
|
|
432
|
+
|
|
433
|
+
if (typeof obj.error === 'string') {
|
|
434
|
+
const url = typeof obj.url === 'string' ? obj.url : '';
|
|
435
|
+
const detail = obj.details && typeof obj.details === 'object'
|
|
436
|
+
? (obj.details as Record<string, unknown>).detail
|
|
437
|
+
: '';
|
|
438
|
+
const errMsg = typeof detail === 'string' && detail ? detail : obj.error;
|
|
439
|
+
lines.push(url ? ` ${url} - ${errMsg}` : ` Error: ${errMsg}`);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const title = obj.title ?? obj.name ?? obj.label;
|
|
444
|
+
const desc = obj.description ?? obj.summary ?? obj.snippet ?? obj.text ?? obj.content;
|
|
445
|
+
const url = obj.url ?? obj.link ?? obj.href;
|
|
446
|
+
|
|
447
|
+
if (typeof title === 'string' && title) {
|
|
448
|
+
let line = ` ${title}`;
|
|
449
|
+
if (typeof url === 'string' && url) {
|
|
450
|
+
try {
|
|
451
|
+
line += ` (${new URL(url).hostname})`;
|
|
452
|
+
} catch {
|
|
453
|
+
line += ` (${url.length > 40 ? url.slice(0, 40) + '...' : url})`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
lines.push(line);
|
|
457
|
+
if (typeof desc === 'string' && desc.trim()) {
|
|
458
|
+
const short = desc.trim().replace(/[\r\n]+/g, ' ');
|
|
459
|
+
lines.push(` ${short.length > 80 ? short.slice(0, 80) + '...' : short}`);
|
|
460
|
+
}
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (typeof url === 'string' && url) {
|
|
465
|
+
let line = ` ${url}`;
|
|
466
|
+
if (typeof desc === 'string' && desc.trim()) {
|
|
467
|
+
const short = desc.trim().replace(/[\r\n]+/g, ' ');
|
|
468
|
+
line += ` - ${short.length > 60 ? short.slice(0, 60) + '...' : short}`;
|
|
469
|
+
}
|
|
470
|
+
lines.push(line);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const keys = Object.keys(obj);
|
|
475
|
+
const summary = keys.slice(0, 3).map(k => {
|
|
476
|
+
const v = obj[k];
|
|
477
|
+
const s = typeof v === 'string' ? v : JSON.stringify(v);
|
|
478
|
+
const short = s && s.length > 30 ? s.slice(0, 30) + '...' : s;
|
|
479
|
+
return `${k}: ${short}`;
|
|
480
|
+
}).join(', ');
|
|
481
|
+
lines.push(` ${summary}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (arr.length > 1) {
|
|
486
|
+
lines.unshift(`${arr.length} results:`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return lines.length > 0 ? lines : ['Completed'];
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function formatMcpObject(obj: Record<string, unknown>): string[] {
|
|
493
|
+
const lines: string[] = [];
|
|
494
|
+
|
|
495
|
+
if (typeof obj.error === 'string') {
|
|
496
|
+
const detail = obj.details && typeof obj.details === 'object'
|
|
497
|
+
? (obj.details as Record<string, unknown>).detail
|
|
498
|
+
: '';
|
|
499
|
+
const errMsg = typeof detail === 'string' && detail ? detail : obj.error;
|
|
500
|
+
return [`Error: ${errMsg}`];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const status = obj.status ?? obj.statusCode ?? obj.code;
|
|
504
|
+
const message = obj.message ?? obj.result ?? obj.output ?? obj.text ?? obj.content ?? obj.data;
|
|
505
|
+
|
|
506
|
+
if (typeof status === 'number' || typeof status === 'string') {
|
|
507
|
+
lines.push(`Status: ${status}`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (typeof message === 'string' && message.trim()) {
|
|
511
|
+
const msgLines = message.trim().split(/\r?\n/);
|
|
512
|
+
lines.push(...msgLines);
|
|
513
|
+
} else if (message && typeof message === 'object') {
|
|
514
|
+
if (Array.isArray(message)) {
|
|
515
|
+
lines.push(...formatMcpArray(message));
|
|
516
|
+
} else {
|
|
517
|
+
const entries = Object.entries(message as Record<string, unknown>).slice(0, 5);
|
|
518
|
+
for (const [k, v] of entries) {
|
|
519
|
+
const s = typeof v === 'string' ? v : JSON.stringify(v);
|
|
520
|
+
const short = s && s.length > 60 ? s.slice(0, 60) + '...' : s;
|
|
521
|
+
lines.push(` ${k}: ${short}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (lines.length === 0) {
|
|
527
|
+
const entries = Object.entries(obj).slice(0, 5);
|
|
528
|
+
for (const [k, v] of entries) {
|
|
529
|
+
const s = typeof v === 'string' ? v : JSON.stringify(v);
|
|
530
|
+
const short = s && s.length > 60 ? s.slice(0, 60) + '...' : s;
|
|
531
|
+
lines.push(` ${k}: ${short}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return lines.length > 0 ? lines : ['Completed'];
|
|
536
|
+
}
|
|
537
|
+
|
|
288
538
|
function formatToolBodyLines(toolName: string, args: Record<string, unknown>, result: unknown): string[] {
|
|
289
539
|
const errorText = getToolErrorText(result);
|
|
290
540
|
if (errorText) {
|
|
@@ -294,6 +544,14 @@ function formatToolBodyLines(toolName: string, args: Record<string, unknown>, re
|
|
|
294
544
|
return [`${statusMatch[1]} - Failed to fetch`];
|
|
295
545
|
}
|
|
296
546
|
}
|
|
547
|
+
if (toolName.startsWith('mcp__')) {
|
|
548
|
+
const statusMatch = errorText.match(/status code (\d+)/i);
|
|
549
|
+
if (statusMatch) {
|
|
550
|
+
return [`Error ${statusMatch[1]}`];
|
|
551
|
+
}
|
|
552
|
+
const short = errorText.length > 80 ? errorText.slice(0, 80) + '...' : errorText;
|
|
553
|
+
return [`Error: ${short}`];
|
|
554
|
+
}
|
|
297
555
|
return [`Tool error: ${errorText}`];
|
|
298
556
|
}
|
|
299
557
|
|
|
@@ -437,6 +695,9 @@ function formatToolBodyLines(toolName: string, args: Record<string, unknown>, re
|
|
|
437
695
|
}
|
|
438
696
|
|
|
439
697
|
default: {
|
|
698
|
+
if (toolName.startsWith('mcp__')) {
|
|
699
|
+
return formatMcpResultBody(result);
|
|
700
|
+
}
|
|
440
701
|
const toolResultText = formatToolResult(result);
|
|
441
702
|
if (!toolResultText) return [];
|
|
442
703
|
return toolResultText.split(/\r?\n/);
|
|
@@ -500,4 +761,4 @@ export function getToolWrapWidth(maxWidth: number, paragraphIndex: number): numb
|
|
|
500
761
|
|
|
501
762
|
export function formatErrorMessage(errorType: 'API' | 'Mosaic' | 'Tool', errorMessage: string): string {
|
|
502
763
|
return `${errorType} Error\n${errorMessage}`;
|
|
503
|
-
}
|
|
764
|
+
}
|
package/src/web/app.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
|
-
import { useState, useEffect,
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
3
|
import ReactDOM from 'react-dom/client';
|
|
4
4
|
import { HomePage } from './components/HomePage';
|
|
5
5
|
import { ChatPage } from './components/ChatPage';
|
|
@@ -42,9 +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);
|
|
47
|
-
const [requireApprovals, setRequireApprovals] = useState(true);
|
|
45
|
+
const [questionRequest, setQuestionRequest] = useState<QuestionRequest | null>(null);
|
|
46
|
+
const [approvalRequest, setApprovalRequest] = useState<ApprovalRequest | null>(null);
|
|
47
|
+
const [requireApprovals, setRequireApprovals] = useState(true);
|
|
48
48
|
|
|
49
49
|
const refreshConversations = useCallback(() => {
|
|
50
50
|
setConversations(getAllConversations());
|
|
@@ -79,27 +79,27 @@ function App() {
|
|
|
79
79
|
.then(data => setWorkspace(data.workspace))
|
|
80
80
|
.catch(() => { });
|
|
81
81
|
|
|
82
|
-
fetch('/api/tui-conversations')
|
|
83
|
-
.then(res => res.ok ? res.json() : [])
|
|
84
|
-
.then((data: Conversation[]) => {
|
|
82
|
+
fetch('/api/tui-conversations')
|
|
83
|
+
.then(res => res.ok ? res.json() : [])
|
|
84
|
+
.then((data: Conversation[]) => {
|
|
85
85
|
if (Array.isArray(data) && data.length > 0) {
|
|
86
86
|
const changed = mergeConversations(data);
|
|
87
87
|
if (changed) {
|
|
88
88
|
refreshConversations();
|
|
89
89
|
}
|
|
90
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]);
|
|
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]);
|
|
103
103
|
|
|
104
104
|
useEffect(() => {
|
|
105
105
|
if (route.page === 'chat' && route.conversationId) {
|
|
@@ -157,15 +157,15 @@ function App() {
|
|
|
157
157
|
}
|
|
158
158
|
}, [messages, currentTitle]);
|
|
159
159
|
|
|
160
|
-
const handleSendMessage = async (content: string, images: Message['images'] = []) => {
|
|
161
|
-
if ((!content.trim() && images.length === 0) || isProcessing) return;
|
|
160
|
+
const handleSendMessage = async (content: string, images: Message['images'] = []) => {
|
|
161
|
+
if ((!content.trim() && images.length === 0) || isProcessing) return;
|
|
162
162
|
|
|
163
|
-
const userMessage: Message = {
|
|
164
|
-
id: createId(),
|
|
165
|
-
role: 'user',
|
|
166
|
-
content: content,
|
|
167
|
-
images: images.length > 0 ? images : undefined,
|
|
168
|
-
};
|
|
163
|
+
const userMessage: Message = {
|
|
164
|
+
id: createId(),
|
|
165
|
+
role: 'user',
|
|
166
|
+
content: content,
|
|
167
|
+
images: images.length > 0 ? images : undefined,
|
|
168
|
+
};
|
|
169
169
|
|
|
170
170
|
let conversation = currentConversation;
|
|
171
171
|
if (!conversation) {
|
|
@@ -190,13 +190,13 @@ function App() {
|
|
|
190
190
|
method: 'POST',
|
|
191
191
|
headers: { 'Content-Type': 'application/json' },
|
|
192
192
|
body: JSON.stringify({
|
|
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
|
-
});
|
|
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
|
+
});
|
|
200
200
|
|
|
201
201
|
if (!response.ok) {
|
|
202
202
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
@@ -509,7 +509,7 @@ function App() {
|
|
|
509
509
|
}
|
|
510
510
|
};
|
|
511
511
|
|
|
512
|
-
const handleRenameConversation = (conversationId: string, newTitle: string) => {
|
|
512
|
+
const handleRenameConversation = (conversationId: string, newTitle: string) => {
|
|
513
513
|
const conversation = getConversation(conversationId);
|
|
514
514
|
if (conversation) {
|
|
515
515
|
const updated: Conversation = {
|
|
@@ -534,26 +534,26 @@ function App() {
|
|
|
534
534
|
}).catch(() => { });
|
|
535
535
|
}
|
|
536
536
|
}
|
|
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
|
-
};
|
|
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
|
+
};
|
|
557
557
|
|
|
558
558
|
const handleNavigateHome = () => {
|
|
559
559
|
navigateTo({ page: 'home' });
|
|
@@ -605,22 +605,22 @@ function App() {
|
|
|
605
605
|
sidebarProps={sidebarProps}
|
|
606
606
|
/>
|
|
607
607
|
) : (
|
|
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
|
-
)}
|
|
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
|
+
)}
|
|
624
624
|
|
|
625
625
|
<Modal
|
|
626
626
|
isOpen={activeModal === 'settings'}
|
|
@@ -663,4 +663,4 @@ function App() {
|
|
|
663
663
|
|
|
664
664
|
|
|
665
665
|
const root = ReactDOM.createRoot(document.getElementById('root')!);
|
|
666
|
-
root.render(<App />);
|
|
666
|
+
root.render(<App />);
|
|
@@ -11,11 +11,11 @@ interface RecentProject {
|
|
|
11
11
|
lastOpened: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
interface HomePageProps {
|
|
15
|
-
onStartChat: (message: string, images?: import("../../utils/images").ImageAttachment[]) => void;
|
|
16
|
-
onOpenProject: (path: string) => void;
|
|
17
|
-
sidebarProps: SidebarProps;
|
|
18
|
-
}
|
|
14
|
+
interface HomePageProps {
|
|
15
|
+
onStartChat: (message: string, images?: import("../../utils/images").ImageAttachment[]) => void;
|
|
16
|
+
onOpenProject: (path: string) => void;
|
|
17
|
+
sidebarProps: SidebarProps;
|
|
18
|
+
}
|
|
19
19
|
|
|
20
20
|
function formatRelativeTime(timestamp: number): string {
|
|
21
21
|
const now = Date.now();
|
|
@@ -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
|
+
}
|
|
@@ -4,8 +4,8 @@ import ReactMarkdown from 'react-markdown';
|
|
|
4
4
|
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
|
-
import { Message } from '../types';
|
|
8
|
-
import { toDataUrl } from '../../utils/images';
|
|
7
|
+
import { Message } from '../types';
|
|
8
|
+
import { toDataUrl } from '../../utils/images';
|
|
9
9
|
import { parseDiffLine, getDiffLineColors } from '../utils';
|
|
10
10
|
import '../assets/css/global.css'
|
|
11
11
|
|
|
@@ -57,7 +57,7 @@ function renderToolLine(line: string, index: number): React.ReactElement {
|
|
|
57
57
|
return (
|
|
58
58
|
<div key={index} className="tool-line plan-line">
|
|
59
59
|
<span className="plan-indent">{leading || ''}</span>
|
|
60
|
-
<span className="plan-prefix"
|
|
60
|
+
<span className="plan-prefix">{'>'}</span>
|
|
61
61
|
<span> </span>
|
|
62
62
|
{bracket && <span className={`plan-bracket${isActive ? ' active' : ''}`}>{bracket}</span>}
|
|
63
63
|
{bracket && <span> </span>}
|
|
@@ -157,22 +157,22 @@ export function MessageItem({ message }: MessageItemProps) {
|
|
|
157
157
|
);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
const hasImages = Array.isArray(message.images) && message.images.length > 0;
|
|
161
|
-
|
|
162
|
-
return (
|
|
163
|
-
<div className={`message ${message.role} ${message.isError ? 'error' : ''}`}>
|
|
164
|
-
<div className="message-content">
|
|
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>
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
);
|
|
178
|
-
}
|
|
160
|
+
const hasImages = Array.isArray(message.images) && message.images.length > 0;
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className={`message ${message.role} ${message.isError ? 'error' : ''}`}>
|
|
164
|
+
<div className="message-content">
|
|
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>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|