ethagent 1.1.1 → 2.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 (271) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +127 -29
  3. package/package.json +16 -9
  4. package/src/app/FirstRun.tsx +192 -146
  5. package/src/app/FirstRunTimeline.tsx +47 -0
  6. package/src/app/input/AppInputProvider.tsx +1 -1
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -1
  8. package/src/chat/ChatBottomPane.tsx +0 -1
  9. package/src/chat/ChatInput.tsx +6 -6
  10. package/src/chat/ChatScreen.tsx +43 -18
  11. package/src/chat/ContextLimitView.tsx +4 -4
  12. package/src/chat/ContinuityEditReviewView.tsx +11 -17
  13. package/src/chat/ConversationStack.tsx +3 -0
  14. package/src/chat/CopyPicker.tsx +0 -1
  15. package/src/chat/MessageList.tsx +62 -45
  16. package/src/chat/PermissionPrompt.tsx +13 -9
  17. package/src/chat/PlanApprovalView.tsx +3 -3
  18. package/src/chat/ResumeView.tsx +1 -4
  19. package/src/chat/RewindView.tsx +2 -2
  20. package/src/chat/TranscriptView.tsx +6 -0
  21. package/src/chat/chatInputState.ts +1 -1
  22. package/src/chat/chatScreenUtils.ts +22 -11
  23. package/src/chat/chatSessionState.ts +2 -2
  24. package/src/chat/chatTurnOrchestrator.ts +16 -81
  25. package/src/chat/commands.ts +1 -1
  26. package/src/chat/textCursor.ts +1 -1
  27. package/src/chat/transcriptViewport.ts +2 -7
  28. package/src/cli/ResetConfirmView.tsx +1 -1
  29. package/src/cli/main.tsx +9 -3
  30. package/src/cli/preview.tsx +0 -5
  31. package/src/cli/updateNotice.ts +5 -3
  32. package/src/identity/continuity/editor.ts +7 -107
  33. package/src/identity/continuity/envelope.ts +1048 -40
  34. package/src/identity/continuity/history.ts +4 -4
  35. package/src/identity/continuity/localBackup.ts +249 -0
  36. package/src/identity/continuity/privateEdit/apply.ts +170 -0
  37. package/src/identity/continuity/privateEdit/diff.ts +82 -0
  38. package/src/identity/continuity/privateEdit/files.ts +23 -0
  39. package/src/identity/continuity/privateEdit/types.ts +28 -0
  40. package/src/identity/continuity/privateEdit.ts +10 -298
  41. package/src/identity/continuity/publicSkills.ts +8 -9
  42. package/src/identity/continuity/snapshots.ts +17 -6
  43. package/src/identity/continuity/storage/defaults.ts +111 -0
  44. package/src/identity/continuity/storage/files.ts +72 -0
  45. package/src/identity/continuity/storage/markdown.ts +81 -0
  46. package/src/identity/continuity/storage/paths.ts +24 -0
  47. package/src/identity/continuity/storage/scaffold.ts +124 -0
  48. package/src/identity/continuity/storage/status.ts +86 -0
  49. package/src/identity/continuity/storage/types.ts +27 -0
  50. package/src/identity/continuity/storage.ts +32 -507
  51. package/src/identity/continuity/zipWriter.ts +95 -0
  52. package/src/identity/crypto/backupEnvelope.ts +14 -247
  53. package/src/identity/crypto/eth.ts +7 -7
  54. package/src/identity/ens/agentRecords.ts +96 -0
  55. package/src/identity/ens/ensAutomation/contracts.ts +38 -0
  56. package/src/identity/ens/ensAutomation/delete.ts +80 -0
  57. package/src/identity/ens/ensAutomation/names.ts +14 -0
  58. package/src/identity/ens/ensAutomation/operators.ts +29 -0
  59. package/src/identity/ens/ensAutomation/read.ts +114 -0
  60. package/src/identity/ens/ensAutomation/root.ts +63 -0
  61. package/src/identity/ens/ensAutomation/setup.ts +284 -0
  62. package/src/identity/ens/ensAutomation/transactions.ts +107 -0
  63. package/src/identity/ens/ensAutomation/types.ts +126 -0
  64. package/src/identity/ens/ensAutomation.ts +29 -0
  65. package/src/identity/ens/ensLookup/client.ts +43 -0
  66. package/src/identity/ens/ensLookup/constants.ts +26 -0
  67. package/src/identity/ens/ensLookup/discovery.ts +70 -0
  68. package/src/identity/ens/ensLookup/names.ts +34 -0
  69. package/src/identity/ens/ensLookup/records.ts +45 -0
  70. package/src/identity/ens/ensLookup/resolve.ts +75 -0
  71. package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
  72. package/src/identity/ens/ensLookup/types.ts +38 -0
  73. package/src/identity/ens/ensLookup/validation.ts +72 -0
  74. package/src/identity/ens/ensLookup.ts +19 -0
  75. package/src/identity/ens/ensRegistration.ts +199 -0
  76. package/src/identity/ens/resolverDelegation.ts +48 -0
  77. package/src/identity/hub/IdentityHub.tsx +13 -815
  78. package/src/identity/hub/OperationalRoutes.tsx +370 -0
  79. package/src/identity/hub/Routes.tsx +361 -0
  80. package/src/identity/hub/advancedEnsValidation.ts +45 -0
  81. package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
  82. package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
  83. package/src/identity/hub/components/FlowTimeline.tsx +27 -0
  84. package/src/identity/hub/components/IdentitySummary.tsx +190 -0
  85. package/src/identity/hub/components/MenuScreen.tsx +237 -0
  86. package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
  87. package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
  88. package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
  89. package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
  90. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
  91. package/src/identity/hub/effects/create.ts +310 -0
  92. package/src/identity/hub/effects/ens/flows.ts +218 -0
  93. package/src/identity/hub/effects/ens/index.ts +11 -0
  94. package/src/identity/hub/effects/ens/transactions.ts +239 -0
  95. package/src/identity/hub/effects/index.ts +74 -0
  96. package/src/identity/hub/effects/profile/profileState.ts +173 -0
  97. package/src/identity/hub/effects/publicProfile/index.ts +5 -0
  98. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
  99. package/src/identity/hub/effects/rebackup/index.ts +7 -0
  100. package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
  101. package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
  102. package/src/identity/hub/effects/receipts.ts +46 -0
  103. package/src/identity/hub/effects/restore/apply.ts +112 -0
  104. package/src/identity/hub/effects/restore/auth.ts +159 -0
  105. package/src/identity/hub/effects/restore/discover.ts +86 -0
  106. package/src/identity/hub/effects/restore/envelopes.ts +21 -0
  107. package/src/identity/hub/effects/restore/fetch.ts +25 -0
  108. package/src/identity/hub/effects/restore/index.ts +22 -0
  109. package/src/identity/hub/effects/restore/recovery.ts +135 -0
  110. package/src/identity/hub/effects/restore/resolve.ts +102 -0
  111. package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
  112. package/src/identity/hub/effects/restore/shared.ts +91 -0
  113. package/src/identity/hub/effects/restoreAdmin.ts +93 -0
  114. package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
  115. package/src/identity/hub/effects/shared/snapshot.ts +336 -0
  116. package/src/identity/hub/effects/shared/sync.ts +190 -0
  117. package/src/identity/hub/effects/token-transfer/index.ts +6 -0
  118. package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
  119. package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
  120. package/src/identity/hub/effects/types.ts +53 -0
  121. package/src/identity/hub/effects/vault/preflight.ts +50 -0
  122. package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
  123. package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
  124. package/src/identity/hub/flows/continuity/RecoveryConfirmScreen.tsx +104 -0
  125. package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
  126. package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
  127. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
  128. package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
  129. package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
  130. package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
  131. package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
  132. package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
  133. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
  134. package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
  135. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
  136. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
  137. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
  138. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
  139. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
  140. package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
  141. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
  142. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
  143. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
  144. package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
  145. package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
  146. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
  147. package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
  148. package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
  149. package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +25 -43
  150. package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
  151. package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
  152. package/src/identity/hub/identityHubReducer.ts +166 -101
  153. package/src/identity/hub/model/continuity.ts +94 -0
  154. package/src/identity/hub/model/copy.ts +35 -0
  155. package/src/identity/hub/model/custody.ts +54 -0
  156. package/src/identity/hub/model/ens.ts +49 -0
  157. package/src/identity/hub/model/errors.ts +140 -0
  158. package/src/identity/hub/model/format.ts +15 -0
  159. package/src/identity/hub/model/identity.ts +94 -0
  160. package/src/identity/hub/model/network.ts +32 -0
  161. package/src/identity/hub/model/transfer.ts +57 -0
  162. package/src/identity/hub/operatorWallets.ts +131 -0
  163. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
  164. package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
  165. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
  166. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
  167. package/src/identity/hub/reconciliation/index.ts +21 -0
  168. package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
  169. package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
  170. package/src/identity/hub/txGuard.ts +51 -0
  171. package/src/identity/hub/types.ts +17 -0
  172. package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
  173. package/src/identity/hub/useIdentityHubController.ts +396 -0
  174. package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
  175. package/src/identity/hub/utils.ts +79 -0
  176. package/src/identity/identityCompat.ts +34 -0
  177. package/src/identity/profile/agentIcon.ts +61 -0
  178. package/src/identity/profile/imagePicker.ts +12 -12
  179. package/src/identity/registry/erc8004/abi.ts +14 -0
  180. package/src/identity/registry/erc8004/chains.ts +150 -0
  181. package/src/identity/registry/erc8004/client.ts +11 -0
  182. package/src/identity/registry/erc8004/discovery.ts +511 -0
  183. package/src/identity/registry/erc8004/metadata.ts +335 -0
  184. package/src/identity/registry/erc8004/ownership.ts +121 -0
  185. package/src/identity/registry/erc8004/preflight.ts +123 -0
  186. package/src/identity/registry/erc8004/transactions.ts +77 -0
  187. package/src/identity/registry/erc8004/types.ts +88 -0
  188. package/src/identity/registry/erc8004/uri.ts +59 -0
  189. package/src/identity/registry/erc8004/utils.ts +58 -0
  190. package/src/identity/registry/erc8004.ts +53 -1106
  191. package/src/identity/registry/fieldParsers.ts +28 -0
  192. package/src/identity/registry/operatorVault/bytecode.ts +98 -0
  193. package/src/identity/registry/operatorVault/constants.ts +38 -0
  194. package/src/identity/registry/operatorVault/read.ts +246 -0
  195. package/src/identity/registry/operatorVault/transactions.ts +81 -0
  196. package/src/identity/registry/operatorVault.ts +44 -0
  197. package/src/identity/storage/ipfs.ts +26 -24
  198. package/src/identity/wallet/browserWallet/gas.ts +41 -0
  199. package/src/identity/wallet/browserWallet/html.ts +106 -0
  200. package/src/identity/wallet/browserWallet/http.ts +28 -0
  201. package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
  202. package/src/identity/wallet/browserWallet/requests.ts +191 -0
  203. package/src/identity/wallet/browserWallet/session.ts +325 -0
  204. package/src/identity/wallet/browserWallet/types.ts +192 -0
  205. package/src/identity/wallet/browserWallet/validation.ts +74 -0
  206. package/src/identity/wallet/browserWallet.ts +30 -393
  207. package/src/identity/wallet/page/constants.ts +5 -0
  208. package/src/identity/wallet/page/controller.ts +251 -0
  209. package/src/identity/wallet/page/copy.ts +340 -0
  210. package/src/identity/wallet/page/grainient.ts +278 -0
  211. package/src/identity/wallet/page/html.ts +28 -0
  212. package/src/identity/wallet/page/markup.ts +50 -0
  213. package/src/identity/wallet/page/state.ts +9 -0
  214. package/src/identity/wallet/page/styles/base.ts +259 -0
  215. package/src/identity/wallet/page/styles/components.ts +262 -0
  216. package/src/identity/wallet/page/styles/index.ts +5 -0
  217. package/src/identity/wallet/page/styles/responsive.ts +247 -0
  218. package/src/identity/wallet/page/types.ts +47 -0
  219. package/src/identity/wallet/page/view.ts +535 -0
  220. package/src/identity/wallet/page/walletProvider.ts +70 -0
  221. package/src/identity/wallet/page.tsx +38 -0
  222. package/src/identity/wallet/walletPurposeCompat.ts +27 -0
  223. package/src/mcp/manager.ts +0 -1
  224. package/src/models/ModelPicker.tsx +36 -30
  225. package/src/models/catalog.ts +5 -2
  226. package/src/models/huggingface.ts +9 -9
  227. package/src/models/llamacpp.ts +13 -13
  228. package/src/models/modelDisplay.ts +75 -0
  229. package/src/models/modelPickerOptions.ts +16 -3
  230. package/src/models/modelRecommendation.ts +0 -1
  231. package/src/providers/errors.ts +16 -0
  232. package/src/providers/gemini.ts +252 -39
  233. package/src/providers/registry.ts +2 -2
  234. package/src/providers/retry.ts +1 -1
  235. package/src/runtime/sessionMode.ts +1 -1
  236. package/src/runtime/systemPrompt.ts +2 -0
  237. package/src/runtime/toolExecution.ts +18 -22
  238. package/src/runtime/toolIntent.ts +0 -20
  239. package/src/runtime/turn.ts +0 -92
  240. package/src/storage/atomicWrite.ts +4 -1
  241. package/src/storage/config.ts +181 -5
  242. package/src/storage/identity.ts +9 -3
  243. package/src/storage/secrets.ts +2 -2
  244. package/src/tools/bashSafety.ts +8 -0
  245. package/src/tools/changeDirectoryTool.ts +1 -1
  246. package/src/tools/deleteFileTool.ts +4 -4
  247. package/src/tools/editTool.ts +4 -4
  248. package/src/tools/editUtils.ts +5 -5
  249. package/src/tools/privateContinuityEditTool.ts +4 -5
  250. package/src/tools/privateContinuityReadTool.ts +1 -2
  251. package/src/tools/registry.ts +30 -0
  252. package/src/tools/writeFileTool.ts +5 -5
  253. package/src/ui/BrandSplash.tsx +20 -85
  254. package/src/ui/ProgressBar.tsx +3 -5
  255. package/src/ui/Select.tsx +21 -9
  256. package/src/ui/Spinner.tsx +38 -3
  257. package/src/ui/Surface.tsx +3 -3
  258. package/src/ui/TextInput.tsx +191 -29
  259. package/src/ui/theme.ts +7 -34
  260. package/src/utils/openExternal.ts +21 -0
  261. package/src/utils/withRetry.ts +47 -3
  262. package/src/identity/hub/identityHubEffects.ts +0 -937
  263. package/src/identity/hub/identityHubModel.ts +0 -291
  264. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -144
  265. package/src/identity/hub/screens/EditProfileFlow.tsx +0 -145
  266. package/src/identity/hub/screens/IdentitySummary.tsx +0 -90
  267. package/src/identity/hub/screens/MenuScreen.tsx +0 -117
  268. package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +0 -87
  269. package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
  270. package/src/identity/wallet/wallet-page/wallet.html +0 -1202
  271. /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
