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.
Files changed (322) hide show
  1. package/.claude-plugin/marketplace.json +11 -0
  2. package/.claude-plugin/plugin.json +35 -0
  3. package/LICENSE +1 -1
  4. package/README.md +64 -104
  5. package/commands/ethagent.md +40 -0
  6. package/package.json +16 -16
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -6
  8. package/src/app/keybindings/types.ts +1 -6
  9. package/src/cli/ResetConfirmView.tsx +54 -53
  10. package/src/cli/demo.ts +86 -0
  11. package/src/cli/hookIo.ts +45 -0
  12. package/src/cli/main.tsx +94 -123
  13. package/src/cli/memoryGuard.ts +49 -0
  14. package/src/cli/reset.ts +28 -70
  15. package/src/cli/sessionStart.ts +33 -0
  16. package/src/cli/status.ts +46 -0
  17. package/src/cli/sync.ts +167 -0
  18. package/src/cli/syncAdapters/claude-code.ts +86 -0
  19. package/src/cli/syncAdapters/codex.ts +66 -0
  20. package/src/cli/syncAdapters/index.ts +45 -0
  21. package/src/cli/syncAdapters/managedBlock.ts +175 -0
  22. package/src/cli/syncAdapters/shared.ts +63 -0
  23. package/src/identity/continuity/envelopeParse.ts +20 -1
  24. package/src/identity/continuity/publicSkills.ts +3 -1
  25. package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
  26. package/src/identity/continuity/skills/scaffold.ts +5 -2
  27. package/src/identity/continuity/snapshots.ts +12 -5
  28. package/src/identity/continuity/storage/defaults.ts +20 -19
  29. package/src/identity/continuity/storage/status.ts +1 -1
  30. package/src/identity/ens/ensLookup/constants.ts +1 -1
  31. package/src/identity/manager/IdentityManager.tsx +33 -0
  32. package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
  33. package/src/identity/{hub → manager}/Routes.tsx +48 -34
  34. package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
  35. package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
  36. package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
  37. package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
  38. package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
  39. package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
  40. package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
  41. package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
  42. package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
  43. package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
  44. package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
  45. package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
  46. package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
  47. package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
  48. package/src/identity/{hub → manager}/create/effects.ts +19 -10
  49. package/src/identity/manager/create/importScan.ts +122 -0
  50. package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
  51. package/src/identity/{hub → manager}/custody/actions.ts +1 -15
  52. package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
  53. package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
  54. package/src/identity/{hub → manager}/custody/types.ts +1 -2
  55. package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
  56. package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
  57. package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
  58. package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
  59. package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
  60. package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
  61. package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
  62. package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
  63. package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
  64. package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
  65. package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
  66. package/src/identity/{hub → manager}/profile/effects.ts +1 -3
  67. package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
  68. package/src/identity/{hub → manager}/profile/state.ts +1 -1
  69. package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
  70. package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
  71. package/src/identity/{hub → manager}/restore/apply.ts +1 -1
  72. package/src/identity/{hub → manager}/restore/auth.ts +1 -1
  73. package/src/identity/{hub → manager}/restore/discover.ts +1 -1
  74. package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
  75. package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
  76. package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
  77. package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
  78. package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
  79. package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
  80. package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
  81. package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
  82. package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
  83. package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
  84. package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
  85. package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
  86. package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
  87. package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
  88. package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
  89. package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
  90. package/src/identity/manager/shared/effects/types.ts +30 -0
  91. package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
  92. package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
  93. package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
  94. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
  95. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
  96. package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
  97. package/src/identity/{hub → manager}/shared/utils.ts +6 -10
  98. package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
  99. package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
  100. package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
  101. package/src/identity/{hub → manager}/types.ts +5 -6
  102. package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
  103. package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
  104. package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
  105. package/src/identity/registry/erc8004/discovery.ts +3 -17
  106. package/src/identity/registry/erc8004/utils.ts +1 -1
  107. package/src/identity/storage/ipfs.ts +21 -1
  108. package/src/identity/wallet/browserWallet/html.ts +10 -2
  109. package/src/identity/wallet/browserWallet/http.ts +18 -0
  110. package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
  111. package/src/identity/wallet/browserWallet/requests.ts +10 -28
  112. package/src/identity/wallet/browserWallet/session.ts +26 -33
  113. package/src/identity/wallet/browserWallet/validation.ts +14 -0
  114. package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
  115. package/src/identity/wallet/page/boot.ts +43 -0
  116. package/src/identity/wallet/page/config.ts +59 -0
  117. package/src/identity/wallet/page/constants.ts +12 -0
  118. package/src/identity/wallet/page/copy.ts +47 -68
  119. package/src/identity/wallet/page/css.ts +638 -0
  120. package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
  121. package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
  122. package/src/identity/wallet/page/markup.ts +44 -34
  123. package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
  124. package/src/identity/wallet/page/resize.ts +95 -0
  125. package/src/identity/wallet/page/state.ts +135 -8
  126. package/src/identity/wallet/page/timeline.ts +161 -0
  127. package/src/identity/wallet/page/view.ts +22 -302
  128. package/src/storage/config.ts +30 -80
  129. package/src/storage/reset.ts +31 -0
  130. package/src/storage/secrets.ts +1 -16
  131. package/src/ui/Select.tsx +27 -5
  132. package/src/ui/Spinner.tsx +16 -15
  133. package/src/ui/Surface.tsx +21 -17
  134. package/src/ui/TextArea.tsx +173 -0
  135. package/src/ui/TextInput.tsx +31 -133
  136. package/src/ui/theme.ts +22 -13
  137. package/src/utils/clipboard.ts +0 -140
  138. package/src/app/FirstRun.tsx +0 -577
  139. package/src/app/FirstRunTimeline.tsx +0 -51
  140. package/src/app/firstRunConfig.ts +0 -26
  141. package/src/app/hooks/useCancelRequest.ts +0 -22
  142. package/src/app/hooks/useDoublePress.ts +0 -46
  143. package/src/app/hooks/useExitOnCtrlC.ts +0 -36
  144. package/src/auth/openaiOAuth/credentials.ts +0 -47
  145. package/src/auth/openaiOAuth/crypto.ts +0 -23
  146. package/src/auth/openaiOAuth/index.ts +0 -238
  147. package/src/auth/openaiOAuth/landingPage.ts +0 -116
  148. package/src/auth/openaiOAuth/listener.ts +0 -151
  149. package/src/auth/openaiOAuth/refresh.ts +0 -70
  150. package/src/auth/openaiOAuth/shared.ts +0 -115
  151. package/src/chat/ChatBottomPane.tsx +0 -296
  152. package/src/chat/ChatScreen.tsx +0 -1685
  153. package/src/chat/ConversationStack.tsx +0 -56
  154. package/src/chat/MessageList.tsx +0 -638
  155. package/src/chat/SessionStatus.tsx +0 -53
  156. package/src/chat/chatEnvironment.ts +0 -16
  157. package/src/chat/chatScreenUtils.ts +0 -194
  158. package/src/chat/chatSessionState.ts +0 -146
  159. package/src/chat/chatTurnContext.ts +0 -50
  160. package/src/chat/chatTurnOrchestrator.ts +0 -603
  161. package/src/chat/chatTurnRows.ts +0 -64
  162. package/src/chat/commands.ts +0 -494
  163. package/src/chat/continuityEditReview.ts +0 -42
  164. package/src/chat/display/DiffView.tsx +0 -193
  165. package/src/chat/display/SyntaxText.tsx +0 -192
  166. package/src/chat/display/toolCallDisplay.ts +0 -103
  167. package/src/chat/display/toolResultDisplay.ts +0 -19
  168. package/src/chat/input/ChatInput.tsx +0 -625
  169. package/src/chat/input/chatInputHelpers.ts +0 -62
  170. package/src/chat/input/chatInputState.ts +0 -247
  171. package/src/chat/input/chatPaste.ts +0 -49
  172. package/src/chat/input/imageRefs.ts +0 -30
  173. package/src/chat/input/inputRendering.tsx +0 -93
  174. package/src/chat/input/textCursor.ts +0 -212
  175. package/src/chat/messageMarkdown.ts +0 -220
  176. package/src/chat/messageRows.ts +0 -43
  177. package/src/chat/planImplementation.ts +0 -62
  178. package/src/chat/slashCommandHandlers.ts +0 -122
  179. package/src/chat/slashCommandViews.ts +0 -120
  180. package/src/chat/transcript/TranscriptView.tsx +0 -184
  181. package/src/chat/transcript/transcriptViewport.ts +0 -295
  182. package/src/chat/views/ContextLimitView.tsx +0 -95
  183. package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
  184. package/src/chat/views/CopyPicker.tsx +0 -50
  185. package/src/chat/views/PermissionPrompt.tsx +0 -156
  186. package/src/chat/views/PermissionsView.tsx +0 -165
  187. package/src/chat/views/PlanApprovalView.tsx +0 -91
  188. package/src/chat/views/ResumeView.tsx +0 -273
  189. package/src/chat/views/RewindView.tsx +0 -412
  190. package/src/cli/preview.tsx +0 -14
  191. package/src/cli/updateNotice.ts +0 -54
  192. package/src/identity/continuity/privateEdit/apply.ts +0 -170
  193. package/src/identity/continuity/privateEdit/diff.ts +0 -6
  194. package/src/identity/continuity/privateEdit/files.ts +0 -23
  195. package/src/identity/continuity/privateEdit/types.ts +0 -28
  196. package/src/identity/continuity/privateEdit.ts +0 -46
  197. package/src/identity/hub/IdentityHub.tsx +0 -14
  198. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
  199. package/src/identity/hub/ens/effects.ts +0 -218
  200. package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
  201. package/src/identity/hub/shared/effects/types.ts +0 -53
  202. package/src/identity/hub/shared/reconciliation/index.ts +0 -14
  203. package/src/identity/wallet/page/grainient.ts +0 -278
  204. package/src/identity/wallet/page/html.ts +0 -28
  205. package/src/identity/wallet/page/styles/base.ts +0 -259
  206. package/src/identity/wallet/page/styles/components.ts +0 -262
  207. package/src/identity/wallet/page/styles/index.ts +0 -5
  208. package/src/identity/wallet/page/styles/responsive.ts +0 -247
  209. package/src/identity/wallet/page.tsx +0 -38
  210. package/src/mcp/approvals.ts +0 -113
  211. package/src/mcp/config.ts +0 -235
  212. package/src/mcp/manager.ts +0 -482
  213. package/src/mcp/managerHelpers.ts +0 -70
  214. package/src/mcp/names.ts +0 -19
  215. package/src/mcp/output.ts +0 -96
  216. package/src/models/ModelPicker.tsx +0 -1009
  217. package/src/models/catalog.ts +0 -327
  218. package/src/models/huggingface.ts +0 -712
  219. package/src/models/huggingfaceStorage.ts +0 -136
  220. package/src/models/llamacpp.ts +0 -848
  221. package/src/models/llamacppCommands.ts +0 -44
  222. package/src/models/llamacppConfig.ts +0 -34
  223. package/src/models/llamacppDiscovery.ts +0 -176
  224. package/src/models/llamacppOutput.ts +0 -65
  225. package/src/models/llamacppPreflight.ts +0 -158
  226. package/src/models/modelDisplay.ts +0 -180
  227. package/src/models/modelPickerCatalogFlow.ts +0 -56
  228. package/src/models/modelPickerCredentials.ts +0 -166
  229. package/src/models/modelPickerData.ts +0 -41
  230. package/src/models/modelPickerDisplay.tsx +0 -132
  231. package/src/models/modelPickerHfFlow.ts +0 -192
  232. package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
  233. package/src/models/modelPickerOptions.ts +0 -457
  234. package/src/models/modelPickerTypes.ts +0 -69
  235. package/src/models/modelPickerUninstallFlow.ts +0 -48
  236. package/src/models/modelPickerViewHelpers.ts +0 -174
  237. package/src/models/modelRecommendation.ts +0 -139
  238. package/src/models/providerDisplay.ts +0 -16
  239. package/src/models/runtimeDetection.ts +0 -81
  240. package/src/models/uncensoredCatalog.ts +0 -86
  241. package/src/providers/anthropic.ts +0 -290
  242. package/src/providers/contracts.ts +0 -71
  243. package/src/providers/errors.ts +0 -80
  244. package/src/providers/gemini.ts +0 -391
  245. package/src/providers/openai-chat.ts +0 -474
  246. package/src/providers/openai-responses-format.ts +0 -177
  247. package/src/providers/openai-responses.ts +0 -306
  248. package/src/providers/openaiChatWire.ts +0 -124
  249. package/src/providers/registry.ts +0 -120
  250. package/src/providers/retry.ts +0 -58
  251. package/src/providers/sse.ts +0 -93
  252. package/src/runtime/compaction.ts +0 -395
  253. package/src/runtime/cwd.ts +0 -43
  254. package/src/runtime/providerTurn.ts +0 -38
  255. package/src/runtime/sessionMode.ts +0 -55
  256. package/src/runtime/systemPrompt.ts +0 -213
  257. package/src/runtime/textToolParser.ts +0 -161
  258. package/src/runtime/toolClaimGuards.ts +0 -143
  259. package/src/runtime/toolExecution.ts +0 -304
  260. package/src/runtime/toolIntent.ts +0 -143
  261. package/src/runtime/turn.ts +0 -369
  262. package/src/runtime/turnNudges.ts +0 -223
  263. package/src/runtime/turnTypes.ts +0 -86
  264. package/src/storage/factoryReset.ts +0 -127
  265. package/src/storage/history.ts +0 -58
  266. package/src/storage/permissions.ts +0 -76
  267. package/src/storage/rewind.ts +0 -266
  268. package/src/storage/sessionExport.ts +0 -49
  269. package/src/storage/sessions.ts +0 -495
  270. package/src/tools/bashSafety.ts +0 -186
  271. package/src/tools/bashTool.ts +0 -140
  272. package/src/tools/changeDirectoryTool.ts +0 -213
  273. package/src/tools/contracts.ts +0 -192
  274. package/src/tools/deleteFileTool.ts +0 -116
  275. package/src/tools/editTool.ts +0 -165
  276. package/src/tools/editUtils.ts +0 -170
  277. package/src/tools/fileDiff.ts +0 -261
  278. package/src/tools/listDirectoryTool.ts +0 -55
  279. package/src/tools/listSkillFilesTool.ts +0 -77
  280. package/src/tools/listSkillsTool.ts +0 -68
  281. package/src/tools/mcpResourceTools.ts +0 -95
  282. package/src/tools/permissionRules.ts +0 -85
  283. package/src/tools/privateContinuityEditTool.ts +0 -187
  284. package/src/tools/privateContinuityReadTool.ts +0 -106
  285. package/src/tools/readSkillTool.ts +0 -107
  286. package/src/tools/readTool.ts +0 -85
  287. package/src/tools/registry.ts +0 -103
  288. package/src/tools/writeFileTool.ts +0 -167
  289. package/src/ui/BrandSplash.tsx +0 -133
  290. package/src/ui/terminalTitle.ts +0 -30
  291. package/src/utils/images.ts +0 -140
  292. package/src/utils/markdownSegments.ts +0 -51
  293. package/src/utils/messages.ts +0 -37
  294. package/src/utils/withRetry.ts +0 -324
  295. /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
  296. /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
  297. /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
  298. /package/src/identity/{hub → manager}/custody/state.ts +0 -0
  299. /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
  300. /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
  301. /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
  302. /package/src/identity/{hub → manager}/ens/state.ts +0 -0
  303. /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
  304. /package/src/identity/{hub → manager}/ens/types.ts +0 -0
  305. /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
  306. /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
  307. /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
  308. /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
  309. /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
  310. /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
  311. /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
  312. /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
  313. /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
  314. /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
  315. /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
  316. /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
  317. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
  318. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
  319. /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
  320. /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
  321. /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
  322. /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'