ethagent 2.3.0 → 3.0.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/README.md +18 -4
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +157 -15
- package/src/app/FirstRunTimeline.tsx +4 -0
- package/src/app/input/AppInputProvider.tsx +19 -0
- package/src/app/input/appInputParser.ts +19 -4
- package/src/chat/ChatBottomPane.tsx +12 -1
- package/src/chat/ChatScreen.tsx +17 -5
- package/src/chat/ConversationStack.tsx +25 -19
- package/src/chat/MessageList.tsx +194 -53
- package/src/chat/chatSessionState.ts +4 -1
- package/src/chat/chatTurnOrchestrator.ts +65 -2
- package/src/chat/input/ChatInput.tsx +28 -2
- package/src/chat/input/imageRefs.ts +30 -0
- package/src/chat/input/textCursor.ts +13 -3
- package/src/chat/transcript/TranscriptView.tsx +7 -5
- package/src/chat/transcript/transcriptViewport.ts +88 -17
- package/src/chat/views/PermissionPrompt.tsx +26 -26
- package/src/chat/views/PermissionsView.tsx +18 -12
- package/src/chat/views/ResumeView.tsx +16 -7
- package/src/chat/views/RewindView.tsx +3 -1
- package/src/cli/ResetConfirmView.tsx +24 -9
- package/src/identity/continuity/editor.ts +27 -2
- package/src/identity/continuity/envelope.ts +125 -0
- package/src/identity/continuity/publicSkills.ts +37 -1
- package/src/identity/continuity/skills/frontmatter.ts +183 -0
- package/src/identity/continuity/skills/loadSkills.ts +609 -0
- package/src/identity/continuity/skills/publicSkillsSync.ts +32 -0
- package/src/identity/continuity/skills/scaffold.ts +52 -0
- package/src/identity/continuity/skills/types.ts +30 -0
- package/src/identity/continuity/storage/defaults.ts +28 -47
- package/src/identity/continuity/storage/files.ts +1 -0
- package/src/identity/continuity/storage/paths.ts +1 -0
- package/src/identity/continuity/storage/scaffold.ts +25 -23
- package/src/identity/continuity/storage/status.ts +34 -5
- package/src/identity/continuity/storage/types.ts +3 -2
- package/src/identity/continuity/storage.ts +3 -0
- package/src/identity/hub/OperationalRoutes.tsx +105 -3
- package/src/identity/hub/Routes.tsx +5 -3
- package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +5 -51
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +1 -1
- package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -0
- package/src/identity/hub/continuity/effects.ts +36 -5
- package/src/identity/hub/continuity/skills/DeleteSkillConfirmScreen.tsx +112 -0
- package/src/identity/hub/continuity/skills/DeleteSkillScreen.tsx +123 -0
- package/src/identity/hub/continuity/skills/NewSkillScreen.tsx +57 -0
- package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
- package/src/identity/hub/continuity/skills/SkillVisibilityScreen.tsx +171 -0
- package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +213 -0
- package/src/identity/hub/continuity/snapshot.ts +3 -0
- package/src/identity/hub/continuity/state.ts +3 -2
- package/src/identity/hub/continuity/vault.ts +42 -10
- package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
- package/src/identity/hub/identityHubReducer.ts +21 -0
- package/src/identity/hub/profile/effects.ts +16 -3
- package/src/identity/hub/restore/RestoreFlow.tsx +43 -6
- package/src/identity/hub/restore/apply.ts +12 -1
- package/src/identity/hub/restore/recovery.ts +11 -1
- package/src/identity/hub/restore/resolve.ts +1 -1
- package/src/identity/hub/restore/useRestoreEffects.ts +4 -6
- package/src/identity/hub/shared/components/DetailsScreen.tsx +4 -1
- package/src/identity/hub/shared/components/IdentitySummary.tsx +97 -53
- package/src/identity/hub/shared/components/MenuScreen.tsx +18 -15
- package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +1 -1
- package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +8 -12
- package/src/identity/hub/shared/effects/sync.ts +16 -3
- package/src/identity/hub/shared/model/copy.ts +2 -4
- package/src/identity/hub/transfer/effects.ts +15 -2
- package/src/identity/hub/useIdentityHubContinuity.ts +145 -23
- package/src/identity/hub/useIdentityHubController.ts +5 -1
- package/src/identity/hub/useIdentityHubSideEffects.ts +2 -4
- package/src/mcp/manager.ts +1 -1
- package/src/models/ModelPicker.tsx +211 -74
- package/src/models/huggingface.ts +180 -2
- package/src/models/llamacpp.ts +261 -17
- package/src/models/llamacppPreflight.ts +16 -12
- package/src/models/modelPickerOptions.ts +57 -38
- package/src/providers/anthropic.ts +36 -5
- package/src/providers/contracts.ts +10 -1
- package/src/providers/gemini.ts +29 -3
- package/src/providers/openai-chat.ts +131 -11
- package/src/providers/openai-responses-format.ts +29 -8
- package/src/providers/openai-responses.ts +41 -11
- package/src/providers/registry.ts +1 -0
- package/src/runtime/toolExecution.ts +4 -3
- package/src/runtime/turn.ts +61 -30
- package/src/storage/config.ts +1 -0
- package/src/storage/sessions.ts +14 -2
- package/src/tools/changeDirectoryTool.ts +1 -1
- package/src/tools/contracts.ts +10 -0
- package/src/tools/deleteFileTool.ts +1 -1
- package/src/tools/editTool.ts +1 -1
- package/src/tools/listDirectoryTool.ts +1 -1
- package/src/tools/listSkillFilesTool.ts +77 -0
- package/src/tools/listSkillsTool.ts +68 -0
- package/src/tools/mcpResourceTools.ts +2 -2
- package/src/tools/privateContinuityReadTool.ts +1 -1
- package/src/tools/readSkillTool.ts +107 -0
- package/src/tools/readTool.ts +1 -1
- package/src/tools/registry.ts +6 -0
- package/src/tools/writeFileTool.ts +22 -2
- package/src/ui/Spinner.tsx +15 -3
- package/src/ui/theme.ts +2 -0
- package/src/utils/images.ts +140 -0
- package/src/utils/messages.ts +2 -0
- package/src/identity/continuity/localBackup.ts +0 -249
- package/src/identity/continuity/zipWriter.ts +0 -95
- package/src/identity/hub/continuity/index.ts +0 -7
- package/src/identity/hub/ens/index.ts +0 -11
- package/src/identity/hub/restore/index.ts +0 -22
|
@@ -7,12 +7,13 @@ import { Surface } from '../ui/Surface.js'
|
|
|
7
7
|
import { ProgressBar } from '../ui/ProgressBar.js'
|
|
8
8
|
import { theme } from '../ui/theme.js'
|
|
9
9
|
import {
|
|
10
|
-
buildLlamaCppRunner,
|
|
11
10
|
DEFAULT_LLAMA_HOST,
|
|
12
11
|
detectLlamaCpp,
|
|
13
12
|
installLlamaCppRunner,
|
|
13
|
+
killRogueLlamaProcesses,
|
|
14
14
|
setLlamaCppServerPath,
|
|
15
15
|
startLlamaCppServer,
|
|
16
|
+
stopLlamaCppServer,
|
|
16
17
|
type LlamaCppInstallProgress,
|
|
17
18
|
type LlamaCppInstallResult,
|
|
18
19
|
type LlamaCppStartResult,
|
|
@@ -32,6 +33,8 @@ import { defaultModelFor, type EthagentConfig, type ProviderId } from '../storag
|
|
|
32
33
|
import { clearModelCatalogCache, discoverProviderModels, isOpenAIOAuthAllowedModel, OPENAI_OAUTH_DEFAULT_MODEL, type ModelCatalogResult } from './catalog.js'
|
|
33
34
|
import { contextWindowInfo } from '../runtime/compaction.js'
|
|
34
35
|
import {
|
|
36
|
+
addMmprojToInstalledModel,
|
|
37
|
+
backfillMmprojForModels,
|
|
35
38
|
createHfDownloadPlan,
|
|
36
39
|
downloadHfModel,
|
|
37
40
|
fetchHuggingFaceRepoInfo,
|
|
@@ -68,7 +71,7 @@ import { formatLocalHfModelDisplayName, formatModelDisplayName } from './modelDi
|
|
|
68
71
|
import { fetchUncensoredGgufCatalog, type UncensoredCatalogEntry } from './uncensoredCatalog.js'
|
|
69
72
|
|
|
70
73
|
export type ModelPickerSelection =
|
|
71
|
-
| { kind: 'llamacpp'; model: string }
|
|
74
|
+
| { kind: 'llamacpp'; model: string; mmprojPath?: string }
|
|
72
75
|
| { kind: 'cloud'; provider: CloudProviderId; model: string; keyJustSet: boolean }
|
|
73
76
|
|
|
74
77
|
type ModelPickerProps = {
|
|
@@ -77,6 +80,7 @@ type ModelPickerProps = {
|
|
|
77
80
|
currentModel: string
|
|
78
81
|
contextFit?: ModelPickerContextFit | null
|
|
79
82
|
featuredHfRepo?: string
|
|
83
|
+
localOnly?: boolean
|
|
80
84
|
onPick: (selection: ModelPickerSelection) => void
|
|
81
85
|
onCancel: () => void
|
|
82
86
|
}
|
|
@@ -113,6 +117,9 @@ type State =
|
|
|
113
117
|
| { kind: 'localRunnerPathEntry'; data: LoadedData; model: LocalHfModel; submitting: boolean; error?: string }
|
|
114
118
|
| { kind: 'localRunnerStarting'; data: LoadedData; model: LocalHfModel; startedAt: number }
|
|
115
119
|
| { kind: 'localRunnerStartFail'; data: LoadedData; model: LocalHfModel; result: Extract<LlamaCppStartResult, { ok: false }> }
|
|
120
|
+
| { kind: 'mmprojOffer'; data: LoadedData; model: LocalHfModel }
|
|
121
|
+
| { kind: 'mmprojDownloading'; data: LoadedData; model: LocalHfModel; progress: HfDownloadProgress }
|
|
122
|
+
| { kind: 'mmprojError'; data: LoadedData; model: LocalHfModel; message: string }
|
|
116
123
|
|
|
117
124
|
export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
118
125
|
currentConfig,
|
|
@@ -120,12 +127,20 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
120
127
|
currentModel,
|
|
121
128
|
contextFit,
|
|
122
129
|
featuredHfRepo,
|
|
130
|
+
localOnly = false,
|
|
123
131
|
onPick,
|
|
124
132
|
onCancel,
|
|
125
133
|
}) => {
|
|
126
134
|
const [state, setState] = useState<State>({ kind: 'loading' })
|
|
127
135
|
const hfAbortRef = useRef<AbortController | null>(null)
|
|
128
136
|
const oauthServiceRef = useRef<OpenAIOAuthService | null>(null)
|
|
137
|
+
const dismissToList = (data: LoadedData) => () => {
|
|
138
|
+
if (localOnly) {
|
|
139
|
+
onCancel()
|
|
140
|
+
} else {
|
|
141
|
+
setState({ kind: 'list', data })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
129
144
|
|
|
130
145
|
useEffect(() => {
|
|
131
146
|
let cancelled = false
|
|
@@ -187,8 +202,20 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
187
202
|
|
|
188
203
|
if (state.kind === 'loading') {
|
|
189
204
|
return (
|
|
190
|
-
<Surface
|
|
191
|
-
|
|
205
|
+
<Surface
|
|
206
|
+
title={localOnly ? 'Local Model' : (contextFit ? 'Switch to Larger-Context Model' : 'Switch Provider · Model')}
|
|
207
|
+
subtitle="Loading providers and models."
|
|
208
|
+
footer="esc back"
|
|
209
|
+
>
|
|
210
|
+
<Spinner label={localOnly ? 'loading local models...' : 'loading providers...'} />
|
|
211
|
+
<Box marginTop={1}>
|
|
212
|
+
<Select<'cancel'>
|
|
213
|
+
options={[{ value: 'cancel', label: 'Back', hint: 'Return to the previous screen', role: 'utility' }]}
|
|
214
|
+
hintLayout="inline"
|
|
215
|
+
onSubmit={() => onCancel()}
|
|
216
|
+
onCancel={() => onCancel()}
|
|
217
|
+
/>
|
|
218
|
+
</Box>
|
|
192
219
|
</Surface>
|
|
193
220
|
)
|
|
194
221
|
}
|
|
@@ -204,7 +231,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
204
231
|
label="Model Link"
|
|
205
232
|
placeholder={LOCAL_MODEL_LINK_HINT}
|
|
206
233
|
onSubmit={value => void inspectHfInput(state, value, setState)}
|
|
207
|
-
onCancel={(
|
|
234
|
+
onCancel={dismissToList(state.data)}
|
|
208
235
|
/>
|
|
209
236
|
{state.error ? <Text color={theme.accentError}>{state.error}</Text> : null}
|
|
210
237
|
</Surface>
|
|
@@ -244,6 +271,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
244
271
|
const canDownload = plan.review.risk !== 'high' && plan.review.runtime === 'llama.cpp runnable'
|
|
245
272
|
const fit = state.data.machineSpec ? estimateGgufMachineFit(plan.sizeBytes, state.data.machineSpec) : null
|
|
246
273
|
const recommended = state.data.machineSpec ? recommendGgufFile(plan.repo, ggufFiles(plan.repo), state.data.machineSpec) : null
|
|
274
|
+
const mmproj = plan.mmprojCandidate
|
|
247
275
|
return (
|
|
248
276
|
<Surface
|
|
249
277
|
title="Review Model Link"
|
|
@@ -260,19 +288,28 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
260
288
|
<Text color={riskColor(plan.review.risk)}>safety: {safetyLabel(plan.review.risk)} · source: {credibilityLabel(plan.review.credibility)}</Text>
|
|
261
289
|
<Text color={theme.dim}>signals: {formatSignals(plan.repo.downloads, plan.repo.likes)}</Text>
|
|
262
290
|
<Text color={theme.dim}>notes: {friendlyReasons(plan.review.reasons).join('; ')}</Text>
|
|
291
|
+
{mmproj ? (
|
|
292
|
+
<Text color={theme.dim}>vision encoder available: {friendlyFileName(mmproj.filename)} (+{formatBytes(mmproj.sizeBytes)})</Text>
|
|
293
|
+
) : null}
|
|
263
294
|
</Box>
|
|
264
|
-
<Select<'download' | 'pick' | 'cancel'>
|
|
295
|
+
<Select<'download' | 'downloadWithMmproj' | 'pick' | 'cancel'>
|
|
265
296
|
options={[
|
|
266
|
-
{ value: 'download',
|
|
297
|
+
{ value: 'download', role: 'section', label: 'Download' },
|
|
298
|
+
...(mmproj ? [{ value: 'downloadWithMmproj' as const, label: `Download Model + Vision Encoder (+${formatBytes(mmproj.sizeBytes)}) · recommended`, disabled: !canDownload }] : []),
|
|
299
|
+
{ value: 'download', label: mmproj ? 'Download Without Vision Encoder' : 'Download This Model', disabled: !canDownload },
|
|
300
|
+
{ value: 'pick', role: 'section', label: 'Navigation' },
|
|
267
301
|
{ value: 'pick', label: 'Pick Another File' },
|
|
268
|
-
{ value: 'cancel', label: 'Cancel' },
|
|
302
|
+
{ value: 'cancel', label: 'Cancel', role: 'utility' },
|
|
269
303
|
]}
|
|
270
304
|
onSubmit={choice => {
|
|
271
305
|
if (choice === 'download') void startHfDownload(state, setState, hfAbortRef, onPick)
|
|
306
|
+
else if (choice === 'downloadWithMmproj') {
|
|
307
|
+
void startHfDownload({ ...state, plan: { ...plan, includeMmproj: true } }, setState, hfAbortRef, onPick)
|
|
308
|
+
}
|
|
272
309
|
else if (choice === 'pick') void inspectHfInput({ kind: 'hfInput', data: state.data }, plan.repoId, setState)
|
|
273
|
-
else
|
|
310
|
+
else dismissToList(state.data)()
|
|
274
311
|
}}
|
|
275
|
-
onCancel={(
|
|
312
|
+
onCancel={dismissToList(state.data)}
|
|
276
313
|
/>
|
|
277
314
|
</Surface>
|
|
278
315
|
)
|
|
@@ -291,6 +328,68 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
291
328
|
)
|
|
292
329
|
}
|
|
293
330
|
|
|
331
|
+
if (state.kind === 'mmprojOffer') {
|
|
332
|
+
const sizeLabel = state.model.mmprojSizeBytes ? `+${formatBytes(state.model.mmprojSizeBytes)}` : 'additional download'
|
|
333
|
+
return (
|
|
334
|
+
<Surface
|
|
335
|
+
title="Add Image Support?"
|
|
336
|
+
subtitle={`${state.model.displayName} has a vision encoder available in its Hugging Face repo.`}
|
|
337
|
+
footer="enter select · esc back"
|
|
338
|
+
>
|
|
339
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
340
|
+
<Text color={theme.dim}>Loading the vision encoder lets this model accept pasted images.</Text>
|
|
341
|
+
<Text color={theme.dim}>Without it, image paste is declined at submit time.</Text>
|
|
342
|
+
</Box>
|
|
343
|
+
<Select<'add' | 'skip' | 'cancel'>
|
|
344
|
+
options={[
|
|
345
|
+
{ value: 'add', label: `Add Vision Encoder (${sizeLabel}) And Use` },
|
|
346
|
+
{ value: 'skip', label: 'Use Without Image Support' },
|
|
347
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
348
|
+
]}
|
|
349
|
+
onSubmit={choice => {
|
|
350
|
+
if (choice === 'add') void downloadMmprojAndContinue(state, setState, onPick)
|
|
351
|
+
else if (choice === 'skip') void startAndPickHfModel({ ...state.model, mmprojAvailable: false }, state, setState, onPick)
|
|
352
|
+
else dismissToList(state.data)()
|
|
353
|
+
}}
|
|
354
|
+
onCancel={dismissToList(state.data)}
|
|
355
|
+
/>
|
|
356
|
+
</Surface>
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (state.kind === 'mmprojDownloading') {
|
|
361
|
+
const total = state.progress.total ?? state.model.mmprojSizeBytes ?? 0
|
|
362
|
+
const completed = state.progress.completed ?? 0
|
|
363
|
+
const progress = total > 0 ? completed / total : 0
|
|
364
|
+
const suffix = total > 0 ? `${formatBytes(completed)} / ${formatBytes(total)}` : formatBytes(completed)
|
|
365
|
+
return (
|
|
366
|
+
<Surface title="Downloading Vision Encoder" subtitle={state.model.displayName}>
|
|
367
|
+
<Text color={theme.dim}>{state.progress.status}</Text>
|
|
368
|
+
<ProgressBar progress={progress} suffix={suffix} />
|
|
369
|
+
</Surface>
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (state.kind === 'mmprojError') {
|
|
374
|
+
return (
|
|
375
|
+
<Surface title="Vision Encoder Download Failed" subtitle={state.message} tone="error" footer="enter select · esc back">
|
|
376
|
+
<Select<'retry' | 'skip' | 'back'>
|
|
377
|
+
options={[
|
|
378
|
+
{ value: 'retry', label: 'Retry Download' },
|
|
379
|
+
{ value: 'skip', label: 'Use Without Image Support' },
|
|
380
|
+
{ value: 'back', label: 'Back To Picker' },
|
|
381
|
+
]}
|
|
382
|
+
onSubmit={choice => {
|
|
383
|
+
if (choice === 'retry') setState({ kind: 'mmprojOffer', data: state.data, model: state.model })
|
|
384
|
+
else if (choice === 'skip') void startAndPickHfModel({ ...state.model, mmprojAvailable: false }, state, setState, onPick)
|
|
385
|
+
else dismissToList(state.data)()
|
|
386
|
+
}}
|
|
387
|
+
onCancel={dismissToList(state.data)}
|
|
388
|
+
/>
|
|
389
|
+
</Surface>
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
294
393
|
if (state.kind === 'hfDone') {
|
|
295
394
|
return (
|
|
296
395
|
<Surface
|
|
@@ -305,9 +404,9 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
305
404
|
]}
|
|
306
405
|
onSubmit={choice => {
|
|
307
406
|
if (choice === 'use') void startAndPickHfModel(state.model, state, setState, onPick)
|
|
308
|
-
else
|
|
407
|
+
else dismissToList(state.data)()
|
|
309
408
|
}}
|
|
310
|
-
onCancel={(
|
|
409
|
+
onCancel={dismissToList(state.data)}
|
|
311
410
|
/>
|
|
312
411
|
</Surface>
|
|
313
412
|
)
|
|
@@ -323,9 +422,9 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
323
422
|
]}
|
|
324
423
|
onSubmit={choice => {
|
|
325
424
|
if (choice === 'retry') setState({ kind: 'hfInput', data: state.data, error: state.input ? undefined : state.message })
|
|
326
|
-
else
|
|
425
|
+
else dismissToList(state.data)()
|
|
327
426
|
}}
|
|
328
|
-
onCancel={(
|
|
427
|
+
onCancel={dismissToList(state.data)}
|
|
329
428
|
/>
|
|
330
429
|
</Surface>
|
|
331
430
|
)
|
|
@@ -355,7 +454,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
355
454
|
const target = targets.find(item => `${item.kind}:${item.id}` === value)
|
|
356
455
|
if (target) setState({ kind: 'localUninstallConfirm', data: state.data, target })
|
|
357
456
|
}}
|
|
358
|
-
onCancel={(
|
|
457
|
+
onCancel={dismissToList(state.data)}
|
|
359
458
|
/>
|
|
360
459
|
)}
|
|
361
460
|
</Surface>
|
|
@@ -401,8 +500,8 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
401
500
|
<Surface title="Local Model Uninstalled" subtitle={state.modelName} footer="enter back to picker · esc close">
|
|
402
501
|
<Select<'back'>
|
|
403
502
|
options={[{ value: 'back', label: 'Back To Picker' }]}
|
|
404
|
-
onSubmit={(
|
|
405
|
-
onCancel={(
|
|
503
|
+
onSubmit={dismissToList(state.data)}
|
|
504
|
+
onCancel={dismissToList(state.data)}
|
|
406
505
|
/>
|
|
407
506
|
</Surface>
|
|
408
507
|
)
|
|
@@ -418,9 +517,9 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
418
517
|
]}
|
|
419
518
|
onSubmit={choice => {
|
|
420
519
|
if (choice === 'retry') void uninstallLocalModel({ kind: 'localUninstallConfirm', data: state.data, target: state.target }, setState)
|
|
421
|
-
else
|
|
520
|
+
else dismissToList(state.data)()
|
|
422
521
|
}}
|
|
423
|
-
onCancel={(
|
|
522
|
+
onCancel={dismissToList(state.data)}
|
|
424
523
|
/>
|
|
425
524
|
</Surface>
|
|
426
525
|
)
|
|
@@ -449,9 +548,9 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
449
548
|
if (choice === 'download') setState({ kind: 'hfInput', data: state.data })
|
|
450
549
|
else if (choice === 'install') void installRunnerAndStart(state, setState, onPick)
|
|
451
550
|
else if (choice === 'path') setState({ kind: 'localRunnerPathEntry', data: state.data, model: state.model, submitting: false })
|
|
452
|
-
else
|
|
551
|
+
else dismissToList(state.data)()
|
|
453
552
|
}}
|
|
454
|
-
onCancel={(
|
|
553
|
+
onCancel={dismissToList(state.data)}
|
|
455
554
|
/>
|
|
456
555
|
</Surface>
|
|
457
556
|
)
|
|
@@ -470,15 +569,20 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
470
569
|
const options = buildRunnerRecoveryOptions(state.result)
|
|
471
570
|
return (
|
|
472
571
|
<Surface title="Runner Setup Needs Attention" subtitle={state.result.message} tone="error" footer="enter select · esc back">
|
|
473
|
-
<Select<'retry' | '
|
|
572
|
+
<Select<'stop-and-retry' | 'path' | 'back'>
|
|
474
573
|
options={options}
|
|
574
|
+
hintLayout="inline"
|
|
475
575
|
onSubmit={choice => {
|
|
476
|
-
if (choice === 'retry')
|
|
477
|
-
|
|
576
|
+
if (choice === 'stop-and-retry') {
|
|
577
|
+
void (async () => {
|
|
578
|
+
await killRogueLlamaProcesses()
|
|
579
|
+
await installRunnerAndStart({ kind: 'localRunnerSetup', data: state.data, model: state.model }, setState, onPick)
|
|
580
|
+
})()
|
|
581
|
+
}
|
|
478
582
|
else if (choice === 'path') setState({ kind: 'localRunnerPathEntry', data: state.data, model: state.model, submitting: false })
|
|
479
|
-
else
|
|
583
|
+
else dismissToList(state.data)()
|
|
480
584
|
}}
|
|
481
|
-
onCancel={(
|
|
585
|
+
onCancel={dismissToList(state.data)}
|
|
482
586
|
/>
|
|
483
587
|
</Surface>
|
|
484
588
|
)
|
|
@@ -528,9 +632,9 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
528
632
|
if (choice === 'retry') void startAndPickHfModel(state.model, { kind: 'hfDone', data: state.data, model: state.model }, setState, onPick)
|
|
529
633
|
else if (choice === 'path') setState({ kind: 'localRunnerPathEntry', data: state.data, model: state.model, submitting: false })
|
|
530
634
|
else if (choice === 'install') void installRunnerAndStart({ kind: 'localRunnerSetup', data: state.data, model: state.model }, setState, onPick)
|
|
531
|
-
else
|
|
635
|
+
else dismissToList(state.data)()
|
|
532
636
|
}}
|
|
533
|
-
onCancel={(
|
|
637
|
+
onCancel={dismissToList(state.data)}
|
|
534
638
|
/>
|
|
535
639
|
</Surface>
|
|
536
640
|
)
|
|
@@ -554,7 +658,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
554
658
|
placeholder={providerKeyPlaceholder(provider)}
|
|
555
659
|
isSecret
|
|
556
660
|
onSubmit={(value) => void submitKey(state, value, currentConfig, setState)}
|
|
557
|
-
onCancel={(
|
|
661
|
+
onCancel={dismissToList(state.data)}
|
|
558
662
|
/>
|
|
559
663
|
)}
|
|
560
664
|
{error ? <Text color={theme.accentError}>{error}</Text> : null}
|
|
@@ -576,9 +680,11 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
576
680
|
) : (
|
|
577
681
|
<Select
|
|
578
682
|
options={[
|
|
683
|
+
{ value: 'edit', role: 'section', label: 'Credential' },
|
|
579
684
|
{ value: 'edit', label: 'Replace Stored API Key' },
|
|
580
685
|
{ value: 'delete', label: 'Remove Stored API Key' },
|
|
581
|
-
{ value: 'cancel', label: '
|
|
686
|
+
{ value: 'cancel', role: 'section', label: 'Navigation' },
|
|
687
|
+
{ value: 'cancel', label: 'Back', role: 'utility' },
|
|
582
688
|
]}
|
|
583
689
|
onSubmit={(value) => {
|
|
584
690
|
if (value === 'edit') {
|
|
@@ -591,7 +697,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
591
697
|
}
|
|
592
698
|
void deleteKey(state, currentConfig, setState, onPick, currentProvider)
|
|
593
699
|
}}
|
|
594
|
-
onCancel={(
|
|
700
|
+
onCancel={dismissToList(state.data)}
|
|
595
701
|
/>
|
|
596
702
|
)}
|
|
597
703
|
{error ? <Text color={theme.accentError}>{error}</Text> : null}
|
|
@@ -629,7 +735,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
629
735
|
}
|
|
630
736
|
void signOutOAuth(state, currentConfig, setState, onPick, currentProvider)
|
|
631
737
|
}}
|
|
632
|
-
onCancel={(
|
|
738
|
+
onCancel={dismissToList(state.data)}
|
|
633
739
|
/>
|
|
634
740
|
)}
|
|
635
741
|
{error ? <Text color={theme.accentError}>{error}</Text> : null}
|
|
@@ -655,9 +761,9 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
655
761
|
onSubmit={choice => {
|
|
656
762
|
if (choice === 'retry') void startOpenAIOAuthFlow(state.data, currentConfig, setState, oauthServiceRef, onPick)
|
|
657
763
|
else if (choice === 'apikey') setState({ kind: 'keyEntry', provider: 'openai', action: 'set', data: state.data, submitting: false })
|
|
658
|
-
else
|
|
764
|
+
else dismissToList(state.data)()
|
|
659
765
|
}}
|
|
660
|
-
onCancel={(
|
|
766
|
+
onCancel={dismissToList(state.data)}
|
|
661
767
|
/>
|
|
662
768
|
</Surface>
|
|
663
769
|
)
|
|
@@ -719,11 +825,12 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
719
825
|
options={options}
|
|
720
826
|
initialIndex={initialIndex === -1 ? 0 : initialIndex}
|
|
721
827
|
maxVisible={12}
|
|
828
|
+
hintLayout="inline"
|
|
722
829
|
onSubmit={(value) => {
|
|
723
830
|
const parsed = parseFullCatalogValue(value)
|
|
724
831
|
if (parsed) onPick({ kind: 'cloud', provider: parsed.provider, model: parsed.model, keyJustSet: false })
|
|
725
832
|
}}
|
|
726
|
-
onCancel={(
|
|
833
|
+
onCancel={dismissToList(state.data)}
|
|
727
834
|
/>
|
|
728
835
|
</Surface>
|
|
729
836
|
)
|
|
@@ -749,9 +856,9 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
749
856
|
onSubmit={choice => {
|
|
750
857
|
if (choice === 'retry') void openLocalCatalog(state.data, setState)
|
|
751
858
|
else if (choice === 'paste') setState({ kind: 'hfInput', data: state.data })
|
|
752
|
-
else
|
|
859
|
+
else dismissToList(state.data)()
|
|
753
860
|
}}
|
|
754
|
-
onCancel={(
|
|
861
|
+
onCancel={dismissToList(state.data)}
|
|
755
862
|
/>
|
|
756
863
|
</Surface>
|
|
757
864
|
)
|
|
@@ -770,28 +877,30 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
770
877
|
options={options}
|
|
771
878
|
initialIndex={initialIndex === -1 ? 0 : initialIndex}
|
|
772
879
|
maxVisible={12}
|
|
773
|
-
|
|
774
|
-
|
|
880
|
+
hintLayout="inline"
|
|
881
|
+
onSubmit={(value) => handleSubmit(value, state, setState, onPick, onCancel, currentConfig, oauthServiceRef, localOnly)}
|
|
882
|
+
onCancel={dismissToList(state.data)}
|
|
775
883
|
/>
|
|
776
884
|
</Surface>
|
|
777
885
|
)
|
|
778
886
|
}
|
|
779
887
|
|
|
780
888
|
const { data } = state
|
|
781
|
-
const options = buildModelPickerOptions(data, { currentProvider, currentModel, contextFit })
|
|
889
|
+
const options = buildModelPickerOptions(data, { currentProvider, currentModel, contextFit }, { localOnly })
|
|
782
890
|
const initialIndex = localOrCloudOptionIndex(options, currentProvider, currentModel)
|
|
783
891
|
|
|
784
892
|
return (
|
|
785
893
|
<Surface
|
|
786
|
-
title={contextFit ? 'Switch to Larger-Context Model' : 'Switch Provider · Model'}
|
|
787
|
-
subtitle={contextFit ? contextFitSubtitle(contextFit) : 'Downloaded GGUF files · cloud providers'}
|
|
788
|
-
footer=
|
|
894
|
+
title={localOnly ? 'Local Model' : (contextFit ? 'Switch to Larger-Context Model' : 'Switch Provider · Model')}
|
|
895
|
+
subtitle={localOnly ? 'Downloaded GGUF files and curated catalog' : (contextFit ? contextFitSubtitle(contextFit) : 'Downloaded GGUF files · cloud providers')}
|
|
896
|
+
footer={localOnly ? 'enter select · esc back' : 'enter select · esc close · /models lists installed models'}
|
|
789
897
|
>
|
|
790
898
|
<Select
|
|
791
899
|
options={options}
|
|
792
900
|
initialIndex={initialIndex === -1 ? 0 : initialIndex}
|
|
793
901
|
maxVisible={10}
|
|
794
|
-
|
|
902
|
+
hintLayout="inline"
|
|
903
|
+
onSubmit={(value) => handleSubmit(value, state, setState, onPick, onCancel, currentConfig, oauthServiceRef, localOnly)}
|
|
795
904
|
onCancel={onCancel}
|
|
796
905
|
/>
|
|
797
906
|
</Surface>
|
|
@@ -806,6 +915,7 @@ function handleSubmit(
|
|
|
806
915
|
onCancel: () => void,
|
|
807
916
|
currentConfig: EthagentConfig,
|
|
808
917
|
oauthServiceRef: React.MutableRefObject<OpenAIOAuthService | null>,
|
|
918
|
+
localOnly: boolean = false,
|
|
809
919
|
): void {
|
|
810
920
|
if (value.startsWith('hdr:')) return
|
|
811
921
|
if (value === 'cancel') {
|
|
@@ -813,7 +923,8 @@ function handleSubmit(
|
|
|
813
923
|
return
|
|
814
924
|
}
|
|
815
925
|
if (value === 'back' && state.kind === 'localCatalog') {
|
|
816
|
-
|
|
926
|
+
if (localOnly) onCancel()
|
|
927
|
+
else setState({ kind: 'list', data: state.data })
|
|
817
928
|
return
|
|
818
929
|
}
|
|
819
930
|
if (value.startsWith('hf:')) {
|
|
@@ -834,6 +945,18 @@ function handleSubmit(
|
|
|
834
945
|
})()
|
|
835
946
|
return
|
|
836
947
|
}
|
|
948
|
+
if (value.startsWith('hfmmproj:') && state.kind === 'list') {
|
|
949
|
+
const id = value.slice('hfmmproj:'.length)
|
|
950
|
+
void (async () => {
|
|
951
|
+
const local = await findLocalHfModel(id)
|
|
952
|
+
if (!local) {
|
|
953
|
+
setState({ kind: 'hfError', data: state.data, message: 'local model metadata was not found' })
|
|
954
|
+
return
|
|
955
|
+
}
|
|
956
|
+
setState({ kind: 'mmprojOffer', data: state.data, model: local })
|
|
957
|
+
})()
|
|
958
|
+
return
|
|
959
|
+
}
|
|
837
960
|
if (value.startsWith('uc:') && state.kind === 'localCatalog') {
|
|
838
961
|
const entry = state.catalog.find(item => catalogOptionValue(item.repo.repoId, item.file.filename) === value)
|
|
839
962
|
if (entry) void reviewCatalogModel(state, entry, setState)
|
|
@@ -1216,24 +1339,13 @@ export function buildHfFileOptions(
|
|
|
1216
1339
|
}
|
|
1217
1340
|
|
|
1218
1341
|
function buildRunnerRecoveryOptions(
|
|
1219
|
-
|
|
1220
|
-
): SelectOption<'retry' | '
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
hint: 'Uses git and CMake if installed',
|
|
1227
|
-
})
|
|
1228
|
-
}
|
|
1229
|
-
if (result.recovery.includes('runner-path')) {
|
|
1230
|
-
options.push({ value: 'path', label: 'Use Existing Runner Path' })
|
|
1231
|
-
}
|
|
1232
|
-
if (result.recovery.includes('retry-install')) {
|
|
1233
|
-
options.push({ value: 'retry', label: 'Retry Automatic Install' })
|
|
1234
|
-
}
|
|
1235
|
-
options.push({ value: 'back', label: 'Back To Picker' })
|
|
1236
|
-
return options
|
|
1342
|
+
_result: Extract<LlamaCppInstallResult, { ok: false }>,
|
|
1343
|
+
): SelectOption<'stop-and-retry' | 'path' | 'back'>[] {
|
|
1344
|
+
return [
|
|
1345
|
+
{ value: 'stop-and-retry', label: 'Stop and Retry', hint: 'Stop background runners and try the install again' },
|
|
1346
|
+
{ value: 'path', label: 'Use Existing Runner Path' },
|
|
1347
|
+
{ value: 'back', label: 'Back To Picker' },
|
|
1348
|
+
]
|
|
1237
1349
|
}
|
|
1238
1350
|
|
|
1239
1351
|
function localRunnerStartFailureSubtitle(result: Extract<LlamaCppStartResult, { ok: false }>): string {
|
|
@@ -1445,6 +1557,30 @@ async function uninstallLocalModel(
|
|
|
1445
1557
|
}
|
|
1446
1558
|
}
|
|
1447
1559
|
|
|
1560
|
+
async function downloadMmprojAndContinue(
|
|
1561
|
+
state: Extract<State, { kind: 'mmprojOffer' }>,
|
|
1562
|
+
setState: (s: State) => void,
|
|
1563
|
+
onPick: (sel: ModelPickerSelection) => void,
|
|
1564
|
+
): Promise<void> {
|
|
1565
|
+
setState({ kind: 'mmprojDownloading', data: state.data, model: state.model, progress: { status: 'starting' } })
|
|
1566
|
+
try {
|
|
1567
|
+
for await (const progress of addMmprojToInstalledModel(state.model.id)) {
|
|
1568
|
+
setState({ kind: 'mmprojDownloading', data: state.data, model: state.model, progress })
|
|
1569
|
+
}
|
|
1570
|
+
} catch (err: unknown) {
|
|
1571
|
+
setState({ kind: 'mmprojError', data: state.data, model: state.model, message: (err as Error).message })
|
|
1572
|
+
return
|
|
1573
|
+
}
|
|
1574
|
+
const updated = await findLocalHfModel(state.model.id)
|
|
1575
|
+
if (!updated || !updated.mmprojPath) {
|
|
1576
|
+
setState({ kind: 'mmprojError', data: state.data, model: state.model, message: 'projector downloaded but path was not persisted' })
|
|
1577
|
+
return
|
|
1578
|
+
}
|
|
1579
|
+
await stopLlamaCppServer().catch(() => null)
|
|
1580
|
+
const data = { ...state.data, hfModels: await loadHfPickerModels() }
|
|
1581
|
+
await startAndPickHfModel(updated, { kind: 'mmprojOffer', data, model: updated }, setState, onPick)
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1448
1584
|
async function refreshLocalModelData(data: LoadedData): Promise<LoadedData> {
|
|
1449
1585
|
const hfModels = await loadHfPickerModels()
|
|
1450
1586
|
return {
|
|
@@ -1455,7 +1591,7 @@ async function refreshLocalModelData(data: LoadedData): Promise<LoadedData> {
|
|
|
1455
1591
|
|
|
1456
1592
|
async function startAndPickHfModel(
|
|
1457
1593
|
model: LocalHfModel,
|
|
1458
|
-
state: Extract<State, { kind: 'list' | 'localCatalog' | 'hfDone' }>,
|
|
1594
|
+
state: Extract<State, { kind: 'list' | 'localCatalog' | 'hfDone' | 'mmprojOffer' | 'mmprojError' }>,
|
|
1459
1595
|
setState: (s: State) => void,
|
|
1460
1596
|
onPick: (sel: ModelPickerSelection) => void,
|
|
1461
1597
|
): Promise<void> {
|
|
@@ -1463,10 +1599,15 @@ async function startAndPickHfModel(
|
|
|
1463
1599
|
setState({ kind: 'hfError', data: state.data, message: 'blocked high-risk model; choose a model from a more credible source' })
|
|
1464
1600
|
return
|
|
1465
1601
|
}
|
|
1602
|
+
if (model.mmprojAvailable && !model.mmprojPath && state.kind !== 'mmprojOffer' && state.kind !== 'mmprojError') {
|
|
1603
|
+
setState({ kind: 'mmprojOffer', data: state.data, model })
|
|
1604
|
+
return
|
|
1605
|
+
}
|
|
1466
1606
|
setState({ kind: 'localRunnerStarting', data: state.data, model, startedAt: Date.now() })
|
|
1467
1607
|
const result = await startLlamaCppServer({
|
|
1468
1608
|
modelPath: model.localPath,
|
|
1469
1609
|
modelAlias: model.id,
|
|
1610
|
+
mmprojPath: model.mmprojPath,
|
|
1470
1611
|
})
|
|
1471
1612
|
const llamaCpp = await probeLlamaCpp()
|
|
1472
1613
|
const data = { ...state.data, llamaCpp }
|
|
@@ -1478,7 +1619,7 @@ async function startAndPickHfModel(
|
|
|
1478
1619
|
setState({ kind: 'localRunnerStartFail', data, model, result })
|
|
1479
1620
|
return
|
|
1480
1621
|
}
|
|
1481
|
-
onPick({ kind: 'llamacpp', model: model.id })
|
|
1622
|
+
onPick({ kind: 'llamacpp', model: model.id, mmprojPath: model.mmprojPath })
|
|
1482
1623
|
}
|
|
1483
1624
|
|
|
1484
1625
|
async function installRunnerAndStart(
|
|
@@ -1489,14 +1630,6 @@ async function installRunnerAndStart(
|
|
|
1489
1630
|
await runRunnerSetup(state, setState, onPick, installLlamaCppRunner)
|
|
1490
1631
|
}
|
|
1491
1632
|
|
|
1492
|
-
async function buildRunnerAndStart(
|
|
1493
|
-
state: Extract<State, { kind: 'localRunnerSetup' }>,
|
|
1494
|
-
setState: (s: State) => void,
|
|
1495
|
-
onPick: (sel: ModelPickerSelection) => void,
|
|
1496
|
-
): Promise<void> {
|
|
1497
|
-
await runRunnerSetup(state, setState, onPick, buildLlamaCppRunner)
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
1633
|
async function runRunnerSetup(
|
|
1501
1634
|
state: Extract<State, { kind: 'localRunnerSetup' }>,
|
|
1502
1635
|
setState: (s: State) => void,
|
|
@@ -1576,7 +1709,8 @@ function formatContextWindow(tokens: number): string {
|
|
|
1576
1709
|
|
|
1577
1710
|
async function loadHfPickerModels(): Promise<ModelPickerOptionsData['hfModels']> {
|
|
1578
1711
|
const installed = await loadLocalHfModels()
|
|
1579
|
-
|
|
1712
|
+
const backfilled = await backfillMmprojForModels(installed)
|
|
1713
|
+
return backfilled.map(model => ({
|
|
1580
1714
|
id: model.id,
|
|
1581
1715
|
displayName: model.displayName,
|
|
1582
1716
|
sizeBytes: model.sizeBytes,
|
|
@@ -1584,6 +1718,9 @@ async function loadHfPickerModels(): Promise<ModelPickerOptionsData['hfModels']>
|
|
|
1584
1718
|
risk: model.risk,
|
|
1585
1719
|
task: model.task,
|
|
1586
1720
|
status: model.status,
|
|
1721
|
+
mmprojPath: model.mmprojPath,
|
|
1722
|
+
mmprojAvailable: model.mmprojAvailable,
|
|
1723
|
+
mmprojSizeBytes: model.mmprojSizeBytes,
|
|
1587
1724
|
}))
|
|
1588
1725
|
}
|
|
1589
1726
|
|