@@ -51,6 +51,7 @@ import {
51
51
  buildLocalModelCatalogOptions,
52
52
  buildModelPickerOptions,
53
53
  catalogOptionValue,
54
+ cloudProviderDisplayName,
54
55
  LOCAL_MODEL_LINK_EXAMPLE,
55
56
  LOCAL_MODEL_LINK_HINT,
56
57
  MODEL_PICKER_CLOUD_PROVIDERS,
@@ -153,7 +154,6 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
153
154
  }
154
155
  }
155
156
  setState({ kind: 'list', data })
156
- // If a featured repo was provided (first-run local flow), auto-inspect it
157
157
  if (featuredHfRepo) {
158
158
  await inspectHfInput({ kind: 'hfInput', data }, featuredHfRepo, setState)
159
159
  }
@@ -168,7 +168,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
168
168
  if (state.kind === 'loading') {
169
169
  return (
170
170
  <Surface title={contextFit ? 'Switch to Larger-Context Model' : 'Switch Provider · Model'} subtitle="Loading providers and models.">
171
- <Spinner label="Loading providers..." />
171
+ <Spinner label="loading providers..." />
172
172
  </Surface>
173
173
  )
174
174
  }
@@ -186,7 +186,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
186
186
  onSubmit={value => void inspectHfInput(state, value, setState)}
