ethagent 3.3.3 → 4.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/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +35 -0
- package/LICENSE +1 -1
- package/README.md +64 -104
- package/commands/ethagent.md +40 -0
- package/package.json +16 -16
- package/src/app/keybindings/KeybindingProvider.tsx +1 -6
- package/src/app/keybindings/types.ts +1 -6
- package/src/cli/ResetConfirmView.tsx +54 -53
- package/src/cli/demo.ts +86 -0
- package/src/cli/hookIo.ts +45 -0
- package/src/cli/main.tsx +94 -123
- package/src/cli/memoryGuard.ts +49 -0
- package/src/cli/reset.ts +28 -70
- package/src/cli/sessionStart.ts +33 -0
- package/src/cli/status.ts +46 -0
- package/src/cli/sync.ts +167 -0
- package/src/cli/syncAdapters/claude-code.ts +86 -0
- package/src/cli/syncAdapters/codex.ts +66 -0
- package/src/cli/syncAdapters/index.ts +45 -0
- package/src/cli/syncAdapters/managedBlock.ts +175 -0
- package/src/cli/syncAdapters/shared.ts +63 -0
- package/src/identity/continuity/envelopeParse.ts +20 -1
- package/src/identity/continuity/publicSkills.ts +3 -1
- package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
- package/src/identity/continuity/skills/scaffold.ts +5 -2
- package/src/identity/continuity/snapshots.ts +12 -5
- package/src/identity/continuity/storage/defaults.ts +20 -19
- package/src/identity/continuity/storage/status.ts +1 -1
- package/src/identity/ens/ensLookup/constants.ts +1 -1
- package/src/identity/manager/IdentityManager.tsx +33 -0
- package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
- package/src/identity/{hub → manager}/Routes.tsx +48 -34
- package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
- package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
- package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
- package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
- package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
- package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
- package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
- package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
- package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
- package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
- package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
- package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
- package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
- package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
- package/src/identity/{hub → manager}/create/effects.ts +19 -10
- package/src/identity/manager/create/importScan.ts +122 -0
- package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
- package/src/identity/{hub → manager}/custody/actions.ts +1 -15
- package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
- package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
- package/src/identity/{hub → manager}/custody/types.ts +1 -2
- package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
- package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
- package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
- package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
- package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
- package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
- package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
- package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
- package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
- package/src/identity/{hub → manager}/profile/effects.ts +1 -3
- package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
- package/src/identity/{hub → manager}/profile/state.ts +1 -1
- package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
- package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
- package/src/identity/{hub → manager}/restore/apply.ts +1 -1
- package/src/identity/{hub → manager}/restore/auth.ts +1 -1
- package/src/identity/{hub → manager}/restore/discover.ts +1 -1
- package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
- package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
- package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
- package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
- package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
- package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
- package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
- package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
- package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
- package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
- package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
- package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
- package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
- package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
- package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
- package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
- package/src/identity/manager/shared/effects/types.ts +30 -0
- package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
- package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
- package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
- package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
- package/src/identity/{hub → manager}/shared/utils.ts +6 -10
- package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
- package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
- package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
- package/src/identity/{hub → manager}/types.ts +5 -6
- package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
- package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
- package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
- package/src/identity/registry/erc8004/discovery.ts +3 -17
- package/src/identity/registry/erc8004/utils.ts +1 -1
- package/src/identity/storage/ipfs.ts +21 -1
- package/src/identity/wallet/browserWallet/html.ts +10 -2
- package/src/identity/wallet/browserWallet/http.ts +18 -0
- package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
- package/src/identity/wallet/browserWallet/requests.ts +10 -28
- package/src/identity/wallet/browserWallet/session.ts +26 -33
- package/src/identity/wallet/browserWallet/validation.ts +14 -0
- package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
- package/src/identity/wallet/page/boot.ts +43 -0
- package/src/identity/wallet/page/config.ts +59 -0
- package/src/identity/wallet/page/constants.ts +12 -0
- package/src/identity/wallet/page/copy.ts +47 -68
- package/src/identity/wallet/page/css.ts +638 -0
- package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
- package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
- package/src/identity/wallet/page/markup.ts +44 -34
- package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
- package/src/identity/wallet/page/resize.ts +95 -0
- package/src/identity/wallet/page/state.ts +135 -8
- package/src/identity/wallet/page/timeline.ts +161 -0
- package/src/identity/wallet/page/view.ts +22 -302
- package/src/storage/config.ts +30 -80
- package/src/storage/reset.ts +31 -0
- package/src/storage/secrets.ts +1 -16
- package/src/ui/Select.tsx +27 -5
- package/src/ui/Spinner.tsx +16 -15
- package/src/ui/Surface.tsx +21 -17
- package/src/ui/TextArea.tsx +173 -0
- package/src/ui/TextInput.tsx +31 -133
- package/src/ui/theme.ts +22 -13
- package/src/utils/clipboard.ts +0 -140
- package/src/app/FirstRun.tsx +0 -577
- package/src/app/FirstRunTimeline.tsx +0 -51
- package/src/app/firstRunConfig.ts +0 -26
- package/src/app/hooks/useCancelRequest.ts +0 -22
- package/src/app/hooks/useDoublePress.ts +0 -46
- package/src/app/hooks/useExitOnCtrlC.ts +0 -36
- package/src/auth/openaiOAuth/credentials.ts +0 -47
- package/src/auth/openaiOAuth/crypto.ts +0 -23
- package/src/auth/openaiOAuth/index.ts +0 -238
- package/src/auth/openaiOAuth/landingPage.ts +0 -116
- package/src/auth/openaiOAuth/listener.ts +0 -151
- package/src/auth/openaiOAuth/refresh.ts +0 -70
- package/src/auth/openaiOAuth/shared.ts +0 -115
- package/src/chat/ChatBottomPane.tsx +0 -296
- package/src/chat/ChatScreen.tsx +0 -1685
- package/src/chat/ConversationStack.tsx +0 -56
- package/src/chat/MessageList.tsx +0 -638
- package/src/chat/SessionStatus.tsx +0 -53
- package/src/chat/chatEnvironment.ts +0 -16
- package/src/chat/chatScreenUtils.ts +0 -194
- package/src/chat/chatSessionState.ts +0 -146
- package/src/chat/chatTurnContext.ts +0 -50
- package/src/chat/chatTurnOrchestrator.ts +0 -603
- package/src/chat/chatTurnRows.ts +0 -64
- package/src/chat/commands.ts +0 -494
- package/src/chat/continuityEditReview.ts +0 -42
- package/src/chat/display/DiffView.tsx +0 -193
- package/src/chat/display/SyntaxText.tsx +0 -192
- package/src/chat/display/toolCallDisplay.ts +0 -103
- package/src/chat/display/toolResultDisplay.ts +0 -19
- package/src/chat/input/ChatInput.tsx +0 -625
- package/src/chat/input/chatInputHelpers.ts +0 -62
- package/src/chat/input/chatInputState.ts +0 -247
- package/src/chat/input/chatPaste.ts +0 -49
- package/src/chat/input/imageRefs.ts +0 -30
- package/src/chat/input/inputRendering.tsx +0 -93
- package/src/chat/input/textCursor.ts +0 -212
- package/src/chat/messageMarkdown.ts +0 -220
- package/src/chat/messageRows.ts +0 -43
- package/src/chat/planImplementation.ts +0 -62
- package/src/chat/slashCommandHandlers.ts +0 -122
- package/src/chat/slashCommandViews.ts +0 -120
- package/src/chat/transcript/TranscriptView.tsx +0 -184
- package/src/chat/transcript/transcriptViewport.ts +0 -295
- package/src/chat/views/ContextLimitView.tsx +0 -95
- package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
- package/src/chat/views/CopyPicker.tsx +0 -50
- package/src/chat/views/PermissionPrompt.tsx +0 -156
- package/src/chat/views/PermissionsView.tsx +0 -165
- package/src/chat/views/PlanApprovalView.tsx +0 -91
- package/src/chat/views/ResumeView.tsx +0 -273
- package/src/chat/views/RewindView.tsx +0 -412
- package/src/cli/preview.tsx +0 -14
- package/src/cli/updateNotice.ts +0 -54
- package/src/identity/continuity/privateEdit/apply.ts +0 -170
- package/src/identity/continuity/privateEdit/diff.ts +0 -6
- package/src/identity/continuity/privateEdit/files.ts +0 -23
- package/src/identity/continuity/privateEdit/types.ts +0 -28
- package/src/identity/continuity/privateEdit.ts +0 -46
- package/src/identity/hub/IdentityHub.tsx +0 -14
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
- package/src/identity/hub/ens/effects.ts +0 -218
- package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
- package/src/identity/hub/shared/effects/types.ts +0 -53
- package/src/identity/hub/shared/reconciliation/index.ts +0 -14
- package/src/identity/wallet/page/grainient.ts +0 -278
- package/src/identity/wallet/page/html.ts +0 -28
- package/src/identity/wallet/page/styles/base.ts +0 -259
- package/src/identity/wallet/page/styles/components.ts +0 -262
- package/src/identity/wallet/page/styles/index.ts +0 -5
- package/src/identity/wallet/page/styles/responsive.ts +0 -247
- package/src/identity/wallet/page.tsx +0 -38
- package/src/mcp/approvals.ts +0 -113
- package/src/mcp/config.ts +0 -235
- package/src/mcp/manager.ts +0 -482
- package/src/mcp/managerHelpers.ts +0 -70
- package/src/mcp/names.ts +0 -19
- package/src/mcp/output.ts +0 -96
- package/src/models/ModelPicker.tsx +0 -1009
- package/src/models/catalog.ts +0 -327
- package/src/models/huggingface.ts +0 -712
- package/src/models/huggingfaceStorage.ts +0 -136
- package/src/models/llamacpp.ts +0 -848
- package/src/models/llamacppCommands.ts +0 -44
- package/src/models/llamacppConfig.ts +0 -34
- package/src/models/llamacppDiscovery.ts +0 -176
- package/src/models/llamacppOutput.ts +0 -65
- package/src/models/llamacppPreflight.ts +0 -158
- package/src/models/modelDisplay.ts +0 -180
- package/src/models/modelPickerCatalogFlow.ts +0 -56
- package/src/models/modelPickerCredentials.ts +0 -166
- package/src/models/modelPickerData.ts +0 -41
- package/src/models/modelPickerDisplay.tsx +0 -132
- package/src/models/modelPickerHfFlow.ts +0 -192
- package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
- package/src/models/modelPickerOptions.ts +0 -457
- package/src/models/modelPickerTypes.ts +0 -69
- package/src/models/modelPickerUninstallFlow.ts +0 -48
- package/src/models/modelPickerViewHelpers.ts +0 -174
- package/src/models/modelRecommendation.ts +0 -139
- package/src/models/providerDisplay.ts +0 -16
- package/src/models/runtimeDetection.ts +0 -81
- package/src/models/uncensoredCatalog.ts +0 -86
- package/src/providers/anthropic.ts +0 -290
- package/src/providers/contracts.ts +0 -71
- package/src/providers/errors.ts +0 -80
- package/src/providers/gemini.ts +0 -391
- package/src/providers/openai-chat.ts +0 -474
- package/src/providers/openai-responses-format.ts +0 -177
- package/src/providers/openai-responses.ts +0 -306
- package/src/providers/openaiChatWire.ts +0 -124
- package/src/providers/registry.ts +0 -120
- package/src/providers/retry.ts +0 -58
- package/src/providers/sse.ts +0 -93
- package/src/runtime/compaction.ts +0 -395
- package/src/runtime/cwd.ts +0 -43
- package/src/runtime/providerTurn.ts +0 -38
- package/src/runtime/sessionMode.ts +0 -55
- package/src/runtime/systemPrompt.ts +0 -213
- package/src/runtime/textToolParser.ts +0 -161
- package/src/runtime/toolClaimGuards.ts +0 -143
- package/src/runtime/toolExecution.ts +0 -304
- package/src/runtime/toolIntent.ts +0 -143
- package/src/runtime/turn.ts +0 -369
- package/src/runtime/turnNudges.ts +0 -223
- package/src/runtime/turnTypes.ts +0 -86
- package/src/storage/factoryReset.ts +0 -127
- package/src/storage/history.ts +0 -58
- package/src/storage/permissions.ts +0 -76
- package/src/storage/rewind.ts +0 -266
- package/src/storage/sessionExport.ts +0 -49
- package/src/storage/sessions.ts +0 -495
- package/src/tools/bashSafety.ts +0 -186
- package/src/tools/bashTool.ts +0 -140
- package/src/tools/changeDirectoryTool.ts +0 -213
- package/src/tools/contracts.ts +0 -192
- package/src/tools/deleteFileTool.ts +0 -116
- package/src/tools/editTool.ts +0 -165
- package/src/tools/editUtils.ts +0 -170
- package/src/tools/fileDiff.ts +0 -261
- package/src/tools/listDirectoryTool.ts +0 -55
- package/src/tools/listSkillFilesTool.ts +0 -77
- package/src/tools/listSkillsTool.ts +0 -68
- package/src/tools/mcpResourceTools.ts +0 -95
- package/src/tools/permissionRules.ts +0 -85
- package/src/tools/privateContinuityEditTool.ts +0 -187
- package/src/tools/privateContinuityReadTool.ts +0 -106
- package/src/tools/readSkillTool.ts +0 -107
- package/src/tools/readTool.ts +0 -85
- package/src/tools/registry.ts +0 -103
- package/src/tools/writeFileTool.ts +0 -167
- package/src/ui/BrandSplash.tsx +0 -133
- package/src/ui/terminalTitle.ts +0 -30
- package/src/utils/images.ts +0 -140
- package/src/utils/markdownSegments.ts +0 -51
- package/src/utils/messages.ts +0 -37
- package/src/utils/withRetry.ts +0 -324
- /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
- /package/src/identity/{hub → manager}/custody/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
- /package/src/identity/{hub → manager}/ens/state.ts +0 -0
- /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
- /package/src/identity/{hub → manager}/ens/types.ts +0 -0
- /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
- /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
- /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
- /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
- /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
- /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
- /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
- /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
- /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
|
@@ -1,1009 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { Box, Text } from 'ink'
|
|
3
|
-
import { Select, type SelectOption } from '../ui/Select.js'
|
|
4
|
-
import { Spinner } from '../ui/Spinner.js'
|
|
5
|
-
import { TextInput } from '../ui/TextInput.js'
|
|
6
|
-
import { Surface } from '../ui/Surface.js'
|
|
7
|
-
import { ProgressBar } from '../ui/ProgressBar.js'
|
|
8
|
-
import { theme } from '../ui/theme.js'
|
|
9
|
-
import {
|
|
10
|
-
DEFAULT_LLAMA_HOST,
|
|
11
|
-
detectLlamaCpp,
|
|
12
|
-
killRogueLlamaProcesses,
|
|
13
|
-
stopLlamaCppServer,
|
|
14
|
-
type LlamaCppInstallResult,
|
|
15
|
-
type LlamaCppStartResult,
|
|
16
|
-
} from './llamacpp.js'
|
|
17
|
-
import { detectSpec, type SpecSnapshot } from './runtimeDetection.js'
|
|
18
|
-
import {
|
|
19
|
-
estimateGgufMachineFit,
|
|
20
|
-
orderGgufFilesForSpec,
|
|
21
|
-
recommendGgufFile,
|
|
22
|
-
type GgufMachineFit,
|
|
23
|
-
} from './modelRecommendation.js'
|
|
24
|
-
import { hasKey, rmKey, setKey } from '../storage/secrets.js'
|
|
25
|
-
import { OpenAIOAuthService } from '../auth/openaiOAuth/index.js'
|
|
26
|
-
import { hasOpenAIOAuthCredentials, rmOpenAIOAuthCredentials } from '../auth/openaiOAuth/credentials.js'
|
|
27
|
-
import { openExternalUrl } from '../utils/openExternal.js'
|
|
28
|
-
import { defaultModelFor, type EthagentConfig, type ProviderId } from '../storage/config.js'
|
|
29
|
-
import { clearModelCatalogCache, discoverProviderModels, isOpenAIOAuthAllowedModel, OPENAI_OAUTH_DEFAULT_MODEL, type ModelCatalogResult } from './catalog.js'
|
|
30
|
-
import {
|
|
31
|
-
createHfDownloadPlan,
|
|
32
|
-
findLocalHfModel,
|
|
33
|
-
ggufFiles,
|
|
34
|
-
loadLocalHfModels,
|
|
35
|
-
localModelId,
|
|
36
|
-
type HfCredibility,
|
|
37
|
-
type HfRisk,
|
|
38
|
-
type HuggingFaceRepoInfo,
|
|
39
|
-
type HuggingFaceSibling,
|
|
40
|
-
type LocalHfModel,
|
|
41
|
-
} from './huggingface.js'
|
|
42
|
-
import {
|
|
43
|
-
buildLocalModelCatalogOptions,
|
|
44
|
-
buildModelPickerOptions,
|
|
45
|
-
catalogOptionValue,
|
|
46
|
-
cloudProviderDisplayName,
|
|
47
|
-
LOCAL_MODEL_LINK_EXAMPLE,
|
|
48
|
-
LOCAL_MODEL_LINK_HINT,
|
|
49
|
-
MODEL_PICKER_CLOUD_PROVIDERS,
|
|
50
|
-
orderModelsForContextFit,
|
|
51
|
-
type CloudCredentialKind,
|
|
52
|
-
type CloudProviderId,
|
|
53
|
-
type ModelPickerContextFit,
|
|
54
|
-
type ModelPickerOptionsData,
|
|
55
|
-
} from './modelPickerOptions.js'
|
|
56
|
-
import { formatLocalHfModelDisplayName, formatModelDisplayName } from './modelDisplay.js'
|
|
57
|
-
import { fetchUncensoredGgufCatalog, type UncensoredCatalogEntry } from './uncensoredCatalog.js'
|
|
58
|
-
import type {
|
|
59
|
-
LoadedModelPickerData as LoadedData,
|
|
60
|
-
LocalUninstallTarget,
|
|
61
|
-
ModelPickerProps,
|
|
62
|
-
ModelPickerSelection,
|
|
63
|
-
ModelPickerState as State,
|
|
64
|
-
} from './modelPickerTypes.js'
|
|
65
|
-
import {
|
|
66
|
-
ElapsedSpinner,
|
|
67
|
-
contextFitLabel,
|
|
68
|
-
contextFitSubtitle,
|
|
69
|
-
credibilityLabel,
|
|
70
|
-
fitColor,
|
|
71
|
-
fitLabel,
|
|
72
|
-
formatBytes,
|
|
73
|
-
formatSignals,
|
|
74
|
-
friendlyFileName,
|
|
75
|
-
friendlyReasons,
|
|
76
|
-
isCloudProvider,
|
|
77
|
-
modelMetadataSubtext,
|
|
78
|
-
providerKeyPlaceholder,
|
|
79
|
-
riskColor,
|
|
80
|
-
runnerPathPlaceholder,
|
|
81
|
-
safetyLabel,
|
|
82
|
-
} from './modelPickerDisplay.js'
|
|
83
|
-
import {
|
|
84
|
-
buildCatalogOptions,
|
|
85
|
-
buildHfFileOptions,
|
|
86
|
-
buildRunnerRecoveryOptions,
|
|
87
|
-
configForProvider,
|
|
88
|
-
localModelOptionIndex,
|
|
89
|
-
localOrCloudOptionIndex,
|
|
90
|
-
parseCloudValue,
|
|
91
|
-
parseFullCatalogValue,
|
|
92
|
-
parseKeyValue,
|
|
93
|
-
pickFallbackSelection,
|
|
94
|
-
} from './modelPickerViewHelpers.js'
|
|
95
|
-
import { openLocalCatalog, reviewCatalogModel } from './modelPickerCatalogFlow.js'
|
|
96
|
-
import { loadHfPickerModels, probeLlamaCpp } from './modelPickerData.js'
|
|
97
|
-
import {
|
|
98
|
-
chooseInstalledHfModelForRepo,
|
|
99
|
-
downloadMmprojAndContinue,
|
|
100
|
-
findInstalledHfModelForInput,
|
|
101
|
-
inspectHfInput,
|
|
102
|
-
reviewHfFile,
|
|
103
|
-
startHfDownload,
|
|
104
|
-
} from './modelPickerHfFlow.js'
|
|
105
|
-
import {
|
|
106
|
-
installRunnerAndStart,
|
|
107
|
-
localRunnerStartFailureSubtitle,
|
|
108
|
-
runRunnerSetup,
|
|
109
|
-
saveRunnerPathAndStart,
|
|
110
|
-
startAndPickHfModel,
|
|
111
|
-
} from './modelPickerLocalRunnerFlow.js'
|
|
112
|
-
import {
|
|
113
|
-
isCurrentLocalUninstallTarget,
|
|
114
|
-
localUninstallBoundaryCopy,
|
|
115
|
-
localUninstallTargets,
|
|
116
|
-
uninstallLocalModel,
|
|
117
|
-
} from './modelPickerUninstallFlow.js'
|
|
118
|
-
import {
|
|
119
|
-
deleteKey,
|
|
120
|
-
signOutOAuth,
|
|
121
|
-
startOpenAIOAuthFlow,
|
|
122
|
-
submitKey,
|
|
123
|
-
} from './modelPickerCredentials.js'
|
|
124
|
-
|
|
125
|
-
export type { ModelPickerSelection } from './modelPickerTypes.js'
|
|
126
|
-
export { chooseInstalledHfModelForRepo } from './modelPickerHfFlow.js'
|
|
127
|
-
|
|
128
|
-
export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
129
|
-
currentConfig,
|
|
130
|
-
currentProvider,
|
|
131
|
-
currentModel,
|
|
132
|
-
contextFit,
|
|
133
|
-
featuredHfRepo,
|
|
134
|
-
localOnly = false,
|
|
135
|
-
onPick,
|
|
136
|
-
onCancel,
|
|
137
|
-
}) => {
|
|
138
|
-
const [state, setState] = useState<State>({ kind: 'loading' })
|
|
139
|
-
const hfAbortRef = useRef<AbortController | null>(null)
|
|
140
|
-
const oauthServiceRef = useRef<OpenAIOAuthService | null>(null)
|
|
141
|
-
const dismissToList = (data: LoadedData) => () => {
|
|
142
|
-
if (localOnly) {
|
|
143
|
-
onCancel()
|
|
144
|
-
} else {
|
|
145
|
-
setState({ kind: 'list', data })
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
let cancelled = false
|
|
151
|
-
void (async () => {
|
|
152
|
-
const [llamaCpp, hfModels, machineSpec, keyEntries, openaiOauth] = await Promise.all([
|
|
153
|
-
probeLlamaCpp(),
|
|
154
|
-
loadHfPickerModels(),
|
|
155
|
-
detectSpec(),
|
|
156
|
-
Promise.all(MODEL_PICKER_CLOUD_PROVIDERS.map(async p => [p, await hasKey(p)] as const)),
|
|
157
|
-
hasOpenAIOAuthCredentials(),
|
|
158
|
-
])
|
|
159
|
-
if (cancelled) return
|
|
160
|
-
const rawCloudKeys = Object.fromEntries(keyEntries) as Partial<Record<ProviderId, boolean>>
|
|
161
|
-
const cloudKeys: Partial<Record<ProviderId, boolean>> = {
|
|
162
|
-
...rawCloudKeys,
|
|
163
|
-
openai: rawCloudKeys.openai === true || openaiOauth,
|
|
164
|
-
}
|
|
165
|
-
const cloudCredentialKinds: Partial<Record<ProviderId, CloudCredentialKind>> = {}
|
|
166
|
-
if (openaiOauth) cloudCredentialKinds.openai = 'oauth'
|
|
167
|
-
else if (rawCloudKeys.openai === true) cloudCredentialKinds.openai = 'apikey'
|
|
168
|
-
if (rawCloudKeys.anthropic === true) cloudCredentialKinds.anthropic = 'apikey'
|
|
169
|
-
if (rawCloudKeys.gemini === true) cloudCredentialKinds.gemini = 'apikey'
|
|
170
|
-
const catalogEntries = await Promise.all(
|
|
171
|
-
MODEL_PICKER_CLOUD_PROVIDERS
|
|
172
|
-
.filter(provider => cloudKeys[provider])
|
|
173
|
-
.map(async provider => [provider, await discoverProviderModels(configForProvider(currentConfig, provider))] as const),
|
|
174
|
-
)
|
|
175
|
-
if (cancelled) return
|
|
176
|
-
const cloudCatalogs = Object.fromEntries(catalogEntries) as Partial<Record<ProviderId, ModelCatalogResult>>
|
|
177
|
-
const data: LoadedData = {
|
|
178
|
-
llamaCpp,
|
|
179
|
-
hfModels,
|
|
180
|
-
machineSpec,
|
|
181
|
-
cloudKeys,
|
|
182
|
-
cloudCatalogs,
|
|
183
|
-
cloudCredentialKinds,
|
|
184
|
-
}
|
|
185
|
-
if (featuredHfRepo) {
|
|
186
|
-
const installedFeatured = await findInstalledHfModelForInput(featuredHfRepo)
|
|
187
|
-
if (cancelled) return
|
|
188
|
-
if (installedFeatured) {
|
|
189
|
-
setState({ kind: 'hfDone', data, model: installedFeatured, alreadyInstalled: true })
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
setState({ kind: 'list', data })
|
|
194
|
-
if (featuredHfRepo) {
|
|
195
|
-
await inspectHfInput({ kind: 'hfInput', data }, featuredHfRepo, setState)
|
|
196
|
-
}
|
|
197
|
-
})()
|
|
198
|
-
return () => { cancelled = true }
|
|
199
|
-
}, [currentConfig, featuredHfRepo])
|
|
200
|
-
|
|
201
|
-
useEffect(() => () => {
|
|
202
|
-
hfAbortRef.current?.abort()
|
|
203
|
-
oauthServiceRef.current?.cleanup()
|
|
204
|
-
oauthServiceRef.current = null
|
|
205
|
-
}, [])
|
|
206
|
-
|
|
207
|
-
if (state.kind === 'loading') {
|
|
208
|
-
return (
|
|
209
|
-
<Surface
|
|
210
|
-
title={localOnly ? 'Local Model' : (contextFit ? 'Switch to Larger-Context Model' : 'Switch Provider · Model')}
|
|
211
|
-
subtitle="Loading providers and models."
|
|
212
|
-
footer="esc back"
|
|
213
|
-
>
|
|
214
|
-
<Spinner label={localOnly ? 'loading local models...' : 'loading providers...'} />
|
|
215
|
-
<Box marginTop={1}>
|
|
216
|
-
<Select<'cancel'>
|
|
217
|
-
options={[{ value: 'cancel', label: 'Back', hint: 'Return to the previous screen', role: 'utility' }]}
|
|
218
|
-
hintLayout="inline"
|
|
219
|
-
onSubmit={() => onCancel()}
|
|
220
|
-
onCancel={() => onCancel()}
|
|
221
|
-
/>
|
|
222
|
-
</Box>
|
|
223
|
-
</Surface>
|
|
224
|
-
)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (state.kind === 'hfInput') {
|
|
228
|
-
return (
|
|
229
|
-
<Surface
|
|
230
|
-
title="Add Local Model"
|
|
231
|
-
subtitle={LOCAL_MODEL_LINK_EXAMPLE}
|
|
232
|
-
footer="enter check link · esc back"
|
|
233
|
-
>
|
|
234
|
-
<TextInput
|
|
235
|
-
label="Model Link"
|
|
236
|
-
placeholder={LOCAL_MODEL_LINK_HINT}
|
|
237
|
-
onSubmit={value => void inspectHfInput(state, value, setState)}
|
|
238
|
-
onCancel={dismissToList(state.data)}
|
|
239
|
-
/>
|
|
240
|
-
{state.error ? <Text color={theme.accentError}>{state.error}</Text> : null}
|
|
241
|
-
</Surface>
|
|
242
|
-
)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (state.kind === 'hfLoading') {
|
|
246
|
-
return (
|
|
247
|
-
<Surface title="Checking Model Link" subtitle={state.input}>
|
|
248
|
-
<Spinner label="reading model page..." />
|
|
249
|
-
</Surface>
|
|
250
|
-
)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (state.kind === 'hfFilePick') {
|
|
254
|
-
const options = buildHfFileOptions(state.repo, state.files, state.data.machineSpec, state.data.hfModels.map(model => model.id))
|
|
255
|
-
const recommendedIndex = Math.max(0, options.findIndex(option => option.subtext?.includes('Recommended')))
|
|
256
|
-
return (
|
|
257
|
-
<Surface
|
|
258
|
-
title="Choose a Compatible File"
|
|
259
|
-
subtitle={`${state.repo.repoId} has ${state.files.length} compatible local model file${state.files.length === 1 ? '' : 's'}.`}
|
|
260
|
-
footer="enter select · esc back"
|
|
261
|
-
>
|
|
262
|
-
<Select
|
|
263
|
-
options={options}
|
|
264
|
-
initialIndex={recommendedIndex}
|
|
265
|
-
maxVisible={10}
|
|
266
|
-
onSubmit={filename => void reviewHfFile(state, filename, setState)}
|
|
267
|
-
onCancel={() => setState({ kind: 'hfInput', data: state.data })}
|
|
268
|
-
/>
|
|
269
|
-
</Surface>
|
|
270
|
-
)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (state.kind === 'hfReview') {
|
|
274
|
-
const { plan } = state
|
|
275
|
-
const canDownload = plan.review.risk !== 'high' && plan.review.runtime === 'llama.cpp runnable'
|
|
276
|
-
const fit = state.data.machineSpec ? estimateGgufMachineFit(plan.sizeBytes, state.data.machineSpec) : null
|
|
277
|
-
const recommended = state.data.machineSpec ? recommendGgufFile(plan.repo, ggufFiles(plan.repo), state.data.machineSpec) : null
|
|
278
|
-
const mmproj = plan.mmprojCandidate
|
|
279
|
-
return (
|
|
280
|
-
<Surface
|
|
281
|
-
title="Review Model Link"
|
|
282
|
-
subtitle="Only download models from creators you trust. Check the license and source before continuing."
|
|
283
|
-
footer="enter select · esc back"
|
|
284
|
-
tone={plan.review.risk === 'high' ? 'error' : plan.review.risk === 'medium' ? 'muted' : 'primary'}
|
|
285
|
-
>
|
|
286
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
287
|
-
<Text color={theme.text}>{plan.displayName}</Text>
|
|
288
|
-
<Text color={theme.dim}>source: huggingface.co/{plan.repoId}</Text>
|
|
289
|
-
<Text color={theme.dim}>file: {friendlyFileName(plan.filename)}</Text>
|
|
290
|
-
<Text color={theme.dim}>license: {plan.repo.license ?? 'unknown'} · size: {formatBytes(plan.sizeBytes)}</Text>
|
|
291
|
-
{fit ? <Text color={fitColor(fit.fit)}>fit: {fitLabel(fit.fit, recommended?.file.filename === plan.filename)}</Text> : null}
|
|
292
|
-
<Text color={riskColor(plan.review.risk)}>safety: {safetyLabel(plan.review.risk)} · source: {credibilityLabel(plan.review.credibility)}</Text>
|
|
293
|
-
<Text color={theme.dim}>signals: {formatSignals(plan.repo.downloads, plan.repo.likes)}</Text>
|
|
294
|
-
<Text color={theme.dim}>notes: {friendlyReasons(plan.review.reasons).join('; ')}</Text>
|
|
295
|
-
{mmproj ? (
|
|
296
|
-
<Text color={theme.dim}>vision encoder available: {friendlyFileName(mmproj.filename)} (+{formatBytes(mmproj.sizeBytes)})</Text>
|
|
297
|
-
) : null}
|
|
298
|
-
</Box>
|
|
299
|
-
<Select<'download' | 'downloadWithMmproj' | 'pick' | 'cancel'>
|
|
300
|
-
options={[
|
|
301
|
-
{ value: 'download', role: 'section', label: 'Download' },
|
|
302
|
-
...(mmproj ? [{ value: 'downloadWithMmproj' as const, label: `Download Model + Vision Encoder (+${formatBytes(mmproj.sizeBytes)}) · recommended`, disabled: !canDownload }] : []),
|
|
303
|
-
{ value: 'download', label: mmproj ? 'Download Without Vision Encoder' : 'Download This Model', disabled: !canDownload },
|
|
304
|
-
{ value: 'pick', role: 'section', label: 'Navigation' },
|
|
305
|
-
{ value: 'pick', label: 'Pick Another File' },
|
|
306
|
-
{ value: 'cancel', label: 'Cancel', role: 'utility' },
|
|
307
|
-
]}
|
|
308
|
-
onSubmit={choice => {
|
|
309
|
-
if (choice === 'download') void startHfDownload(state, setState, hfAbortRef, onPick)
|
|
310
|
-
else if (choice === 'downloadWithMmproj') {
|
|
311
|
-
void startHfDownload({ ...state, plan: { ...plan, includeMmproj: true } }, setState, hfAbortRef, onPick)
|
|
312
|
-
}
|
|
313
|
-
else if (choice === 'pick') void inspectHfInput({ kind: 'hfInput', data: state.data }, plan.repoId, setState)
|
|
314
|
-
else dismissToList(state.data)()
|
|
315
|
-
}}
|
|
316
|
-
onCancel={dismissToList(state.data)}
|
|
317
|
-
/>
|
|
318
|
-
</Surface>
|
|
319
|
-
)
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (state.kind === 'hfDownloading') {
|
|
323
|
-
const total = state.progress.total ?? state.plan.sizeBytes
|
|
324
|
-
const completed = state.progress.completed ?? 0
|
|
325
|
-
const progress = total > 0 ? completed / total : 0
|
|
326
|
-
const suffix = total > 0 ? `${formatBytes(completed)} / ${formatBytes(total)}` : formatBytes(completed)
|
|
327
|
-
return (
|
|
328
|
-
<Surface title="Downloading Model" subtitle={state.plan.displayName}>
|
|
329
|
-
<Text color={theme.dim}>{state.progress.status}</Text>
|
|
330
|
-
<ProgressBar progress={progress} suffix={suffix} />
|
|
331
|
-
</Surface>
|
|
332
|
-
)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (state.kind === 'mmprojOffer') {
|
|
336
|
-
const sizeLabel = state.model.mmprojSizeBytes ? `+${formatBytes(state.model.mmprojSizeBytes)}` : 'additional download'
|
|
337
|
-
return (
|
|
338
|
-
<Surface
|
|
339
|
-
title="Add Image Support?"
|
|
340
|
-
subtitle={`${state.model.displayName} has a vision encoder available in its Hugging Face repo.`}
|
|
341
|
-
footer="enter select · esc back"
|
|
342
|
-
>
|
|
343
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
344
|
-
<Text color={theme.dim}>Loading the vision encoder lets this model accept pasted images.</Text>
|
|
345
|
-
<Text color={theme.dim}>Without it, image paste is declined at submit time.</Text>
|
|
346
|
-
</Box>
|
|
347
|
-
<Select<'add' | 'skip' | 'cancel'>
|
|
348
|
-
options={[
|
|
349
|
-
{ value: 'add', label: `Add Vision Encoder (${sizeLabel}) And Use` },
|
|
350
|
-
{ value: 'skip', label: 'Use Without Image Support' },
|
|
351
|
-
{ value: 'cancel', label: 'Cancel' },
|
|
352
|
-
]}
|
|
353
|
-
onSubmit={choice => {
|
|
354
|
-
if (choice === 'add') void downloadMmprojAndContinue(state, setState, onPick)
|
|
355
|
-
else if (choice === 'skip') void startAndPickHfModel({ ...state.model, mmprojAvailable: false }, state, setState, onPick)
|
|
356
|
-
else dismissToList(state.data)()
|
|
357
|
-
}}
|
|
358
|
-
onCancel={dismissToList(state.data)}
|
|
359
|
-
/>
|
|
360
|
-
</Surface>
|
|
361
|
-
)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (state.kind === 'mmprojDownloading') {
|
|
365
|
-
const total = state.progress.total ?? state.model.mmprojSizeBytes ?? 0
|
|
366
|
-
const completed = state.progress.completed ?? 0
|
|
367
|
-
const progress = total > 0 ? completed / total : 0
|
|
368
|
-
const suffix = total > 0 ? `${formatBytes(completed)} / ${formatBytes(total)}` : formatBytes(completed)
|
|
369
|
-
return (
|
|
370
|
-
<Surface title="Downloading Vision Encoder" subtitle={state.model.displayName}>
|
|
371
|
-
<Text color={theme.dim}>{state.progress.status}</Text>
|
|
372
|
-
<ProgressBar progress={progress} suffix={suffix} />
|
|
373
|
-
</Surface>
|
|
374
|
-
)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (state.kind === 'mmprojError') {
|
|
378
|
-
return (
|
|
379
|
-
<Surface title="Vision Encoder Download Failed" subtitle={state.message} tone="error" footer="enter select · esc back">
|
|
380
|
-
<Select<'retry' | 'skip' | 'back'>
|
|
381
|
-
options={[
|
|
382
|
-
{ value: 'retry', label: 'Retry Download' },
|
|
383
|
-
{ value: 'skip', label: 'Use Without Image Support' },
|
|
384
|
-
{ value: 'back', label: 'Back To Picker' },
|
|
385
|
-
]}
|
|
386
|
-
onSubmit={choice => {
|
|
387
|
-
if (choice === 'retry') setState({ kind: 'mmprojOffer', data: state.data, model: state.model })
|
|
388
|
-
else if (choice === 'skip') void startAndPickHfModel({ ...state.model, mmprojAvailable: false }, state, setState, onPick)
|
|
389
|
-
else dismissToList(state.data)()
|
|
390
|
-
}}
|
|
391
|
-
onCancel={dismissToList(state.data)}
|
|
392
|
-
/>
|
|
393
|
-
</Surface>
|
|
394
|
-
)
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (state.kind === 'hfDone') {
|
|
398
|
-
return (
|
|
399
|
-
<Surface
|
|
400
|
-
title={state.alreadyInstalled ? 'Model Already Downloaded' : 'Model Ready'}
|
|
401
|
-
subtitle={state.model.displayName}
|
|
402
|
-
footer="enter select · esc back"
|
|
403
|
-
>
|
|
404
|
-
<Select<'use' | 'back'>
|
|
405
|
-
options={[
|
|
406
|
-
{ value: 'use', label: 'Use This Model Now' },
|
|
407
|
-
{ value: 'back', label: 'Back To Picker' },
|
|
408
|
-
]}
|
|
409
|
-
onSubmit={choice => {
|
|
410
|
-
if (choice === 'use') void startAndPickHfModel(state.model, state, setState, onPick)
|
|
411
|
-
else dismissToList(state.data)()
|
|
412
|
-
}}
|
|
413
|
-
onCancel={dismissToList(state.data)}
|
|
414
|
-
/>
|
|
415
|
-
</Surface>
|
|
416
|
-
)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (state.kind === 'hfError') {
|
|
420
|
-
return (
|
|
421
|
-
<Surface title="Model Link Failed" subtitle={state.message} tone="error" footer="enter select · esc back">
|
|
422
|
-
<Select<'retry' | 'back'>
|
|
423
|
-
options={[
|
|
424
|
-
{ value: 'retry', label: state.input ? 'Retry Link' : 'Download Another Model' },
|
|
425
|
-
{ value: 'back', label: 'Back To Picker' },
|
|
426
|
-
]}
|
|
427
|
-
onSubmit={choice => {
|
|
428
|
-
if (choice === 'retry') setState({ kind: 'hfInput', data: state.data, error: state.input ? undefined : state.message })
|
|
429
|
-
else dismissToList(state.data)()
|
|
430
|
-
}}
|
|
431
|
-
onCancel={dismissToList(state.data)}
|
|
432
|
-
/>
|
|
433
|
-
</Surface>
|
|
434
|
-
)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (state.kind === 'localUninstallPick') {
|
|
438
|
-
const targets = localUninstallTargets(state.data)
|
|
439
|
-
const options = targets.map(target => ({
|
|
440
|
-
value: `${target.kind}:${target.id}`,
|
|
441
|
-
label: target.displayName,
|
|
442
|
-
subtext: [
|
|
443
|
-
'downloaded GGUF file',
|
|
444
|
-
formatBytes(target.sizeBytes),
|
|
445
|
-
isCurrentLocalUninstallTarget(target, currentProvider, currentModel) ? 'currently selected' : '',
|
|
446
|
-
].filter(Boolean).join(' · '),
|
|
447
|
-
role: 'option' as const,
|
|
448
|
-
}))
|
|
449
|
-
return (
|
|
450
|
-
<Surface title="Uninstall Downloaded GGUF" subtitle="Choose a downloaded model file to remove." footer="enter select · esc back">
|
|
451
|
-
{options.length === 0 ? (
|
|
452
|
-
<Text color={theme.dim}>No local models found.</Text>
|
|
453
|
-
) : (
|
|
454
|
-
<Select
|
|
455
|
-
options={options}
|
|
456
|
-
maxVisible={10}
|
|
457
|
-
onSubmit={value => {
|
|
458
|
-
const target = targets.find(item => `${item.kind}:${item.id}` === value)
|
|
459
|
-
if (target) setState({ kind: 'localUninstallConfirm', data: state.data, target })
|
|
460
|
-
}}
|
|
461
|
-
onCancel={dismissToList(state.data)}
|
|
462
|
-
/>
|
|
463
|
-
)}
|
|
464
|
-
</Surface>
|
|
465
|
-
)
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (state.kind === 'localUninstallConfirm') {
|
|
469
|
-
const modelName = state.target.displayName
|
|
470
|
-
return (
|
|
471
|
-
<Surface title="Confirm Uninstall" subtitle={modelName} footer="enter select · esc back">
|
|
472
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
473
|
-
<Text color={theme.dim}>{localUninstallBoundaryCopy(state.target)}</Text>
|
|
474
|
-
<Text color={theme.dim}>Runner binaries are left unchanged.</Text>
|
|
475
|
-
</Box>
|
|
476
|
-
<Select<'confirm' | 'back'>
|
|
477
|
-
options={[
|
|
478
|
-
{ value: 'confirm', label: 'Uninstall Local Model' },
|
|
479
|
-
{ value: 'back', label: 'Back' },
|
|
480
|
-
]}
|
|
481
|
-
onSubmit={choice => {
|
|
482
|
-
if (choice === 'confirm') void uninstallLocalModel(state, setState)
|
|
483
|
-
else setState({ kind: 'localUninstallPick', data: state.data })
|
|
484
|
-
}}
|
|
485
|
-
onCancel={() => setState({ kind: 'localUninstallPick', data: state.data })}
|
|
486
|
-
/>
|
|
487
|
-
</Surface>
|
|
488
|
-
)
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (state.kind === 'localUninstalling') {
|
|
492
|
-
return (
|
|
493
|
-
<Surface
|
|
494
|
-
title="Uninstalling Local Model"
|
|
495
|
-
subtitle={state.target.displayName}
|
|
496
|
-
>
|
|
497
|
-
<Spinner label="removing local model..." />
|
|
498
|
-
</Surface>
|
|
499
|
-
)
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (state.kind === 'localUninstallDone') {
|
|
503
|
-
return (
|
|
504
|
-
<Surface title="Local Model Uninstalled" subtitle={state.modelName} footer="enter back to picker · esc close">
|
|
505
|
-
<Select<'back'>
|
|
506
|
-
options={[{ value: 'back', label: 'Back To Picker' }]}
|
|
507
|
-
onSubmit={dismissToList(state.data)}
|
|
508
|
-
onCancel={dismissToList(state.data)}
|
|
509
|
-
/>
|
|
510
|
-
</Surface>
|
|
511
|
-
)
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (state.kind === 'localUninstallError') {
|
|
515
|
-
return (
|
|
516
|
-
<Surface title="Could Not Uninstall Local Model" subtitle={state.message} tone="error" footer="enter select · esc back">
|
|
517
|
-
<Select<'retry' | 'back'>
|
|
518
|
-
options={[
|
|
519
|
-
{ value: 'retry', label: 'Try Again' },
|
|
520
|
-
{ value: 'back', label: 'Back To Picker' },
|
|
521
|
-
]}
|
|
522
|
-
onSubmit={choice => {
|
|
523
|
-
if (choice === 'retry') void uninstallLocalModel({ kind: 'localUninstallConfirm', data: state.data, target: state.target }, setState)
|
|
524
|
-
else dismissToList(state.data)()
|
|
525
|
-
}}
|
|
526
|
-
onCancel={dismissToList(state.data)}
|
|
527
|
-
/>
|
|
528
|
-
</Surface>
|
|
529
|
-
)
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (state.kind === 'localRunnerSetup') {
|
|
533
|
-
return (
|
|
534
|
-
<Surface
|
|
535
|
-
title="Install Local Runner"
|
|
536
|
-
subtitle="This model is downloaded. Install the local runner once to start it here."
|
|
537
|
-
footer="enter select · esc back"
|
|
538
|
-
>
|
|
539
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
540
|
-
<Text color={theme.dim}>Ethagent tried to start {friendlyFileName(state.model.filename)} automatically.</Text>
|
|
541
|
-
<Text color={theme.dim}>After this one-time install, downloaded local models start automatically.</Text>
|
|
542
|
-
<Text color={theme.dim}>Advanced: paste an existing llama-server path or run a compatible server at {DEFAULT_LLAMA_HOST}.</Text>
|
|
543
|
-
</Box>
|
|
544
|
-
<Select<'install' | 'path' | 'back' | 'download'>
|
|
545
|
-
options={[
|
|
546
|
-
{ value: 'install', label: 'Install Local Runner' },
|
|
547
|
-
{ value: 'path', label: 'Use Existing Runner Path' },
|
|
548
|
-
{ value: 'back', label: 'Back To Picker' },
|
|
549
|
-
{ value: 'download', label: 'Add Another Local Model' },
|
|
550
|
-
]}
|
|
551
|
-
onSubmit={choice => {
|
|
552
|
-
if (choice === 'download') setState({ kind: 'hfInput', data: state.data })
|
|
553
|
-
else if (choice === 'install') void installRunnerAndStart(state, setState, onPick)
|
|
554
|
-
else if (choice === 'path') setState({ kind: 'localRunnerPathEntry', data: state.data, model: state.model, submitting: false })
|
|
555
|
-
else dismissToList(state.data)()
|
|
556
|
-
}}
|
|
557
|
-
onCancel={dismissToList(state.data)}
|
|
558
|
-
/>
|
|
559
|
-
</Surface>
|
|
560
|
-
)
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (state.kind === 'localRunnerInstalling') {
|
|
564
|
-
return (
|
|
565
|
-
<Surface title="Installing Local Runner" subtitle="This may take a few minutes.">
|
|
566
|
-
<ElapsedSpinner startedAt={state.startedAt} label={state.progress.label} />
|
|
567
|
-
<ProgressBar progress={state.progress.progress} />
|
|
568
|
-
</Surface>
|
|
569
|
-
)
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
if (state.kind === 'localRunnerInstallFail') {
|
|
573
|
-
const options = buildRunnerRecoveryOptions(state.result)
|
|
574
|
-
return (
|
|
575
|
-
<Surface title="Runner Setup Needs Attention" subtitle={state.result.message} tone="error" footer="enter select · esc back">
|
|
576
|
-
<Select<'stop-and-retry' | 'path' | 'back'>
|
|
577
|
-
options={options}
|
|
578
|
-
hintLayout="inline"
|
|
579
|
-
onSubmit={choice => {
|
|
580
|
-
if (choice === 'stop-and-retry') {
|
|
581
|
-
void (async () => {
|
|
582
|
-
await killRogueLlamaProcesses()
|
|
583
|
-
await installRunnerAndStart({ kind: 'localRunnerSetup', data: state.data, model: state.model }, setState, onPick)
|
|
584
|
-
})()
|
|
585
|
-
}
|
|
586
|
-
else if (choice === 'path') setState({ kind: 'localRunnerPathEntry', data: state.data, model: state.model, submitting: false })
|
|
587
|
-
else dismissToList(state.data)()
|
|
588
|
-
}}
|
|
589
|
-
onCancel={dismissToList(state.data)}
|
|
590
|
-
/>
|
|
591
|
-
</Surface>
|
|
592
|
-
)
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (state.kind === 'localRunnerPathEntry') {
|
|
596
|
-
return (
|
|
597
|
-
<Surface
|
|
598
|
-
title="Runner Path"
|
|
599
|
-
subtitle="Paste the full path to llama-server."
|
|
600
|
-
footer="enter save · esc back"
|
|
601
|
-
>
|
|
602
|
-
{state.submitting ? (
|
|
603
|
-
<Spinner label="checking runner path..." />
|
|
604
|
-
) : (
|
|
605
|
-
<TextInput
|
|
606
|
-
label="llama-server"
|
|
607
|
-
placeholder={runnerPathPlaceholder()}
|
|
608
|
-
onSubmit={value => void saveRunnerPathAndStart(state, value, setState, onPick)}
|
|
609
|
-
onCancel={() => setState({ kind: 'localRunnerSetup', data: state.data, model: state.model })}
|
|
610
|
-
/>
|
|
611
|
-
)}
|
|
612
|
-
{state.error ? <Text color={theme.accentError}>{state.error}</Text> : null}
|
|
613
|
-
</Surface>
|
|
614
|
-
)
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
if (state.kind === 'localRunnerStarting') {
|
|
618
|
-
return (
|
|
619
|
-
<Surface title="Starting Local Model" subtitle={state.model.displayName}>
|
|
620
|
-
<ElapsedSpinner startedAt={state.startedAt} label="starting local runner..." />
|
|
621
|
-
</Surface>
|
|
622
|
-
)
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if (state.kind === 'localRunnerStartFail') {
|
|
626
|
-
return (
|
|
627
|
-
<Surface title="Local Model Failed to Start" subtitle={localRunnerStartFailureSubtitle(state.result)} tone="error" footer="enter select · esc back">
|
|
628
|
-
<Select<'retry' | 'path' | 'install' | 'back'>
|
|
629
|
-
options={[
|
|
630
|
-
{ value: 'retry', label: 'Try Again' },
|
|
631
|
-
{ value: 'path', label: 'Use Existing Runner Path' },
|
|
632
|
-
{ value: 'install', label: 'Install Local Runner' },
|
|
633
|
-
{ value: 'back', label: 'Back To Picker' },
|
|
634
|
-
]}
|
|
635
|
-
onSubmit={choice => {
|
|
636
|
-
if (choice === 'retry') void startAndPickHfModel(state.model, { kind: 'hfDone', data: state.data, model: state.model }, setState, onPick)
|
|
637
|
-
else if (choice === 'path') setState({ kind: 'localRunnerPathEntry', data: state.data, model: state.model, submitting: false })
|
|
638
|
-
else if (choice === 'install') void installRunnerAndStart({ kind: 'localRunnerSetup', data: state.data, model: state.model }, setState, onPick)
|
|
639
|
-
else dismissToList(state.data)()
|
|
640
|
-
}}
|
|
641
|
-
onCancel={dismissToList(state.data)}
|
|
642
|
-
/>
|
|
643
|
-
</Surface>
|
|
644
|
-
)
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
if (state.kind === 'keyEntry') {
|
|
648
|
-
const { provider, action, submitting, error } = state
|
|
649
|
-
const providerName = cloudProviderDisplayName(provider)
|
|
650
|
-
const actionLabel = action === 'set' ? 'Add' : 'Replace'
|
|
651
|
-
return (
|
|
652
|
-
<Surface
|
|
653
|
-
title={`${actionLabel} ${providerName} API Key`}
|
|
654
|
-
subtitle="Stored in your OS keyring when available; never written to config in plaintext."
|
|
655
|
-
footer="enter save · esc back"
|
|
656
|
-
>
|
|
657
|
-
{submitting ? (
|
|
658
|
-
<Spinner label={`saving ${providerName} key...`} />
|
|
659
|
-
) : (
|
|
660
|
-
<TextInput
|
|
661
|
-
label={`${providerName} key`}
|
|
662
|
-
placeholder={providerKeyPlaceholder(provider)}
|
|
663
|
-
isSecret
|
|
664
|
-
onSubmit={(value) => void submitKey(state, value, currentConfig, setState)}
|
|
665
|
-
onCancel={dismissToList(state.data)}
|
|
666
|
-
/>
|
|
667
|
-
)}
|
|
668
|
-
{error ? <Text color={theme.accentError}>{error}</Text> : null}
|
|
669
|
-
</Surface>
|
|
670
|
-
)
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
if (state.kind === 'keyManage') {
|
|
674
|
-
const { provider, submitting, error } = state
|
|
675
|
-
const providerName = cloudProviderDisplayName(provider)
|
|
676
|
-
return (
|
|
677
|
-
<Surface
|
|
678
|
-
title={`${providerName} API Key`}
|
|
679
|
-
subtitle="Manage the stored key for this provider."
|
|
680
|
-
footer="enter select · esc back"
|
|
681
|
-
>
|
|
682
|
-
{submitting ? (
|
|
683
|
-
<Spinner label={`removing ${providerName} key...`} />
|
|
684
|
-
) : (
|
|
685
|
-
<Select
|
|
686
|
-
options={[
|
|
687
|
-
{ value: 'edit', role: 'section', label: 'Credential' },
|
|
688
|
-
{ value: 'edit', label: 'Replace Stored API Key' },
|
|
689
|
-
{ value: 'delete', label: 'Remove Stored API Key' },
|
|
690
|
-
{ value: 'cancel', role: 'section', label: 'Navigation' },
|
|
691
|
-
{ value: 'cancel', label: 'Back', role: 'utility' },
|
|
692
|
-
]}
|
|
693
|
-
onSubmit={(value) => {
|
|
694
|
-
if (value === 'edit') {
|
|
695
|
-
setState({ kind: 'keyEntry', provider, action: 'edit', data: state.data, submitting: false })
|
|
696
|
-
return
|
|
697
|
-
}
|
|
698
|
-
if (value === 'cancel') {
|
|
699
|
-
setState({ kind: 'list', data: state.data })
|
|
700
|
-
return
|
|
701
|
-
}
|
|
702
|
-
void deleteKey(state, currentConfig, setState, onPick, currentProvider)
|
|
703
|
-
}}
|
|
704
|
-
onCancel={dismissToList(state.data)}
|
|
705
|
-
/>
|
|
706
|
-
)}
|
|
707
|
-
{error ? <Text color={theme.accentError}>{error}</Text> : null}
|
|
708
|
-
</Surface>
|
|
709
|
-
)
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
if (state.kind === 'oauthManage') {
|
|
713
|
-
const { submitting, error } = state
|
|
714
|
-
return (
|
|
715
|
-
<Surface
|
|
716
|
-
title="ChatGPT Sign-in"
|
|
717
|
-
subtitle="Manage your ChatGPT sign-in."
|
|
718
|
-
footer="enter select · esc back"
|
|
719
|
-
>
|
|
720
|
-
{submitting ? (
|
|
721
|
-
<Spinner label="signing out..." />
|
|
722
|
-
) : (
|
|
723
|
-
<Select
|
|
724
|
-
options={[
|
|
725
|
-
{ value: 'hdr:account', label: 'Account', disabled: true, role: 'section', bold: true },
|
|
726
|
-
{ value: 'signin', label: 'Sign in Again', indent: 2 },
|
|
727
|
-
{ value: 'signout', label: 'Sign Out', indent: 2 },
|
|
728
|
-
{ value: 'hdr:nav', label: 'Navigation', disabled: true, role: 'section', bold: true },
|
|
729
|
-
{ value: 'cancel', label: 'Back', indent: 2 },
|
|
730
|
-
]}
|
|
731
|
-
onSubmit={(value) => {
|
|
732
|
-
if (value === 'signin') {
|
|
733
|
-
void startOpenAIOAuthFlow(state.data, currentConfig, setState, oauthServiceRef, onPick)
|
|
734
|
-
return
|
|
735
|
-
}
|
|
736
|
-
if (value === 'cancel') {
|
|
737
|
-
setState({ kind: 'list', data: state.data })
|
|
738
|
-
return
|
|
739
|
-
}
|
|
740
|
-
void signOutOAuth(state, currentConfig, setState, onPick, currentProvider)
|
|
741
|
-
}}
|
|
742
|
-
onCancel={dismissToList(state.data)}
|
|
743
|
-
/>
|
|
744
|
-
)}
|
|
745
|
-
{error ? <Text color={theme.accentError}>{error}</Text> : null}
|
|
746
|
-
</Surface>
|
|
747
|
-
)
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
if (state.kind === 'oauthLogin') {
|
|
751
|
-
if (state.phase === 'error') {
|
|
752
|
-
return (
|
|
753
|
-
<Surface
|
|
754
|
-
title="OpenAI Sign-in Failed"
|
|
755
|
-
subtitle={state.message ?? 'Sign-in did not complete.'}
|
|
756
|
-
tone="error"
|
|
757
|
-
footer="enter select · esc back"
|
|
758
|
-
>
|
|
759
|
-
<Select<'retry' | 'apikey' | 'back'>
|
|
760
|
-
options={[
|
|
761
|
-
{ value: 'retry', label: 'Try Again' },
|
|
762
|
-
{ value: 'apikey', label: 'Add API Key Instead' },
|
|
763
|
-
{ value: 'back', label: 'Back To Picker' },
|
|
764
|
-
]}
|
|
765
|
-
onSubmit={choice => {
|
|
766
|
-
if (choice === 'retry') void startOpenAIOAuthFlow(state.data, currentConfig, setState, oauthServiceRef, onPick)
|
|
767
|
-
else if (choice === 'apikey') setState({ kind: 'keyEntry', provider: 'openai', action: 'set', data: state.data, submitting: false })
|
|
768
|
-
else dismissToList(state.data)()
|
|
769
|
-
}}
|
|
770
|
-
onCancel={dismissToList(state.data)}
|
|
771
|
-
/>
|
|
772
|
-
</Surface>
|
|
773
|
-
)
|
|
774
|
-
}
|
|
775
|
-
if (state.phase === 'exchanging') {
|
|
776
|
-
return (
|
|
777
|
-
<Surface title="Finishing OpenAI Sign-in" subtitle="Exchanging credentials with auth.openai.com.">
|
|
778
|
-
<Spinner label="completing sign-in..." />
|
|
779
|
-
</Surface>
|
|
780
|
-
)
|
|
781
|
-
}
|
|
782
|
-
return (
|
|
783
|
-
<Surface
|
|
784
|
-
title="Sign in with ChatGPT"
|
|
785
|
-
subtitle="Opened your browser to auth.openai.com. Approve to continue."
|
|
786
|
-
footer="esc cancel"
|
|
787
|
-
>
|
|
788
|
-
<Spinner label="waiting for browser sign-in..." />
|
|
789
|
-
{state.url ? (
|
|
790
|
-
<Box flexDirection="column" marginTop={1}>
|
|
791
|
-
<Text color={theme.dim}>If the browser did not open, visit:</Text>
|
|
792
|
-
<Text color={theme.dim}>{state.url}</Text>
|
|
793
|
-
</Box>
|
|
794
|
-
) : null}
|
|
795
|
-
<Box marginTop={1}>
|
|
796
|
-
<Select<'cancel'>
|
|
797
|
-
options={[{ value: 'cancel', label: 'Cancel Sign-in' }]}
|
|
798
|
-
onSubmit={() => {
|
|
799
|
-
oauthServiceRef.current?.cleanup()
|
|
800
|
-
oauthServiceRef.current = null
|
|
801
|
-
setState({ kind: 'list', data: state.data })
|
|
802
|
-
}}
|
|
803
|
-
onCancel={() => {
|
|
804
|
-
oauthServiceRef.current?.cleanup()
|
|
805
|
-
oauthServiceRef.current = null
|
|
806
|
-
setState({ kind: 'list', data: state.data })
|
|
807
|
-
}}
|
|
808
|
-
/>
|
|
809
|
-
</Box>
|
|
810
|
-
</Surface>
|
|
811
|
-
)
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
if (state.kind === 'catalog') {
|
|
815
|
-
const catalog = state.data.cloudCatalogs[state.provider]
|
|
816
|
-
const options = buildCatalogOptions(state.provider, catalog, currentProvider, currentModel, contextFit)
|
|
817
|
-
const initialIndex = options.findIndex(opt => {
|
|
818
|
-
if (opt.disabled) return false
|
|
819
|
-
const parsed = parseFullCatalogValue(opt.value)
|
|
820
|
-
return parsed?.provider === currentProvider && parsed.model === currentModel
|
|
821
|
-
})
|
|
822
|
-
return (
|
|
823
|
-
<Surface
|
|
824
|
-
title={`${cloudProviderDisplayName(state.provider)} Full Catalog`}
|
|
825
|
-
subtitle={contextFit ? contextFitSubtitle(contextFit) : 'All discovered models for this provider'}
|
|
826
|
-
footer="enter select · esc back"
|
|
827
|
-
>
|
|
828
|
-
<Select
|
|
829
|
-
options={options}
|
|
830
|
-
initialIndex={initialIndex === -1 ? 0 : initialIndex}
|
|
831
|
-
maxVisible={12}
|
|
832
|
-
hintLayout="inline"
|
|
833
|
-
onSubmit={(value) => {
|
|
834
|
-
const parsed = parseFullCatalogValue(value)
|
|
835
|
-
if (parsed) onPick({ kind: 'cloud', provider: parsed.provider, model: parsed.model, keyJustSet: false })
|
|
836
|
-
}}
|
|
837
|
-
onCancel={dismissToList(state.data)}
|
|
838
|
-
/>
|
|
839
|
-
</Surface>
|
|
840
|
-
)
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
if (state.kind === 'localCatalogLoading') {
|
|
844
|
-
return (
|
|
845
|
-
<Surface title="View Full Catalog" subtitle="Loading curated local GGUF files.">
|
|
846
|
-
<Spinner label="reading hugging face files..." />
|
|
847
|
-
</Surface>
|
|
848
|
-
)
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
if (state.kind === 'localCatalogError') {
|
|
852
|
-
return (
|
|
853
|
-
<Surface title="Catalog Failed" subtitle={state.message} tone="error" footer="enter select · esc back">
|
|
854
|
-
<Select<'retry' | 'paste' | 'back'>
|
|
855
|
-
options={[
|
|
856
|
-
{ value: 'retry', label: 'Retry Catalog' },
|
|
857
|
-
{ value: 'paste', label: 'Paste GGUF Link' },
|
|
858
|
-
{ value: 'back', label: 'Back To Picker' },
|
|
859
|
-
]}
|
|
860
|
-
onSubmit={choice => {
|
|
861
|
-
if (choice === 'retry') void openLocalCatalog(state.data, setState)
|
|
862
|
-
else if (choice === 'paste') setState({ kind: 'hfInput', data: state.data })
|
|
863
|
-
else dismissToList(state.data)()
|
|
864
|
-
}}
|
|
865
|
-
onCancel={dismissToList(state.data)}
|
|
866
|
-
/>
|
|
867
|
-
</Surface>
|
|
868
|
-
)
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
if (state.kind === 'localCatalog') {
|
|
872
|
-
const options = buildLocalModelCatalogOptions(state.data, { currentProvider, currentModel, contextFit }, state.catalog)
|
|
873
|
-
const initialIndex = localModelOptionIndex(options, currentProvider, currentModel)
|
|
874
|
-
return (
|
|
875
|
-
<Surface
|
|
876
|
-
title="View Full Catalog"
|
|
877
|
-
subtitle="Curated local GGUF files with recommendation and install status."
|
|
878
|
-
footer="enter select · esc back"
|
|
879
|
-
>
|
|
880
|
-
<Select
|
|
881
|
-
options={options}
|
|
882
|
-
initialIndex={initialIndex === -1 ? 0 : initialIndex}
|
|
883
|
-
maxVisible={12}
|
|
884
|
-
hintLayout="inline"
|
|
885
|
-
onSubmit={(value) => handleSubmit(value, state, setState, onPick, onCancel, currentConfig, oauthServiceRef, localOnly)}
|
|
886
|
-
onCancel={dismissToList(state.data)}
|
|
887
|
-
/>
|
|
888
|
-
</Surface>
|
|
889
|
-
)
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
const { data } = state
|
|
893
|
-
const options = buildModelPickerOptions(data, { currentProvider, currentModel, contextFit }, { localOnly })
|
|
894
|
-
const initialIndex = localOrCloudOptionIndex(options, currentProvider, currentModel)
|
|
895
|
-
|
|
896
|
-
return (
|
|
897
|
-
<Surface
|
|
898
|
-
title={localOnly ? 'Local Model' : (contextFit ? 'Switch to Larger-Context Model' : 'Switch Provider · Model')}
|
|
899
|
-
subtitle={localOnly ? 'Downloaded GGUF files and curated catalog' : (contextFit ? contextFitSubtitle(contextFit) : 'Downloaded GGUF files · cloud providers')}
|
|
900
|
-
footer={localOnly ? 'enter select · esc back' : 'enter select · esc close · /models lists installed models'}
|
|
901
|
-
>
|
|
902
|
-
<Select
|
|
903
|
-
options={options}
|
|
904
|
-
initialIndex={initialIndex === -1 ? 0 : initialIndex}
|
|
905
|
-
maxVisible={10}
|
|
906
|
-
hintLayout="inline"
|
|
907
|
-
onSubmit={(value) => handleSubmit(value, state, setState, onPick, onCancel, currentConfig, oauthServiceRef, localOnly)}
|
|
908
|
-
onCancel={onCancel}
|
|
909
|
-
/>
|
|
910
|
-
</Surface>
|
|
911
|
-
)
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
function handleSubmit(
|
|
915
|
-
value: string,
|
|
916
|
-
state: Extract<State, { kind: 'list' | 'localCatalog' }>,
|
|
917
|
-
setState: (s: State) => void,
|
|
918
|
-
onPick: (sel: ModelPickerSelection) => void,
|
|
919
|
-
onCancel: () => void,
|
|
920
|
-
currentConfig: EthagentConfig,
|
|
921
|
-
oauthServiceRef: React.MutableRefObject<OpenAIOAuthService | null>,
|
|
922
|
-
localOnly: boolean = false,
|
|
923
|
-
): void {
|
|
924
|
-
if (value.startsWith('hdr:')) return
|
|
925
|
-
if (value === 'cancel') {
|
|
926
|
-
onCancel()
|
|
927
|
-
return
|
|
928
|
-
}
|
|
929
|
-
if (value === 'back' && state.kind === 'localCatalog') {
|
|
930
|
-
if (localOnly) onCancel()
|
|
931
|
-
else setState({ kind: 'list', data: state.data })
|
|
932
|
-
return
|
|
933
|
-
}
|
|
934
|
-
if (value.startsWith('hf:')) {
|
|
935
|
-
const id = value.slice(3)
|
|
936
|
-
if (id === 'download') {
|
|
937
|
-
setState({ kind: 'hfInput', data: state.data })
|
|
938
|
-
return
|
|
939
|
-
}
|
|
940
|
-
const model = state.data.hfModels.find(item => item.id === id)
|
|
941
|
-
if (!model) return
|
|
942
|
-
void (async () => {
|
|
943
|
-
const local = await findLocalHfModel(id)
|
|
944
|
-
if (!local) {
|
|
945
|
-
setState({ kind: 'hfError', data: state.data, message: 'local model metadata was not found' })
|
|
946
|
-
return
|
|
947
|
-
}
|
|
948
|
-
await startAndPickHfModel(local, state, setState, onPick)
|
|
949
|
-
})()
|
|
950
|
-
return
|
|
951
|
-
}
|
|
952
|
-
if (value.startsWith('hfmmproj:') && state.kind === 'list') {
|
|
953
|
-
const id = value.slice('hfmmproj:'.length)
|
|
954
|
-
void (async () => {
|
|
955
|
-
const local = await findLocalHfModel(id)
|
|
956
|
-
if (!local) {
|
|
957
|
-
setState({ kind: 'hfError', data: state.data, message: 'local model metadata was not found' })
|
|
958
|
-
return
|
|
959
|
-
}
|
|
960
|
-
setState({ kind: 'mmprojOffer', data: state.data, model: local })
|
|
961
|
-
})()
|
|
962
|
-
return
|
|
963
|
-
}
|
|
964
|
-
if (value.startsWith('uc:') && state.kind === 'localCatalog') {
|
|
965
|
-
const entry = state.catalog.find(item => catalogOptionValue(item.repo.repoId, item.file.filename) === value)
|
|
966
|
-
if (entry) void reviewCatalogModel(state, entry, setState)
|
|
967
|
-
return
|
|
968
|
-
}
|
|
969
|
-
if (value === 'local:uninstall') {
|
|
970
|
-
setState({ kind: 'localUninstallPick', data: state.data })
|
|
971
|
-
return
|
|
972
|
-
}
|
|
973
|
-
if (value === 'local:catalog') {
|
|
974
|
-
void openLocalCatalog(state.data, setState)
|
|
975
|
-
return
|
|
976
|
-
}
|
|
977
|
-
if (value.startsWith('key:')) {
|
|
978
|
-
const parsed = parseKeyValue(value)
|
|
979
|
-
if (!parsed) return
|
|
980
|
-
if (parsed.action === 'manage') {
|
|
981
|
-
if (parsed.provider === 'openai' && state.data.cloudCredentialKinds?.openai === 'oauth') {
|
|
982
|
-
setState({ kind: 'oauthManage', data: state.data, submitting: false })
|
|
983
|
-
return
|
|
984
|
-
}
|
|
985
|
-
setState({ kind: 'keyManage', provider: parsed.provider, data: state.data, submitting: false })
|
|
986
|
-
return
|
|
987
|
-
}
|
|
988
|
-
setState({ kind: 'keyEntry', provider: parsed.provider, action: parsed.action, data: state.data, submitting: false })
|
|
989
|
-
return
|
|
990
|
-
}
|
|
991
|
-
if (value === 'oauth:openai') {
|
|
992
|
-
void startOpenAIOAuthFlow(state.data, currentConfig, setState, oauthServiceRef, onPick)
|
|
993
|
-
return
|
|
994
|
-
}
|
|
995
|
-
if (value.startsWith('catalog:')) {
|
|
996
|
-
const provider = value.slice('catalog:'.length)
|
|
997
|
-
if (isCloudProvider(provider)) setState({ kind: 'catalog', provider, data: state.data })
|
|
998
|
-
return
|
|
999
|
-
}
|
|
1000
|
-
if (value.startsWith('c:')) {
|
|
1001
|
-
const parsed = parseCloudValue(value)
|
|
1002
|
-
if (parsed) {
|
|
1003
|
-
onPick({ kind: 'cloud', provider: parsed.provider, model: parsed.model, keyJustSet: false })
|
|
1004
|
-
return
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
export { buildHfFileOptions } from './modelPickerViewHelpers.js'
|