@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
package/src/utils/markdown.tsx
CHANGED
|
@@ -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] === '
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
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">
|
|
@@ -121,112 +155,176 @@ export function parseMarkdownContent(content: string): ParsedMarkdownLine[] {
|
|
|
121
155
|
return result;
|
|
122
156
|
}
|
|
123
157
|
|
|
124
|
-
export function wrapMarkdownText(text: string, maxWidth: number): { text: string; segments: MarkdownSegment[] }[] {
|
|
125
|
-
if (!text || maxWidth <= 0) return [{ text: '', segments: [] }];
|
|
158
|
+
export function wrapMarkdownText(text: string, maxWidth: number): { text: string; segments: MarkdownSegment[] }[] {
|
|
159
|
+
if (!text || maxWidth <= 0) return [{ text: '', segments: [] }];
|
|
160
|
+
|
|
161
|
+
const segments = parseMarkdownLine(text);
|
|
162
|
+
const lines: { text: string; segments: MarkdownSegment[] }[] = [];
|
|
163
|
+
let currentLine = '';
|
|
164
|
+
let currentSegments: MarkdownSegment[] = [];
|
|
165
|
+
|
|
166
|
+
const splitSegment = (segment: MarkdownSegment): MarkdownSegment[] => {
|
|
167
|
+
if (segment.type === 'code') return [segment];
|
|
168
|
+
const parts = segment.content.match(/\s+|[^\s]+/g);
|
|
169
|
+
if (!parts) return [segment];
|
|
170
|
+
return parts.map(part => ({ ...segment, content: part }));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const pushLine = () => {
|
|
174
|
+
if (!currentLine) return;
|
|
175
|
+
lines.push({ text: currentLine, segments: currentSegments });
|
|
176
|
+
currentLine = '';
|
|
177
|
+
currentSegments = [];
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const addPiece = (piece: MarkdownSegment) => {
|
|
181
|
+
let remaining = piece.content;
|
|
182
|
+
while (remaining.length > 0) {
|
|
183
|
+
if (!currentLine) {
|
|
184
|
+
if (remaining.trim() === '') {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (remaining.length <= maxWidth) {
|
|
188
|
+
currentLine = remaining;
|
|
189
|
+
currentSegments = [{ ...piece, content: remaining }];
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const chunk = remaining.slice(0, maxWidth);
|
|
193
|
+
lines.push({ text: chunk, segments: [{ ...piece, content: chunk }] });
|
|
194
|
+
remaining = remaining.slice(maxWidth);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if ((currentLine + remaining).length <= maxWidth) {
|
|
199
|
+
currentLine += remaining;
|
|
200
|
+
currentSegments.push({ ...piece, content: remaining });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
pushLine();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
for (const segment of segments) {
|
|
209
|
+
const pieces = splitSegment(segment);
|
|
210
|
+
for (const piece of pieces) {
|
|
211
|
+
addPiece(piece);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (currentLine) {
|
|
216
|
+
lines.push({ text: currentLine, segments: currentSegments });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return lines.length > 0 ? lines : [{ text: '', segments: [] }];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface WrappedMarkdownBlock {
|
|
223
|
+
type: 'line' | 'code';
|
|
224
|
+
wrappedLines?: { text: string; segments: MarkdownSegment[] }[];
|
|
225
|
+
codeLines?: string[];
|
|
226
|
+
language?: string;
|
|
227
|
+
}
|
|
126
228
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
229
|
+
function wrapCodeLine(line: string, maxWidth: number): string[] {
|
|
230
|
+
if (!line) return [''];
|
|
231
|
+
if (maxWidth <= 0) return [line];
|
|
232
|
+
if (line.length <= maxWidth) return [line];
|
|
131
233
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
234
|
+
const chunks: string[] = [];
|
|
235
|
+
let i = 0;
|
|
236
|
+
while (i < line.length) {
|
|
237
|
+
chunks.push(line.slice(i, i + maxWidth));
|
|
238
|
+
i += maxWidth;
|
|
239
|
+
}
|
|
240
|
+
return chunks;
|
|
241
|
+
}
|
|
135
242
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
243
|
+
function reflowParagraphs(text: string): string {
|
|
244
|
+
const rawLines = text.split('\n');
|
|
245
|
+
const result: string[] = [];
|
|
246
|
+
let inCodeBlock = false;
|
|
247
|
+
|
|
248
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
249
|
+
const line = rawLines[i]!;
|
|
250
|
+
|
|
251
|
+
if (/^```/.test(line)) {
|
|
252
|
+
inCodeBlock = !inCodeBlock;
|
|
253
|
+
result.push(line);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (inCodeBlock) {
|
|
258
|
+
result.push(line);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (line.trim() === '') {
|
|
263
|
+
result.push(line);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (/^#{1,6}\s/.test(line) || /^[-*+]\s/.test(line) || /^\d+\.\s/.test(line)) {
|
|
268
|
+
result.push(line);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const prev = result.length > 0 ? result[result.length - 1]! : '';
|
|
273
|
+
const prevIsText = prev.trim() !== '' &&
|
|
274
|
+
!/^```/.test(prev) &&
|
|
275
|
+
!/^#{1,6}\s/.test(prev) &&
|
|
276
|
+
!/^[-*+]\s/.test(prev) &&
|
|
277
|
+
!/^\d+\.\s/.test(prev);
|
|
278
|
+
|
|
279
|
+
if (prevIsText) {
|
|
280
|
+
result[result.length - 1] = prev + ' ' + line;
|
|
169
281
|
} else {
|
|
170
|
-
|
|
171
|
-
|
|
282
|
+
result.push(line);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return result.join('\n');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function parseAndWrapMarkdown(text: string, maxWidth: number): WrappedMarkdownBlock[] {
|
|
290
|
+
const lines = reflowParagraphs(text).split('\n');
|
|
291
|
+
const blocks: WrappedMarkdownBlock[] = [];
|
|
292
|
+
let inCodeBlock = false;
|
|
293
|
+
let codeLines: string[] = [];
|
|
294
|
+
let language: string | undefined;
|
|
172
295
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
296
|
+
for (const line of lines) {
|
|
297
|
+
const fenceMatch = line.match(/^```([A-Za-z0-9_-]+)?\s*$/);
|
|
298
|
+
if (fenceMatch) {
|
|
299
|
+
if (!inCodeBlock) {
|
|
300
|
+
inCodeBlock = true;
|
|
301
|
+
language = fenceMatch[1];
|
|
302
|
+
codeLines = [];
|
|
176
303
|
} else {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
let remaining = content;
|
|
183
|
-
while (remaining) {
|
|
184
|
-
if (remaining.length <= maxWidth) {
|
|
185
|
-
currentLine = remaining;
|
|
186
|
-
currentSegments = [{ ...segment, content: remaining }];
|
|
187
|
-
remaining = '';
|
|
188
|
-
} else {
|
|
189
|
-
const breakPoint = remaining.lastIndexOf(' ', maxWidth);
|
|
190
|
-
if (breakPoint > 0) {
|
|
191
|
-
const chunk = remaining.slice(0, breakPoint);
|
|
192
|
-
currentLine = chunk;
|
|
193
|
-
currentSegments = [{ ...segment, content: chunk }];
|
|
194
|
-
lines.push({ text: currentLine, segments: currentSegments });
|
|
195
|
-
currentLine = '';
|
|
196
|
-
currentSegments = [];
|
|
197
|
-
remaining = remaining.slice(breakPoint + 1);
|
|
198
|
-
} else {
|
|
199
|
-
const chunk = remaining.slice(0, maxWidth);
|
|
200
|
-
currentLine = chunk;
|
|
201
|
-
currentSegments = [{ ...segment, content: chunk }];
|
|
202
|
-
lines.push({ text: currentLine, segments: currentSegments });
|
|
203
|
-
currentLine = '';
|
|
204
|
-
currentSegments = [];
|
|
205
|
-
remaining = remaining.slice(maxWidth);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
304
|
+
const wrapped = codeLines.flatMap(codeLine => wrapCodeLine(codeLine, maxWidth));
|
|
305
|
+
blocks.push({ type: 'code', codeLines: wrapped, language });
|
|
306
|
+
inCodeBlock = false;
|
|
307
|
+
codeLines = [];
|
|
308
|
+
language = undefined;
|
|
210
309
|
}
|
|
310
|
+
continue;
|
|
211
311
|
}
|
|
212
|
-
}
|
|
213
312
|
|
|
214
|
-
|
|
215
|
-
|
|
313
|
+
if (inCodeBlock) {
|
|
314
|
+
codeLines.push(line);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
blocks.push({
|
|
319
|
+
type: 'line',
|
|
320
|
+
wrappedLines: wrapMarkdownText(line, maxWidth)
|
|
321
|
+
});
|
|
216
322
|
}
|
|
217
323
|
|
|
218
|
-
|
|
219
|
-
|
|
324
|
+
if (inCodeBlock) {
|
|
325
|
+
const wrapped = codeLines.flatMap(codeLine => wrapCodeLine(codeLine, maxWidth));
|
|
326
|
+
blocks.push({ type: 'code', codeLines: wrapped, language });
|
|
327
|
+
}
|
|
220
328
|
|
|
221
|
-
|
|
222
|
-
type: 'line';
|
|
223
|
-
wrappedLines?: { text: string; segments: MarkdownSegment[] }[];
|
|
329
|
+
return blocks;
|
|
224
330
|
}
|
|
225
|
-
|
|
226
|
-
export function parseAndWrapMarkdown(text: string, maxWidth: number): WrappedMarkdownBlock[] {
|
|
227
|
-
const lines = text.split('\n');
|
|
228
|
-
return lines.map((line) => ({
|
|
229
|
-
type: 'line',
|
|
230
|
-
wrappedLines: wrapMarkdownText(line, maxWidth)
|
|
231
|
-
}));
|
|
232
|
-
}
|
package/src/utils/models.ts
CHANGED
|
@@ -286,12 +286,34 @@ export async function findModelsDevModelById(
|
|
|
286
286
|
return modelsDev.getModelById(modelId, options);
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
export async function searchModelsDev(query: ModelsDevSearchQuery, options: { refresh?: boolean } = {}): Promise<ModelsDevSearchResult[]> {
|
|
290
|
-
return modelsDev.search(query, options);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export function
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
289
|
+
export async function searchModelsDev(query: ModelsDevSearchQuery, options: { refresh?: boolean } = {}): Promise<ModelsDevSearchResult[]> {
|
|
290
|
+
return modelsDev.search(query, options);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function getModelsDevContextLimit(
|
|
294
|
+
providerId: string,
|
|
295
|
+
modelId: string,
|
|
296
|
+
options: { refresh?: boolean } = {}
|
|
297
|
+
): Promise<number | null> {
|
|
298
|
+
try {
|
|
299
|
+
const direct = await getModelsDevModel(providerId, modelId, options);
|
|
300
|
+
const limit = direct?.limit?.context;
|
|
301
|
+
if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) return limit;
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const byId = await findModelsDevModelById(modelId, options);
|
|
307
|
+
const limit = byId?.model?.limit?.context;
|
|
308
|
+
if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) return limit;
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function modelAcceptsImages(model: ModelsDevModel): boolean {
|
|
316
|
+
if (!model.modalities) return false;
|
|
317
|
+
const { input } = model.modalities;
|
|
318
|
+
return Array.isArray(input) && input.includes("image");
|
|
319
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export interface QuestionOption {
|
|
2
2
|
label: string;
|
|
3
3
|
value?: string | null;
|
|
4
|
+
group?: string;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export interface QuestionRequest {
|
|
7
8
|
id: string;
|
|
8
9
|
prompt: string;
|
|
9
10
|
options: QuestionOption[];
|
|
11
|
+
timeout?: number;
|
|
12
|
+
validation?: { pattern: string; message?: string };
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export interface QuestionAnswer {
|
|
@@ -23,6 +26,7 @@ let currentRequest: QuestionRequest | null = null;
|
|
|
23
26
|
let listeners = new Set<QuestionListener>();
|
|
24
27
|
let pendingResolve: ((answer: QuestionAnswer) => void) | null = null;
|
|
25
28
|
let pendingReject: ((reason?: any) => void) | null = null;
|
|
29
|
+
let pendingTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
26
30
|
|
|
27
31
|
function notify(): void {
|
|
28
32
|
for (const listener of listeners) {
|
|
@@ -46,7 +50,12 @@ export function getCurrentQuestion(): QuestionRequest | null {
|
|
|
46
50
|
return currentRequest;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
export async function askQuestion(
|
|
53
|
+
export async function askQuestion(
|
|
54
|
+
prompt: string,
|
|
55
|
+
options: QuestionOption[],
|
|
56
|
+
timeout?: number,
|
|
57
|
+
validation?: { pattern: string; message?: string },
|
|
58
|
+
): Promise<QuestionAnswer> {
|
|
50
59
|
if (pendingResolve) {
|
|
51
60
|
throw new Error('A question is already pending');
|
|
52
61
|
}
|
|
@@ -63,6 +72,8 @@ export async function askQuestion(prompt: string, options: QuestionOption[]): Pr
|
|
|
63
72
|
id: createId(),
|
|
64
73
|
prompt,
|
|
65
74
|
options,
|
|
75
|
+
...(timeout !== undefined && { timeout }),
|
|
76
|
+
...(validation !== undefined && { validation }),
|
|
66
77
|
};
|
|
67
78
|
|
|
68
79
|
currentRequest = request;
|
|
@@ -71,6 +82,20 @@ export async function askQuestion(prompt: string, options: QuestionOption[]): Pr
|
|
|
71
82
|
const answer = await new Promise<QuestionAnswer>((resolve, reject) => {
|
|
72
83
|
pendingResolve = resolve;
|
|
73
84
|
pendingReject = reject;
|
|
85
|
+
|
|
86
|
+
if (timeout !== undefined && timeout > 0) {
|
|
87
|
+
pendingTimeoutId = setTimeout(() => {
|
|
88
|
+
pendingTimeoutId = null;
|
|
89
|
+
if (pendingReject) {
|
|
90
|
+
const rej = pendingReject;
|
|
91
|
+
pendingResolve = null;
|
|
92
|
+
pendingReject = null;
|
|
93
|
+
currentRequest = null;
|
|
94
|
+
notify();
|
|
95
|
+
rej(new Error(`Question timed out after ${timeout}s`));
|
|
96
|
+
}
|
|
97
|
+
}, timeout * 1000);
|
|
98
|
+
}
|
|
74
99
|
});
|
|
75
100
|
|
|
76
101
|
return answer;
|
|
@@ -101,6 +126,11 @@ export function answerQuestion(index: number, customText?: string): void {
|
|
|
101
126
|
|
|
102
127
|
if (!answer) return;
|
|
103
128
|
|
|
129
|
+
if (pendingTimeoutId !== null) {
|
|
130
|
+
clearTimeout(pendingTimeoutId);
|
|
131
|
+
pendingTimeoutId = null;
|
|
132
|
+
}
|
|
133
|
+
|
|
104
134
|
const resolve = pendingResolve;
|
|
105
135
|
pendingResolve = null;
|
|
106
136
|
pendingReject = null;
|
|
@@ -112,6 +142,11 @@ export function answerQuestion(index: number, customText?: string): void {
|
|
|
112
142
|
export function cancelQuestion(): void {
|
|
113
143
|
if (!currentRequest || !pendingReject) return;
|
|
114
144
|
|
|
145
|
+
if (pendingTimeoutId !== null) {
|
|
146
|
+
clearTimeout(pendingTimeoutId);
|
|
147
|
+
pendingTimeoutId = null;
|
|
148
|
+
}
|
|
149
|
+
|
|
115
150
|
const reject = pendingReject;
|
|
116
151
|
pendingResolve = null;
|
|
117
152
|
pendingReject = null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const CODE_CHARS = /[{}()\[\]=<>:;]/g;
|
|
2
|
+
const CODE_DENSITY_THRESHOLD = 0.04;
|
|
3
|
+
|
|
4
|
+
export function estimateTokensFromText(text: string): number {
|
|
5
|
+
if (!text) return 0;
|
|
6
|
+
const matches = text.match(CODE_CHARS);
|
|
7
|
+
const density = matches ? matches.length / text.length : 0;
|
|
8
|
+
const ratio = density >= CODE_DENSITY_THRESHOLD ? 2.8 : 3.3;
|
|
9
|
+
return Math.ceil(text.length / ratio);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function estimateTokensForContent(content: string, thinkingContent?: string): number {
|
|
13
|
+
const contentTokens = estimateTokensFromText(content);
|
|
14
|
+
const thinkingTokens = thinkingContent ? estimateTokensFromText(thinkingContent) : 0;
|
|
15
|
+
return contentTokens + thinkingTokens + 4;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CONTEXT_BUDGETS: Record<string, number> = {
|
|
19
|
+
anthropic: 180000,
|
|
20
|
+
openai: 115000,
|
|
21
|
+
google: 900000,
|
|
22
|
+
mistral: 28000,
|
|
23
|
+
xai: 117000,
|
|
24
|
+
ollama: 7000,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const DEFAULT_BUDGET = 12000;
|
|
28
|
+
|
|
29
|
+
export function getDefaultContextBudget(provider?: string): number {
|
|
30
|
+
if (!provider) return DEFAULT_BUDGET;
|
|
31
|
+
return CONTEXT_BUDGETS[provider] ?? DEFAULT_BUDGET;
|
|
32
|
+
}
|