ethagent 2.2.0 → 2.4.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 +11 -0
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +3 -7
- package/src/app/FirstRunTimeline.tsx +1 -1
- package/src/chat/ChatBottomPane.tsx +29 -11
- package/src/chat/ChatScreen.tsx +169 -38
- package/src/chat/ConversationStack.tsx +1 -1
- package/src/chat/MessageList.tsx +185 -72
- package/src/chat/SessionStatus.tsx +3 -1
- package/src/chat/chatScreenUtils.ts +11 -15
- package/src/chat/chatSessionState.ts +5 -2
- package/src/chat/chatTurnOrchestrator.ts +7 -9
- package/src/chat/commands.ts +26 -26
- package/src/chat/display/DiffView.tsx +193 -0
- package/src/chat/display/SyntaxText.tsx +192 -0
- package/src/chat/display/toolCallDisplay.ts +103 -0
- package/src/chat/display/toolResultDisplay.ts +19 -0
- package/src/chat/{ChatInput.tsx → input/ChatInput.tsx} +61 -25
- package/src/chat/input/imageRefs.ts +30 -0
- package/src/chat/{TranscriptView.tsx → transcript/TranscriptView.tsx} +24 -50
- package/src/chat/{transcriptViewport.ts → transcript/transcriptViewport.ts} +12 -30
- package/src/chat/{ContextLimitView.tsx → views/ContextLimitView.tsx} +3 -3
- package/src/chat/{ContinuityEditReviewView.tsx → views/ContinuityEditReviewView.tsx} +11 -3
- package/src/chat/{CopyPicker.tsx → views/CopyPicker.tsx} +4 -5
- package/src/chat/{PermissionPrompt.tsx → views/PermissionPrompt.tsx} +16 -17
- package/src/chat/{PermissionsView.tsx → views/PermissionsView.tsx} +6 -6
- package/src/chat/{PlanApprovalView.tsx → views/PlanApprovalView.tsx} +4 -4
- package/src/chat/{ResumeView.tsx → views/ResumeView.tsx} +50 -41
- package/src/chat/views/RewindView.tsx +410 -0
- package/src/identity/continuity/privateEdit/diff.ts +2 -78
- package/src/identity/hub/OperationalRoutes.tsx +21 -21
- package/src/identity/hub/Routes.tsx +13 -13
- package/src/identity/hub/{flows/continuity → continuity}/ContinuityDashboardScreen.tsx +9 -9
- package/src/identity/hub/{flows/continuity → continuity}/RebackupStorageScreen.tsx +2 -2
- package/src/identity/hub/{flows/continuity → continuity}/RecoveryConfirmScreen.tsx +5 -5
- package/src/identity/hub/{flows/continuity → continuity}/SavePromptScreen.tsx +5 -5
- package/src/identity/hub/{effects/rebackup/runRebackup.ts → continuity/effects.ts} +17 -17
- package/src/identity/hub/{effects/rebackup → continuity}/index.ts +1 -1
- package/src/identity/hub/{effects/shared → continuity}/snapshot.ts +8 -8
- package/src/identity/hub/{effects/rebackup → continuity}/vault.ts +15 -15
- package/src/identity/hub/{flows/create → create}/CreateFlow.tsx +13 -13
- package/src/identity/hub/{effects/create.ts → create/effects.ts} +4 -4
- package/src/identity/hub/{flows/custody → custody}/CustodyEditFlow.tsx +9 -9
- package/src/identity/hub/{flows/custody/custodyFlowActions.ts → custody/actions.ts} +6 -6
- package/src/identity/hub/{flows/custody/custodyFlowHelpers.ts → custody/helpers.ts} +4 -4
- package/src/identity/hub/{effects/vault → custody}/preflight.ts +5 -5
- package/src/identity/hub/{flows/custody/custodyFlowRoutes.tsx → custody/routes.tsx} +8 -8
- package/src/identity/hub/{flows/custody/custodyEffects.ts → custody/transactions.ts} +9 -9
- package/src/identity/hub/{flows/custody/custodyFlowTypes.ts → custody/types.ts} +5 -5
- package/src/identity/hub/{flows/custody/custodyFlowEffects.ts → custody/useCustodyEffects.ts} +7 -7
- package/src/identity/hub/{flows/custody → custody}/useCustodyFlow.tsx +5 -5
- package/src/identity/hub/{flows/ens → ens}/EnsEditAdvancedScreens.tsx +13 -13
- package/src/identity/hub/{flows/ens → ens}/EnsEditFlow.tsx +7 -7
- package/src/identity/hub/{flows/ens → ens}/EnsEditMaintenanceScreens.tsx +10 -10
- package/src/identity/hub/{flows/ens → ens}/EnsEditReviewScreens.tsx +12 -12
- package/src/identity/hub/{flows/ens → ens}/EnsEditRunners.tsx +5 -5
- package/src/identity/hub/{flows/ens → ens}/EnsEditShared.tsx +10 -10
- package/src/identity/hub/{flows/ens → ens}/EnsEditSimpleScreens.tsx +14 -14
- package/src/identity/hub/{flows/ens/IdentityHubEnsFlow.tsx → ens/EnsFlow.tsx} +12 -12
- package/src/identity/hub/{flows/ens/OperatorWalletsScreen.tsx → ens/EnsOperatorWalletsScreen.tsx} +17 -17
- package/src/identity/hub/{advancedEnsValidation.ts → ens/advancedEnsValidation.ts} +2 -2
- package/src/identity/hub/{flows/ens/ensEditCopy.ts → ens/editCopy.ts} +3 -3
- package/src/identity/hub/{effects/ens/flows.ts → ens/effects.ts} +7 -7
- package/src/identity/hub/{effects/ens → ens}/index.ts +1 -1
- package/src/identity/hub/{model/ens.ts → ens/state.ts} +1 -1
- package/src/identity/hub/{effects/ens → ens}/transactions.ts +239 -239
- package/src/identity/hub/{flows/ens/ensEditTypes.ts → ens/types.ts} +7 -7
- package/src/identity/hub/identityHubReducer.ts +3 -3
- package/src/identity/hub/{flows/profile → profile}/EditProfileFlow.tsx +11 -11
- package/src/identity/hub/{effects/publicProfile/runPublicProfileSave.ts → profile/effects.ts} +18 -18
- package/src/identity/hub/{model → profile}/identity.ts +3 -3
- package/src/identity/hub/{effects/profile/profileState.ts → profile/state.ts} +181 -181
- package/src/identity/hub/{flows/restore → restore}/RestoreFlow.tsx +16 -16
- package/src/identity/hub/{effects/restore → restore}/apply.ts +10 -10
- package/src/identity/hub/{effects/restore → restore}/auth.ts +7 -7
- package/src/identity/hub/{effects/restore → restore}/discover.ts +6 -6
- package/src/identity/hub/{effects/restore → restore}/envelopes.ts +2 -2
- package/src/identity/hub/{effects/restore → restore}/fetch.ts +3 -3
- package/src/identity/hub/{effects/restore/shared.ts → restore/helpers.ts} +6 -6
- package/src/identity/hub/{effects/restore → restore}/recovery.ts +10 -10
- package/src/identity/hub/{effects/restore → restore}/resolve.ts +4 -4
- package/src/identity/hub/{effects → restore}/restoreAdmin.ts +1 -1
- package/src/identity/hub/{flows/restore/useRestoreFlowEffects.ts → restore/useRestoreEffects.ts} +5 -5
- package/src/identity/hub/{flows/settings → settings}/StorageCredentialScreen.tsx +5 -5
- package/src/identity/hub/{components → shared/components}/BusyScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/DetailsScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/ErrorScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/FlowTimeline.tsx +1 -1
- package/src/identity/hub/{components → shared/components}/IdentitySummary.tsx +8 -8
- package/src/identity/hub/{components → shared/components}/MenuScreen.tsx +7 -7
- package/src/identity/hub/{components → shared/components}/NetworkScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/PinataJwtInput.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/UnlinkedIdentityScreen.tsx +5 -5
- package/src/identity/hub/{components → shared/components}/WalletApprovalScreen.tsx +6 -6
- package/src/identity/hub/{components → shared/components}/menuFlagsFromReconciliation.ts +1 -1
- package/src/identity/hub/{effects/shared → shared/effects}/profilePrep.ts +1 -1
- package/src/identity/hub/{effects → shared/effects}/receipts.ts +2 -2
- package/src/identity/hub/{effects/shared → shared/effects}/sync.ts +4 -4
- package/src/identity/hub/{effects → shared/effects}/types.ts +3 -3
- package/src/identity/hub/{model → shared/model}/copy.ts +2 -2
- package/src/identity/hub/{model → shared/model}/errors.ts +5 -5
- package/src/identity/hub/{model → shared/model}/network.ts +3 -3
- package/src/identity/hub/{operatorWallets.ts → shared/operatorWallets.ts} +1 -1
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/hook.ts +1 -1
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/ownership.ts +2 -2
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/run.ts +6 -6
- package/src/identity/hub/{utils.ts → shared/utils.ts} +5 -5
- package/src/identity/hub/{flows/token-transfer/IdentityHubTokenTransferFlow.tsx → transfer/TokenTransferFlow.tsx} +8 -8
- package/src/identity/hub/{flows/token-transfer → transfer}/TokenTransferScreens.tsx +14 -14
- package/src/identity/hub/{effects/token-transfer/runTokenTransfer.ts → transfer/effects.ts} +16 -16
- package/src/identity/hub/{effects/token-transfer → transfer}/progress.ts +1 -1
- package/src/identity/hub/useIdentityHubController.ts +11 -11
- package/src/identity/hub/useIdentityHubSideEffects.ts +11 -11
- package/src/models/ModelPicker.tsx +143 -9
- package/src/models/catalog.ts +2 -1
- package/src/models/huggingface.ts +180 -2
- package/src/models/llamacpp.ts +110 -15
- package/src/models/llamacppPreflight.ts +30 -11
- package/src/models/modelPickerOptions.ts +16 -15
- package/src/models/providerDisplay.ts +16 -0
- package/src/providers/anthropic.ts +36 -5
- package/src/providers/contracts.ts +9 -1
- package/src/providers/errors.ts +6 -4
- package/src/providers/gemini.ts +29 -3
- package/src/providers/openai-chat.ts +83 -3
- package/src/providers/openai-responses-format.ts +29 -8
- package/src/providers/openai-responses.ts +22 -7
- package/src/providers/registry.ts +1 -0
- package/src/runtime/sessionMode.ts +1 -1
- package/src/runtime/systemPrompt.ts +3 -1
- package/src/runtime/toolExecution.ts +9 -6
- package/src/runtime/turn.ts +29 -0
- package/src/storage/config.ts +1 -0
- package/src/storage/rewind.ts +20 -0
- package/src/storage/sessions.ts +16 -3
- package/src/tools/bashSafety.ts +7 -3
- package/src/tools/bashTool.ts +1 -1
- package/src/tools/contracts.ts +3 -0
- package/src/tools/deleteFileTool.ts +8 -3
- package/src/tools/editTool.ts +10 -5
- package/src/tools/fileDiff.ts +261 -0
- package/src/tools/privateContinuityEditTool.ts +5 -1
- package/src/tools/writeFileTool.ts +8 -3
- package/src/ui/Spinner.tsx +39 -5
- package/src/ui/TextInput.tsx +2 -2
- package/src/ui/theme.ts +19 -0
- package/src/utils/clipboard.ts +10 -7
- package/src/utils/images.ts +140 -0
- package/src/utils/messages.ts +2 -0
- package/src/chat/RewindView.tsx +0 -386
- package/src/chat/toolResultDisplay.ts +0 -8
- package/src/identity/hub/effects/index.ts +0 -73
- package/src/identity/hub/effects/publicProfile/index.ts +0 -5
- package/src/identity/hub/effects/restore/restoreEffects.ts +0 -22
- package/src/identity/hub/effects/token-transfer/index.ts +0 -6
- /package/src/chat/{chatInputState.ts → input/chatInputState.ts} +0 -0
- /package/src/chat/{chatPaste.ts → input/chatPaste.ts} +0 -0
- /package/src/chat/{textCursor.ts → input/textCursor.ts} +0 -0
- /package/src/identity/hub/{model/continuity.ts → continuity/state.ts} +0 -0
- /package/src/identity/hub/{model/custody.ts → custody/state.ts} +0 -0
- /package/src/identity/hub/{effects/restore → restore}/index.ts +0 -0
- /package/src/identity/hub/{model → shared/model}/format.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/types.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/index.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/useAgentReconciliation.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/walletSetup.ts +0 -0
- /package/src/identity/hub/{txGuard.ts → shared/txGuard.ts} +0 -0
- /package/src/identity/hub/{model/transfer.ts → transfer/state.ts} +0 -0
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
installLlamaCppRunner,
|
|
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 = {
|
|
@@ -113,6 +116,9 @@ type State =
|
|
|
113
116
|
| { kind: 'localRunnerPathEntry'; data: LoadedData; model: LocalHfModel; submitting: boolean; error?: string }
|
|
114
117
|
| { kind: 'localRunnerStarting'; data: LoadedData; model: LocalHfModel; startedAt: number }
|
|
115
118
|
| { kind: 'localRunnerStartFail'; data: LoadedData; model: LocalHfModel; result: Extract<LlamaCppStartResult, { ok: false }> }
|
|
119
|
+
| { kind: 'mmprojOffer'; data: LoadedData; model: LocalHfModel }
|
|
120
|
+
| { kind: 'mmprojDownloading'; data: LoadedData; model: LocalHfModel; progress: HfDownloadProgress }
|
|
121
|
+
| { kind: 'mmprojError'; data: LoadedData; model: LocalHfModel; message: string }
|
|
116
122
|
|
|
117
123
|
export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
118
124
|
currentConfig,
|
|
@@ -244,6 +250,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
244
250
|
const canDownload = plan.review.risk !== 'high' && plan.review.runtime === 'llama.cpp runnable'
|
|
245
251
|
const fit = state.data.machineSpec ? estimateGgufMachineFit(plan.sizeBytes, state.data.machineSpec) : null
|
|
246
252
|
const recommended = state.data.machineSpec ? recommendGgufFile(plan.repo, ggufFiles(plan.repo), state.data.machineSpec) : null
|
|
253
|
+
const mmproj = plan.mmprojCandidate
|
|
247
254
|
return (
|
|
248
255
|
<Surface
|
|
249
256
|
title="Review Model Link"
|
|
@@ -260,15 +267,22 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
260
267
|
<Text color={riskColor(plan.review.risk)}>safety: {safetyLabel(plan.review.risk)} · source: {credibilityLabel(plan.review.credibility)}</Text>
|
|
261
268
|
<Text color={theme.dim}>signals: {formatSignals(plan.repo.downloads, plan.repo.likes)}</Text>
|
|
262
269
|
<Text color={theme.dim}>notes: {friendlyReasons(plan.review.reasons).join('; ')}</Text>
|
|
270
|
+
{mmproj ? (
|
|
271
|
+
<Text color={theme.dim}>vision encoder available: {friendlyFileName(mmproj.filename)} (+{formatBytes(mmproj.sizeBytes)})</Text>
|
|
272
|
+
) : null}
|
|
263
273
|
</Box>
|
|
264
|
-
<Select<'download' | 'pick' | 'cancel'>
|
|
274
|
+
<Select<'download' | 'downloadWithMmproj' | 'pick' | 'cancel'>
|
|
265
275
|
options={[
|
|
266
|
-
{ value: '
|
|
276
|
+
...(mmproj ? [{ value: 'downloadWithMmproj' as const, label: `Download Model + Vision Encoder (+${formatBytes(mmproj.sizeBytes)}) · recommended`, disabled: !canDownload }] : []),
|
|
277
|
+
{ value: 'download', label: mmproj ? 'Download Without Vision Encoder' : 'Download This Model', disabled: !canDownload },
|
|
267
278
|
{ value: 'pick', label: 'Pick Another File' },
|
|
268
279
|
{ value: 'cancel', label: 'Cancel' },
|
|
269
280
|
]}
|
|
270
281
|
onSubmit={choice => {
|
|
271
282
|
if (choice === 'download') void startHfDownload(state, setState, hfAbortRef, onPick)
|
|
283
|
+
else if (choice === 'downloadWithMmproj') {
|
|
284
|
+
void startHfDownload({ ...state, plan: { ...plan, includeMmproj: true } }, setState, hfAbortRef, onPick)
|
|
285
|
+
}
|
|
272
286
|
else if (choice === 'pick') void inspectHfInput({ kind: 'hfInput', data: state.data }, plan.repoId, setState)
|
|
273
287
|
else setState({ kind: 'list', data: state.data })
|
|
274
288
|
}}
|
|
@@ -291,6 +305,68 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
291
305
|
)
|
|
292
306
|
}
|
|
293
307
|
|
|
308
|
+
if (state.kind === 'mmprojOffer') {
|
|
309
|
+
const sizeLabel = state.model.mmprojSizeBytes ? `+${formatBytes(state.model.mmprojSizeBytes)}` : 'additional download'
|
|
310
|
+
return (
|
|
311
|
+
<Surface
|
|
312
|
+
title="Add Image Support?"
|
|
313
|
+
subtitle={`${state.model.displayName} has a vision encoder available in its Hugging Face repo.`}
|
|
314
|
+
footer="enter select · esc back"
|
|
315
|
+
>
|
|
316
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
317
|
+
<Text color={theme.dim}>Loading the vision encoder lets this model accept pasted images.</Text>
|
|
318
|
+
<Text color={theme.dim}>Without it, image paste is declined at submit time.</Text>
|
|
319
|
+
</Box>
|
|
320
|
+
<Select<'add' | 'skip' | 'cancel'>
|
|
321
|
+
options={[
|
|
322
|
+
{ value: 'add', label: `Add Vision Encoder (${sizeLabel}) And Use` },
|
|
323
|
+
{ value: 'skip', label: 'Use Without Image Support' },
|
|
324
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
325
|
+
]}
|
|
326
|
+
onSubmit={choice => {
|
|
327
|
+
if (choice === 'add') void downloadMmprojAndContinue(state, setState, onPick)
|
|
328
|
+
else if (choice === 'skip') void startAndPickHfModel({ ...state.model, mmprojAvailable: false }, state, setState, onPick)
|
|
329
|
+
else setState({ kind: 'list', data: state.data })
|
|
330
|
+
}}
|
|
331
|
+
onCancel={() => setState({ kind: 'list', data: state.data })}
|
|
332
|
+
/>
|
|
333
|
+
</Surface>
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (state.kind === 'mmprojDownloading') {
|
|
338
|
+
const total = state.progress.total ?? state.model.mmprojSizeBytes ?? 0
|
|
339
|
+
const completed = state.progress.completed ?? 0
|
|
340
|
+
const progress = total > 0 ? completed / total : 0
|
|
341
|
+
const suffix = total > 0 ? `${formatBytes(completed)} / ${formatBytes(total)}` : formatBytes(completed)
|
|
342
|
+
return (
|
|
343
|
+
<Surface title="Downloading Vision Encoder" subtitle={state.model.displayName}>
|
|
344
|
+
<Text color={theme.dim}>{state.progress.status}</Text>
|
|
345
|
+
<ProgressBar progress={progress} suffix={suffix} />
|
|
346
|
+
</Surface>
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (state.kind === 'mmprojError') {
|
|
351
|
+
return (
|
|
352
|
+
<Surface title="Vision Encoder Download Failed" subtitle={state.message} tone="error" footer="enter select · esc back">
|
|
353
|
+
<Select<'retry' | 'skip' | 'back'>
|
|
354
|
+
options={[
|
|
355
|
+
{ value: 'retry', label: 'Retry Download' },
|
|
356
|
+
{ value: 'skip', label: 'Use Without Image Support' },
|
|
357
|
+
{ value: 'back', label: 'Back To Picker' },
|
|
358
|
+
]}
|
|
359
|
+
onSubmit={choice => {
|
|
360
|
+
if (choice === 'retry') setState({ kind: 'mmprojOffer', data: state.data, model: state.model })
|
|
361
|
+
else if (choice === 'skip') void startAndPickHfModel({ ...state.model, mmprojAvailable: false }, state, setState, onPick)
|
|
362
|
+
else setState({ kind: 'list', data: state.data })
|
|
363
|
+
}}
|
|
364
|
+
onCancel={() => setState({ kind: 'list', data: state.data })}
|
|
365
|
+
/>
|
|
366
|
+
</Surface>
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
294
370
|
if (state.kind === 'hfDone') {
|
|
295
371
|
return (
|
|
296
372
|
<Surface
|
|
@@ -612,9 +688,11 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
612
688
|
) : (
|
|
613
689
|
<Select
|
|
614
690
|
options={[
|
|
615
|
-
{ value: '
|
|
616
|
-
{ value: '
|
|
617
|
-
{ value: '
|
|
691
|
+
{ value: 'hdr:account', label: 'Account', disabled: true, role: 'section', bold: true },
|
|
692
|
+
{ value: 'signin', label: 'Sign in Again', indent: 2 },
|
|
693
|
+
{ value: 'signout', label: 'Sign Out', indent: 2 },
|
|
694
|
+
{ value: 'hdr:nav', label: 'Navigation', disabled: true, role: 'section', bold: true },
|
|
695
|
+
{ value: 'cancel', label: 'Back', indent: 2 },
|
|
618
696
|
]}
|
|
619
697
|
onSubmit={(value) => {
|
|
620
698
|
if (value === 'signin') {
|
|
@@ -832,6 +910,18 @@ function handleSubmit(
|
|
|
832
910
|
})()
|
|
833
911
|
return
|
|
834
912
|
}
|
|
913
|
+
if (value.startsWith('hfmmproj:') && state.kind === 'list') {
|
|
914
|
+
const id = value.slice('hfmmproj:'.length)
|
|
915
|
+
void (async () => {
|
|
916
|
+
const local = await findLocalHfModel(id)
|
|
917
|
+
if (!local) {
|
|
918
|
+
setState({ kind: 'hfError', data: state.data, message: 'local model metadata was not found' })
|
|
919
|
+
return
|
|
920
|
+
}
|
|
921
|
+
setState({ kind: 'mmprojOffer', data: state.data, model: local })
|
|
922
|
+
})()
|
|
923
|
+
return
|
|
924
|
+
}
|
|
835
925
|
if (value.startsWith('uc:') && state.kind === 'localCatalog') {
|
|
836
926
|
const entry = state.catalog.find(item => catalogOptionValue(item.repo.repoId, item.file.filename) === value)
|
|
837
927
|
if (entry) void reviewCatalogModel(state, entry, setState)
|
|
@@ -1248,6 +1338,8 @@ function localRunnerStartFailureSubtitle(result: Extract<LlamaCppStartResult, {
|
|
|
1248
1338
|
return result.message
|
|
1249
1339
|
case 'runner-not-installed':
|
|
1250
1340
|
return 'this machine still needs a local runner'
|
|
1341
|
+
case 'untracked-server':
|
|
1342
|
+
return result.message
|
|
1251
1343
|
}
|
|
1252
1344
|
}
|
|
1253
1345
|
|
|
@@ -1443,6 +1535,39 @@ async function uninstallLocalModel(
|
|
|
1443
1535
|
}
|
|
1444
1536
|
}
|
|
1445
1537
|
|
|
1538
|
+
async function downloadMmprojAndContinue(
|
|
1539
|
+
state: Extract<State, { kind: 'mmprojOffer' }>,
|
|
1540
|
+
setState: (s: State) => void,
|
|
1541
|
+
onPick: (sel: ModelPickerSelection) => void,
|
|
1542
|
+
): Promise<void> {
|
|
1543
|
+
setState({ kind: 'mmprojDownloading', data: state.data, model: state.model, progress: { status: 'starting' } })
|
|
1544
|
+
try {
|
|
1545
|
+
for await (const progress of addMmprojToInstalledModel(state.model.id)) {
|
|
1546
|
+
setState({ kind: 'mmprojDownloading', data: state.data, model: state.model, progress })
|
|
1547
|
+
}
|
|
1548
|
+
} catch (err: unknown) {
|
|
1549
|
+
setState({ kind: 'mmprojError', data: state.data, model: state.model, message: (err as Error).message })
|
|
1550
|
+
return
|
|
1551
|
+
}
|
|
1552
|
+
const updated = await findLocalHfModel(state.model.id)
|
|
1553
|
+
if (!updated || !updated.mmprojPath) {
|
|
1554
|
+
setState({ kind: 'mmprojError', data: state.data, model: state.model, message: 'projector downloaded but path was not persisted' })
|
|
1555
|
+
return
|
|
1556
|
+
}
|
|
1557
|
+
const stopResult = await stopLlamaCppServer().catch(() => null)
|
|
1558
|
+
if (stopResult && stopResult.ok && stopResult.reason === 'untracked-server') {
|
|
1559
|
+
setState({
|
|
1560
|
+
kind: 'mmprojError',
|
|
1561
|
+
data: state.data,
|
|
1562
|
+
model: updated,
|
|
1563
|
+
message: 'Vision encoder downloaded, but a llama-server is already running and ethagent did not launch it. Quit ethagent, stop the external llama-server (taskkill /F /IM llama-server.exe on Windows, pkill llama-server on macOS or Linux), then reopen ethagent to load the projector.',
|
|
1564
|
+
})
|
|
1565
|
+
return
|
|
1566
|
+
}
|
|
1567
|
+
const data = { ...state.data, hfModels: await loadHfPickerModels() }
|
|
1568
|
+
await startAndPickHfModel(updated, { kind: 'mmprojOffer', data, model: updated }, setState, onPick)
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1446
1571
|
async function refreshLocalModelData(data: LoadedData): Promise<LoadedData> {
|
|
1447
1572
|
const hfModels = await loadHfPickerModels()
|
|
1448
1573
|
return {
|
|
@@ -1453,7 +1578,7 @@ async function refreshLocalModelData(data: LoadedData): Promise<LoadedData> {
|
|
|
1453
1578
|
|
|
1454
1579
|
async function startAndPickHfModel(
|
|
1455
1580
|
model: LocalHfModel,
|
|
1456
|
-
state: Extract<State, { kind: 'list' | 'localCatalog' | 'hfDone' }>,
|
|
1581
|
+
state: Extract<State, { kind: 'list' | 'localCatalog' | 'hfDone' | 'mmprojOffer' | 'mmprojError' }>,
|
|
1457
1582
|
setState: (s: State) => void,
|
|
1458
1583
|
onPick: (sel: ModelPickerSelection) => void,
|
|
1459
1584
|
): Promise<void> {
|
|
@@ -1461,10 +1586,15 @@ async function startAndPickHfModel(
|
|
|
1461
1586
|
setState({ kind: 'hfError', data: state.data, message: 'blocked high-risk model; choose a model from a more credible source' })
|
|
1462
1587
|
return
|
|
1463
1588
|
}
|
|
1589
|
+
if (model.mmprojAvailable && !model.mmprojPath && state.kind !== 'mmprojOffer' && state.kind !== 'mmprojError') {
|
|
1590
|
+
setState({ kind: 'mmprojOffer', data: state.data, model })
|
|
1591
|
+
return
|
|
1592
|
+
}
|
|
1464
1593
|
setState({ kind: 'localRunnerStarting', data: state.data, model, startedAt: Date.now() })
|
|
1465
1594
|
const result = await startLlamaCppServer({
|
|
1466
1595
|
modelPath: model.localPath,
|
|
1467
1596
|
modelAlias: model.id,
|
|
1597
|
+
mmprojPath: model.mmprojPath,
|
|
1468
1598
|
})
|
|
1469
1599
|
const llamaCpp = await probeLlamaCpp()
|
|
1470
1600
|
const data = { ...state.data, llamaCpp }
|
|
@@ -1476,7 +1606,7 @@ async function startAndPickHfModel(
|
|
|
1476
1606
|
setState({ kind: 'localRunnerStartFail', data, model, result })
|
|
1477
1607
|
return
|
|
1478
1608
|
}
|
|
1479
|
-
onPick({ kind: 'llamacpp', model: model.id })
|
|
1609
|
+
onPick({ kind: 'llamacpp', model: model.id, mmprojPath: model.mmprojPath })
|
|
1480
1610
|
}
|
|
1481
1611
|
|
|
1482
1612
|
async function installRunnerAndStart(
|
|
@@ -1574,7 +1704,8 @@ function formatContextWindow(tokens: number): string {
|
|
|
1574
1704
|
|
|
1575
1705
|
async function loadHfPickerModels(): Promise<ModelPickerOptionsData['hfModels']> {
|
|
1576
1706
|
const installed = await loadLocalHfModels()
|
|
1577
|
-
|
|
1707
|
+
const backfilled = await backfillMmprojForModels(installed)
|
|
1708
|
+
return backfilled.map(model => ({
|
|
1578
1709
|
id: model.id,
|
|
1579
1710
|
displayName: model.displayName,
|
|
1580
1711
|
sizeBytes: model.sizeBytes,
|
|
@@ -1582,6 +1713,9 @@ async function loadHfPickerModels(): Promise<ModelPickerOptionsData['hfModels']>
|
|
|
1582
1713
|
risk: model.risk,
|
|
1583
1714
|
task: model.task,
|
|
1584
1715
|
status: model.status,
|
|
1716
|
+
mmprojPath: model.mmprojPath,
|
|
1717
|
+
mmprojAvailable: model.mmprojAvailable,
|
|
1718
|
+
mmprojSizeBytes: model.mmprojSizeBytes,
|
|
1585
1719
|
}))
|
|
1586
1720
|
}
|
|
1587
1721
|
|
package/src/models/catalog.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { defaultModelFor, type EthagentConfig, type ProviderId } from '../storag
|
|
|
2
2
|
import { getKey } from '../storage/secrets.js'
|
|
3
3
|
import { loadLocalHfModels } from './huggingface.js'
|
|
4
4
|
import { hasOpenAIOAuthCredentials } from '../auth/openaiOAuth/credentials.js'
|
|
5
|
+
import { providerDisplayName } from './providerDisplay.js'
|
|
5
6
|
|
|
6
7
|
const OPENAI_OAUTH_MODEL_IDS = ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.2'] as const
|
|
7
8
|
|
|
@@ -85,7 +86,7 @@ export async function discoverProviderModels(
|
|
|
85
86
|
if (provider === 'openai' && await hasOpenAIOAuthCredentials()) {
|
|
86
87
|
return openAIOAuthCatalog()
|
|
87
88
|
}
|
|
88
|
-
return fallbackResult(config, `missing ${provider} API key`)
|
|
89
|
+
return fallbackResult(config, `missing ${providerDisplayName(provider)} API key`)
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
const baseUrl = provider === 'openai' ? openAIBaseUrlFor(config) : ''
|
|
@@ -54,6 +54,12 @@ export type HfSafetyReview = {
|
|
|
54
54
|
reasons: string[]
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export type HfMmprojCandidate = {
|
|
58
|
+
filename: string
|
|
59
|
+
sizeBytes: number
|
|
60
|
+
localPath: string
|
|
61
|
+
}
|
|
62
|
+
|
|
57
63
|
export type HfDownloadPlan = {
|
|
58
64
|
repo: HuggingFaceRepoInfo
|
|
59
65
|
repoId: string
|
|
@@ -64,6 +70,8 @@ export type HfDownloadPlan = {
|
|
|
64
70
|
localPath: string
|
|
65
71
|
displayName: string
|
|
66
72
|
review: HfSafetyReview
|
|
73
|
+
mmprojCandidate?: HfMmprojCandidate
|
|
74
|
+
includeMmproj?: boolean
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
export type LocalHfModel = {
|
|
@@ -90,6 +98,9 @@ export type LocalHfModel = {
|
|
|
90
98
|
installedAt: string
|
|
91
99
|
status: LocalHfStatus
|
|
92
100
|
sha256?: string
|
|
101
|
+
mmprojPath?: string
|
|
102
|
+
mmprojAvailable?: boolean
|
|
103
|
+
mmprojSizeBytes?: number
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
export type HfDownloadProgress = {
|
|
@@ -291,6 +302,14 @@ export function ggufFiles(repo: HuggingFaceRepoInfo): HuggingFaceSibling[] {
|
|
|
291
302
|
.sort((a, b) => a.filename.localeCompare(b.filename))
|
|
292
303
|
}
|
|
293
304
|
|
|
305
|
+
export function isMmprojFilename(filename: string): boolean {
|
|
306
|
+
return filename.toLowerCase().startsWith('mmproj-') && filename.toLowerCase().endsWith('.gguf')
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function findMmprojSibling(repo: HuggingFaceRepoInfo): HuggingFaceSibling | undefined {
|
|
310
|
+
return repo.siblings.find(file => isMmprojFilename(file.filename))
|
|
311
|
+
}
|
|
312
|
+
|
|
294
313
|
export async function createHfDownloadPlan(
|
|
295
314
|
input: string,
|
|
296
315
|
filename?: string,
|
|
@@ -320,6 +339,14 @@ export async function createHfDownloadPlan(
|
|
|
320
339
|
requestedRevision,
|
|
321
340
|
resolvedRevision,
|
|
322
341
|
})
|
|
342
|
+
const mmprojSibling = findMmprojSibling(repo)
|
|
343
|
+
const mmprojCandidate: HfMmprojCandidate | undefined = mmprojSibling
|
|
344
|
+
? {
|
|
345
|
+
filename: mmprojSibling.filename,
|
|
346
|
+
sizeBytes: mmprojSibling.sizeBytes ?? 0,
|
|
347
|
+
localPath: localPathFor(repo.repoId, resolvedRevision, mmprojSibling.filename),
|
|
348
|
+
}
|
|
349
|
+
: undefined
|
|
323
350
|
return {
|
|
324
351
|
repo,
|
|
325
352
|
repoId: repo.repoId,
|
|
@@ -330,6 +357,7 @@ export async function createHfDownloadPlan(
|
|
|
330
357
|
localPath: localPathFor(repo.repoId, resolvedRevision, selected.filename),
|
|
331
358
|
displayName: displayNameFor(repo.repoId, selected.filename),
|
|
332
359
|
review,
|
|
360
|
+
mmprojCandidate,
|
|
333
361
|
}
|
|
334
362
|
}
|
|
335
363
|
|
|
@@ -432,10 +460,151 @@ export async function* downloadHfModel(
|
|
|
432
460
|
}
|
|
433
461
|
|
|
434
462
|
await fs.rename(partialPath, plan.localPath)
|
|
435
|
-
|
|
463
|
+
|
|
464
|
+
let mmprojPath: string | undefined
|
|
465
|
+
if (plan.includeMmproj && plan.mmprojCandidate) {
|
|
466
|
+
yield* downloadMmprojFile(plan.repoId, plan.resolvedRevision, plan.mmprojCandidate, signal, fetchImpl)
|
|
467
|
+
mmprojPath = plan.mmprojCandidate.localPath
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
await upsertLocalHfModel(modelFromPlan(plan, hash.digest('hex'), 'ready', mmprojPath))
|
|
436
471
|
yield { status: 'success', completed, total: Number.isFinite(total) ? total : completed }
|
|
437
472
|
}
|
|
438
473
|
|
|
474
|
+
async function* downloadMmprojFile(
|
|
475
|
+
repoId: string,
|
|
476
|
+
resolvedRevision: string,
|
|
477
|
+
candidate: HfMmprojCandidate,
|
|
478
|
+
signal: AbortSignal | undefined,
|
|
479
|
+
fetchImpl: FetchImpl,
|
|
480
|
+
): AsyncIterable<HfDownloadProgress> {
|
|
481
|
+
await fs.mkdir(path.dirname(candidate.localPath), { recursive: true })
|
|
482
|
+
const partialPath = `${candidate.localPath}.partial`
|
|
483
|
+
const response = await fetchImpl(resolveUrl(repoId, resolvedRevision, candidate.filename), { signal })
|
|
484
|
+
if (!response.ok || !response.body) {
|
|
485
|
+
throw new Error(response.ok ? 'empty projector download body' : `projector download HTTP ${response.status}`)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const total = Number.parseInt(response.headers.get('content-length') ?? '', 10)
|
|
489
|
+
const handle = await fs.open(partialPath, 'w')
|
|
490
|
+
let completed = 0
|
|
491
|
+
let complete = false
|
|
492
|
+
let lastProgressAt = Date.now()
|
|
493
|
+
let lastProgressBytes = 0
|
|
494
|
+
yield { status: 'downloading-mmproj', completed, total: Number.isFinite(total) ? total : undefined }
|
|
495
|
+
try {
|
|
496
|
+
const reader = response.body.getReader()
|
|
497
|
+
while (true) {
|
|
498
|
+
const { done, value } = await reader.read()
|
|
499
|
+
if (done) break
|
|
500
|
+
if (signal?.aborted) throw new Error('Cancelled')
|
|
501
|
+
const buffer = Buffer.from(value)
|
|
502
|
+
await handle.write(buffer)
|
|
503
|
+
completed += buffer.byteLength
|
|
504
|
+
const now = Date.now()
|
|
505
|
+
if (shouldReportDownloadProgress(completed, lastProgressBytes, now, lastProgressAt)) {
|
|
506
|
+
lastProgressAt = now
|
|
507
|
+
lastProgressBytes = completed
|
|
508
|
+
yield { status: 'downloading-mmproj', completed, total: Number.isFinite(total) ? total : undefined }
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
complete = true
|
|
512
|
+
} finally {
|
|
513
|
+
await handle.close()
|
|
514
|
+
if (!complete) {
|
|
515
|
+
await fs.unlink(partialPath).catch(() => {})
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
await fs.rename(partialPath, candidate.localPath)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export async function backfillMmprojAvailability(
|
|
523
|
+
model: LocalHfModel,
|
|
524
|
+
fetchImpl: FetchImpl = fetch,
|
|
525
|
+
): Promise<LocalHfModel> {
|
|
526
|
+
if (model.mmprojAvailable !== undefined) return model
|
|
527
|
+
try {
|
|
528
|
+
const repo = await fetchHuggingFaceRepoInfo({ repoId: model.repoId }, fetchImpl)
|
|
529
|
+
const sibling = findMmprojSibling(repo)
|
|
530
|
+
const next: LocalHfModel = {
|
|
531
|
+
...model,
|
|
532
|
+
mmprojAvailable: Boolean(sibling),
|
|
533
|
+
mmprojSizeBytes: sibling?.sizeBytes,
|
|
534
|
+
}
|
|
535
|
+
await upsertLocalHfModel(next)
|
|
536
|
+
return next
|
|
537
|
+
} catch {
|
|
538
|
+
return model
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export async function backfillMmprojForModels(
|
|
543
|
+
models: LocalHfModel[],
|
|
544
|
+
fetchImpl: FetchImpl = fetch,
|
|
545
|
+
): Promise<LocalHfModel[]> {
|
|
546
|
+
const repoIdToProbe = new Map<string, Promise<HuggingFaceRepoInfo | null>>()
|
|
547
|
+
for (const model of models) {
|
|
548
|
+
if (model.mmprojAvailable !== undefined) continue
|
|
549
|
+
if (repoIdToProbe.has(model.repoId)) continue
|
|
550
|
+
repoIdToProbe.set(
|
|
551
|
+
model.repoId,
|
|
552
|
+
fetchHuggingFaceRepoInfo({ repoId: model.repoId }, fetchImpl).catch(() => null),
|
|
553
|
+
)
|
|
554
|
+
}
|
|
555
|
+
if (repoIdToProbe.size === 0) return models
|
|
556
|
+
const resolved = new Map<string, HuggingFaceRepoInfo | null>()
|
|
557
|
+
for (const [repoId, promise] of repoIdToProbe) {
|
|
558
|
+
resolved.set(repoId, await promise)
|
|
559
|
+
}
|
|
560
|
+
const out: LocalHfModel[] = []
|
|
561
|
+
for (const model of models) {
|
|
562
|
+
if (model.mmprojAvailable !== undefined) {
|
|
563
|
+
out.push(model)
|
|
564
|
+
continue
|
|
565
|
+
}
|
|
566
|
+
const repo = resolved.get(model.repoId)
|
|
567
|
+
if (!repo) {
|
|
568
|
+
out.push(model)
|
|
569
|
+
continue
|
|
570
|
+
}
|
|
571
|
+
const sibling = findMmprojSibling(repo)
|
|
572
|
+
const next: LocalHfModel = {
|
|
573
|
+
...model,
|
|
574
|
+
mmprojAvailable: Boolean(sibling),
|
|
575
|
+
mmprojSizeBytes: sibling?.sizeBytes,
|
|
576
|
+
}
|
|
577
|
+
await upsertLocalHfModel(next)
|
|
578
|
+
out.push(next)
|
|
579
|
+
}
|
|
580
|
+
return out
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export async function* addMmprojToInstalledModel(
|
|
584
|
+
modelId: string,
|
|
585
|
+
signal?: AbortSignal,
|
|
586
|
+
deps: { fetchImpl?: FetchImpl } = {},
|
|
587
|
+
): AsyncIterable<HfDownloadProgress> {
|
|
588
|
+
const fetchImpl = deps.fetchImpl ?? fetch
|
|
589
|
+
const existing = await findLocalHfModel(modelId)
|
|
590
|
+
if (!existing) throw new Error(`model not installed: ${modelId}`)
|
|
591
|
+
if (existing.mmprojPath) {
|
|
592
|
+
yield { status: 'success', completed: 0 }
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
const repo = await fetchHuggingFaceRepoInfo({ repoId: existing.repoId }, fetchImpl)
|
|
596
|
+
const sibling = findMmprojSibling(repo)
|
|
597
|
+
if (!sibling) throw new Error(`no vision encoder available for ${existing.repoId}`)
|
|
598
|
+
const candidate: HfMmprojCandidate = {
|
|
599
|
+
filename: sibling.filename,
|
|
600
|
+
sizeBytes: sibling.sizeBytes ?? 0,
|
|
601
|
+
localPath: localPathFor(existing.repoId, existing.resolvedRevision, sibling.filename),
|
|
602
|
+
}
|
|
603
|
+
yield* downloadMmprojFile(existing.repoId, existing.resolvedRevision, candidate, signal, fetchImpl)
|
|
604
|
+
await upsertLocalHfModel({ ...existing, mmprojPath: candidate.localPath })
|
|
605
|
+
yield { status: 'success', completed: candidate.sizeBytes }
|
|
606
|
+
}
|
|
607
|
+
|
|
439
608
|
export function shouldReportDownloadProgress(
|
|
440
609
|
completed: number,
|
|
441
610
|
lastCompleted: number,
|
|
@@ -446,7 +615,13 @@ export function shouldReportDownloadProgress(
|
|
|
446
615
|
|| completed - lastCompleted >= DOWNLOAD_PROGRESS_MIN_BYTES
|
|
447
616
|
}
|
|
448
617
|
|
|
449
|
-
export function modelFromPlan(
|
|
618
|
+
export function modelFromPlan(
|
|
619
|
+
plan: HfDownloadPlan,
|
|
620
|
+
sha256: string | undefined,
|
|
621
|
+
status: LocalHfStatus,
|
|
622
|
+
mmprojPath?: string,
|
|
623
|
+
): LocalHfModel {
|
|
624
|
+
const mmprojAvailable = Boolean(plan.mmprojCandidate)
|
|
450
625
|
const now = new Date().toISOString()
|
|
451
626
|
return {
|
|
452
627
|
id: localModelId(plan.repoId, plan.filename),
|
|
@@ -472,6 +647,9 @@ export function modelFromPlan(plan: HfDownloadPlan, sha256: string | undefined,
|
|
|
472
647
|
installedAt: now,
|
|
473
648
|
status,
|
|
474
649
|
sha256,
|
|
650
|
+
mmprojPath,
|
|
651
|
+
mmprojAvailable,
|
|
652
|
+
mmprojSizeBytes: plan.mmprojCandidate?.sizeBytes,
|
|
475
653
|
}
|
|
476
654
|
}
|
|
477
655
|
|