@kirosnn/mosaic 0.0.9 → 0.71.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 +83 -19
- package/package.json +52 -47
- package/src/agent/prompts/systemPrompt.ts +198 -68
- package/src/agent/prompts/toolsPrompt.ts +217 -135
- package/src/agent/provider/anthropic.ts +19 -15
- package/src/agent/provider/google.ts +21 -17
- package/src/agent/provider/ollama.ts +80 -41
- package/src/agent/provider/openai.ts +107 -67
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +19 -15
- package/src/agent/tools/definitions.ts +9 -5
- package/src/agent/tools/executor.ts +655 -46
- package/src/agent/tools/exploreExecutor.ts +12 -12
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +62 -8
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +6 -6
- package/src/components/App.tsx +67 -25
- package/src/components/CustomInput.tsx +274 -68
- package/src/components/Main.tsx +323 -168
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/main/ChatPage.tsx +217 -58
- package/src/components/main/HomePage.tsx +5 -1
- package/src/components/main/ThinkingIndicator.tsx +11 -1
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +3 -5
- package/src/utils/approvalBridge.ts +29 -8
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +5 -1
- package/src/utils/diffRendering.tsx +13 -14
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/models.ts +0 -7
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/toolFormatting.ts +162 -43
- package/src/web/app.tsx +94 -34
- 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 +6 -6
- package/src/web/components/MessageItem.tsx +88 -89
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -1
- package/src/web/components/ThinkingIndicator.tsx +40 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +187 -39
- package/src/web/storage.ts +23 -1
- package/src/web/types.ts +7 -6
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Command } from './types'
|
|
2
|
+
import { getCurrentApproval, respondApproval } from '../approvalBridge'
|
|
3
|
+
import { shouldRequireApprovals, setRequireApprovals } from '../config'
|
|
4
|
+
import { notifyNotification } from '../notificationBridge'
|
|
5
|
+
import { emitApprovalMode } from '../approvalModeBridge'
|
|
6
|
+
|
|
7
|
+
export const approvalsCommand: Command = {
|
|
8
|
+
name: 'approvals',
|
|
9
|
+
description: 'Toggle approval prompts for agent changes',
|
|
10
|
+
usage: '/approvals on|off|toggle|status',
|
|
11
|
+
aliases: ['approval', 'autoapprove', 'auto-approve'],
|
|
12
|
+
execute: (args: string[]) => {
|
|
13
|
+
const raw = args[0]?.toLowerCase()
|
|
14
|
+
const current = shouldRequireApprovals()
|
|
15
|
+
let next = current
|
|
16
|
+
|
|
17
|
+
if (!raw || raw === 'toggle') {
|
|
18
|
+
next = !current
|
|
19
|
+
} else if (raw === 'on' || raw === 'true' || raw === 'yes') {
|
|
20
|
+
next = true
|
|
21
|
+
} else if (raw === 'off' || raw === 'false' || raw === 'no') {
|
|
22
|
+
next = false
|
|
23
|
+
} else if (raw === 'status') {
|
|
24
|
+
return {
|
|
25
|
+
success: true,
|
|
26
|
+
content: current ? 'Approvals are enabled.' : 'Auto-approve is enabled.'
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
content: 'Usage: /approvals on|off|toggle|status'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setRequireApprovals(next)
|
|
36
|
+
if (!next && getCurrentApproval()) {
|
|
37
|
+
respondApproval(true)
|
|
38
|
+
}
|
|
39
|
+
emitApprovalMode(next)
|
|
40
|
+
|
|
41
|
+
notifyNotification(next ? 'Approvals enabled.' : 'Auto-approve enabled.', 'info', 2500)
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
success: true,
|
|
45
|
+
content: next ? 'Approvals enabled.' : 'Auto-approve enabled.'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
2
|
+
import { basename } from "path";
|
|
3
|
+
import type { Command } from "./types";
|
|
4
|
+
import { guessImageMimeType } from "../images";
|
|
5
|
+
import { emitImageCommand, canUseImages } from "../imageBridge";
|
|
6
|
+
|
|
7
|
+
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function parseImagePath(fullCommand: string): string {
|
|
10
|
+
const trimmed = fullCommand.trim();
|
|
11
|
+
const without = trimmed.replace(/^\/(image|img)\s+/i, "");
|
|
12
|
+
return without.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const imageCommand: Command = {
|
|
16
|
+
name: "image",
|
|
17
|
+
description: "Attach an image for the next message",
|
|
18
|
+
usage: "/image <path> | /image clear",
|
|
19
|
+
aliases: ["img"],
|
|
20
|
+
execute: (args, fullCommand) => {
|
|
21
|
+
const first = args[0]?.toLowerCase();
|
|
22
|
+
if (!first) {
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
content: "Usage: /image <path> | /image clear",
|
|
26
|
+
shouldAddToHistory: false
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (first === "clear") {
|
|
31
|
+
emitImageCommand({ type: "clear" });
|
|
32
|
+
return {
|
|
33
|
+
success: true,
|
|
34
|
+
content: "Image list cleared.",
|
|
35
|
+
shouldAddToHistory: false
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!canUseImages()) {
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
content: "Images are not supported by the current model.",
|
|
43
|
+
shouldAddToHistory: false
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const path = parseImagePath(fullCommand);
|
|
48
|
+
if (!path) {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
content: "Missing image path.",
|
|
52
|
+
shouldAddToHistory: false
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!existsSync(path)) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
content: "File not found.",
|
|
60
|
+
shouldAddToHistory: false
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const stat = statSync(path);
|
|
65
|
+
if (!stat.isFile()) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
content: "Not a file.",
|
|
69
|
+
shouldAddToHistory: false
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (stat.size > MAX_IMAGE_BYTES) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
content: "Image too large (max 10 MB).",
|
|
77
|
+
shouldAddToHistory: false
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const name = basename(path);
|
|
82
|
+
const mimeType = guessImageMimeType(name);
|
|
83
|
+
if (!mimeType.startsWith("image/")) {
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
content: "Unsupported image type.",
|
|
87
|
+
shouldAddToHistory: false
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = readFileSync(path).toString("base64");
|
|
92
|
+
emitImageCommand({
|
|
93
|
+
type: "add",
|
|
94
|
+
image: {
|
|
95
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
96
|
+
name,
|
|
97
|
+
mimeType,
|
|
98
|
+
data,
|
|
99
|
+
size: stat.size
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
content: `Image attached: ${name}`,
|
|
106
|
+
shouldAddToHistory: false
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
@@ -7,6 +7,8 @@ import { undoCommand } from './undo';
|
|
|
7
7
|
import { redoCommand } from './redo';
|
|
8
8
|
import { sessionsCommand } from './sessions';
|
|
9
9
|
import { webCommand } from './web';
|
|
10
|
+
import { imageCommand } from './image';
|
|
11
|
+
import { approvalsCommand } from './approvals';
|
|
10
12
|
|
|
11
13
|
export { commandRegistry } from './registry';
|
|
12
14
|
export type { Command, CommandResult, CommandRegistry } from './types';
|
|
@@ -65,4 +67,6 @@ export function initializeCommands(): void {
|
|
|
65
67
|
commandRegistry.register(redoCommand);
|
|
66
68
|
commandRegistry.register(sessionsCommand);
|
|
67
69
|
commandRegistry.register(webCommand);
|
|
68
|
-
|
|
70
|
+
commandRegistry.register(imageCommand);
|
|
71
|
+
commandRegistry.register(approvalsCommand);
|
|
72
|
+
}
|
|
@@ -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,28 @@ 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);
|
|
37
|
+
const colors = getDiffLineColors(parsed);
|
|
35
38
|
|
|
36
39
|
if (parsed.isDiffLine) {
|
|
37
|
-
const colors = getDiffLineColors(parsed);
|
|
38
|
-
|
|
39
40
|
return (
|
|
40
41
|
<>
|
|
41
42
|
<box>
|
|
42
|
-
<text fg="
|
|
43
|
+
<text fg="white" attributes={TextAttributes.DIM}>
|
|
43
44
|
{parsed.prefix}{parsed.lineNumber?.padStart(5) || ''}{' '}
|
|
44
45
|
</text>
|
|
45
46
|
</box>
|
|
46
|
-
<box
|
|
47
|
-
<text fg="
|
|
47
|
+
<box>
|
|
48
|
+
<text fg="white">
|
|
48
49
|
{" "}{parsed.content || ''}
|
|
49
50
|
</text>
|
|
50
51
|
</box>
|
|
@@ -56,7 +57,5 @@ export function renderInlineDiffLine(content: string) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export function getDiffLineBackground(content: string): string | null {
|
|
59
|
-
|
|
60
|
-
const colors = getDiffLineColors(parsed);
|
|
61
|
-
return colors.contentBackground !== 'transparent' ? colors.contentBackground : null;
|
|
60
|
+
return null;
|
|
62
61
|
}
|
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/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
|
}
|
|
@@ -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
|
+
}
|