@kirosnn/mosaic 0.71.0 → 0.74.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 (79) 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 +136 -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 +552 -339
  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 +156 -0
  32. package/src/mcp/cli/add.ts +185 -0
  33. package/src/mcp/cli/doctor.ts +74 -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 +234 -0
  41. package/src/mcp/index.ts +80 -0
  42. package/src/mcp/processManager.ts +304 -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/browser.ts +151 -0
  47. package/src/mcp/servers/navigation/index.ts +23 -0
  48. package/src/mcp/servers/navigation/tools.ts +263 -0
  49. package/src/mcp/servers/navigation/types.ts +17 -0
  50. package/src/mcp/servers/navigation/utils.ts +20 -0
  51. package/src/mcp/toolCatalog.ts +182 -0
  52. package/src/mcp/types.ts +116 -0
  53. package/src/utils/approvalBridge.ts +17 -5
  54. package/src/utils/commands/compact.ts +30 -0
  55. package/src/utils/commands/echo.ts +1 -1
  56. package/src/utils/commands/index.ts +4 -6
  57. package/src/utils/commands/new.ts +15 -0
  58. package/src/utils/commands/types.ts +3 -0
  59. package/src/utils/config.ts +3 -1
  60. package/src/utils/diffRendering.tsx +1 -3
  61. package/src/utils/exploreBridge.ts +10 -0
  62. package/src/utils/markdown.tsx +220 -122
  63. package/src/utils/models.ts +31 -9
  64. package/src/utils/questionBridge.ts +36 -1
  65. package/src/utils/tokenEstimator.ts +32 -0
  66. package/src/utils/toolFormatting.ts +317 -7
  67. package/src/web/app.tsx +72 -72
  68. package/src/web/components/HomePage.tsx +7 -7
  69. package/src/web/components/MessageItem.tsx +66 -35
  70. package/src/web/components/QuestionPanel.tsx +72 -12
  71. package/src/web/components/Sidebar.tsx +0 -2
  72. package/src/web/components/ThinkingIndicator.tsx +1 -0
  73. package/src/web/server.tsx +767 -683
  74. package/src/utils/commands/redo.ts +0 -74
  75. package/src/utils/commands/sessions.ts +0 -129
  76. package/src/utils/commands/undo.ts +0 -75
  77. package/src/utils/undoRedo.ts +0 -429
  78. package/src/utils/undoRedoBridge.ts +0 -45
  79. package/src/utils/undoRedoDb.ts +0 -338
@@ -1,4 +1,5 @@
1
1
  import { formatWriteToolResult, formatEditToolResult } from './diff';
2
+ import { isNativeMcpTool, getNativeMcpToolName } from '../mcp/types';
2
3
 
3
4
  const TOOL_BODY_INDENT = 2;
4
5
 
@@ -18,7 +19,39 @@ const TOOL_DISPLAY_NAMES: Record<string, string> = {
18
19
  plan: 'Plan',
19
20
  };
20
21
 
22
+ function parseMcpSafeId(toolName: string): { serverId: string; tool: string } | null {
23
+ if (!toolName.startsWith('mcp__')) return null;
24
+ const parts = toolName.slice(5).split('__');
25
+ if (parts.length < 2) return null;
26
+ const tool = parts.pop()!;
27
+ const serverId = parts.join('__');
28
+ return { serverId, tool };
29
+ }
30
+
31
+ function getMcpToolDisplayName(tool: string): string {
32
+ const words = tool.replace(/[-_]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2');
33
+ return words.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
34
+ }
35
+
36
+ function getNativeToolDisplayName(safeId: string): string | null {
37
+ const toolName = getNativeMcpToolName(safeId);
38
+ if (!toolName) return null;
39
+ const mcp = parseMcpSafeId(safeId);
40
+ if (!mcp) return null;
41
+ const serverPrefix = mcp.serverId + '_';
42
+ const stripped = toolName.startsWith(serverPrefix) ? toolName.slice(serverPrefix.length) : toolName;
43
+ return getMcpToolDisplayName(stripped);
44
+ }
45
+
21
46
  function getToolDisplayName(toolName: string): string {
47
+ if (isNativeMcpTool(toolName)) {
48
+ const nativeName = getNativeToolDisplayName(toolName);
49
+ if (nativeName) return nativeName;
50
+ }
51
+ const mcp = parseMcpSafeId(toolName);
52
+ if (mcp) {
53
+ return getMcpToolDisplayName(mcp.tool);
54
+ }
22
55
  return TOOL_DISPLAY_NAMES[toolName] || toolName;
23
56
  }
24
57
 
@@ -43,6 +76,50 @@ export function formatToolResult(result: unknown): string {
43
76
  }
44
77
  }