187
187
  onCancel={() => setState({ kind: 'list', data: state.data })}
188
188
  />
189
- {state.error ? <Text color="#e87070">{state.error}</Text> : null}
189
+ {state.error ? <Text color={theme.accentError}>{state.error}</Text> : null}
190
190
  </Surface>
191
191
  )
192
192
  }
@@ -194,7 +194,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
194
194
  if (state.kind === 'hfLoading') {
195
195
  return (
196
196
  <Surface title="Checking Model Link" subtitle={state.input}>
197
- <Spinner label="Reading model page..." />
197
+ <Spinner label="reading model page..." />
198
198
  </Surface>
199
199
  )
200
200
  }
@@ -266,7 +266,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
266
266
  return (
267
267
  <Surface title="Downloading Model" subtitle={state.plan.displayName}>
268
268
  <Text color={theme.dim}>{state.progress.status}</Text>
269
- <ProgressBar progress={progress} suffix={suffix} variant="rainbow" />
269
+ <ProgressBar progress={progress} suffix={suffix} />
270
270
  </Surface>
271
271
  )
272
272
  }
@@ -441,7 +441,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
441
441
  return (
442
442
  <Surface title="Installing Local Runner" subtitle="This may take a few minutes.">
443
443
  <ElapsedSpinner startedAt={state.startedAt} label={state.progress.label} />
444
- <ProgressBar progress={state.progress.progress} variant="rainbow" />
444
+ <ProgressBar progress={state.progress.progress} />
445
445
  </Surface>
446
446
  )
