@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.
- 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 +136 -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 +552 -339
- 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 +156 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +74 -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 +234 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +304 -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/browser.ts +151 -0
- package/src/mcp/servers/navigation/index.ts +23 -0
- package/src/mcp/servers/navigation/tools.ts +263 -0
- package/src/mcp/servers/navigation/types.ts +17 -0
- package/src/mcp/servers/navigation/utils.ts +20 -0
- package/src/mcp/toolCatalog.ts +182 -0
- package/src/mcp/types.ts +116 -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 +220 -122
- 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 +317 -7
- package/src/web/app.tsx +72 -72
- package/src/web/components/HomePage.tsx +7 -7
- package/src/web/components/MessageItem.tsx +66 -35
- 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
|
@@ -4,17 +4,35 @@ 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';
|
|
9
|
-
import { parseDiffLine, getDiffLineColors } from '../utils';
|
|
10
|
-
import '../assets/css/global.css'
|
|
11
|
-
|
|
12
|
-
interface MessageItemProps {
|
|
13
|
-
message: Message;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
import { Message } from '../types';
|
|
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
|
+
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 (
|
|
@@ -57,7 +75,7 @@ function renderToolLine(line: string, index: number): React.ReactElement {
|
|
|
57
75
|
return (
|
|
58
76
|
<div key={index} className="tool-line plan-line">
|
|
59
77
|
<span className="plan-indent">{leading || ''}</span>
|
|
60
|
-
<span className="plan-prefix"
|
|
78
|
+
<span className="plan-prefix">{'>'}</span>
|
|
61
79
|
<span> </span>
|
|
62
80
|
{bracket && <span className={`plan-bracket${isActive ? ' active' : ''}`}>{bracket}</span>}
|
|
63
81
|
{bracket && <span> </span>}
|
|
@@ -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
|
-
|
|
132
|
-
|
|
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 ? (
|
|
@@ -157,22 +188,22 @@ export function MessageItem({ message }: MessageItemProps) {
|
|
|
157
188
|
);
|
|
158
189
|
}
|
|
159
190
|
|
|
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
|
-
}
|
|
191
|
+
const hasImages = Array.isArray(message.images) && message.images.length > 0;
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div className={`message ${message.role} ${message.isError ? 'error' : ''}`}>
|
|
195
|
+
<div className="message-content">
|
|
196
|
+
{hasImages && (
|
|
197
|
+
<div className="message-images">
|
|
198
|
+
{message.images!.map((img) => (
|
|
199
|
+
<img key={img.id} src={toDataUrl(img)} alt={img.name} />
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
<div className="message-text">
|
|
204
|
+
{message.displayContent || message.content}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -10,14 +10,41 @@ interface QuestionPanelProps {
|
|
|
10
10
|
export function QuestionPanel({ request, onAnswer }: QuestionPanelProps) {
|
|
11
11
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
12
12
|
const [customText, setCustomText] = useState('');
|
|
13
|
+
const [remaining, setRemaining] = useState<number | null>(request.timeout ?? null);
|
|
14
|
+
const [validationError, setValidationError] = useState<string | null>(null);
|
|
13
15
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
14
16
|
|
|
15
17
|
useEffect(() => {
|
|
16
18
|
setSelectedIndex(0);
|
|
17
19
|
setCustomText('');
|
|
20
|
+
setValidationError(null);
|
|
21
|
+
setRemaining(request.timeout ?? null);
|
|
18
22
|
if (inputRef.current) inputRef.current.focus();
|
|
19
23
|
}, [request.id]);
|
|
20
24
|
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (remaining === null || remaining <= 0) return;
|
|
27
|
+
const id = setInterval(() => {
|
|
28
|
+
setRemaining(prev => (prev !== null && prev > 0 ? prev - 1 : prev));
|
|
29
|
+
}, 1000);
|
|
30
|
+
return () => clearInterval(id);
|
|
31
|
+
}, [remaining !== null]);
|
|
32
|
+
|
|
33
|
+
const validateCustomText = (text: string): boolean => {
|
|
34
|
+
if (!request.validation) return true;
|
|
35
|
+
try {
|
|
36
|
+
if (!new RegExp(request.validation.pattern).test(text)) {
|
|
37
|
+
setValidationError(request.validation.message || `Input must match: ${request.validation.pattern}`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
setValidationError('Invalid validation pattern');
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
setValidationError(null);
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
|
|
21
48
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
22
49
|
if (e.key === 'ArrowUp') {
|
|
23
50
|
e.preventDefault();
|
|
@@ -28,7 +55,9 @@ export function QuestionPanel({ request, onAnswer }: QuestionPanelProps) {
|
|
|
28
55
|
} else if (e.key === 'Enter') {
|
|
29
56
|
e.preventDefault();
|
|
30
57
|
if (customText.trim()) {
|
|
31
|
-
|
|
58
|
+
if (validateCustomText(customText)) {
|
|
59
|
+
onAnswer(0, customText);
|
|
60
|
+
}
|
|
32
61
|
} else {
|
|
33
62
|
onAnswer(selectedIndex);
|
|
34
63
|
}
|
|
@@ -46,15 +75,22 @@ export function QuestionPanel({ request, onAnswer }: QuestionPanelProps) {
|
|
|
46
75
|
|
|
47
76
|
const handleSubmitCustom = (e: React.FormEvent) => {
|
|
48
77
|
e.preventDefault();
|
|
49
|
-
if (customText.trim()) {
|
|
78
|
+
if (customText.trim() && validateCustomText(customText)) {
|
|
50
79
|
onAnswer(0, customText);
|
|
51
80
|
}
|
|
52
81
|
};
|
|
53
82
|
|
|
83
|
+
let lastGroup: string | undefined;
|
|
84
|
+
|
|
54
85
|
return (
|
|
55
86
|
<div className="panel question-panel">
|
|
56
87
|
<div className="panel-header">
|
|
57
88
|
<strong>Question</strong>
|
|
89
|
+
{remaining !== null && (
|
|
90
|
+
<span style={{ marginLeft: '1rem', color: remaining <= 5 ? '#ff4444' : 'var(--text-muted)' }}>
|
|
91
|
+
Timeout: {remaining}s
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
58
94
|
</div>
|
|
59
95
|
<div className="panel-content">
|
|
60
96
|
<div className="question-prompt">
|
|
@@ -63,17 +99,28 @@ export function QuestionPanel({ request, onAnswer }: QuestionPanelProps) {
|
|
|
63
99
|
))}
|
|
64
100
|
</div>
|
|
65
101
|
<div className="question-options">
|
|
66
|
-
{request.options.map((option, index) =>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
102
|
+
{request.options.map((option, index) => {
|
|
103
|
+
const showGroupHeader = option.group && option.group !== lastGroup;
|
|
104
|
+
lastGroup = option.group;
|
|
105
|
+
return (
|
|
106
|
+
<React.Fragment key={index}>
|
|
107
|
+
{showGroupHeader && (
|
|
108
|
+
<div className="option-group-header">{option.group}</div>
|
|
109
|
+
)}
|
|
110
|
+
<div
|
|
111
|
+
className={`question-option ${index === selectedIndex ? 'selected' : ''}`}
|
|
112
|
+
onClick={() => handleOptionClick(index)}
|
|
113
|
+
>
|
|
114
|
+
<span className="option-key">{index + 1}.</span>
|
|
115
|
+
<span className="option-label">{option.label}</span>
|
|
116
|
+
</div>
|
|
117
|
+
</React.Fragment>
|
|
118
|
+
);
|
|
119
|
+
})}
|
|
76
120
|
</div>
|
|
121
|
+
{validationError && (
|
|
122
|
+
<div className="validation-error">{validationError}</div>
|
|
123
|
+
)}
|
|
77
124
|
<div className="custom-input-container">
|
|
78
125
|
<form onSubmit={handleSubmitCustom}>
|
|
79
126
|
<input
|
|
@@ -111,6 +158,13 @@ export function QuestionPanel({ request, onAnswer }: QuestionPanelProps) {
|
|
|
111
158
|
gap: 0.5rem;
|
|
112
159
|
margin-bottom: 1rem;
|
|
113
160
|
}
|
|
161
|
+
.option-group-header {
|
|
162
|
+
font-size: 0.85rem;
|
|
163
|
+
color: var(--text-muted);
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
margin-top: 0.5rem;
|
|
166
|
+
padding-left: 0.25rem;
|
|
167
|
+
}
|
|
114
168
|
.question-option {
|
|
115
169
|
padding: 0.5rem;
|
|
116
170
|
border-radius: 4px;
|
|
@@ -130,6 +184,12 @@ export function QuestionPanel({ request, onAnswer }: QuestionPanelProps) {
|
|
|
130
184
|
color: var(--text-muted);
|
|
131
185
|
width: 1.5rem;
|
|
132
186
|
}
|
|
187
|
+
.validation-error {
|
|
188
|
+
color: #ff4444;
|
|
189
|
+
font-size: 0.85rem;
|
|
190
|
+
margin-bottom: 0.5rem;
|
|
191
|
+
padding-left: 0.25rem;
|
|
192
|
+
}
|
|
133
193
|
.panel-input {
|
|
134
194
|
width: 100%;
|
|
135
195
|
padding: 0.75rem;
|
|
@@ -12,8 +12,6 @@ interface GroupedConversations {
|
|
|
12
12
|
|
|
13
13
|
function getTimePeriod(timestamp: number): TimePeriod {
|
|
14
14
|
const now = new Date();
|
|
15
|
-
const date = new Date(timestamp);
|
|
16
|
-
|
|
17
15
|
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
18
16
|
const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000;
|
|
19
17
|
const startOf7DaysAgo = startOfToday - 7 * 24 * 60 * 60 * 1000;
|