45
78
 
79
+ function getMcpHeaderInfo(_tool: string, args: Record<string, unknown>): string {
80
+ const query = args.query ?? args.search ?? args.prompt ?? args.input ?? args.text ?? args.q;
81
+ if (typeof query === 'string' && query.trim()) {
82
+ const clean = query.replace(/[\r\n]+/g, ' ').trim();
83
+ return clean.length > 50 ? clean.slice(0, 50) + '...' : clean;
84
+ }
85
+
86
+ const url = args.url ?? args.uri ?? args.href;
87
+ if (typeof url === 'string' && url.trim()) {
88
+ try {
89
+ const u = new URL(url);
90
+ return u.hostname + (u.pathname !== '/' ? u.pathname : '');
91
+ } catch {
92
+ return url.length > 50 ? url.slice(0, 50) + '...' : url;
93
+ }
94
+ }
95
+
96
+ const urls = args.urls;
97
+ if (Array.isArray(urls) && urls.length > 0) {
98
+ const first = typeof urls[0] === 'string' ? urls[0] : '';
99
+ if (urls.length === 1) {
100
+ try {
101
+ return new URL(first).hostname;
102
+ } catch {
103
+ return first.length > 40 ? first.slice(0, 40) + '...' : first;
104
+ }
105
+ }
106
+ return `${urls.length} URLs`;
107
+ }
108
+
109
+ const path = args.path ?? args.file ?? args.filename ?? args.filepath ?? args.name;
110
+ if (typeof path === 'string' && path.trim()) {
111
+ return path.length > 50 ? path.slice(0, 50) + '...' : path;
112
+ }
113
+
114
+ const command = args.command ?? args.cmd;
115
+ if (typeof command === 'string' && command.trim()) {
116
+ const clean = command.replace(/[\r\n]+/g, ' ').trim();
117
+ return clean.length > 50 ? clean.slice(0, 50) + '...' : clean;
118
+ }
119
+
120
+ return '';
121
+ }
122
+
46
123
  function formatKnownToolArgs(toolName: string, args: Record<string, unknown>): string | null {
47
124
  switch (toolName) {
48
125
  case 'read':
@@ -67,6 +144,9 @@ function formatKnownToolArgs(toolName: string, args: Record<string, unknown>): s
67
144
  }
68
145
 
69
146
  default: {
147
+ if (toolName.startsWith('mcp__')) {
148
+ return null;
149
+ }
70
150
  const keys = Object.keys(args);
71
151
  if (keys.length === 0) return null;
72
152
  try {
@@ -78,6 +158,15 @@ function formatKnownToolArgs(toolName: string, args: Record<string, unknown>): s
78
158
  }
79
159
  }
80
160
 
161
+ function formatNativeMcpError(errorText: string): string[] {
162
+ const statusMatch = errorText.match(/status code (\d+)/i);
163
+ if (statusMatch) {
164
+ return [`Error ${statusMatch[1]}`];
165
+ }
166
+ const short = errorText.length > 80 ? errorText.slice(0, 80) + '...' : errorText;
167
+ return [`Error: ${short}`];
168
+ }
169
+
81
170
  export function isToolSuccess(result: unknown): boolean {
82
171
  if (result === null || result === undefined) return false;
83
172
 
@@ -143,8 +232,16 @@ function formatToolHeader(toolName: string, args: Record<string, unknown>): stri
143
232
  }
144
233
  case 'plan':
145
234
  return displayName;
146
- default:
235
+ default: {
236
+ if (toolName.startsWith('mcp__')) {
237
+ const info = getMcpHeaderInfo('', args);
238
+ if (isNativeMcpTool(toolName)) {
239
+ return info ? `${displayName} (${info})` : displayName;
240
+ }
241
+ return info ? `${displayName} ("${info}")` : displayName;
242
+ }
147
243
  return displayName;
244
+ }
148
245
  }
