@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.
Files changed (154) hide show
  1. package/.mosaic/mosaic.local.jsonc +0 -0
  2. package/MOSAIC.md +188 -0
  3. package/README.md +127 -0
  4. package/docs/mosaic.png +0 -0
  5. package/package.json +42 -0
  6. package/src/agent/Agent.ts +131 -0
  7. package/src/agent/context.ts +96 -0
  8. package/src/agent/index.ts +2 -0
  9. package/src/agent/prompts/systemPrompt.ts +138 -0
  10. package/src/agent/prompts/toolsPrompt.ts +139 -0
  11. package/src/agent/provider/anthropic.ts +122 -0
  12. package/src/agent/provider/google.ts +124 -0
  13. package/src/agent/provider/mistral.ts +117 -0
  14. package/src/agent/provider/ollama.ts +531 -0
  15. package/src/agent/provider/openai.ts +220 -0
  16. package/src/agent/provider/xai.ts +122 -0
  17. package/src/agent/tools/bash.ts +20 -0
  18. package/src/agent/tools/definitions.ts +27 -0
  19. package/src/agent/tools/edit.ts +23 -0
  20. package/src/agent/tools/executor.ts +751 -0
  21. package/src/agent/tools/explore.ts +18 -0
  22. package/src/agent/tools/exploreExecutor.ts +320 -0
  23. package/src/agent/tools/glob.ts +16 -0
  24. package/src/agent/tools/grep.ts +19 -0
  25. package/src/agent/tools/index.ts +4 -0
  26. package/src/agent/tools/list.ts +20 -0
  27. package/src/agent/tools/question.ts +20 -0
  28. package/src/agent/tools/read.ts +15 -0
  29. package/src/agent/tools/write.ts +21 -0
  30. package/src/agent/types.ts +155 -0
  31. package/src/components/App.tsx +174 -0
  32. package/src/components/CommandsModal.tsx +77 -0
  33. package/src/components/CustomInput.tsx +328 -0
  34. package/src/components/Main.tsx +1112 -0
  35. package/src/components/Notification.tsx +91 -0
  36. package/src/components/SelectList.tsx +47 -0
  37. package/src/components/Setup.tsx +528 -0
  38. package/src/components/ShortcutsModal.tsx +67 -0
  39. package/src/components/Welcome.tsx +39 -0
  40. package/src/components/main/ApprovalPanel.tsx +134 -0
  41. package/src/components/main/ChatPage.tsx +516 -0
  42. package/src/components/main/HomePage.tsx +111 -0
  43. package/src/components/main/QuestionPanel.tsx +85 -0
  44. package/src/components/main/ThinkingIndicator.tsx +101 -0
  45. package/src/components/main/types.ts +55 -0
  46. package/src/components/main/wrapText.ts +41 -0
  47. package/src/index.tsx +212 -0
  48. package/src/utils/approvalBridge.ts +129 -0
  49. package/src/utils/commands/echo.ts +22 -0
  50. package/src/utils/commands/help.ts +25 -0
  51. package/src/utils/commands/index.ts +68 -0
  52. package/src/utils/commands/init.ts +68 -0
  53. package/src/utils/commands/redo.ts +74 -0
  54. package/src/utils/commands/registry.ts +29 -0
  55. package/src/utils/commands/sessions.ts +129 -0
  56. package/src/utils/commands/types.ts +20 -0
  57. package/src/utils/commands/undo.ts +75 -0
  58. package/src/utils/commands/web.ts +77 -0
  59. package/src/utils/config.ts +357 -0
  60. package/src/utils/diff.ts +201 -0
  61. package/src/utils/diffRendering.tsx +62 -0
  62. package/src/utils/exploreBridge.ts +87 -0
  63. package/src/utils/fileChangeTracker.ts +98 -0
  64. package/src/utils/fileChangesBridge.ts +18 -0
  65. package/src/utils/history.ts +106 -0
  66. package/src/utils/markdown.tsx +232 -0
  67. package/src/utils/models.ts +304 -0
  68. package/src/utils/questionBridge.ts +122 -0
  69. package/src/utils/terminalUtils.ts +25 -0
  70. package/src/utils/toolFormatting.ts +384 -0
  71. package/src/utils/undoRedo.ts +429 -0
  72. package/src/utils/undoRedoBridge.ts +45 -0
  73. package/src/utils/undoRedoDb.ts +338 -0
  74. package/src/utils/uninstall.ts +45 -0
  75. package/src/utils/version.ts +3 -0
  76. package/src/web/app.tsx +606 -0
  77. package/src/web/assets/css/ChatPage.css +212 -0
  78. package/src/web/assets/css/FileExplorer.css +202 -0
  79. package/src/web/assets/css/HomePage.css +119 -0
  80. package/src/web/assets/css/Markdown.css +178 -0
  81. package/src/web/assets/css/MessageItem.css +160 -0
  82. package/src/web/assets/css/Sidebar.css +208 -0
  83. package/src/web/assets/css/SidebarModal.css +137 -0
  84. package/src/web/assets/css/ThinkingIndicator.css +47 -0
  85. package/src/web/assets/css/ToolMessage.css +148 -0
  86. package/src/web/assets/css/global.css +226 -0
  87. package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
  88. package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
  89. package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
  90. package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
  91. package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
  92. package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
  93. package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
  94. package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
  95. package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
  96. package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
  97. package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
  98. package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
  99. package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
  100. package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
  101. package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
  102. package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
  103. package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
  104. package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
  105. package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
  106. package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
  107. package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
  108. package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
  109. package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
  110. package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
  111. package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
  112. package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
  113. package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
  114. package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
  115. package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
  116. package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
  117. package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
  118. package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
  119. package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
  120. package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
  121. package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
  122. package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
  123. package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
  124. package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
  125. package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
  126. package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
  127. package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
  128. package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
  129. package/src/web/assets/images/favicon-v2.svg +6 -0
  130. package/src/web/assets/images/favicon.png +0 -0
  131. package/src/web/assets/images/foruse.svg +5 -0
  132. package/src/web/assets/images/logo_black.svg +5 -0
  133. package/src/web/assets/images/logo_white.svg +5 -0
  134. package/src/web/assets/images/logoblack.png +0 -0
  135. package/src/web/assets/images/logowhite.png +0 -0
  136. package/src/web/build.ts +23 -0
  137. package/src/web/components/ApprovalPanel.tsx +191 -0
  138. package/src/web/components/ChatPage.tsx +273 -0
  139. package/src/web/components/FileExplorer.tsx +162 -0
  140. package/src/web/components/HomePage.tsx +121 -0
  141. package/src/web/components/MessageItem.tsx +178 -0
  142. package/src/web/components/Modal.tsx +30 -0
  143. package/src/web/components/QuestionPanel.tsx +149 -0
  144. package/src/web/components/Setup.tsx +211 -0
  145. package/src/web/components/Sidebar.tsx +292 -0
  146. package/src/web/components/ThinkingIndicator.tsx +85 -0
  147. package/src/web/logo_black.svg +5 -0
  148. package/src/web/logo_white.svg +5 -0
  149. package/src/web/router.ts +46 -0
  150. package/src/web/server.tsx +662 -0
  151. package/src/web/storage.ts +92 -0
  152. package/src/web/types.ts +17 -0
  153. package/src/web/utils.ts +61 -0
  154. 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
+ }