@kirosnn/mosaic 0.73.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.
@@ -1,10 +1,22 @@
1
1
  import { TextAttributes } from "@opentui/core";
2
2
 
3
- export interface MarkdownSegment {
4
- type: 'text' | 'bold' | 'italic' | 'code' | 'heading' | 'listitem';
5
- content: string;
6
- level?: number;
7
- }
3
+ export interface MarkdownSegment {
4
+ type: 'text' | 'bold' | 'italic' | 'code' | 'heading' | 'listitem' | 'link';
5
+ content: string;
6
+ level?: number;
7
+ href?: string;
8
+ }
9
+
10
+ const linkSchemePattern = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
11
+
12
+ function normalizeLinkUri(href: string) {
13
+ const trimmed = href.trim();
14
+ if (!trimmed) return trimmed;
15
+ if (linkSchemePattern.test(trimmed)) return trimmed;
16
+ if (trimmed.startsWith('//')) return `https:${trimmed}`;
17
+ if (trimmed.startsWith('/') || trimmed.startsWith('#') || trimmed.startsWith('.') || trimmed.startsWith('?')) return trimmed;
18
+ return `https://${trimmed}`;
19
+ }
8
20
 
9
21
  function parseInline(text: string): MarkdownSegment[] {
10
22
  const segments: MarkdownSegment[] = [];
@@ -30,21 +42,36 @@ function parseInline(text: string): MarkdownSegment[] {
30
42
  }
31
43
  }
32
44
 
