free-coding-models 0.5.6 → 0.5.8

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 (48) hide show
  1. package/README.md +14 -9
  2. package/changelog/v0.5.7.md +20 -0
  3. package/package.json +1 -1
  4. package/src/core/endpoint-installer.js +3 -3
  5. package/src/core/tool-launchers.js +2 -2
  6. package/web/dist/assets/index-DKmmiDip.css +1 -0
  7. package/web/dist/assets/index-DzVt42Nf.js +39 -0
  8. package/web/dist/index.html +2 -2
  9. package/web/server.js +386 -1
  10. package/web/src/App.jsx +145 -26
  11. package/web/src/components/analytics/AnalyticsView.jsx +4 -0
  12. package/web/src/components/analytics/TokenUsagePanel.jsx +105 -0
  13. package/web/src/components/analytics/TokenUsagePanel.module.css +126 -0
  14. package/web/src/components/atoms/TierBadge.module.css +3 -3
  15. package/web/src/components/dashboard/DetailPanel.jsx +29 -4
  16. package/web/src/components/dashboard/DetailPanel.module.css +26 -0
  17. package/web/src/components/dashboard/FilterBar.jsx +17 -2
  18. package/web/src/components/dashboard/FilterBar.module.css +17 -0
  19. package/web/src/components/dashboard/ModelTable.jsx +17 -5
  20. package/web/src/components/dashboard/ModelTable.module.css +14 -1
  21. package/web/src/components/help/HelpView.jsx +1 -1
  22. package/web/src/components/install/InstallEndpointsView.jsx +278 -0
  23. package/web/src/components/install/InstallEndpointsView.module.css +351 -0
  24. package/web/src/components/installed/InstalledModelsView.jsx +89 -0
  25. package/web/src/components/installed/InstalledModelsView.module.css +200 -0
  26. package/web/src/components/launch/IncompatibleFallbackModal.jsx +69 -0
  27. package/web/src/components/launch/LaunchButton.jsx +30 -0
  28. package/web/src/components/launch/LaunchButton.module.css +35 -0
  29. package/web/src/components/launch/LaunchModal.module.css +125 -0
  30. package/web/src/components/layout/Header.jsx +70 -11
  31. package/web/src/components/layout/Header.module.css +62 -2
  32. package/web/src/components/recommend/RecommendView.jsx +121 -0
  33. package/web/src/components/recommend/RecommendView.module.css +55 -0
  34. package/web/src/components/router/RouterView.jsx +286 -0
  35. package/web/src/components/router/RouterView.module.css +357 -0
  36. package/web/src/components/tools/ToolPicker.jsx +86 -0
  37. package/web/src/components/tools/ToolPicker.module.css +84 -0
  38. package/web/src/global.css +23 -0
  39. package/web/src/hooks/urlState.constants.js +3 -0
  40. package/web/src/hooks/useInstalledModels.js +42 -0
  41. package/web/src/hooks/useRecommend.js +67 -0
  42. package/web/src/hooks/useRouterDashboard.js +133 -0
  43. package/web/src/hooks/useTokenUsage.js +103 -0
  44. package/web/src/hooks/useToolMode.js +77 -0
  45. package/web/src/hooks/useUrlState.js +4 -3
  46. package/web/src/utils/m3.js +55 -0
  47. package/web/dist/assets/index-Blp9QJev.js +0 -39
  48. package/web/dist/assets/index-Cz_aCLTR.css +0 -1
