@kirosnn/mosaic 0.0.91 → 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.
- package/LICENSE +1 -1
- package/README.md +2 -6
- package/package.json +55 -48
- package/src/agent/Agent.ts +353 -131
- package/src/agent/context.ts +4 -4
- package/src/agent/prompts/systemPrompt.ts +209 -70
- package/src/agent/prompts/toolsPrompt.ts +285 -138
- package/src/agent/provider/anthropic.ts +109 -105
- package/src/agent/provider/google.ts +111 -107
- package/src/agent/provider/mistral.ts +95 -95
- package/src/agent/provider/ollama.ts +73 -17
- package/src/agent/provider/openai.ts +146 -102
- package/src/agent/provider/rateLimit.ts +178 -0
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +108 -104
- package/src/agent/tools/definitions.ts +15 -1
- package/src/agent/tools/executor.ts +717 -98
- package/src/agent/tools/exploreExecutor.ts +20 -22
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +64 -9
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/question.ts +7 -1
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +15 -14
- package/src/components/App.tsx +50 -8
- package/src/components/CustomInput.tsx +461 -77
- package/src/components/Main.tsx +1459 -1112
- package/src/components/Setup.tsx +1 -1
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/Welcome.tsx +1 -1
- package/src/components/main/ApprovalPanel.tsx +4 -3
- package/src/components/main/ChatPage.tsx +858 -516
- package/src/components/main/HomePage.tsx +58 -39
- package/src/components/main/QuestionPanel.tsx +52 -7
- package/src/components/main/ThinkingIndicator.tsx +13 -2
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +53 -25
- package/src/mcp/approvalPolicy.ts +148 -0
- package/src/mcp/cli/add.ts +185 -0
- package/src/mcp/cli/doctor.ts +77 -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 +223 -0
- package/src/mcp/index.ts +80 -0
- package/src/mcp/processManager.ts +299 -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.ts +854 -0
- package/src/mcp/toolCatalog.ts +169 -0
- package/src/mcp/types.ts +95 -0
- package/src/utils/approvalBridge.ts +45 -12
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/compact.ts +30 -0
- package/src/utils/commands/echo.ts +1 -1
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +9 -7
- 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 +13 -16
- package/src/utils/exploreBridge.ts +10 -0
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/markdown.tsx +163 -99
- package/src/utils/models.ts +31 -16
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/questionBridge.ts +36 -1
- package/src/utils/tokenEstimator.ts +32 -0
- package/src/utils/toolFormatting.ts +428 -48
- package/src/web/app.tsx +65 -5
- package/src/web/assets/css/ChatPage.css +102 -30
- package/src/web/assets/css/MessageItem.css +26 -29
- package/src/web/assets/css/ThinkingIndicator.css +44 -6
- package/src/web/assets/css/ToolMessage.css +36 -14
- package/src/web/components/ChatPage.tsx +228 -105
- package/src/web/components/HomePage.tsx +3 -3
- package/src/web/components/MessageItem.tsx +80 -81
- package/src/web/components/QuestionPanel.tsx +72 -12
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -3
- package/src/web/components/ThinkingIndicator.tsx +41 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +894 -662
- package/src/web/storage.ts +23 -1
- package/src/web/types.ts +7 -6
- 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
|
@@ -8,13 +8,13 @@ export function renderDiffLine(line: string, key: string) {
|
|
|
8
8
|
const colors = getDiffLineColors(parsed);
|
|
9
9
|
|
|
10
10
|
return (
|
|
11
|
-
<box key={key} flexDirection="row">
|
|
12
|
-
<box backgroundColor={colors.labelBackground}>
|
|
11
|
+
<box key={key} flexDirection="row" width="100%" alignItems="stretch">
|
|
12
|
+
<box backgroundColor={colors.labelBackground} flexShrink={0}>
|
|
13
13
|
<text fg="#ffffff">
|
|
14
14
|
{" "}{parsed.prefix}{parsed.lineNumber?.padStart(5) || ''}{' '}
|
|
15
15
|
</text>
|
|
16
16
|
</box>
|
|
17
|
-
<box flexGrow={1} backgroundColor={colors.contentBackground}>
|
|
17
|
+
<box flexGrow={1} backgroundColor={colors.contentBackground} minWidth={0}>
|
|
18
18
|
<text fg="#ffffff">
|
|
19
19
|
{" "}{parsed.content || ''}
|
|
20
20
|
</text>
|
|
@@ -24,27 +24,26 @@ export function renderDiffLine(line: string, key: string) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
<box key={key} width="100%">
|
|
28
|
+
<text fg="#ffffff">
|
|
29
|
+
{line || ' '}
|
|
30
|
+
</text>
|
|
31
|
+
</box>
|
|
30
32
|
);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export function renderInlineDiffLine(content: string) {
|
|
34
36
|
const parsed = parseDiffLine(content);
|
|
35
|
-
|
|
36
37
|
if (parsed.isDiffLine) {
|
|
37
|
-
const colors = getDiffLineColors(parsed);
|
|
38
|
-
|
|
39
38
|
return (
|
|
40
39
|
<>
|
|
41
40
|
<box>
|
|
42
|
-
<text fg="
|
|
41
|
+
<text fg="white" attributes={TextAttributes.DIM}>
|
|
43
42
|
{parsed.prefix}{parsed.lineNumber?.padStart(5) || ''}{' '}
|
|
44
43
|
</text>
|
|
45
44
|
</box>
|
|
46
|
-
<box
|
|
47
|
-
<text fg="
|
|
45
|
+
<box>
|
|
46
|
+
<text fg="white">
|
|
48
47
|
{" "}{parsed.content || ''}
|
|
49
48
|
</text>
|
|
50
49
|
</box>
|
|
@@ -55,8 +54,6 @@ export function renderInlineDiffLine(content: string) {
|
|
|
55
54
|
return null;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
export function getDiffLineBackground(
|
|
59
|
-
|
|
60
|
-
const colors = getDiffLineColors(parsed);
|
|
61
|
-
return colors.contentBackground !== 'transparent' ? colors.contentBackground : null;
|
|
57
|
+
export function getDiffLineBackground(_content: string): string | null {
|
|
58
|
+
return null;
|
|
62
59
|
}
|
|
@@ -15,6 +15,7 @@ interface ExploreBridgeGlobal {
|
|
|
15
15
|
toolCallback: ExploreToolCallback | null;
|
|
16
16
|
totalExploreTokens: number;
|
|
17
17
|
subscribers: Set<ExploreToolSubscriber>;
|
|
18
|
+
parentContext: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const globalKey = '__mosaic_explore_bridge__';
|
|
@@ -26,6 +27,7 @@ if (!g[globalKey]) {
|
|
|
26
27
|
toolCallback: null,
|
|
27
28
|
totalExploreTokens: 0,
|
|
28
29
|
subscribers: new Set<ExploreToolSubscriber>(),
|
|
30
|
+
parentContext: '',
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -85,3 +87,11 @@ export function subscribeExploreTool(callback: ExploreToolSubscriber): () => voi
|
|
|
85
87
|
export function getExploreTokens(): number {
|
|
86
88
|
return state.totalExploreTokens;
|
|
87
89
|
}
|
|
90
|
+
|
|
91
|
+
export function setExploreContext(context: string): void {
|
|
92
|
+
state.parentContext = context;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getExploreContext(): string {
|
|
96
|
+
return state.parentContext;
|
|
97
|
+
}
|
package/src/utils/history.ts
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, unlinkSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
|
|
5
|
-
export interface ConversationStep {
|
|
6
|
-
type: 'user' | 'assistant' | 'tool';
|
|
7
|
-
content: string;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
5
|
+
export interface ConversationStep {
|
|
6
|
+
type: 'user' | 'assistant' | 'tool';
|
|
7
|
+
content: string;
|
|
8
|
+
images?: import("./images").ImageAttachment[];
|
|
9
|
+
toolName?: string;
|
|
10
|
+
toolArgs?: Record<string, unknown>;
|
|
11
|
+
toolResult?: unknown;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
responseDuration?: number;
|
|
14
|
+
blendWord?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ConversationHistory {
|
|
18
|
+
id: string;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
steps: ConversationStep[];
|
|
21
|
+
totalSteps: number;
|
|
22
|
+
title?: string | null;
|
|
23
|
+
workspace?: string | null;
|
|
24
|
+
totalTokens?: {
|
|
25
|
+
prompt: number;
|
|
26
|
+
completion: number;
|
|
27
|
+
total: number;
|
|
28
|
+
};
|
|
24
29
|
model?: string;
|
|
25
30
|
provider?: string;
|
|
26
31
|
}
|
|
@@ -36,14 +41,49 @@ export function getHistoryDir(): string {
|
|
|
36
41
|
return historyDir;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
export function saveConversation(conversation: ConversationHistory): void {
|
|
40
|
-
const historyDir = getHistoryDir();
|
|
41
|
-
const filename = `${conversation.id}.json`;
|
|
42
|
-
const filepath = join(historyDir, filename);
|
|
43
|
-
|
|
44
|
-
writeFileSync(filepath, JSON.stringify(conversation, null, 2), 'utf-8');
|
|
45
|
-
}
|
|
46
|
-
|
|
44
|
+
export function saveConversation(conversation: ConversationHistory): void {
|
|
45
|
+
const historyDir = getHistoryDir();
|
|
46
|
+
const filename = `${conversation.id}.json`;
|
|
47
|
+
const filepath = join(historyDir, filename);
|
|
48
|
+
|
|
49
|
+
writeFileSync(filepath, JSON.stringify(conversation, null, 2), 'utf-8');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function updateConversationTitle(id: string, title: string | null): boolean {
|
|
53
|
+
const historyDir = getHistoryDir();
|
|
54
|
+
const filepath = join(historyDir, `${id}.json`);
|
|
55
|
+
|
|
56
|
+
if (!existsSync(filepath)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const content = readFileSync(filepath, 'utf-8');
|
|
62
|
+
const data = JSON.parse(content) as ConversationHistory;
|
|
63
|
+
data.title = title;
|
|
64
|
+
writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8');
|
|
65
|
+
return true;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function deleteConversation(id: string): boolean {
|
|
72
|
+
const historyDir = getHistoryDir();
|
|
73
|
+
const filepath = join(historyDir, `${id}.json`);
|
|
74
|
+
|
|
75
|
+
if (!existsSync(filepath)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
unlinkSync(filepath);
|
|
81
|
+
return true;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
47
87
|
export function loadConversations(): ConversationHistory[] {
|
|
48
88
|
const historyDir = getHistoryDir();
|
|
49
89
|
|
|
@@ -51,17 +91,19 @@ export function loadConversations(): ConversationHistory[] {
|
|
|
51
91
|
return [];
|
|
52
92
|
}
|
|
53
93
|
|
|
54
|
-
const files = readdirSync(historyDir).filter(f => f.endsWith('.json'));
|
|
55
|
-
const conversations: ConversationHistory[] = [];
|
|
56
|
-
|
|
57
|
-
for (const file of files) {
|
|
58
|
-
try {
|
|
59
|
-
const content = readFileSync(join(historyDir, file), 'utf-8');
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
94
|
+
const files = readdirSync(historyDir).filter(f => f.endsWith('.json') && f !== 'inputs.json');
|
|
95
|
+
const conversations: ConversationHistory[] = [];
|
|
96
|
+
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
try {
|
|
99
|
+
const content = readFileSync(join(historyDir, file), 'utf-8');
|
|
100
|
+
const parsed = JSON.parse(content) as ConversationHistory;
|
|
101
|
+
if (!parsed || !Array.isArray(parsed.steps)) continue;
|
|
102
|
+
conversations.push(parsed);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`Failed to load ${file}:`, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
65
107
|
|
|
66
108
|
return conversations.sort((a, b) => b.timestamp - a.timestamp);
|
|
67
109
|
}
|
|
@@ -103,4 +145,4 @@ export function addInputToHistory(input: string): void {
|
|
|
103
145
|
|
|
104
146
|
saveInputHistory(history);
|
|
105
147
|
}
|
|
106
|
-
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ImageAttachment } from "./images";
|
|
2
|
+
|
|
3
|
+
export type ImageCommandEvent =
|
|
4
|
+
| { type: "add"; image: ImageAttachment }
|
|
5
|
+
| { type: "clear" }
|
|
6
|
+
| { type: "remove"; id: string };
|
|
7
|
+
|
|
8
|
+
const listeners = new Set<(event: ImageCommandEvent) => void>();
|
|
9
|
+
let imageSupport = false;
|
|
10
|
+
|
|
11
|
+
export function subscribeImageCommand(listener: (event: ImageCommandEvent) => void): () => void {
|
|
12
|
+
listeners.add(listener);
|
|
13
|
+
return () => {
|
|
14
|
+
listeners.delete(listener);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function emitImageCommand(event: ImageCommandEvent): void {
|
|
19
|
+
listeners.forEach((listener) => listener(event));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function setImageSupport(enabled: boolean): void {
|
|
23
|
+
imageSupport = enabled;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function canUseImages(): boolean {
|
|
27
|
+
return imageSupport;
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type ImageAttachment = {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
mimeType: string;
|
|
5
|
+
data: string;
|
|
6
|
+
size: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const EXT_TO_MIME: Record<string, string> = {
|
|
10
|
+
png: "image/png",
|
|
11
|
+
jpg: "image/jpeg",
|
|
12
|
+
jpeg: "image/jpeg",
|
|
13
|
+
webp: "image/webp",
|
|
14
|
+
gif: "image/gif",
|
|
15
|
+
bmp: "image/bmp",
|
|
16
|
+
svg: "image/svg+xml",
|
|
17
|
+
tif: "image/tiff",
|
|
18
|
+
tiff: "image/tiff"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function guessImageMimeType(filename: string): string {
|
|
22
|
+
const clean = filename.trim().toLowerCase();
|
|
23
|
+
const idx = clean.lastIndexOf(".");
|
|
24
|
+
if (idx === -1) return "application/octet-stream";
|
|
25
|
+
const ext = clean.slice(idx + 1);
|
|
26
|
+
return EXT_TO_MIME[ext] || "application/octet-stream";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function toDataUrl(image: ImageAttachment): string {
|
|
30
|
+
return `data:${image.mimeType};base64,${image.data}`;
|
|
31
|
+
}
|
package/src/utils/markdown.tsx
CHANGED
|
@@ -121,112 +121,176 @@ export function parseMarkdownContent(content: string): ParsedMarkdownLine[] {
|
|
|
121
121
|
return result;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
export function wrapMarkdownText(text: string, maxWidth: number): { text: string; segments: MarkdownSegment[] }[] {
|
|
125
|
-
if (!text || maxWidth <= 0) return [{ text: '', segments: [] }];
|
|
126
|
-
|
|
127
|
-
const segments = parseMarkdownLine(text);
|
|
128
|
-
const lines: { text: string; segments: MarkdownSegment[] }[] = [];
|
|
129
|
-
let currentLine = '';
|
|
130
|
-
let currentSegments: MarkdownSegment[] = [];
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
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
|
-
}
|
|
124
|
+
export function wrapMarkdownText(text: string, maxWidth: number): { text: string; segments: MarkdownSegment[] }[] {
|
|
125
|
+
if (!text || maxWidth <= 0) return [{ text: '', segments: [] }];
|
|
126
|
+
|
|
127
|
+
const segments = parseMarkdownLine(text);
|
|
128
|
+
const lines: { text: string; segments: MarkdownSegment[] }[] = [];
|
|
129
|
+
let currentLine = '';
|
|
130
|
+
let currentSegments: MarkdownSegment[] = [];
|
|
131
|
+
|
|
132
|
+
const splitSegment = (segment: MarkdownSegment): MarkdownSegment[] => {
|
|
133
|
+
if (segment.type === 'code') return [segment];
|
|
134
|
+
const parts = segment.content.match(/\s+|[^\s]+/g);
|
|
135
|
+
if (!parts) return [segment];
|
|
136
|
+
return parts.map(part => ({ ...segment, content: part }));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const pushLine = () => {
|
|
140
|
+
if (!currentLine) return;
|
|
141
|
+
lines.push({ text: currentLine, segments: currentSegments });
|
|
142
|
+
currentLine = '';
|
|
143
|
+
currentSegments = [];
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const addPiece = (piece: MarkdownSegment) => {
|
|
147
|
+
let remaining = piece.content;
|
|
148
|
+
while (remaining.length > 0) {
|
|
149
|
+
if (!currentLine) {
|
|
150
|
+
if (remaining.trim() === '') {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (remaining.length <= maxWidth) {
|
|
154
|
+
currentLine = remaining;
|
|
155
|
+
currentSegments = [{ ...piece, content: remaining }];
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const chunk = remaining.slice(0, maxWidth);
|
|
159
|
+
lines.push({ text: chunk, segments: [{ ...piece, content: chunk }] });
|
|
160
|
+
remaining = remaining.slice(maxWidth);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if ((currentLine + remaining).length <= maxWidth) {
|
|
165
|
+
currentLine += remaining;
|
|
166
|
+
currentSegments.push({ ...piece, content: remaining });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pushLine();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
for (const segment of segments) {
|
|
175
|
+
const pieces = splitSegment(segment);
|
|
176
|
+
for (const piece of pieces) {
|
|
177
|
+
addPiece(piece);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (currentLine) {
|
|
182
|
+
lines.push({ text: currentLine, segments: currentSegments });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return lines.length > 0 ? lines : [{ text: '', segments: [] }];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface WrappedMarkdownBlock {
|
|
189
|
+
type: 'line' | 'code';
|
|
190
|
+
wrappedLines?: { text: string; segments: MarkdownSegment[] }[];
|
|
191
|
+
codeLines?: string[];
|
|
192
|
+
language?: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function wrapCodeLine(line: string, maxWidth: number): string[] {
|
|
196
|
+
if (!line) return [''];
|
|
197
|
+
if (maxWidth <= 0) return [line];
|
|
198
|
+
if (line.length <= maxWidth) return [line];
|
|
199
|
+
|
|
200
|
+
const chunks: string[] = [];
|
|
201
|
+
let i = 0;
|
|
202
|
+
while (i < line.length) {
|
|
203
|
+
chunks.push(line.slice(i, i + maxWidth));
|
|
204
|
+
i += maxWidth;
|
|
205
|
+
}
|
|
206
|
+
return chunks;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function reflowParagraphs(text: string): string {
|
|
210
|
+
const rawLines = text.split('\n');
|
|
211
|
+
const result: string[] = [];
|
|
212
|
+
let inCodeBlock = false;
|
|
213
|
+
|
|
214
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
215
|
+
const line = rawLines[i]!;
|
|
216
|
+
|
|
217
|
+
if (/^```/.test(line)) {
|
|
218
|
+
inCodeBlock = !inCodeBlock;
|
|
219
|
+
result.push(line);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (inCodeBlock) {
|
|
224
|
+
result.push(line);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (line.trim() === '') {
|
|
229
|
+
result.push(line);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (/^#{1,6}\s/.test(line) || /^[-*+]\s/.test(line) || /^\d+\.\s/.test(line)) {
|
|
234
|
+
result.push(line);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const prev = result.length > 0 ? result[result.length - 1]! : '';
|
|
239
|
+
const prevIsText = prev.trim() !== '' &&
|
|
240
|
+
!/^```/.test(prev) &&
|
|
241
|
+
!/^#{1,6}\s/.test(prev) &&
|
|
242
|
+
!/^[-*+]\s/.test(prev) &&
|
|
243
|
+
!/^\d+\.\s/.test(prev);
|
|
244
|
+
|
|
245
|
+
if (prevIsText) {
|
|
246
|
+
result[result.length - 1] = prev + ' ' + line;
|
|
169
247
|
} else {
|
|
170
|
-
|
|
171
|
-
|
|
248
|
+
result.push(line);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
172
251
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
252
|
+
return result.join('\n');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function parseAndWrapMarkdown(text: string, maxWidth: number): WrappedMarkdownBlock[] {
|
|
256
|
+
const lines = reflowParagraphs(text).split('\n');
|
|
257
|
+
const blocks: WrappedMarkdownBlock[] = [];
|
|
258
|
+
let inCodeBlock = false;
|
|
259
|
+
let codeLines: string[] = [];
|
|
260
|
+
let language: string | undefined;
|
|
261
|
+
|
|
262
|
+
for (const line of lines) {
|
|
263
|
+
const fenceMatch = line.match(/^```([A-Za-z0-9_-]+)?\s*$/);
|
|
264
|
+
if (fenceMatch) {
|
|
265
|
+
if (!inCodeBlock) {
|
|
266
|
+
inCodeBlock = true;
|
|
267
|
+
language = fenceMatch[1];
|
|
268
|
+
codeLines = [];
|
|
176
269
|
} 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
|
-
}
|
|
270
|
+
const wrapped = codeLines.flatMap(codeLine => wrapCodeLine(codeLine, maxWidth));
|
|
271
|
+
blocks.push({ type: 'code', codeLines: wrapped, language });
|
|
272
|
+
inCodeBlock = false;
|
|
273
|
+
codeLines = [];
|
|
274
|
+
language = undefined;
|
|
210
275
|
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (inCodeBlock) {
|
|
280
|
+
codeLines.push(line);
|
|
281
|
+
continue;
|
|
211
282
|
}
|
|
212
|
-
}
|
|
213
283
|
|
|
214
|
-
|
|
215
|
-
|
|
284
|
+
blocks.push({
|
|
285
|
+
type: 'line',
|
|
286
|
+
wrappedLines: wrapMarkdownText(line, maxWidth)
|
|
287
|
+
});
|
|
216
288
|
}
|
|
217
289
|
|
|
218
|
-
|
|
219
|
-
|
|
290
|
+
if (inCodeBlock) {
|
|
291
|
+
const wrapped = codeLines.flatMap(codeLine => wrapCodeLine(codeLine, maxWidth));
|
|
292
|
+
blocks.push({ type: 'code', codeLines: wrapped, language });
|
|
293
|
+
}
|
|
220
294
|
|
|
221
|
-
|
|
222
|
-
type: 'line';
|
|
223
|
-
wrappedLines?: { text: string; segments: MarkdownSegment[] }[];
|
|
295
|
+
return blocks;
|
|
224
296
|
}
|
|
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
|
@@ -152,23 +152,16 @@ export class ModelsDevClient {
|
|
|
152
152
|
|
|
153
153
|
async getModelById(modelId: ModelsDevModelId, options: { refresh?: boolean } = {}): Promise<ModelsDevSearchResult | null> {
|
|
154
154
|
const data = await this.getAll(options);
|
|
155
|
-
// Try exact match first
|
|
156
155
|
for (const provider of Object.values(data)) {
|
|
157
156
|
const model = provider.models?.[modelId];
|
|
158
157
|
if (model) return { provider, model };
|
|
159
158
|
}
|
|
160
|
-
|
|
161
|
-
// Try semantic/partial match
|
|
162
|
-
// e.g. gpt-5.2-2025-12-11 should match gpt-5.2 or vice versa
|
|
163
159
|
const lowerSearch = modelId.toLowerCase();
|
|
164
160
|
|
|
165
161
|
for (const provider of Object.values(data)) {
|
|
166
162
|
const models = provider.models ?? {};
|
|
167
163
|
for (const [id, model] of Object.entries(models)) {
|
|
168
164
|
const lowerId = id.toLowerCase();
|
|
169
|
-
// If the known model ID is a prefix of our search (e.g. search gpt-5.2-v1 matches model gpt-5.2)
|
|
170
|
-
// OR if our search is a prefix of the known model ID (e.g. search gpt-5.2 matches model gpt-5.2-preview)
|
|
171
|
-
// OR if one contains the other
|
|
172
165
|
if (lowerSearch.includes(lowerId) || lowerId.includes(lowerSearch)) {
|
|
173
166
|
return { provider, model };
|
|
174
167
|
}
|
|
@@ -293,12 +286,34 @@ export async function findModelsDevModelById(
|
|
|
293
286
|
return modelsDev.getModelById(modelId, options);
|
|
294
287
|
}
|
|
295
288
|
|
|
296
|
-
export async function searchModelsDev(query: ModelsDevSearchQuery, options: { refresh?: boolean } = {}): Promise<ModelsDevSearchResult[]> {
|
|
297
|
-
return modelsDev.search(query, options);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export function
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type NotificationType = 'info' | 'success' | 'error' | 'warning'
|
|
2
|
+
|
|
3
|
+
export interface NotificationPayload {
|
|
4
|
+
message: string
|
|
5
|
+
type?: NotificationType
|
|
6
|
+
duration?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type NotificationListener = (payload: NotificationPayload) => void
|
|
10
|
+
|
|
11
|
+
const listeners = new Set<NotificationListener>()
|
|
12
|
+
|
|
13
|
+
export function subscribeNotifications(listener: NotificationListener): () => void {
|
|
14
|
+
listeners.add(listener)
|
|
15
|
+
return () => {
|
|
16
|
+
listeners.delete(listener)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function notifyNotification(message: string, type: NotificationType = 'info', duration?: number): void {
|
|
21
|
+
const payload: NotificationPayload = { message, type, duration }
|
|
22
|
+
listeners.forEach((listener) => listener(payload))
|
|
23
|
+
}
|