447
447
  }
@@ -481,7 +481,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
481
481
  onCancel={() => setState({ kind: 'localRunnerSetup', data: state.data, model: state.model })}
482
482
  />
483
483
  )}
484
- {state.error ? <Text color="#e87070">{state.error}</Text> : null}
484
+ {state.error ? <Text color={theme.accentError}>{state.error}</Text> : null}
485
485
  </Surface>
486
486
  )
487
487
  }
@@ -489,7 +489,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
489
489
  if (state.kind === 'localRunnerStarting') {
490
490
  return (
491
491
  <Surface title="Starting Local Model" subtitle={state.model.displayName}>
492
- <ElapsedSpinner startedAt={state.startedAt} label="Starting local runner" />
492
+ <ElapsedSpinner startedAt={state.startedAt} label="starting local runner..." />
493
493
  </Surface>
494
494
  )
495
495
  }
@@ -518,38 +518,41 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
518
518
 
519
519
  if (state.kind === 'keyEntry') {
520
520
  const { provider, action, submitting, error } = state
521
+ const providerName = cloudProviderDisplayName(provider)
522
+ const actionLabel = action === 'set' ? 'Add' : 'Replace'
521
523
  return (
522
524
  <Surface
523
- title={`${capitalize(action)} ${provider} API Key`}
525
+ title={`${actionLabel} ${providerName} API Key`}
524
526
  subtitle="Stored in your OS keyring when available; never written to config in plaintext."
525
527
  footer="enter save · esc back"
526
528
  >
527
529
  {submitting ? (
528
- <Spinner label={`saving ${provider} key...`} />
530
+ <Spinner label={`saving ${providerName} key...`} />
529
531
  ) : (
530
532
  <TextInput
531
- label={`${provider} key`}
533
+ label={`${providerName} key`}
532
534
  placeholder={providerKeyPlaceholder(provider)}
533
535
  isSecret
534
536
  onSubmit={(value) => void submitKey(state, value, currentConfig, setState)}
535
537
  onCancel={() => setState({ kind: 'list', data: state.data })}
536
538
  />
537
539
  )}
538
- {error ? <Text color="#e87070">{error}</Text> : null}
540
+ {error ? <Text color={theme.accentError}>{error}</Text> : null}
539
541
  </Surface>
540
542
  )
541
543
  }
542
544
 