149
246
  }
150
247
 
@@ -217,11 +314,20 @@ export function parseToolHeader(toolName: string, args: Record<string, unknown>)
217
314
  }
218
315
  case 'plan':
219
316
  return { name: displayName, info: null };
220
- default:
317
+ default: {
318
+ if (toolName.startsWith('mcp__')) {
319
+ const info = getMcpHeaderInfo('', args);
320
+ return { name: displayName, info: info || null };
321
+ }
221
322
  return { name: displayName, info: null };
323
+ }
222
324
  }
223
325
  }
224
326
 
327
+ export function isNativeMcpToolName(toolName: string): boolean {
328
+ return isNativeMcpTool(toolName);
329
+ }
330
+
225
331
  function getLineCount(text: string): number {
226
332
  if (!text) return 0;
227
333
  return text.split(/\r?\n/).length;
@@ -230,10 +336,25 @@ function getLineCount(text: string): number {
230
336
  function formatListTree(result: unknown): string[] {
231
337
  if (typeof result !== 'string') return [];
232
338
  try {
233
- const parsed = JSON.parse(result) as Array<{ name?: string; path?: string; type?: string }>;
234
- if (!Array.isArray(parsed)) return [];
339
+ const parsed = JSON.parse(result);
340
+
341
+ let items: Array<{ name?: string; path?: string; type?: string }>;
342
+ let errors: string[] = [];
343
+
344
+ if (Array.isArray(parsed)) {
345
+ items = parsed;
346
+ } else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.files)) {
347
+ items = parsed.files;
348
+ if (Array.isArray(parsed.errors)) {
349
+ errors = parsed.errors
350
+ .map((e: unknown) => (typeof e === 'string' ? e : ''))
351
+ .filter((e: string) => e);
352
+ }
353
+ } else {
354
+ return [];
355
+ }
235
356
 
236
- const entries = parsed
357
+ const entries = items
237
358
  .map((e) => ({
238
359
  name: typeof e.name === 'string' ? e.name : (typeof e.path === 'string' ? e.path : ''),
239
360
  type: typeof e.type === 'string' ? e.type : '',
@@ -250,7 +371,16 @@ function formatListTree(result: unknown): string[] {
250
371
  .map((e) => e.name)
251
372
  .sort((a, b) => a.localeCompare(b));
252
373
 
253
- return [...dirs, ...files];
374
+ const lines = [...dirs, ...files];
375
+
376
+ if (errors.length > 0) {
377
+ lines.push('', `Errors (${errors.length}):`);
378
+ for (const err of errors) {
379
+ lines.push(` ${err}`);
380
+ }
381
+ }
382
+
383
+ return lines;
254
384
  } catch {
255
385
  return [];
256
386
  }
@@ -285,6 +415,175 @@ function getToolErrorText(result: unknown): string | null {
285
415
  return typeof error === 'string' && error.trim() ? error.trim() : null;
286
416
  }
287
417
 
418
+ function formatSearchResultBody(result: unknown): string[] {
419
+ if (typeof result !== 'string') return [];
420
+ try {
421
+ const parsed = JSON.parse(result);
422
+ if (typeof parsed !== 'object' || parsed === null) return [];
423
+
424
+ if (typeof parsed.error === 'string') {
425
+ return [`Error: ${parsed.error}`];
426
+ }
427
+
428
+ const count = typeof parsed.resultCount === 'number' ? parsed.resultCount : 0;
429
+ if (count === 0) return ['No results'];
430
+ return [`${count} results`];
431
+ } catch {
432
+ return [];
433
+ }
434
+ }
435
+
436
+ function formatMcpResultBody(result: unknown): string[] {
437
+ if (typeof result !== 'string') {
438
+ if (result && typeof result === 'object') {
439
+ const obj = result as Record<string, unknown>;
440
+ if (typeof obj.error === 'string') {
441
+ return [`Error: ${obj.error}`];
442
+ }
443
+ }
444
+ return ['Completed'];
445
+ }
446
+
447
+ const text = result.trim();
448
+ if (!text) return ['(empty result)'];
449
+
450
+ try {
451
+ const parsed = JSON.parse(text);
452
+
453
+ if (Array.isArray(parsed)) {
454
+ return formatMcpArray(parsed);
455
+ }
456
+
457
+ if (typeof parsed === 'object' && parsed !== null) {
458
+ return formatMcpObject(parsed);
459
+ }
460
+
461
+ return [String(parsed)];
462
+ } catch {
463
+ const lines = text.split(/\r?\n/).filter(l => l.trim());
464
+ return lines.length > 0 ? lines : ['Completed'];
465
+ }
466
+ }
467
+
468
+ function formatMcpArray(arr: unknown[]): string[] {
469
+ if (arr.length === 0) return ['(no results)'];
470
+
471
+ const lines: string[] = [];
472
+
473
+ for (const item of arr) {
474
+ if (typeof item === 'string') {
475
+ lines.push(` ${item}`);
476
+ continue;
477
+ }
478
+
479
+ if (item && typeof item === 'object') {
480
+ const obj = item as Record<string, unknown>;
481
+
482
+ if (typeof obj.error === 'string') {
483
+ const url = typeof obj.url === 'string' ? obj.url : '';
484
+ const detail = obj.details && typeof obj.details === 'object'
485
+ ? (obj.details as Record<string, unknown>).detail
486
+ : '';
487
+ const errMsg = typeof detail === 'string' && detail ? detail : obj.error;
488
+ lines.push(url ? ` ${url} - ${errMsg}` : ` Error: ${errMsg}`);
489
+ continue;
490
+ }
491
+
492
+ const title = obj.title ?? obj.name ?? obj.label;
493
+ const desc = obj.description ?? obj.summary ?? obj.snippet ?? obj.text ?? obj.content;
494
+ const url = obj.url ?? obj.link ?? obj.href;
495
+
496
+ if (typeof title === 'string' && title) {
497
+ let line = ` ${title}`;
498
+ if (typeof url === 'string' && url) {
499
+ try {
500
+ line += ` (${new URL(url).hostname})`;
501
+ } catch {
502
+ line += ` (${url.length > 40 ? url.slice(0, 40) + '...' : url})`;
503
+ }
504
+ }
505
+ lines.push(line);
506
+ if (typeof desc === 'string' && desc.trim()) {
507
+ const short = desc.trim().replace(/[\r\n]+/g, ' ');
508
+ lines.push(` ${short.length > 80 ? short.slice(0, 80) + '...' : short}`);
509
+ }
510
+ continue;
511
+ }
512
+
513
+ if (typeof url === 'string' && url) {
514
+ let line = ` ${url}`;
515
+ if (typeof desc === 'string' && desc.trim()) {
516
+ const short = desc.trim().replace(/[\r\n]+/g, ' ');
517
+ line += ` - ${short.length > 60 ? short.slice(0, 60) + '...' : short}`;
518
+ }
519
+ lines.push(line);
520
+ continue;
521
+ }
522
+
523
+ const keys = Object.keys(obj);
524
+ const summary = keys.slice(0, 3).map(k => {
525
+ const v = obj[k];
526
+ const s = typeof v === 'string' ? v : JSON.stringify(v);
527
+ const short = s && s.length > 30 ? s.slice(0, 30) + '...' : s;
528
+ return `${k}: ${short}`;
529
+ }).join(', ');
530
+ lines.push(` ${summary}`);
531
+ }
532
+ }
533
+
534
+ if (arr.length > 1) {
535
+ lines.unshift(`${arr.length} results:`);
536
+ }
537
+
538
+ return lines.length > 0 ? lines : ['Completed'];
539
+ }
540
+
541
+ function formatMcpObject(obj: Record<string, unknown>): string[] {
542
+ const lines: string[] = [];
543
+
544
+ if (typeof obj.error === 'string') {
545
+ const detail = obj.details && typeof obj.details === 'object'
546
+ ? (obj.details as Record<string, unknown>).detail
547
+ : '';
548
+ const errMsg = typeof detail === 'string' && detail ? detail : obj.error;
549
+ return [`Error: ${errMsg}`];
550
+ }
551
+
552
+ const status = obj.status ?? obj.statusCode ?? obj.code;
553
+ const message = obj.message ?? obj.result ?? obj.output ?? obj.text ?? obj.content ?? obj.data;
554
+
555
+ if (typeof status === 'number' || typeof status === 'string') {
556
+ lines.push(`Status: ${status}`);
557
+ }
558
+
559
+ if (typeof message === 'string' && message.trim()) {
560
+ const msgLines = message.trim().split(/\r?\n/);
561
+ lines.push(...msgLines);
562
+ } else if (message && typeof message === 'object') {
563
+ if (Array.isArray(message)) {
564
+ lines.push(...formatMcpArray(message));
565
+ } else {
566
+ const entries = Object.entries(message as Record<string, unknown>).slice(0, 5);
567
+ for (const [k, v] of entries) {
568
+ const s = typeof v === 'string' ? v : JSON.stringify(v);
569
+ const short = s && s.length > 60 ? s.slice(0, 60) + '...' : s;
570
+ lines.push(` ${k}: ${short}`);
571
+ }
572
+ }
573
+ }
574
+
575
+ if (lines.length === 0) {
576
+ const entries = Object.entries(obj).slice(0, 5);
577
+ for (const [k, v] of entries) {
578
+ const s = typeof v === 'string' ? v : JSON.stringify(v);
579
+ const short = s && s.length > 60 ? s.slice(0, 60) + '...' : s;
580
+ lines.push(` ${k}: ${short}`);
581
+ }
582
+ }
583
+
584
+ return lines.length > 0 ? lines : ['Completed'];
585
+ }
586
+
288
587
  function formatToolBodyLines(toolName: string, args: Record<string, unknown>, result: unknown): string[] {
289
588
  const errorText = getToolErrorText(result);
290
589
  if (errorText) {
@@ -294,6 +593,9 @@ function formatToolBodyLines(toolName: string, args: Record<string, unknown>, re
294
593
  return [`${statusMatch[1]} - Failed to fetch`];
295
594
  }
296
595
  }
596
+ if (toolName.startsWith('mcp__')) {
597
+ return formatNativeMcpError(errorText);
598
+ }
297
599
  return [`Tool error: ${errorText}`];
298
600
  }
299
601
 
@@ -437,6 +739,14 @@ function formatToolBodyLines(toolName: string, args: Record<string, unknown>, re
437
739
  }
438
740
 
439
741
  default: {
742
+ if (toolName.startsWith('mcp__')) {
743
+ const nativeName = getNativeMcpToolName(toolName);
744
+ if (nativeName === 'navigation_search') {
745
+ const searchLines = formatSearchResultBody(result);
746
+ if (searchLines.length > 0) return searchLines;
747
+ }
748
+ return formatMcpResultBody(result);
749
+ }
440
750
  const toolResultText = formatToolResult(result);
441
751
  if (!toolResultText) return [];
442
752
  return toolResultText.split(/\r?\n/);
@@ -500,4 +810,4 @@ export function getToolWrapWidth(maxWidth: number, paragraphIndex: number): numb
500
810
 
501
811
  export function formatErrorMessage(errorType: 'API' | 'Mosaic' | 'Tool', errorMessage: string): string {
502
812
  return `${errorType} Error\n${errorMessage}`;
503
- }
813
+ }
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
+ }