@kirosnn/mosaic 0.0.7
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/.mosaic/mosaic.local.jsonc +0 -0
- package/MOSAIC.md +188 -0
- package/README.md +127 -0
- package/docs/mosaic.png +0 -0
- package/package.json +42 -0
- package/src/agent/Agent.ts +131 -0
- package/src/agent/context.ts +96 -0
- package/src/agent/index.ts +2 -0
- package/src/agent/prompts/systemPrompt.ts +138 -0
- package/src/agent/prompts/toolsPrompt.ts +139 -0
- package/src/agent/provider/anthropic.ts +122 -0
- package/src/agent/provider/google.ts +124 -0
- package/src/agent/provider/mistral.ts +117 -0
- package/src/agent/provider/ollama.ts +531 -0
- package/src/agent/provider/openai.ts +220 -0
- package/src/agent/provider/xai.ts +122 -0
- package/src/agent/tools/bash.ts +20 -0
- package/src/agent/tools/definitions.ts +27 -0
- package/src/agent/tools/edit.ts +23 -0
- package/src/agent/tools/executor.ts +751 -0
- package/src/agent/tools/explore.ts +18 -0
- package/src/agent/tools/exploreExecutor.ts +320 -0
- package/src/agent/tools/glob.ts +16 -0
- package/src/agent/tools/grep.ts +19 -0
- package/src/agent/tools/index.ts +4 -0
- package/src/agent/tools/list.ts +20 -0
- package/src/agent/tools/question.ts +20 -0
- package/src/agent/tools/read.ts +15 -0
- package/src/agent/tools/write.ts +21 -0
- package/src/agent/types.ts +155 -0
- package/src/components/App.tsx +174 -0
- package/src/components/CommandsModal.tsx +77 -0
- package/src/components/CustomInput.tsx +328 -0
- package/src/components/Main.tsx +1112 -0
- package/src/components/Notification.tsx +91 -0
- package/src/components/SelectList.tsx +47 -0
- package/src/components/Setup.tsx +528 -0
- package/src/components/ShortcutsModal.tsx +67 -0
- package/src/components/Welcome.tsx +39 -0
- package/src/components/main/ApprovalPanel.tsx +134 -0
- package/src/components/main/ChatPage.tsx +516 -0
- package/src/components/main/HomePage.tsx +111 -0
- package/src/components/main/QuestionPanel.tsx +85 -0
- package/src/components/main/ThinkingIndicator.tsx +101 -0
- package/src/components/main/types.ts +55 -0
- package/src/components/main/wrapText.ts +41 -0
- package/src/index.tsx +212 -0
- package/src/utils/approvalBridge.ts +129 -0
- package/src/utils/commands/echo.ts +22 -0
- package/src/utils/commands/help.ts +25 -0
- package/src/utils/commands/index.ts +68 -0
- package/src/utils/commands/init.ts +68 -0
- package/src/utils/commands/redo.ts +74 -0
- package/src/utils/commands/registry.ts +29 -0
- package/src/utils/commands/sessions.ts +129 -0
- package/src/utils/commands/types.ts +20 -0
- package/src/utils/commands/undo.ts +75 -0
- package/src/utils/commands/web.ts +77 -0
- package/src/utils/config.ts +357 -0
- package/src/utils/diff.ts +201 -0
- package/src/utils/diffRendering.tsx +62 -0
- package/src/utils/exploreBridge.ts +87 -0
- package/src/utils/fileChangeTracker.ts +98 -0
- package/src/utils/fileChangesBridge.ts +18 -0
- package/src/utils/history.ts +106 -0
- package/src/utils/markdown.tsx +232 -0
- package/src/utils/models.ts +304 -0
- package/src/utils/questionBridge.ts +122 -0
- package/src/utils/terminalUtils.ts +25 -0
- package/src/utils/toolFormatting.ts +384 -0
- package/src/utils/undoRedo.ts +429 -0
- package/src/utils/undoRedoBridge.ts +45 -0
- package/src/utils/undoRedoDb.ts +338 -0
- package/src/utils/uninstall.ts +45 -0
- package/src/utils/version.ts +3 -0
- package/src/web/app.tsx +606 -0
- package/src/web/assets/css/ChatPage.css +212 -0
- package/src/web/assets/css/FileExplorer.css +202 -0
- package/src/web/assets/css/HomePage.css +119 -0
- package/src/web/assets/css/Markdown.css +178 -0
- package/src/web/assets/css/MessageItem.css +160 -0
- package/src/web/assets/css/Sidebar.css +208 -0
- package/src/web/assets/css/SidebarModal.css +137 -0
- package/src/web/assets/css/ThinkingIndicator.css +47 -0
- package/src/web/assets/css/ToolMessage.css +148 -0
- package/src/web/assets/css/global.css +226 -0
- package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
- package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
- package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
- package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
- package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
- package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
- package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
- package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
- package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
- package/src/web/assets/images/favicon-v2.svg +6 -0
- package/src/web/assets/images/favicon.png +0 -0
- package/src/web/assets/images/foruse.svg +5 -0
- package/src/web/assets/images/logo_black.svg +5 -0
- package/src/web/assets/images/logo_white.svg +5 -0
- package/src/web/assets/images/logoblack.png +0 -0
- package/src/web/assets/images/logowhite.png +0 -0
- package/src/web/build.ts +23 -0
- package/src/web/components/ApprovalPanel.tsx +191 -0
- package/src/web/components/ChatPage.tsx +273 -0
- package/src/web/components/FileExplorer.tsx +162 -0
- package/src/web/components/HomePage.tsx +121 -0
- package/src/web/components/MessageItem.tsx +178 -0
- package/src/web/components/Modal.tsx +30 -0
- package/src/web/components/QuestionPanel.tsx +149 -0
- package/src/web/components/Setup.tsx +211 -0
- package/src/web/components/Sidebar.tsx +292 -0
- package/src/web/components/ThinkingIndicator.tsx +85 -0
- package/src/web/logo_black.svg +5 -0
- package/src/web/logo_white.svg +5 -0
- package/src/web/router.ts +46 -0
- package/src/web/server.tsx +662 -0
- package/src/web/storage.ts +92 -0
- package/src/web/types.ts +17 -0
- package/src/web/utils.ts +61 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export type NotificationType = 'info' | 'success' | 'error' | 'warning';
|
|
4
|
+
|
|
5
|
+
export interface NotificationData {
|
|
6
|
+
id: string;
|
|
7
|
+
message: string;
|
|
8
|
+
type: NotificationType;
|
|
9
|
+
duration?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface NotificationProps {
|
|
13
|
+
notifications: NotificationData[];
|
|
14
|
+
onRemove: (id: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Notification({ notifications, onRemove }: NotificationProps) {
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const timers: NodeJS.Timeout[] = [];
|
|
20
|
+
|
|
21
|
+
notifications.forEach((notification) => {
|
|
22
|
+
const duration = notification.duration ?? 3000;
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
onRemove(notification.id);
|
|
25
|
+
}, duration);
|
|
26
|
+
timers.push(timer);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
timers.forEach((timer) => clearTimeout(timer));
|
|
31
|
+
};
|
|
32
|
+
}, [notifications, onRemove]);
|
|
33
|
+
|
|
34
|
+
if (notifications.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const getTypeColor = (type: NotificationType): string => {
|
|
39
|
+
switch (type) {
|
|
40
|
+
case 'success':
|
|
41
|
+
return '#38ff65';
|
|
42
|
+
case 'error':
|
|
43
|
+
return '#ff3838';
|
|
44
|
+
case 'warning':
|
|
45
|
+
return '#ffca38';
|
|
46
|
+
case 'info':
|
|
47
|
+
default:
|
|
48
|
+
return '#3899ff';
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<box
|
|
54
|
+
position="absolute"
|
|
55
|
+
top={1}
|
|
56
|
+
right={2}
|
|
57
|
+
flexDirection="column"
|
|
58
|
+
alignItems="flex-end"
|
|
59
|
+
gap={1}
|
|
60
|
+
>
|
|
61
|
+
{notifications.map((notification) => (
|
|
62
|
+
<box
|
|
63
|
+
key={notification.id}
|
|
64
|
+
flexDirection="column"
|
|
65
|
+
>
|
|
66
|
+
<box
|
|
67
|
+
flexDirection="row"
|
|
68
|
+
backgroundColor="#1a1a1a"
|
|
69
|
+
>
|
|
70
|
+
<text fg={getTypeColor(notification.type)}>▎ </text>
|
|
71
|
+
<text fg="white"> </text>
|
|
72
|
+
</box>
|
|
73
|
+
<box
|
|
74
|
+
flexDirection="row"
|
|
75
|
+
backgroundColor="#1a1a1a"
|
|
76
|
+
>
|
|
77
|
+
<text fg={getTypeColor(notification.type)}>▎ </text>
|
|
78
|
+
<text fg="white">{notification.message} </text>
|
|
79
|
+
</box>
|
|
80
|
+
<box
|
|
81
|
+
flexDirection="row"
|
|
82
|
+
backgroundColor="#1a1a1a"
|
|
83
|
+
>
|
|
84
|
+
<text fg={getTypeColor(notification.type)}>▎ </text>
|
|
85
|
+
<text fg="white"> </text>
|
|
86
|
+
</box>
|
|
87
|
+
</box>
|
|
88
|
+
))}
|
|
89
|
+
</box>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { TextAttributes } from "@opentui/core";
|
|
3
|
+
import { useKeyboard } from "@opentui/react";
|
|
4
|
+
|
|
5
|
+
export interface SelectOption {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
value: any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SelectListProps {
|
|
12
|
+
options: SelectOption[];
|
|
13
|
+
onSelect: (value: any) => void;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function SelectList({ options, onSelect, disabled = false }: SelectListProps) {
|
|
18
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
19
|
+
|
|
20
|
+
useKeyboard((key) => {
|
|
21
|
+
if (disabled) return;
|
|
22
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
23
|
+
setSelectedIndex(prev => prev === 0 ? options.length - 1 : prev - 1);
|
|
24
|
+
} else if (key.name === 'down' || key.name === 'j') {
|
|
25
|
+
setSelectedIndex(prev => prev === options.length - 1 ? 0 : prev + 1);
|
|
26
|
+
} else if (key.name === 'return') {
|
|
27
|
+
onSelect(options[selectedIndex]?.value);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<box flexDirection="column">
|
|
33
|
+
{options.map((option, index) => (
|
|
34
|
+
<box
|
|
35
|
+
key={index}
|
|
36
|
+
padding={1}
|
|
37
|
+
backgroundColor={index === selectedIndex ? '#2a2a2a' : 'transparent'}
|
|
38
|
+
>
|
|
39
|
+
<box flexDirection="column">
|
|
40
|
+
<text fg={index === selectedIndex ? "#ffca38" : undefined} attributes={index === selectedIndex ? TextAttributes.BOLD : TextAttributes.NONE}>{index === selectedIndex ? '> ' : ' '}{option.name}</text>
|
|
41
|
+
<text attributes={TextAttributes.DIM}>{' '}{option.description}</text>
|
|
42
|
+
</box>
|
|
43
|
+
</box>
|
|
44
|
+
))}
|
|
45
|
+
</box>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { TextAttributes, type KeyEvent } from "@opentui/core";
|
|
3
|
+
import { useRenderer } from "@opentui/react";
|
|
4
|
+
import { getAllProviders, getProviderById, modelRequiresApiKey, addCustomProvider, addCustomModel, type CustomProvider, type AIModel } from '../utils/config';
|
|
5
|
+
import { SelectList, type SelectOption } from './SelectList';
|
|
6
|
+
import { CustomInput } from './CustomInput';
|
|
7
|
+
|
|
8
|
+
interface SetupProps {
|
|
9
|
+
onComplete: (provider: string, model: string, apiKey?: string) => void;
|
|
10
|
+
pasteRequestId?: number;
|
|
11
|
+
shortcutsOpen?: boolean;
|
|
12
|
+
commandsOpen?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type SetupStep =
|
|
16
|
+
| 'provider'
|
|
17
|
+
| 'custom-name'
|
|
18
|
+
| 'custom-description'
|
|
19
|
+
| 'custom-baseurl'
|
|
20
|
+
| 'custom-apikey-required'
|
|
21
|
+
| 'custom-model-name'
|
|
22
|
+
| 'custom-model-id'
|
|
23
|
+
| 'custom-model-description'
|
|
24
|
+
| 'custom-add-another-model'
|
|
25
|
+
| 'model'
|
|
26
|
+
| 'add-custom-model'
|
|
27
|
+
| 'custom-model-name-existing'
|
|
28
|
+
| 'custom-model-id-existing'
|
|
29
|
+
| 'custom-model-description-existing'
|
|
30
|
+
| 'apikey'
|
|
31
|
+
| 'confirm';
|
|
32
|
+
|
|
33
|
+
export function Setup({ onComplete, pasteRequestId = 0, shortcutsOpen = false, commandsOpen = false }: SetupProps) {
|
|
34
|
+
const [step, setStep] = useState<SetupStep>('provider');
|
|
35
|
+
const [selectedProvider, setSelectedProvider] = useState<string>('');
|
|
36
|
+
const [selectedModel, setSelectedModel] = useState<string>('');
|
|
37
|
+
const [apiKey, setApiKey] = useState<string>('');
|
|
38
|
+
|
|
39
|
+
const [customName, setCustomName] = useState<string>('');
|
|
40
|
+
const [customDescription, setCustomDescription] = useState<string>('');
|
|
41
|
+
const [customBaseUrl, setCustomBaseUrl] = useState<string>('');
|
|
42
|
+
const [customRequiresApiKey, setCustomRequiresApiKey] = useState<boolean>(true);
|
|
43
|
+
const [customModels, setCustomModels] = useState<AIModel[]>([]);
|
|
44
|
+
const [tempModelName, setTempModelName] = useState<string>('');
|
|
45
|
+
const [tempModelId, setTempModelId] = useState<string>('');
|
|
46
|
+
const [tempModelDescription, setTempModelDescription] = useState<string>('');
|
|
47
|
+
|
|
48
|
+
const renderer = useRenderer();
|
|
49
|
+
const allProviders = getAllProviders();
|
|
50
|
+
const providerOptions: SelectOption[] = [
|
|
51
|
+
...allProviders.map(p => ({
|
|
52
|
+
name: p.name,
|
|
53
|
+
description: p.description,
|
|
54
|
+
value: p.id
|
|
55
|
+
})),
|
|
56
|
+
{
|
|
57
|
+
name: 'Custom Provider',
|
|
58
|
+
description: 'Add your own API provider',
|
|
59
|
+
value: '__custom__'
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const currentProvider = getProviderById(selectedProvider);
|
|
64
|
+
const isOllamaCloudModel = selectedProvider === 'ollama' && (selectedModel.includes(':cloud') || selectedModel.includes('-cloud'));
|
|
65
|
+
const modelOptions: SelectOption[] = currentProvider?.models.map(m => ({
|
|
66
|
+
name: m.name,
|
|
67
|
+
description: m.description,
|
|
68
|
+
value: m.id
|
|
69
|
+
})) || [];
|
|
70
|
+
|
|
71
|
+
if (currentProvider && !('isCustom' in currentProvider)) {
|
|
72
|
+
modelOptions.push({
|
|
73
|
+
name: 'Add Custom Model',
|
|
74
|
+
description: 'Add your own model for this provider',
|
|
75
|
+
value: '__add-custom-model__'
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const handleProviderSelect = (value: any) => {
|
|
80
|
+
if (value === '__custom__') {
|
|
81
|
+
setStep('custom-name');
|
|
82
|
+
} else {
|
|
83
|
+
setSelectedProvider(value);
|
|
84
|
+
setStep('model');
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handleModelSelect = (value: any) => {
|
|
89
|
+
if (value === '__add-custom-model__') {
|
|
90
|
+
setStep('add-custom-model');
|
|
91
|
+
} else {
|
|
92
|
+
setSelectedModel(value);
|
|
93
|
+
const requiresKey = !!currentProvider && (currentProvider.requiresApiKey || modelRequiresApiKey(currentProvider.id, value));
|
|
94
|
+
if (requiresKey) {
|
|
95
|
+
setStep('apikey');
|
|
96
|
+
} else {
|
|
97
|
+
setStep('confirm');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleApiKeySubmit = (value: string) => {
|
|
103
|
+
setApiKey(value.trim().replace(/[\r\n]+/g, ''));
|
|
104
|
+
setStep('confirm');
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleCustomNameSubmit = (value: string) => {
|
|
108
|
+
setCustomName(value.trim().replace(/[\r\n]+/g, ''));
|
|
109
|
+
setStep('custom-description');
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleCustomDescriptionSubmit = (value: string) => {
|
|
113
|
+
setCustomDescription(value.trim().replace(/[\r\n]+/g, ' '));
|
|
114
|
+
setStep('custom-baseurl');
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleCustomBaseUrlSubmit = (value: string) => {
|
|
118
|
+
setCustomBaseUrl(value.trim().replace(/[\r\n]+/g, ''));
|
|
119
|
+
setStep('custom-apikey-required');
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleCustomModelNameSubmit = (value: string) => {
|
|
123
|
+
setTempModelName(value.trim().replace(/[\r\n]+/g, ''));
|
|
124
|
+
setStep('custom-model-id');
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const handleCustomModelIdSubmit = (value: string) => {
|
|
128
|
+
setTempModelId(value.trim().replace(/[\r\n]+/g, ''));
|
|
129
|
+
setStep('custom-model-description');
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleCustomModelDescriptionSubmit = (value: string) => {
|
|
133
|
+
setTempModelDescription(value.trim().replace(/[\r\n]+/g, ' '));
|
|
134
|
+
|
|
135
|
+
const newModel: AIModel = {
|
|
136
|
+
id: tempModelId.trim().replace(/[\r\n]+/g, ''),
|
|
137
|
+
name: tempModelName.trim().replace(/[\r\n]+/g, ''),
|
|
138
|
+
description: value.trim().replace(/[\r\n]+/g, ' ')
|
|
139
|
+
};
|
|
140
|
+
setCustomModels([...customModels, newModel]);
|
|
141
|
+
|
|
142
|
+
setTempModelName('');
|
|
143
|
+
setTempModelId('');
|
|
144
|
+
setTempModelDescription('');
|
|
145
|
+
|
|
146
|
+
setStep('custom-add-another-model');
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handleCustomModelNameExistingSubmit = (value: string) => {
|
|
150
|
+
setTempModelName(value.trim().replace(/[\r\n]+/g, ''));
|
|
151
|
+
setStep('custom-model-id-existing');
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleCustomModelIdExistingSubmit = (value: string) => {
|
|
155
|
+
setTempModelId(value.trim().replace(/[\r\n]+/g, ''));
|
|
156
|
+
setStep('custom-model-description-existing');
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const handleCustomModelDescriptionExistingSubmit = (value: string) => {
|
|
160
|
+
const newModel: AIModel = {
|
|
161
|
+
id: tempModelId.trim().replace(/[\r\n]+/g, ''),
|
|
162
|
+
name: tempModelName.trim().replace(/[\r\n]+/g, ''),
|
|
163
|
+
description: value.trim().replace(/[\r\n]+/g, ' ')
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
addCustomModel(selectedProvider, newModel);
|
|
167
|
+
|
|
168
|
+
setTempModelName('');
|
|
169
|
+
setTempModelId('');
|
|
170
|
+
setTempModelDescription('');
|
|
171
|
+
|
|
172
|
+
setStep('model');
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const finalizeCustomProvider = () => {
|
|
176
|
+
const cleanedName = customName.trim().replace(/[\r\n]+/g, '');
|
|
177
|
+
const customProviderId = cleanedName.toLowerCase().replace(/\s+/g, '-');
|
|
178
|
+
const newProvider: CustomProvider = {
|
|
179
|
+
id: customProviderId,
|
|
180
|
+
name: cleanedName,
|
|
181
|
+
description: customDescription.trim().replace(/[\r\n]+/g, ' '),
|
|
182
|
+
baseUrl: customBaseUrl.trim().replace(/[\r\n]+/g, ''),
|
|
183
|
+
requiresApiKey: customRequiresApiKey,
|
|
184
|
+
models: customModels,
|
|
185
|
+
isCustom: true
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
addCustomProvider(newProvider);
|
|
189
|
+
setSelectedProvider(customProviderId);
|
|
190
|
+
setStep('model');
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const getPreviousStep = (currentStep: SetupStep): SetupStep | null => {
|
|
194
|
+
switch (currentStep) {
|
|
195
|
+
case 'provider':
|
|
196
|
+
return null;
|
|
197
|
+
case 'custom-name':
|
|
198
|
+
return 'provider';
|
|
199
|
+
case 'custom-description':
|
|
200
|
+
return 'custom-name';
|
|
201
|
+
case 'custom-baseurl':
|
|
202
|
+
return 'custom-description';
|
|
203
|
+
case 'custom-apikey-required':
|
|
204
|
+
return 'custom-baseurl';
|
|
205
|
+
case 'custom-model-name':
|
|
206
|
+
return 'custom-apikey-required';
|
|
207
|
+
case 'custom-model-id':
|
|
208
|
+
return 'custom-model-name';
|
|
209
|
+
case 'custom-model-description':
|
|
210
|
+
return 'custom-model-id';
|
|
211
|
+
case 'custom-add-another-model':
|
|
212
|
+
return 'custom-model-description';
|
|
213
|
+
case 'model':
|
|
214
|
+
return selectedProvider === '__custom__' ? 'custom-add-another-model' : 'provider';
|
|
215
|
+
case 'add-custom-model':
|
|
216
|
+
return 'model';
|
|
217
|
+
case 'custom-model-name-existing':
|
|
218
|
+
return 'add-custom-model';
|
|
219
|
+
case 'custom-model-id-existing':
|
|
220
|
+
return 'custom-model-name-existing';
|
|
221
|
+
case 'custom-model-description-existing':
|
|
222
|
+
return 'custom-model-id-existing';
|
|
223
|
+
case 'apikey':
|
|
224
|
+
return 'model';
|
|
225
|
+
case 'confirm':
|
|
226
|
+
return currentProvider && (currentProvider.requiresApiKey || modelRequiresApiKey(currentProvider.id, selectedModel)) ? 'apikey' : 'model';
|
|
227
|
+
default:
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const goBack = () => {
|
|
233
|
+
const previousStep = getPreviousStep(step);
|
|
234
|
+
if (previousStep) {
|
|
235
|
+
setStep(previousStep);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
const handleKeyPress = (key: KeyEvent) => {
|
|
241
|
+
if (shortcutsOpen) return;
|
|
242
|
+
if (key.name === 'escape') {
|
|
243
|
+
goBack();
|
|
244
|
+
} else if (step === 'confirm' && key.name === 'return') {
|
|
245
|
+
onComplete(selectedProvider, selectedModel, apiKey || undefined);
|
|
246
|
+
} else if (step === 'add-custom-model' && key.name === 'return') {
|
|
247
|
+
setStep('custom-model-name-existing');
|
|
248
|
+
} else if (step === 'custom-apikey-required') {
|
|
249
|
+
if (key.name === 'y') {
|
|
250
|
+
setCustomRequiresApiKey(true);
|
|
251
|
+
setStep('custom-model-name');
|
|
252
|
+
} else if (key.name === 'n') {
|
|
253
|
+
setCustomRequiresApiKey(false);
|
|
254
|
+
setStep('custom-model-name');
|
|
255
|
+
}
|
|
256
|
+
} else if (step === 'custom-add-another-model') {
|
|
257
|
+
if (key.name === 'y') {
|
|
258
|
+
setStep('custom-model-name');
|
|
259
|
+
} else if (key.name === 'n') {
|
|
260
|
+
finalizeCustomProvider();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
renderer.keyInput.on('keypress', handleKeyPress);
|
|
266
|
+
|
|
267
|
+
return () => {
|
|
268
|
+
renderer.keyInput.off('keypress', handleKeyPress);
|
|
269
|
+
};
|
|
270
|
+
}, [step, selectedProvider, selectedModel, apiKey, customRequiresApiKey, customModels, customName, customDescription, customBaseUrl, tempModelName, tempModelId, tempModelDescription, shortcutsOpen, renderer.keyInput]);
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<box width="100%" height="100%" flexDirection="column" padding={2}>
|
|
274
|
+
<box marginBottom={2}>
|
|
275
|
+
<text attributes={TextAttributes.BOLD}>Ready to innovate ?</text>
|
|
276
|
+
</box>
|
|
277
|
+
|
|
278
|
+
{step === 'provider' && (
|
|
279
|
+
<box flexDirection="column" flexGrow={1}>
|
|
280
|
+
<box marginBottom={1}>
|
|
281
|
+
<text>Select your AI provider (↑/↓ to navigate, Enter to select):</text>
|
|
282
|
+
</box>
|
|
283
|
+
<SelectList options={providerOptions} onSelect={handleProviderSelect} disabled={shortcutsOpen} />
|
|
284
|
+
</box>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{step === 'custom-name' && (
|
|
288
|
+
<box flexDirection="column" flexGrow={1}>
|
|
289
|
+
<box marginBottom={1}>
|
|
290
|
+
<text>Enter custom provider name:</text>
|
|
291
|
+
</box>
|
|
292
|
+
<CustomInput
|
|
293
|
+
focused={!shortcutsOpen}
|
|
294
|
+
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
295
|
+
onSubmit={handleCustomNameSubmit}
|
|
296
|
+
placeholder="My Custom Provider"
|
|
297
|
+
/>
|
|
298
|
+
<box marginTop={1}>
|
|
299
|
+
<text attributes={TextAttributes.DIM}>Press Enter when done, Escape to go back</text>
|
|
300
|
+
</box>
|
|
301
|
+
</box>
|
|
302
|
+
)}
|
|
303
|
+
|
|
304
|
+
{step === 'custom-description' && (
|
|
305
|
+
<box flexDirection="column" flexGrow={1}>
|
|
306
|
+
<box marginBottom={1}>
|
|
307
|
+
<text>Enter a description for {customName}:</text>
|
|
308
|
+
</box>
|
|
309
|
+
<CustomInput
|
|
310
|
+
focused={!shortcutsOpen}
|
|
311
|
+
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
312
|
+
onSubmit={handleCustomDescriptionSubmit}
|
|
313
|
+
placeholder="Description of the provider"
|
|
314
|
+
/>
|
|
315
|
+
<box marginTop={1}>
|
|
316
|
+
<text attributes={TextAttributes.DIM}>Press Enter when done, Escape to go back</text>
|
|
317
|
+
</box>
|
|
318
|
+
</box>
|
|
319
|
+
)}
|
|
320
|
+
|
|
321
|
+
{step === 'custom-baseurl' && (
|
|
322
|
+
<box flexDirection="column" flexGrow={1}>
|
|
323
|
+
<box marginBottom={1}>
|
|
324
|
+
<text>Enter the API base URL:</text>
|
|
325
|
+
</box>
|
|
326
|
+
<CustomInput
|
|
327
|
+
focused={!shortcutsOpen}
|
|
328
|
+
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
329
|
+
onSubmit={handleCustomBaseUrlSubmit}
|
|
330
|
+
placeholder="https://api.example.com/v1"
|
|
331
|
+
/>
|
|
332
|
+
<box marginTop={1}>
|
|
333
|
+
<text attributes={TextAttributes.DIM}>Press Enter when done, Escape to go back</text>
|
|
334
|
+
</box>
|
|
335
|
+
</box>
|
|
336
|
+
)}
|
|
337
|
+
|
|
338
|
+
{step === 'custom-apikey-required' && (
|
|
339
|
+
<box flexDirection="column" flexGrow={1} justifyContent="center">
|
|
340
|
+
<text>Does this provider require an API key?</text>
|
|
341
|
+
<box marginTop={1}>
|
|
342
|
+
<text attributes={TextAttributes.DIM}>Press Y for Yes, N for No</text>
|
|
343
|
+
</box>
|
|
344
|
+
</box>
|
|
345
|
+
)}
|
|
346
|
+
|
|
347
|
+
{step === 'custom-model-name' && (
|
|
348
|
+
<box flexDirection="column" flexGrow={1}>
|
|
349
|
+
<box marginBottom={1}>
|
|
350
|
+
<text>Enter model name{customModels.length > 0 ? ` (${customModels.length} added)` : ''}:</text>
|
|
351
|
+
</box>
|
|
352
|
+
<CustomInput
|
|
353
|
+
focused={!shortcutsOpen}
|
|
354
|
+
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
355
|
+
onSubmit={handleCustomModelNameSubmit}
|
|
356
|
+
placeholder="GPT-4 or Claude Opus"
|
|
357
|
+
/>
|
|
358
|
+
<box marginTop={1}>
|
|
359
|
+
<text attributes={TextAttributes.DIM}>Press Enter when done, Escape to go back</text>
|
|
360
|
+
</box>
|
|
361
|
+
</box>
|
|
362
|
+
)}
|
|
363
|
+
|
|
364
|
+
{step === 'custom-model-id' && (
|
|
365
|
+
<box flexDirection="column" flexGrow={1}>
|
|
366
|
+
<box marginBottom={1}>
|
|
367
|
+
<text>Enter model ID for {tempModelName}:</text>
|
|
368
|
+
</box>
|
|
369
|
+
<CustomInput
|
|
370
|
+
focused={!shortcutsOpen}
|
|
371
|
+
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
372
|
+
onSubmit={handleCustomModelIdSubmit}
|
|
373
|
+
placeholder="gpt-4 or claude-opus-4"
|
|
374
|
+
/>
|
|
375
|
+
<box marginTop={1}>
|
|
376
|
+
<text attributes={TextAttributes.DIM}>Press Enter when done, Escape to go back</text>
|
|
377
|
+
</box>
|
|
378
|
+
</box>
|
|
379
|
+
)}
|
|
380
|
+
|
|
381
|
+
{step === 'custom-model-description' && (
|
|
382
|
+
<box flexDirection="column" flexGrow={1}>
|
|
383
|
+
<box marginBottom={1}>
|
|
384
|
+
<text>Enter description for {tempModelName}:</text>
|
|
385
|
+
</box>
|
|
386
|
+
<CustomInput
|
|
387
|
+
focused={!shortcutsOpen}
|
|
388
|
+
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
389
|
+
onSubmit={handleCustomModelDescriptionSubmit}
|
|
390
|
+
placeholder="Best for complex tasks"
|
|
391
|
+
/>
|
|
392
|
+
<box marginTop={1}>
|
|
393
|
+
<text attributes={TextAttributes.DIM}>Press Enter when done, Escape to go back</text>
|
|
394
|
+
</box>
|
|
395
|
+
</box>
|
|
396
|
+
)}
|
|
397
|
+
|
|
398
|
+
{step === 'custom-add-another-model' && (
|
|
399
|
+
<box flexDirection="column" flexGrow={1} justifyContent="center">
|
|
400
|
+
<text>Model added: {customModels[customModels.length - 1]?.name}</text>
|
|
401
|
+
<box marginTop={1}>
|
|
402
|
+
<text>Add another model?</text>
|
|
403
|
+
</box>
|
|
404
|
+
<box marginTop={1}>
|
|
405
|
+
<text attributes={TextAttributes.DIM}>Press Y for Yes, N for No</text>
|
|
406
|
+
</box>
|
|
407
|
+
</box>
|
|
408
|
+
)}
|
|
409
|
+
|
|
410
|
+
{step === 'model' && (
|
|
411
|
+
<box flexDirection="column" flexGrow={1}>
|
|
412
|
+
<box marginBottom={1}>
|
|
413
|
+
<text>Select the AI model (↑/↓ to navigate, Enter to select):</text>
|
|
414
|
+
</box>
|
|
415
|
+
<SelectList options={modelOptions} onSelect={handleModelSelect} disabled={shortcutsOpen} />
|
|
416
|
+
<box marginTop={1}>
|
|
417
|
+
<text attributes={TextAttributes.DIM}>Escape to go back</text>
|
|
418
|
+
</box>
|
|
419
|
+
</box>
|
|
420
|
+
)}
|
|
421
|
+
|
|
422
|
+
{step === 'add-custom-model' && (
|
|
423
|
+
<box flexDirection="column" flexGrow={1} justifyContent="center">
|
|
424
|
+
<text>Add a custom model for {currentProvider?.name}</text>
|
|
425
|
+
<box marginTop={2} flexDirection="column" alignItems="flex-start">
|
|
426
|
+
<text>You can add custom models that are not in the default list.</text>
|
|
427
|
+
<text>This is useful for new models or specific configurations.</text>
|
|
428
|
+
</box>
|
|
429
|
+
<box marginTop={2}>
|
|
430
|
+
<text attributes={TextAttributes.DIM}>Press Enter to continue, Escape to go back</text>
|
|
431
|
+
</box>
|
|
432
|
+
</box>
|
|
433
|
+
)}
|
|
434
|
+
|
|
435
|
+
{step === 'custom-model-name-existing' && (
|
|
436
|
+
<box flexDirection="column" flexGrow={1}>
|
|
437
|
+
<box marginBottom={1}>
|
|
438
|
+
<text>Enter model name:</text>
|
|
439
|
+
</box>
|
|
440
|
+
<CustomInput
|
|
441
|
+
focused={!shortcutsOpen}
|
|
442
|
+
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
443
|
+
onSubmit={handleCustomModelNameExistingSubmit}
|
|
444
|
+
placeholder="GPT-4o-mini or Claude 3.5 Sonnet"
|
|
445
|
+
/>
|
|
446
|
+
<box marginTop={1}>
|
|
447
|
+
<text attributes={TextAttributes.DIM}>Press Enter when done, Escape to go back</text>
|
|
448
|
+
</box>
|
|
449
|
+
</box>
|
|
450
|
+
)}
|
|
451
|
+
|
|
452
|
+
{step === 'custom-model-id-existing' && (
|
|
453
|
+
<box flexDirection="column" flexGrow={1}>
|
|
454
|
+
<box marginBottom={1}>
|
|
455
|
+
<text>Enter model ID for {tempModelName}:</text>
|
|
456
|
+
</box>
|
|
457
|
+
<CustomInput
|
|
458
|
+
focused={!shortcutsOpen}
|
|
459
|
+
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
460
|
+
onSubmit={handleCustomModelIdExistingSubmit}
|
|
461
|
+
placeholder="gpt-4o-mini or claude-3-5-sonnet-20241022"
|
|
462
|
+
/>
|
|
463
|
+
<box marginTop={1}>
|
|
464
|
+
<text attributes={TextAttributes.DIM}>Press Enter when done, Escape to go back</text>
|
|
465
|
+
</box>
|
|
466
|
+
</box>
|
|
467
|
+
)}
|
|
468
|
+
|
|
469
|
+
{step === 'custom-model-description-existing' && (
|
|
470
|
+
<box flexDirection="column" flexGrow={1}>
|
|
471
|
+
<box marginBottom={1}>
|
|
472
|
+
<text>Enter description for {tempModelName}:</text>
|
|
473
|
+
</box>
|
|
474
|
+
<CustomInput
|
|
475
|
+
focused={!shortcutsOpen}
|
|
476
|
+
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
477
|
+
onSubmit={handleCustomModelDescriptionExistingSubmit}
|
|
478
|
+
placeholder="Fast and efficient model for general tasks"
|
|
479
|
+
/>
|
|
480
|
+
<box marginTop={1}>
|
|
481
|
+
<text attributes={TextAttributes.DIM}>Press Enter when done, Escape to go back</text>
|
|
482
|
+
</box>
|
|
483
|
+
</box>
|
|
484
|
+
)}
|
|
485
|
+
|
|
486
|
+
{step === 'apikey' && (
|
|
487
|
+
<box flexDirection="column" flexGrow={1}>
|
|
488
|
+
<box marginBottom={1}>
|
|
489
|
+
<text>
|
|
490
|
+
Enter your {currentProvider?.name} API key:
|
|
491
|
+
</text>
|
|
492
|
+
</box>
|
|
493
|
+
{isOllamaCloudModel && (
|
|
494
|
+
<box marginBottom={1} flexDirection="column" alignItems="flex-start">
|
|
495
|
+
<text>
|
|
496
|
+
This is an Ollama Cloud model. You must create an API key on:
|
|
497
|
+
</text>
|
|
498
|
+
<text attributes={TextAttributes.DIM}>https://ollama.com/settings/keys</text>
|
|
499
|
+
</box>
|
|
500
|
+
)}
|
|
501
|
+
<CustomInput
|
|
502
|
+
onSubmit={handleApiKeySubmit}
|
|
503
|
+
focused={!shortcutsOpen}
|
|
504
|
+
pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
|
|
505
|
+
placeholder={isOllamaCloudModel ? "ollama_..." : "sk-..."}
|
|
506
|
+
/>
|
|
507
|
+
<box marginTop={1}>
|
|
508
|
+
<text attributes={TextAttributes.DIM}>Press Enter when done, Escape to go back</text>
|
|
509
|
+
</box>
|
|
510
|
+
</box>
|
|
511
|
+
)}
|
|
512
|
+
|
|
513
|
+
{step === 'confirm' && (
|
|
514
|
+
<box flexDirection="column" justifyContent="center" alignItems="center" flexGrow={1}>
|
|
515
|
+
<text attributes={TextAttributes.BOLD}>Configuration Complete!</text>
|
|
516
|
+
<box marginTop={2} flexDirection="column" alignItems="flex-start">
|
|
517
|
+
<text>Provider: {currentProvider?.name}</text>
|
|
518
|
+
<text>Model: {currentProvider?.models.find(m => m.id === selectedModel)?.name}</text>
|
|
519
|
+
{apiKey && <text>API Key: ********************</text>}
|
|
520
|
+
</box>
|
|
521
|
+
<box marginTop={2}>
|
|
522
|
+
<text attributes={TextAttributes.DIM}>Press Enter to continue, Escape to go back</text>
|
|
523
|
+
</box>
|
|
524
|
+
</box>
|
|
525
|
+
)}
|
|
526
|
+
</box>
|
|
527
|
+
);
|
|
528
|
+
}
|