@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.
Files changed (75) hide show
  1. package/README.md +1 -5
  2. package/package.json +4 -2
  3. package/src/agent/Agent.ts +353 -131
  4. package/src/agent/context.ts +4 -4
  5. package/src/agent/prompts/systemPrompt.ts +15 -6
  6. package/src/agent/prompts/toolsPrompt.ts +75 -10
  7. package/src/agent/provider/anthropic.ts +100 -100
  8. package/src/agent/provider/google.ts +102 -102
  9. package/src/agent/provider/mistral.ts +95 -95
  10. package/src/agent/provider/ollama.ts +77 -60
  11. package/src/agent/provider/openai.ts +42 -38
  12. package/src/agent/provider/rateLimit.ts +178 -0
  13. package/src/agent/provider/xai.ts +99 -99
  14. package/src/agent/tools/definitions.ts +19 -9
  15. package/src/agent/tools/executor.ts +95 -85
  16. package/src/agent/tools/exploreExecutor.ts +8 -10
  17. package/src/agent/tools/grep.ts +30 -29
  18. package/src/agent/tools/question.ts +7 -1
  19. package/src/agent/types.ts +9 -8
  20. package/src/components/App.tsx +45 -45
  21. package/src/components/CustomInput.tsx +214 -36
  22. package/src/components/Main.tsx +1146 -954
  23. package/src/components/Setup.tsx +1 -1
  24. package/src/components/Welcome.tsx +1 -1
  25. package/src/components/main/ApprovalPanel.tsx +4 -3
  26. package/src/components/main/ChatPage.tsx +858 -675
  27. package/src/components/main/HomePage.tsx +53 -38
  28. package/src/components/main/QuestionPanel.tsx +52 -7
  29. package/src/components/main/ThinkingIndicator.tsx +2 -1
  30. package/src/index.tsx +50 -20
  31. package/src/mcp/approvalPolicy.ts +148 -0
  32. package/src/mcp/cli/add.ts +185 -0
  33. package/src/mcp/cli/doctor.ts +77 -0
  34. package/src/mcp/cli/index.ts +85 -0
  35. package/src/mcp/cli/list.ts +50 -0
  36. package/src/mcp/cli/logs.ts +24 -0
  37. package/src/mcp/cli/manage.ts +99 -0
  38. package/src/mcp/cli/show.ts +53 -0
  39. package/src/mcp/cli/tools.ts +77 -0
  40. package/src/mcp/config.ts +223 -0
  41. package/src/mcp/index.ts +80 -0
  42. package/src/mcp/processManager.ts +299 -0
  43. package/src/mcp/rateLimiter.ts +50 -0
  44. package/src/mcp/registry.ts +151 -0
  45. package/src/mcp/schemaConverter.ts +100 -0
  46. package/src/mcp/servers/navigation.ts +854 -0
  47. package/src/mcp/toolCatalog.ts +169 -0
  48. package/src/mcp/types.ts +95 -0
  49. package/src/utils/approvalBridge.ts +17 -5
  50. package/src/utils/commands/compact.ts +30 -0
  51. package/src/utils/commands/echo.ts +1 -1
  52. package/src/utils/commands/index.ts +4 -6
  53. package/src/utils/commands/new.ts +15 -0
  54. package/src/utils/commands/types.ts +3 -0
  55. package/src/utils/config.ts +3 -1
  56. package/src/utils/diffRendering.tsx +1 -3
  57. package/src/utils/exploreBridge.ts +10 -0
  58. package/src/utils/markdown.tsx +163 -99
  59. package/src/utils/models.ts +31 -9
  60. package/src/utils/questionBridge.ts +36 -1
  61. package/src/utils/tokenEstimator.ts +32 -0
  62. package/src/utils/toolFormatting.ts +268 -7
  63. package/src/web/app.tsx +72 -72
  64. package/src/web/components/HomePage.tsx +7 -7
  65. package/src/web/components/MessageItem.tsx +22 -22
  66. package/src/web/components/QuestionPanel.tsx +72 -12
  67. package/src/web/components/Sidebar.tsx +0 -2
  68. package/src/web/components/ThinkingIndicator.tsx +1 -0
  69. package/src/web/server.tsx +767 -683
  70. package/src/utils/commands/redo.ts +0 -74
  71. package/src/utils/commands/sessions.ts +0 -129
  72. package/src/utils/commands/undo.ts +0 -75
  73. package/src/utils/undoRedo.ts +0 -429
  74. package/src/utils/undoRedoBridge.ts +0 -45
  75. 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) as Array<{ name?: string; path?: string; type?: string }>;
234
- if (!Array.isArray(parsed)) return [];
308
+ const parsed = JSON.parse(result);
235
309
 
236
- const entries = parsed
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
- return [...dirs, ...files];
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, useRef, useCallback } from 'react';
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">></span>
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
+ }