@@ -0,0 +1,351 @@
1
+ .overlay {
2
+ position: fixed;
3
+ inset: 0;
4
+ z-index: 200;
5
+ background: rgba(0, 0, 0, 0.6);
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: center;
9
+ animation: fadeIn 0.15s ease;
10
+ }
11
+
12
+ .modal {
13
+ background: var(--bg-primary, #0f0f17);
14
+ border: 1px solid var(--border, #1e1e2e);
15
+ border-radius: 12px;
16
+ width: 680px;
17
+ max-width: 95vw;
18
+ max-height: 85vh;
19
+ display: flex;
20
+ flex-direction: column;
21
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
22
+ }
23
+
24
+ .header {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ padding: 16px 20px;
29
+ border-bottom: 1px solid var(--border, #1e1e2e);
30
+ }
31
+
32
+ .title {
33
+ margin: 0;
34
+ font-size: 16px;
35
+ font-weight: 600;
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 8px;
39
+ color: var(--text-primary, #e0e0e0);
40
+ }
41
+
42
+ .closeBtn {
43
+ background: none;
44
+ border: none;
45
+ color: var(--text-muted, #888);
46
+ font-size: 18px;
47
+ cursor: pointer;
48
+ padding: 4px 8px;
49
+ border-radius: 6px;
50
+ &:hover { background: var(--bg-hover, #1a1a2e); color: var(--text-primary, #e0e0e0); }
51
+ }
52
+
53
+ /* Steps */
54
+ .steps {
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ gap: 24px;
59
+ padding: 14px 20px;
60
+ border-bottom: 1px solid var(--border, #1e1e2e);
61
+ }
62
+
63
+ .step {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 6px;
67
+ font-size: 12px;
68
+ color: var(--text-muted, #555);
69
+ }
70
+
71
+ .stepActive {
72
+ color: var(--text-primary, #e0e0e0);
73
+ font-weight: 600;
74
+ }
75
+
76
+ .stepDone {
77
+ color: #00c864;
78
+ }
79
+
80
+ .stepNum {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ width: 22px;
85
+ height: 22px;
86
+ border-radius: 50%;
87
+ font-size: 11px;
88
+ font-weight: 700;
89
+ background: var(--bg-secondary, #14141f);
90
+ border: 1px solid var(--border, #1e1e2e);
91
+ }
92
+
93
+ .stepActive .stepNum {
94
+ background: rgba(0, 200, 100, 0.15);
95
+ border-color: rgba(0, 200, 100, 0.3);
96
+ color: #00c864;
97
+ }
98
+
99
+ .stepDone .stepNum {
100
+ background: rgba(0, 200, 100, 0.15);
101
+ border-color: #00c864;
102
+ color: #00c864;
103
+ }
104
+
105
+ .stepLabel {
106
+ text-transform: uppercase;
107
+ letter-spacing: 0.3px;
108
+ }
109
+
110
+ /* Body */
111
+ .body {
112
+ padding: 20px;
113
+ overflow-y: auto;
114
+ flex: 1;
115
+ min-height: 300px;
116
+ }
117
+
118
+ .stepContent {}
119
+
120
+ .stepDesc {
121
+ font-size: 14px;
122
+ color: var(--text-secondary, #aaa);
123
+ margin: 0 0 16px 0;
124
+ }
125
+
126
+ .loading, .empty {
127
+ color: var(--text-muted, #666);
128
+ font-size: 13px;
129
+ text-align: center;
130
+ padding: 30px 0;
131
+ }
132
+
133
+ /* Pick Grid (Provider + Tool) */
134
+ .grid {
135
+ display: grid;
136
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
137
+ gap: 8px;
138
+ }
139
+
140
+ .pickCard {
141
+ display: flex;
142
+ flex-direction: column;
143
+ align-items: center;
144
+ gap: 4px;
145
+ padding: 14px 10px;
146
+ background: var(--bg-secondary, #14141f);
147
+ border: 1px solid var(--border, #1e1e2e);
148
+ border-radius: 8px;
149
+ cursor: pointer;
150
+ color: var(--text-secondary, #aaa);
151
+ font-size: 13px;
152
+ transition: all 0.15s ease;
153
+ &:hover { border-color: var(--text-muted, #666); color: var(--text-primary, #e0e0e0); }
154
+ }
155
+
156
+ .pickActive {
157
+ border-color: #00c864;
158
+ background: rgba(0, 200, 100, 0.08);
159
+ color: #00c864;
160
+ }
161
+
162
+ .pickEmoji {
163
+ font-size: 20px;
164
+ }
165
+
166
+ .pickLabel {
167
+ font-weight: 600;
168
+ font-size: 12px;
169
+ }
170
+
171
+ .pickMeta {
172
+ font-size: 10px;
173
+ color: var(--text-muted, #888);
174
+ }
175
+
176
+ /* Scope Toggle */
177
+ .scopeToggle {
178
+ display: flex;
179
+ gap: 8px;
180
+ margin-bottom: 12px;
181
+ }
182
+
183
+ .scopeBtn {
184
+ padding: 6px 14px;
185
+ border: 1px solid var(--border, #1e1e2e);
186
+ border-radius: 6px;
187
+ background: var(--bg-secondary, #14141f);
188
+ color: var(--text-secondary, #aaa);
189
+ font-size: 12px;
190
+ cursor: pointer;
191
+ &:hover { border-color: var(--text-muted, #888); }
192
+ }
193
+
194
+ .scopeActive {
195
+ background: rgba(0, 200, 100, 0.1);
196
+ border-color: rgba(0, 200, 100, 0.3);
197
+ color: #00c864;
198
+ font-weight: 600;
199
+ }
200
+
201
+ /* Model Selection */
202
+ .modelGrid {
203
+ display: grid;
204
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
205
+ gap: 6px;
206
+ max-height: 240px;
207
+ overflow-y: auto;
208
+ }
209
+
210
+ .modelCard {
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: space-between;
214
+ padding: 8px 10px;
215
+ background: var(--bg-secondary, #14141f);
216
+ border: 1px solid var(--border, #1e1e2e);
217
+ border-radius: 6px;
218
+ cursor: pointer;
219
+ color: var(--text-secondary, #aaa);
220
+ font-size: 12px;
221
+ &:hover { border-color: var(--text-muted, #666); }
222
+ }
223
+
224
+ .modelSelected {
225
+ border-color: #00c864;
226
+ background: rgba(0, 200, 100, 0.08);
227
+ color: #00c864;
228
+ }
229
+
230
+ .modelName {
231
+ overflow: hidden;
232
+ text-overflow: ellipsis;
233
+ white-space: nowrap;
234
+ }
235
+
236
+ .modelTier {
237
+ font-size: 10px;
238
+ font-weight: 700;
239
+ padding: 1px 4px;
240
+ background: rgba(0, 200, 255, 0.1);
241
+ color: #00c8ff;
242
+ border-radius: 3px;
243
+ margin-left: 6px;
244
+ flex-shrink: 0;
245
+ }
246
+
247
+ .allNotice {
248
+ color: var(--text-muted, #888);
249
+ font-size: 13px;
250
+ text-align: center;
251
+ padding: 30px 0;
252
+ }
253
+
254
+ /* Install State */
255
+ .installingState {
256
+ display: flex;
257
+ flex-direction: column;
258
+ align-items: center;
259
+ justify-content: center;
260
+ gap: 12px;
261
+ padding: 40px 0;
262
+ color: var(--text-secondary, #aaa);
263
+ }
264
+
265
+ .spinner {
266
+ animation: spin 1s linear infinite;
267
+ }
268
+
269
+ .doneState {
270
+ display: flex;
271
+ flex-direction: column;
272
+ align-items: center;
273
+ gap: 8px;
274
+ padding: 30px 0;
275
+ text-align: center;
276
+ h3 { margin: 0; color: #00c864; font-size: 18px; }
277
+ p { margin: 0; color: var(--text-secondary, #aaa); font-size: 13px; }
278
+ }
279
+
280
+ .installPath {
281
+ font-size: 11px;
282
+ color: var(--text-muted, #888);
283
+ background: var(--bg-secondary, #14141f);
284
+ padding: 4px 10px;
285
+ border-radius: 4px;
286
+ max-width: 100%;
287
+ overflow: hidden;
288
+ text-overflow: ellipsis;
289
+ }
290
+
291
+ /* Footer */
292
+ .footer {
293
+ display: flex;
294
+ justify-content: space-between;
295
+ padding: 14px 20px;
296
+ border-top: 1px solid var(--border, #1e1e2e);
297
+ }
298
+
299
+ .backBtn {
300
+ display: flex;
301
+ align-items: center;
302
+ gap: 4px;
303
+ padding: 8px 14px;
304
+ background: var(--bg-secondary, #14141f);
305
+ border: 1px solid var(--border, #1e1e2e);
306
+ border-radius: 8px;
307
+ color: var(--text-secondary, #aaa);
308
+ font-size: 13px;
309
+ cursor: pointer;
310
+ &:hover { color: var(--text-primary, #e0e0e0); }
311
+ }
312
+
313
+ .nextBtn {
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 4px;
317
+ padding: 8px 18px;
318
+ background: #00c864;
319
+ color: #000;
320
+ border: none;
321
+ border-radius: 8px;
322
+ font-size: 13px;
323
+ font-weight: 600;
324
+ cursor: pointer;
325
+ margin-left: auto;
326
+ &:hover { background: #00d874; }
327
+ &:disabled { opacity: 0.4; cursor: not-allowed; }
328
+ }
329
+
330
+ .doneBtn {
331
+ padding: 8px 24px;
332
+ background: #00c864;
333
+ color: #000;
334
+ border: none;
335
+ border-radius: 8px;
336
+ font-size: 13px;
337
+ font-weight: 600;
338
+ cursor: pointer;
339
+ margin-left: auto;
340
+ &:hover { background: #00d874; }
341
+ }
342
+
343
+ @keyframes fadeIn {
344
+ from { opacity: 0; }
345
+ to { opacity: 1; }
346
+ }
347
+
348
+ @keyframes spin {
349
+ from { transform: rotate(0deg); }
350
+ to { transform: rotate(360deg); }
351
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @file web/src/components/installed/InstalledModelsView.jsx
3
+ * @description Installed Models modal — scans all tool configs, shows models, soft-delete.
4
+ * 📖 M4: Full TUI parity for installed models management.
5
+ */
6
+ import { useInstalledModels } from '../../hooks/useInstalledModels.js'
7
+ import { IconFolders, IconTrash, IconRefresh } from '@tabler/icons-react'
8
+ import styles from './InstalledModelsView.module.css'
9
+
10
+ export default function InstalledModelsView({ onClose, onToast }) {
11
+ const { results, loading, refresh, disableModel } = useInstalledModels()
12
+
13
+ const handleDisable = async (toolMode, modelId) => {
14
+ try {
15
+ const result = await disableModel(toolMode, modelId)
16
+ if (result.success) {
17
+ onToast?.(`Disabled ${modelId} in ${toolMode}.`, 'success')
18
+ } else {
19
+ onToast?.(`Failed: ${result.error || 'unknown error'}`, 'error')
20
+ }
21
+ } catch (err) {
22
+ onToast?.(`Error: ${err.message}`, 'error')
23
+ }
24
+ }
25
+
26
+ const totalModels = results.reduce((sum, r) => sum + r.models.length, 0)
27
+
28
+ return (
29
+ <div className={styles.overlay} onClick={(e) => e.target === e.currentTarget && onClose()}>
30
+ <div className={styles.modal}>
31
+ <div className={styles.header}>
32
+ <h2 className={styles.title}>
33
+ <IconFolders size={20} stroke={1.5} />
34
+ Installed Models
35
+ {!loading && <span className={styles.count}>{totalModels}</span>}
36
+ </h2>
37
+ <div className={styles.headerActions}>
38
+ <button className={styles.refreshBtn} onClick={refresh} title="Refresh">
39
+ <IconRefresh size={14} />
40
+ </button>
41
+ <button className={styles.closeBtn} onClick={onClose} aria-label="Close">✕</button>
42
+ </div>
43
+ </div>
44
+
45
+ <div className={styles.body}>
46
+ {loading && <div className={styles.empty}>Scanning tool configs…</div>}
47
+
48
+ {!loading && totalModels === 0 && (
49
+ <div className={styles.empty}>
50
+ No models found in tool configs.
51
+ <br />
52
+ <span className={styles.hint}>Use "Install Endpoints" to configure models for your tools.</span>
53
+ </div>
54
+ )}
55
+
56
+ {!loading && results.map((tool) => (
57
+ <div key={tool.toolMode} className={styles.toolGroup}>
58
+ <div className={styles.toolHeader}>
59
+ <span className={styles.toolEmoji}>{tool.toolEmoji}</span>
60
+ <span className={styles.toolLabel}>{tool.toolLabel}</span>
61
+ <span className={styles.toolCount}>{tool.models.length} model{tool.models.length !== 1 ? 's' : ''}</span>
62
+ {!tool.isValid && <span className={styles.toolMissing}>Not installed</span>}
63
+ </div>
64
+ {tool.models.length > 0 && (
65
+ <div className={styles.modelList}>
66
+ {tool.models.map((model) => (
67
+ <div key={`${tool.toolMode}-${model.modelId}`} className={styles.modelRow}>
68
+ <span className={styles.modelIcon}>{model.isExternal ? '🔗' : '✅'}</span>
69
+ <span className={styles.modelName}>{model.label}</span>
70
+ {model.tier && model.tier !== '-' && <span className={styles.modelTier}>{model.tier}</span>}
71
+ <span className={styles.modelId} title={model.modelId}>{model.modelId}</span>
72
+ <button
73
+ className={styles.disableBtn}
74
+ onClick={() => handleDisable(tool.toolMode, model.modelId)}
75
+ title="Remove model from config (backup saved)"
76
+ >
77
+ <IconTrash size={12} />
78
+ </button>
79
+ </div>
80
+ ))}
81
+ </div>
82
+ )}
83
+ </div>
84
+ ))}
85
+ </div>
86
+ </div>
87
+ </div>
88
+ )
89
+ }
@@ -0,0 +1,200 @@
1
+ .overlay {
2
+ position: fixed;
3
+ inset: 0;
4
+ z-index: 200;
5
+ background: rgba(0, 0, 0, 0.6);
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: center;
9
+ animation: fadeIn 0.15s ease;
10
+ }
11
+
12
+ .modal {
13
+ background: var(--bg-primary, #0f0f17);
14
+ border: 1px solid var(--border, #1e1e2e);
15
+ border-radius: 12px;
16
+ width: 640px;
17
+ max-width: 95vw;
18
+ max-height: 85vh;
19
+ display: flex;
20
+ flex-direction: column;
21
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
22
+ }
23
+
24
+ .header {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ padding: 16px 20px;
29
+ border-bottom: 1px solid var(--border, #1e1e2e);
30
+ }
31
+
32
+ .title {
33
+ margin: 0;
34
+ font-size: 16px;
35
+ font-weight: 600;
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 8px;
39
+ color: var(--text-primary, #e0e0e0);
40
+ }
41
+
42
+ .count {
43
+ font-size: 12px;
44
+ background: var(--bg-secondary, #14141f);
45
+ border: 1px solid var(--border, #1e1e2e);
46
+ border-radius: 10px;
47
+ padding: 2px 8px;
48
+ color: var(--text-muted, #888);
49
+ }
50
+
51
+ .headerActions {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 6px;
55
+ }
56
+
57
+ .refreshBtn {
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ width: 28px;
62
+ height: 28px;
63
+ background: var(--bg-secondary, #14141f);
64
+ border: 1px solid var(--border, #1e1e2e);
65
+ border-radius: 6px;
66
+ color: var(--text-muted, #888);
67
+ cursor: pointer;
68
+ &:hover { color: var(--text-primary, #e0e0e0); }
69
+ }
70
+
71
+ .closeBtn {
72
+ background: none;
73
+ border: none;
74
+ color: var(--text-muted, #888);
75
+ font-size: 18px;
76
+ cursor: pointer;
77
+ padding: 4px 8px;
78
+ border-radius: 6px;
79
+ &:hover { background: var(--bg-hover, #1a1a2e); color: var(--text-primary, #e0e0e0); }
80
+ }
81
+
82
+ .body {
83
+ padding: 16px 20px;
84
+ overflow-y: auto;
85
+ flex: 1;
86
+ }
87
+
88
+ .empty {
89
+ color: var(--text-muted, #666);
90
+ font-size: 13px;
91
+ padding: 30px 0;
92
+ text-align: center;
93
+ line-height: 1.6;
94
+ }
95
+
96
+ .hint {
97
+ font-size: 12px;
98
+ color: var(--text-muted, #555);
99
+ }
100
+
101
+ /* Tool Groups */
102
+ .toolGroup {
103
+ margin-bottom: 16px;
104
+ }
105
+
106
+ .toolHeader {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 8px;
110
+ margin-bottom: 6px;
111
+ }
112
+
113
+ .toolEmoji {
114
+ font-size: 16px;
115
+ }
116
+
117
+ .toolLabel {
118
+ font-size: 14px;
119
+ font-weight: 600;
120
+ color: var(--text-primary, #e0e0e0);
121
+ }
122
+
123
+ .toolCount {
124
+ font-size: 11px;
125
+ color: var(--text-muted, #888);
126
+ }
127
+
128
+ .toolMissing {
129
+ font-size: 10px;
130
+ padding: 2px 6px;
131
+ background: rgba(255, 140, 0, 0.15);
132
+ color: #ff8c00;
133
+ border-radius: 4px;
134
+ text-transform: uppercase;
135
+ letter-spacing: 0.3px;
136
+ }
137
+
138
+ /* Model Rows */
139
+ .modelList {
140
+ display: flex;
141
+ flex-direction: column;
142
+ gap: 3px;
143
+ padding-left: 24px;
144
+ }
145
+
146
+ .modelRow {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 8px;
150
+ padding: 6px 8px;
151
+ border-radius: 6px;
152
+ font-size: 12px;
153
+ background: var(--bg-secondary, #14141f);
154
+ &:hover { background: var(--bg-hover, #1a1a2e); }
155
+ }
156
+
157
+ .modelIcon {
158
+ font-size: 12px;
159
+ }
160
+
161
+ .modelName {
162
+ color: var(--text-primary, #e0e0e0);
163
+ min-width: 120px;
164
+ }
165
+
166
+ .modelTier {
167
+ font-size: 10px;
168
+ font-weight: 700;
169
+ padding: 1px 5px;
170
+ background: rgba(0, 200, 255, 0.1);
171
+ color: #00c8ff;
172
+ border-radius: 3px;
173
+ }
174
+
175
+ .modelId {
176
+ flex: 1;
177
+ color: var(--text-muted, #888);
178
+ font-family: 'SF Mono', monospace;
179
+ overflow: hidden;
180
+ text-overflow: ellipsis;
181
+ white-space: nowrap;
182
+ font-size: 11px;
183
+ }
184
+
185
+ .disableBtn {
186
+ background: none;
187
+ border: none;
188
+ color: var(--text-muted, #666);
189
+ cursor: pointer;
190
+ padding: 4px;
191
+ border-radius: 4px;
192
+ display: flex;
193
+ align-items: center;
194
+ &:hover { color: #dc3545; background: rgba(220, 53, 69, 0.1); }
195
+ }
196
+
197
+ @keyframes fadeIn {
198
+ from { opacity: 0; }
199
+ to { opacity: 1; }
200
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @file web/src/components/launch/IncompatibleFallbackModal.jsx
3
+ * @description Web equivalent of the TUI incompatible-fallback overlay. Lets the
4
+ * user switch to a compatible endpoint target or install a similar model endpoint.
5
+ *
6
+ * @functions IncompatibleFallbackModal → switch-tool and similar-model picker
7
+ * @exports IncompatibleFallbackModal
8
+ */
9
+ import { IconAlertTriangle, IconPlugConnected, IconTool, IconX } from '@tabler/icons-react'
10
+ import {
11
+ findSimilarCompatibleModels,
12
+ getCompatibleTools,
13
+ getToolMeta,
14
+ } from '../../../../src/core/tool-metadata.js'
15
+ import styles from './LaunchModal.module.css'
16
+
17
+ export default function IncompatibleFallbackModal({ request, models = [], onClose, onSwitchToolLaunch, onLaunchSimilar }) {
18
+ if (!request?.model) return null
19
+ const { model, toolMode } = request
20
+ const activeMeta = getToolMeta(toolMode)
21
+ const compatibleTools = getCompatibleTools(model.providerKey)
22
+ const similar = findSimilarCompatibleModels(model.sweScore, toolMode, models, 3)
23
+ .filter((candidate) => candidate.modelId !== model.modelId || candidate.providerKey !== model.providerKey)
24
+
25
+ return (
26
+ <div className={styles.backdrop} onClick={onClose}>
27
+ <div className={styles.modalWide} onClick={(event) => event.stopPropagation()}>
28
+ <div className={styles.header}>
29
+ <div>
30
+ <p className={styles.eyebrow}>Incompatible model</p>
31
+ <h2><IconAlertTriangle size={18} /> {model.label} cannot be installed in {activeMeta.emoji} {activeMeta.label}</h2>
32
+ </div>
33
+ <button className={styles.close} onClick={onClose} aria-label="Close"><IconX size={18} /></button>
34
+ </div>
35
+ <div className={styles.grid}>
36
+ <section className={styles.card}>
37
+ <h3><IconTool size={15} /> Switch tool</h3>
38
+ <p className={styles.muted}>This provider can be configured in these tools. Pick one and FCM installs the endpoint.</p>
39
+ <div className={styles.stack}>
40
+ {compatibleTools.map((mode) => {
41
+ const meta = getToolMeta(mode)
42
+ return (
43
+ <button key={mode} className={styles.option} onClick={() => onSwitchToolLaunch?.(mode, model)}>
44
+ <span>{meta.emoji}</span>
45
+ <strong>{meta.label}</strong>
46
+ </button>
47
+ )
48
+ })}
49
+ </div>
50
+ </section>
51
+ <section className={styles.card}>
52
+ <h3><IconPlugConnected size={15} /> Similar models</h3>
53
+ <p className={styles.muted}>Or keep {activeMeta.label} and install a compatible model close to the same SWE score.</p>
54
+ <div className={styles.stack}>
55
+ {similar.length === 0 && <p className={styles.muted}>No close compatible alternative found yet.</p>}
56
+ {similar.map((candidate) => (
57
+ <button key={`${candidate.providerKey}/${candidate.modelId}`} className={styles.option} onClick={() => onLaunchSimilar?.(candidate)}>
58
+ <span>{candidate.tier}</span>
59
+ <strong>{candidate.label}</strong>
60
+ <small>{candidate.providerKey} · {candidate.sweScore}</small>
61
+ </button>
62
+ ))}
63
+ </div>
64
+ </section>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ )
69
+ }