33
- if (text.substring(i, i + 2) === '**') {
34
- const j = text.indexOf('**', i + 2);
35
- if (j !== -1) {
36
- flushText();
37
- segments.push({ type: 'bold', content: text.substring(i + 2, j) });
38
- i = j + 2;
39
- continue;
40
- }
41
- }
42
-
43
- if (text[i] === '*' && text.substring(i, i + 2) !== '**') {
44
- const j = text.indexOf('*', i + 1);
45
- if (j !== -1) {
46
- flushText();
47
- segments.push({ type: 'italic', content: text.substring(i + 1, j) });
45
+ if (text.substring(i, i + 2) === '**') {
46
+ const j = text.indexOf('**', i + 2);
47
+ if (j !== -1) {
48
+ flushText();
49
+ segments.push({ type: 'bold', content: text.substring(i + 2, j) });
50
+ i = j + 2;
51
+ continue;
52
+ }
53
+ }
54
+
55
+ if (text[i] === '[') {
56
+ const labelEnd = text.indexOf(']', i + 1);
57
+ if (labelEnd !== -1 && text[labelEnd + 1] === '(') {
58
+ const urlEnd = text.indexOf(')', labelEnd + 2);
59
+ if (urlEnd !== -1) {
60
+ const label = text.substring(i + 1, labelEnd);
61
+ const href = text.substring(labelEnd + 2, urlEnd).trim();
62
+ flushText();
63
+ segments.push({ type: 'link', content: label, href });
64
+ i = urlEnd + 1;
65
+ continue;
66
+ }
67
+ }
68
+ }
69
+
70
+ if (text[i] === '*' && text.substring(i, i + 2) !== '**') {
71
+ const j = text.indexOf('*', i + 1);
72
+ if (j !== -1) {
73
+ flushText();
74
+ segments.push({ type: 'italic', content: text.substring(i + 1, j) });
48
75
  i = j + 1;
49
76
  continue;
50
77
  }
@@ -82,12 +109,19 @@ export function renderMarkdownSegment(segment: MarkdownSegment, key: number) {
82
109
  case 'italic':
83
110
  return <text key={key} fg="white" attributes={TextAttributes.DIM}>{segment.content}</text>;
84
111
 
85
- case 'code':
86
- return <text key={key} fg="#ffdd80">{`${segment.content}`}</text>;
87
-
88
- case 'heading':
89
- return <text key={key} fg="#ffca38" attributes={TextAttributes.BOLD}>{segment.content}</text>;
90
-
112
+ case 'code':
113
+ return <text key={key} fg="#ffdd80">{`${segment.content}`}</text>;
114
+
115
+ case 'heading':
116
+ return <text key={key} fg="#ffca38" attributes={TextAttributes.BOLD}>{segment.content}</text>;
117
+
118
+ case 'link':
119
+ return (
120
+ <text key={key} fg="#7fbfff" attributes={TextAttributes.UNDERLINE}>
121
+ <a href={normalizeLinkUri(segment.href || '')}>{segment.content}</a>
122
+ </text>
123
+ );
124
+
91
125
  case 'listitem':
92
126
  return (
93
127
  <box key={key} flexDirection="row">
@@ -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
 
@@ -32,7 +33,21 @@ function getMcpToolDisplayName(tool: string): string {
32
33
  return words.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
33
34
  }
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
+
35
46
  function getToolDisplayName(toolName: string): string {
47
+ if (isNativeMcpTool(toolName)) {
48
+ const nativeName = getNativeToolDisplayName(toolName);
49
+ if (nativeName) return nativeName;
50
+ }
36
51
  const mcp = parseMcpSafeId(toolName);
37
52
  if (mcp) {
38
53
  return getMcpToolDisplayName(mcp.tool);
@@ -143,6 +158,15 @@ function formatKnownToolArgs(toolName: string, args: Record<string, unknown>): s
143
158
  }
144
159
  }
145
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
+
146
170
  export function isToolSuccess(result: unknown): boolean {
147
171
  if (result === null || result === undefined) return false;
148
172
 
@@ -211,6 +235,9 @@ function formatToolHeader(toolName: string, args: Record<string, unknown>): stri
211
235
  default: {
212
236
  if (toolName.startsWith('mcp__')) {
213
237
  const info = getMcpHeaderInfo('', args);
238
+ if (isNativeMcpTool(toolName)) {
239
+ return info ? `${displayName} (${info})` : displayName;
240
+ }
214
241
  return info ? `${displayName} ("${info}")` : displayName;
215
242
  }
216
243
  return displayName;
@@ -297,6 +324,10 @@ export function parseToolHeader(toolName: string, args: Record<string, unknown>)
297
324
  }
298
325
  }
299
326
 
327
+ export function isNativeMcpToolName(toolName: string): boolean {
328
+ return isNativeMcpTool(toolName);
329
+ }
330
+
300
331
  function getLineCount(text: string): number {
301
332
  if (!text) return 0;
302
333
  return text.split(/\r?\n/).length;
@@ -384,6 +415,24 @@ function getToolErrorText(result: unknown): string | null {
384
415
  return typeof error === 'string' && error.trim() ? error.trim() : null;
385
416
  }
386
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
+
387
436
  function formatMcpResultBody(result: unknown): string[] {
388
437
  if (typeof result !== 'string') {
389
438
  if (result && typeof result === 'object') {
@@ -545,12 +594,7 @@ function formatToolBodyLines(toolName: string, args: Record<string, unknown>, re
545
594
  }
546
595
  }
547
596
  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}`];
597
+ return formatNativeMcpError(errorText);
554
598
  }
555
599
  return [`Tool error: ${errorText}`];
556
600
  }
@@ -696,6 +740,11 @@ function formatToolBodyLines(toolName: string, args: Record<string, unknown>, re
696
740
 
697
741
  default: {
698
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
+ }
699
748
  return formatMcpResultBody(result);
700
749
  }
701
750
  const toolResultText = formatToolResult(result);
@@ -6,15 +6,33 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
6
6
  import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
7
7
  import { Message } from '../types';
8
8
  import { toDataUrl } from '../../utils/images';
9
- import { parseDiffLine, getDiffLineColors } from '../utils';
10
- import '../assets/css/global.css'
11
-
12
- interface MessageItemProps {
13
- message: Message;
14
- }
15
-
16
- function renderDiffLine(line: string, index: number): React.ReactElement {
17
- const parsed = parseDiffLine(line);
9
+ import { parseDiffLine, getDiffLineColors } from '../utils';
10
+ import '../assets/css/global.css'
11
+
12
+ interface MessageItemProps {
13
+ message: Message;
14
+ }
15
+
16
+ const linkSchemePattern = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
17
+
18
+ function normalizeLinkUri(href: string) {
19
+ const trimmed = href.trim();
20
+ if (!trimmed) return trimmed;
21
+ if (linkSchemePattern.test(trimmed)) return trimmed;
22
+ if (trimmed.startsWith('//')) return `https:${trimmed}`;
23
+ if (trimmed.startsWith('/') || trimmed.startsWith('#') || trimmed.startsWith('.') || trimmed.startsWith('?')) return trimmed;
24
+ return `https://${trimmed}`;
25
+ }
26
+
27
+ function handleMarkdownLinkClick(event: React.MouseEvent<HTMLAnchorElement>, href?: string | null) {
28
+ if (!href) return;
29
+ if (!event.ctrlKey && !event.metaKey) return;
30
+ event.preventDefault();
31
+ window.open(href, "_blank", "noopener,noreferrer");
32
+ }
33
+
34
+ function renderDiffLine(line: string, index: number): React.ReactElement {
35
+ const parsed = parseDiffLine(line);
18
36
 
19
37
  if (!parsed.isDiffLine) {
20
38
  return (
@@ -126,10 +144,23 @@ export function MessageItem({ message }: MessageItemProps) {
126
144
  </details>
127
145
  )}
128
146
  <div className="markdown-content">
129
- <ReactMarkdown
130
- remarkPlugins={[remarkGfm]}
131
- components={{
132
- code({ node, className, children, ...props }) {
147
+ <ReactMarkdown
148
+ remarkPlugins={[remarkGfm]}
149
+ transformLinkUri={normalizeLinkUri}
150
+ components={{
151
+ a({ href, children, ...props }) {
152
+ const normalized = normalizeLinkUri(href || '');
153
+ return (
154
+ <a
155
+ href={normalized}
156
+ onClick={(event) => handleMarkdownLinkClick(event, normalized)}
157
+ {...props}
158
+ >
159
+ {children}
160
+ </a>
161
+ );
162
+ },
163
+ code({ node, className, children, ...props }) {
133
164
  const match = /language-(\w+)/.exec(className || '');
134
165
  const { ref, ...rest } = props as any;
135
166
  return match ? (