543
545
  if (state.kind === 'keyManage') {
544
546
  const { provider, submitting, error } = state
547
+ const providerName = cloudProviderDisplayName(provider)
545
548
  return (
546
549
  <Surface
547
- title={`${capitalize(provider)} API Key`}
550
+ title={`${providerName} API Key`}
548
551
  subtitle="Manage the stored key for this provider."
549
552
  footer="enter select · esc back"
550
553
  >
551
554
  {submitting ? (
552
- <Spinner label={`removing ${provider} key...`} />
555
+ <Spinner label={`removing ${providerName} key...`} />
553
556
  ) : (
554
557
  <Select
555
558
  options={[
@@ -571,13 +574,11 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
571
574
  onCancel={() => setState({ kind: 'list', data: state.data })}
572
575
  />
573
576
  )}
574
- {error ? <Text color="#e87070">{error}</Text> : null}
577
+ {error ? <Text color={theme.accentError}>{error}</Text> : null}
575
578
  </Surface>
576
579
  )
577
580
  }
578
581
 
579
-
580
-
581
582
  if (state.kind === 'catalog') {
582
583
  const catalog = state.data.cloudCatalogs[state.provider]
583
584
  const options = buildCatalogOptions(state.provider, catalog, currentProvider, currentModel, contextFit)
@@ -588,7 +589,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
588
589
  })
589
590
  return (
590
591
  <Surface
591
- title={`${capitalize(state.provider)} Full Catalog`}
592
+ title={`${cloudProviderDisplayName(state.provider)} Full Catalog`}
592
593
  subtitle={contextFit ? contextFitSubtitle(contextFit) : 'All discovered models for this provider'}
593
594
  footer="enter select · esc back"
594
595
  >
@@ -609,7 +610,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
609
610
  if (state.kind === 'localCatalogLoading') {
610
611
  return (
611
612
  <Surface title="View Full Catalog" subtitle="Loading curated local GGUF files.">
612
- <Spinner label="Reading Hugging Face files..." />
613
+ <Spinner label="reading hugging face files..." />
613
614
  </Surface>
614
615
  )
615
616
  }
@@ -647,7 +648,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
647
648
  options={options}
648
649
  initialIndex={initialIndex === -1 ? 0 : initialIndex}
649
650
  maxVisible={12}
650
- onSubmit={(value) => handleSubmit(value, state, setState, onPick)}
651
+ onSubmit={(value) => handleSubmit(value, state, setState, onPick, onCancel)}
651
652
  onCancel={() => setState({ kind: 'list', data: state.data })}
652
653
  />
653
654
  </Surface>
@@ -668,7 +669,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
668
669
  options={options}
669
670
  initialIndex={initialIndex === -1 ? 0 : initialIndex}
670
671
  maxVisible={10}
671
- onSubmit={(value) => handleSubmit(value, state, setState, onPick)}
672
+ onSubmit={(value) => handleSubmit(value, state, setState, onPick, onCancel)}
672
673
  onCancel={onCancel}
673
674
  />
674
675
  </Surface>
@@ -680,8 +681,17 @@ function handleSubmit(
680
681
  state: Extract<State, { kind: 'list' | 'localCatalog' }>,
681
682
  setState: (s: State) => void,
682
683
  onPick: (sel: ModelPickerSelection) => void,
684
+ onCancel: () => void,
683
685
  ): void {
684
686
  if (value.startsWith('hdr:')) return
687
+ if (value === 'cancel') {
688
+ onCancel()
689
+ return
690
+ }
691
+ if (value === 'back' && state.kind === 'localCatalog') {
692
+ setState({ kind: 'list', data: state.data })
693
+ return
694
+ }
685
695
  if (value.startsWith('hf:')) {
686
696
  const id = value.slice(3)
687
697
  if (id === 'download') {
@@ -1256,7 +1266,7 @@ async function runRunnerSetup(
1256
1266
  const startedAt = Date.now()
1257
1267
  const initialProgress: LlamaCppInstallProgress = {
1258
1268
  phase: 'checking',
1259
- label: 'Preparing local runner',
1269
+ label: 'preparing local runner...',
1260
1270
  progress: 0.04,
1261
1271
  }
1262
1272
  const updateProgress = (progress: LlamaCppInstallProgress): void => {
@@ -1361,14 +1371,14 @@ function modelMetadataSubtext(size: string, indicators: string[]): string | unde
1361
1371
  }
1362
1372
 
1363
1373
  function riskColor(risk: string): string {
1364
- if (risk === 'high') return '#e87070'
1374
+ if (risk === 'high') return theme.accentError
1365
1375
  if (risk === 'medium') return theme.dim
1366
- return theme.accentSecondary
1376
+ return theme.accentPeriwinkle
1367
1377
  }
1368
1378
 
1369
1379
  function fitColor(fit: GgufMachineFit): string {
1370
- if (fit === 'too-large') return '#e87070'
1371
- if (fit === 'tight') return theme.accentWarm
1380
+ if (fit === 'too-large') return theme.accentError
1381
+ if (fit === 'tight') return theme.accentPeriwinkle
1372
1382
  return theme.dim
1373
1383
  }
1374
1384
 
@@ -1433,10 +1443,6 @@ function runnerPathPlaceholder(): string {
1433
1443
  return '/path/to/llama-server'
1434
1444
  }
1435
1445
 
1436
- function capitalize(value: string): string {
1437
- return value.charAt(0).toUpperCase() + value.slice(1)
1438
- }
1439
-
1440
1446
  function isCloudProvider(value: string | undefined): value is CloudProviderId {
1441
1447
  return value === 'openai' || value === 'anthropic' || value === 'gemini'
1442
1448
  }
@@ -236,10 +236,13 @@ async function discoverGeminiModels(
236
236
  apiKey: string,
237
237
  ): Promise<ModelCatalogEntry[]> {
238
238
  const response = await fetchImpl(
239
- `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`,
239
+ 'https://generativelanguage.googleapis.com/v1beta/models',
240
240
  {
241
241
  method: 'GET',
242
- headers: { Accept: 'application/json' },
242
+ headers: {
243
+ Accept: 'application/json',
244
+ 'x-goog-api-key': apiKey.trim(),
245
+ },
243
246
  },
244
247
  )
245
248
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
@@ -182,7 +182,7 @@ export async function uninstallLocalHfModel(
182
182
  const modelPath = path.resolve(model.localPath)
183
183
  const partialPath = path.resolve(`${model.localPath}.partial`)
184
184
  if (!isPathInside(cacheRoot, modelPath) || !isPathInside(cacheRoot, partialPath)) {
185
- throw new Error('refusing to uninstall a local model outside EthAgent model cache')
185
+ throw new Error('Refusing to uninstall a local model outside EthAgent model cache')
186
186
  }
187
187
 
188
188
  const unlink = deps.unlink ?? ((target: string) => fs.unlink(target))
@@ -203,10 +203,10 @@ export function parseHuggingFaceRef(input: string): HuggingFaceRef {
203
203
  const url = new URL(trimmed)
204
204
  const host = url.hostname.toLowerCase()
205
205
  if (host !== 'huggingface.co' && host !== 'www.huggingface.co') {
206
- throw new Error('expected a huggingface.co model link')
206
+ throw new Error('Expected a huggingface.co model link')
207
207
  }
208
208
  const parts = url.pathname.split('/').filter(Boolean)
209
- if (parts.length < 2) throw new Error('expected a Hugging Face repo link')
209
+ if (parts.length < 2) throw new Error('Expected a Hugging Face repo link')
210
210
  const repoId = `${decodeURIComponent(parts[0]!)}/${decodeURIComponent(parts[1]!)}`
211
211
  const mode = parts[2]
212
212
  if (mode === 'blob' || mode === 'resolve' || mode === 'tree') {
@@ -221,7 +221,7 @@ export function parseHuggingFaceRef(input: string): HuggingFaceRef {
221
221
 
222
222
  const withoutPrefix = trimmed.replace(/^hf:\/\//i, '')
223
223
  const parts = withoutPrefix.split('/').filter(Boolean)
224
- if (parts.length < 2) throw new Error('expected repo id like org/model or a huggingface.co link')
224
+ if (parts.length < 2) throw new Error('Expected repo id like org/model or a huggingface.co link')
225
225
  const repoId = `${parts[0]!}/${parts[1]!}`
226
226
  let fileParts = parts.slice(2)
227
227
  const mode = fileParts[0]
@@ -242,7 +242,7 @@ export async function fetchHuggingFaceRepoInfo(
242
242
  const response = await fetchImpl(url, { headers: { Accept: 'application/json' } })
243
243
  if (!response.ok) {
244
244
  if (response.status === 401 || response.status === 403) {
245
- throw new Error('repo is gated or private')
245
+ throw new Error('Repo is gated or private')
246
246
  }
247
247
  if (response.status === 404) throw new Error('Hugging Face repo not found')
248
248
  throw new Error(`Hugging Face API HTTP ${response.status}`)
@@ -301,7 +301,7 @@ export async function createHfDownloadPlan(
301
301
  const repo = await fetchHuggingFaceRepoInfo(ref, deps.fetchImpl)
302
302
  const files = ggufFiles(repo)
303
303
  if (files.length === 0) {
304
- throw new Error('no compatible local model files found for this link')
304
+ throw new Error('No compatible local model files found for this link')
305
305
  }
306
306
  const selected = selectedFilename
307
307
  ? files.find(file => file.filename === selectedFilename)
@@ -388,7 +388,7 @@ export async function* downloadHfModel(
388
388
  fetchImpl: FetchImpl = fetch,
389
389
  ): AsyncIterable<HfDownloadProgress> {
390
390
  if (plan.review.runtime !== 'llama.cpp runnable') {
391
- throw new Error('selected file is not compatible with local chat')
391
+ throw new Error('Selected file is not compatible with local chat')
392
392
  }
393
393
 
394
394
  await fs.mkdir(path.dirname(plan.localPath), { recursive: true })
@@ -411,7 +411,7 @@ export async function* downloadHfModel(
411
411
  while (true) {
412
412
  const { done, value } = await reader.read()
413
413
  if (done) break
414
- if (signal?.aborted) throw new Error('cancelled')
414
+ if (signal?.aborted) throw new Error('Cancelled')
415
415
  const buffer = Buffer.from(value)
416
416
  hash.update(buffer)
417
417
  await handle.write(buffer)
@@ -513,7 +513,7 @@ async function unlinkIfPresent(
513
513
  const code = (err as NodeJS.ErrnoException).code
514
514
  if (code === 'ENOENT') return
515
515
  if (code === 'EBUSY' || code === 'EPERM' || code === 'EACCES') {
516
- throw new Error('that model file is currently in use. stop the local runner and try uninstall again.')
516
+ throw new Error('That model file is currently in use. Stop the local runner and try uninstall again.')
517
517
  }
518
518
  throw err
519
519
  }
@@ -278,12 +278,12 @@ export async function installLlamaCppRunner(
278
278
  ): Promise<LlamaCppInstallResult> {
279
279
  const plans = llamaCppInstallPlans(platform)
280
280
  const failures: string[] = []
281
- onProgress?.({ phase: 'checking', label: 'checking local runner installers', progress: 0.08 })
281
+ onProgress?.({ phase: 'checking', label: 'checking local runner installers...', progress: 0.08 })
282
282
  for (const plan of plans) {
283
283
  onProgress?.({ phase: 'installing', label: installerProgressLabel(plan), progress: 0.34 })
284
284
  const result = await runInstallCommand(plan, plan.timeoutMs ?? 10 * 60_000)
285
285
  if (result.ok) {
286
- onProgress?.({ phase: 'finding', label: 'finding llama-server', progress: 0.78 })
286
+ onProgress?.({ phase: 'finding', label: 'finding llama-server...', progress: 0.78 })
287
287
  const binary = await findAndPersistLlamaCppServer(platform)
288
288
  if (binary.path) return { ok: true, serverPath: binary.path }
289
289
  const cliPaths = await discoverLlamaCppCliPaths(process.env, platform)
@@ -679,11 +679,11 @@ function cleanInstallLine(line: string): string {
679
679
  }
680
680
 
681
681
  function installerProgressLabel(plan: LlamaCppInstallPlan): string {
682
- if (plan.command === 'winget') return 'installing with Windows package manager'
683
- if (plan.command === 'brew') return 'installing with Homebrew'
684
- if (plan.command === 'nix') return 'installing with Nix'
685
- if (plan.command === 'port') return 'installing with MacPorts'
686
- return `installing with ${plan.label}`
682
+ if (plan.command === 'winget') return 'installing with Windows package manager...'
683
+ if (plan.command === 'brew') return 'installing with Homebrew...'
684
+ if (plan.command === 'nix') return 'installing with Nix...'
685
+ if (plan.command === 'port') return 'installing with MacPorts...'
686
+ return `installing with ${plan.label}...`
687
687
  }
688
688
 
689
689
  function formatInstallFailure(label: string, result: RunInstallResult): string {
@@ -735,7 +735,7 @@ async function installLlamaCppFromSource(
735
735
  await ensureConfigDir()
736
736
  await fs.mkdir(root, { recursive: true })
737
737
 
738
- onProgress?.({ phase: 'checking', label: 'checking build tools', progress: 0.08 })
738
+ onProgress?.({ phase: 'checking', label: 'checking build tools...', progress: 0.08 })
739
739
  const hasGit = await runCommand('git', ['--version'])
740
740
  if (!hasGit || hasGit.code !== 0) {
741
741
  return {
@@ -757,14 +757,14 @@ async function installLlamaCppFromSource(
757
757
 
758
758
  try {
759
759
  await fs.access(path.join(repoDir, '.git'))
760
- onProgress?.({ phase: 'building', label: 'updating local runner source', progress: 0.22 })
760
+ onProgress?.({ phase: 'building', label: 'updating local runner source...', progress: 0.22 })
761
761
  const update = await runInstallCommand(
762
762
  { label: 'update llama.cpp source', command: 'git', args: ['-C', repoDir, 'pull', '--ff-only'], timeoutMs: 5 * 60_000 },
763
763
  5 * 60_000,
764
764
  )
765
765
  if (!update.ok) return buildFailure(update)
766
766
  } catch {
767
- onProgress?.({ phase: 'building', label: 'downloading local runner source', progress: 0.22 })
767
+ onProgress?.({ phase: 'building', label: 'downloading local runner source...', progress: 0.22 })
768
768
  const clone = await runInstallCommand(
769
769
  { label: 'clone llama.cpp source', command: 'git', args: ['clone', '--depth', '1', 'https://github.com/ggml-org/llama.cpp.git', repoDir], timeoutMs: 10 * 60_000 },
770
770
  10 * 60_000,
@@ -772,14 +772,14 @@ async function installLlamaCppFromSource(
772
772
  if (!clone.ok) return buildFailure(clone)
773
773
  }
774
774
 
775
- onProgress?.({ phase: 'building', label: 'configuring local runner', progress: 0.48 })
775
+ onProgress?.({ phase: 'building', label: 'configuring local runner...', progress: 0.48 })
776
776
  const configure = await runInstallCommand(
777
777
  { label: 'configure llama.cpp', command: 'cmake', args: ['-S', repoDir, '-B', buildDir, '-DCMAKE_BUILD_TYPE=Release'], timeoutMs: 5 * 60_000 },
778
778
  5 * 60_000,
779
779
  )
780
780
  if (!configure.ok) return buildFailure(configure)
781
781
 
782
- onProgress?.({ phase: 'building', label: 'building local runner', progress: 0.68 })
782
+ onProgress?.({ phase: 'building', label: 'building local runner...', progress: 0.68 })
783
783
  const build = await runInstallCommand(
784
784
  {
785
785
  label: 'build llama-server',
@@ -795,7 +795,7 @@ async function installLlamaCppFromSource(
795
795
  ?? (await discoverLlamaCppServerPaths(process.env, platform))[0]
796
796
  if (builtServerPath) {
797
797
  await setLlamaCppServerPath(builtServerPath)
798
- onProgress?.({ phase: 'finding', label: 'local runner ready', progress: 1 })
798
+ onProgress?.({ phase: 'finding', label: 'local runner ready...', progress: 1 })
799
799
  return { ok: true, serverPath: builtServerPath }
800
800
  }
801
801
 
@@ -74,6 +74,18 @@ function formatRepoAndFile(repoId: string, filename: string, maxLength: number):
74
74
  const full = `${repoId}${HF_SEPARATOR}${file}`
75
75
  if (full.length <= maxLength) return full
76
76
 
77
+ const compactFile = compactModelFilename(file, maxLength)
78
+ if (maxLength <= 32 || compactFile.length >= maxLength - HF_SEPARATOR.length - 6) {
79
+ return truncateEndClean(compactFile, maxLength)
80
+ }
81
+
82
+ const repoBudget = maxLength - HF_SEPARATOR.length - compactFile.length
83
+ const compactRepo = compactRepoId(repoId, repoBudget)
84
+ if (compactRepo) {
85
+ const compact = `${compactRepo}${HF_SEPARATOR}${compactFile}`
86
+ if (compact.length <= maxLength) return compact
87
+ }
88
+
77
89
  const separatorBudget = HF_SEPARATOR.length
78
90
  const partBudget = maxLength - separatorBudget
79
91
  if (partBudget <= 8) return truncateMiddle(full, maxLength)
@@ -103,3 +115,66 @@ function formatRepoAndFile(repoId: string, filename: string, maxLength: number):
103
115
  function friendlyFilename(filename: string): string {
104
116
  return filename.split('/').pop() ?? filename
105
117
  }
118
+
119
+ function compactRepoId(repoId: string, maxLength: number): string {
120
+ if (maxLength <= 0) return ''
121
+ if (repoId.length <= maxLength) return repoId
122
+ const parts = repoId.split('/').filter(Boolean)
123
+ const owner = parts.length > 1 ? parts[0] ?? '' : ''
124
+ const repoName = parts.at(-1) ?? repoId
125
+ if (owner.length > 0) {
126
+ const nameBudget = maxLength - owner.length - 1
127
+ if (nameBudget >= 6) {
128
+ const compactName = compactModelCore(repoName, nameBudget)
129
+ const withOwner = `${owner}/${compactName}`
130
+ if (withOwner.length <= maxLength) return withOwner
131
+ }
132
+ }
133
+ return compactModelCore(repoName, maxLength)
134
+ }
135
+
136
+ function compactModelFilename(filename: string, maxLength: number): string {
137
+ const withoutExtension = filename.replace(/\.gguf$/i, '')
138
+ const compact = compactModelCore(withoutExtension, maxLength)
139
+ if (compact.length <= maxLength) return compact
140
+ return truncateEndClean(compact, maxLength)
141
+ }
142
+
143
+ function compactModelCore(value: string, maxLength: number): string {
144
+ if (maxLength <= 0) return ''
145
+ const cleaned = value
146
+ .replace(/\.gguf$/i, '')
147
+ .replace(/[_-]+/g, ' ')
148
+ .replace(/(^|[^0-9])\./g, '$1 ')
149
+ .replace(/\.(?=[^0-9]|$)/g, ' ')
150
+ .replace(/\bgguf\b/gi, '')
151
+ .replace(/\s+/g, ' ')
152
+ .trim()
153
+ if (!cleaned) return truncateEndClean(value, maxLength)
154
+
155
+ const tokens = cleaned.split(' ')
156
+ const sizeIndex = tokens.findIndex(token => /^\d+(?:\.\d+)?[bm]$/i.test(token))
157
+ const familyTokens = sizeIndex > 0 ? tokens.slice(0, Math.min(sizeIndex, 3)) : tokens.slice(0, Math.min(tokens.length, 3))
158
+ const size = sizeIndex >= 0 ? tokens[sizeIndex] : undefined
159
+ const context = tokens.find(token => /^\d+k$/i.test(token))
160
+ const quant = quantizationLabel(value)
161
+ const parts = [familyTokens.join(' '), size, context, quant]
162
+ .filter((part): part is string => Boolean(part))
163
+ .filter((part, index, all) => all.findIndex(other => other.toLowerCase() === part.toLowerCase()) === index)
164
+ const compact = parts.join(' ').trim() || cleaned
165
+ if (compact.length <= maxLength) return compact
166
+ return truncateEndClean(compact, maxLength)
167
+ }
168
+
169
+ function quantizationLabel(value: string): string | undefined {
170
+ const match = value.match(/(?:^|[-_.\s])((?:Q\d(?:_[A-Za-z0-9]+)*)|BF16|F16|FP16)(?:$|[-_.\s])/i)
171
+ return match?.[1]?.toUpperCase()
172
+ }
173
+
174
+ function truncateEndClean(value: string, maxLength: number): string {
175
+ if (maxLength <= 0) return ''
176
+ if (value.length <= maxLength) return value
177
+ if (maxLength <= 3) return value.slice(0, maxLength)
178
+ const sliced = value.slice(0, maxLength - 3).replace(/[\s._/-]+$/g, '')
179
+ return `${sliced || value.slice(0, maxLength - 3)}...`
180
+ }
@@ -14,6 +14,14 @@ export const MODEL_PICKER_CLOUD_PROVIDERS: CloudProviderId[] = ['openai', 'anthr
14
14
  export const LOCAL_MODEL_LINK_HINT = 'Paste a GGUF link'
15
15
  export const LOCAL_MODEL_LINK_EXAMPLE = 'e.g. https://huggingface.co/Qwen/Qwen3-8B-GGUF'
16
16
 
17
+ export function cloudProviderDisplayName(provider: CloudProviderId): string {
18
+ switch (provider) {
19
+ case 'openai': return 'OpenAI'
20
+ case 'anthropic': return 'Anthropic'
21
+ case 'gemini': return 'Gemini'
22
+ }
23
+ }
24
+
17
25
  export type LocalHfPickerModel = {
18
26
  id: string
19
27
  displayName: string
@@ -67,10 +75,10 @@ export function buildModelPickerOptions(
67
75
 
68
76
  options.push(sectionOption('hdr:cloud', 'Cloud'))
69
77
  for (const provider of MODEL_PICKER_CLOUD_PROVIDERS) {
70
- options.push(groupOption(`hdr:cloud:${provider}`, provider))
78
+ options.push(groupOption(`hdr:cloud:${provider}`, cloudProviderDisplayName(provider)))
71
79
  const keySet = data.cloudKeys[provider] === true
72
80
  if (!keySet) {
73
- options.push(utilityOption(`key:set:${provider}`, 'API Key · Add'))
81
+ options.push(utilityOption(`key:set:${provider}`, 'Add API Key'))
74
82
  continue
75
83
  }
76
84
 
@@ -97,9 +105,12 @@ export function buildModelPickerOptions(
97
105
  ))
98
106
  }
99
107
  options.push(utilityOption(`catalog:${provider}`, 'Full Catalog'))
100
- options.push(utilityOption(`key:manage:${provider}`, 'API Key · Manage'))
108
+ options.push(utilityOption(`key:manage:${provider}`, 'Manage API Key'))
101
109
  }
102
110
 
111
+ options.push(sectionOption('hdr:exit', 'Exit'))
112
+ options.push(utilityOption('cancel', 'Close Model Picker', 'Return to chat without changing model'))
113
+
103
114
  return options
104
115
  }
105
116
 
@@ -139,6 +150,8 @@ export function buildLocalModelCatalogOptions(
139
150
  if (data.hfModels.length > 0) {
140
151
  options.push(utilityOption('local:uninstall', 'Uninstall Downloaded GGUF'))
141
152
  }
153
+ options.push(sectionOption('hdr:navigation', 'Navigation'))
154
+ options.push(utilityOption('back', 'Back To Picker', 'Return to model picker'))
142
155
  return options
143
156
  }
144
157
 
@@ -8,7 +8,6 @@ import type { SpecSnapshot } from './runtimeDetection.js'
8
8
 
9
9
  const GB = 1024 * 1024 * 1024
10
10
 
11
- /** Featured local model repo for first-run setup and the model picker catalog. */
12
11
  export const FEATURED_HF_REPO = 'HauhauCS/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive'
13
12
  export const FEATURED_HF_REPO_URL = `https://huggingface.co/${FEATURED_HF_REPO}`
14
13
 
@@ -1,5 +1,6 @@
1
1
  import type { ProviderId } from '../storage/config.js'
2
2
  import { ProviderError } from './contracts.js'
3
+ import { formatGeminiRateLimitMessage } from './gemini.js'
3
4
 
4
5
  type ErrorBody =
5
6
  | string
@@ -16,8 +17,23 @@ export async function providerErrorFromResponse(
16
17
  provider: ProviderId,
17
18
  response: Response,
18
19
  ): Promise<ProviderError> {
20
+ if (provider === 'gemini' && response.status === 429) {
21
+ const short = await formatGeminiRateLimitMessage(response.clone())
22
+ if (short) return new ProviderError(short, { transient: true })
23
+ }
24
+
19
25
  const detail = await readErrorDetail(response)
20
26
 
27
+ if (
28
+ provider === 'gemini'
29
+ && response.status === 400
30
+ && /API[_ ]?key( not valid| not found|_invalid)|invalid api key/i.test(detail)
31
+ ) {
32
+ return new ProviderError(
33
+ 'gemini: API key rejected — verify your key at https://aistudio.google.com/app/apikey, then run /key gemini to set it again',
34
+ )
35
+ }
36
+
21
37
  if (provider !== 'llamacpp') {
22
38
  if (response.status === 401 || response.status === 403) {
23
39
  return new ProviderError(`auth failed: check your ${provider} key (/doctor to verify)`)