ethagent 1.1.2 → 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 (268) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +126 -30
  3. package/package.json +7 -2
  4. package/src/app/FirstRun.tsx +190 -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 +35 -15
  11. package/src/chat/ContextLimitView.tsx +4 -4
  12. package/src/chat/ContinuityEditReviewView.tsx +10 -22
  13. package/src/chat/CopyPicker.tsx +0 -1
  14. package/src/chat/MessageList.tsx +62 -45
  15. package/src/chat/PermissionPrompt.tsx +13 -9
  16. package/src/chat/PlanApprovalView.tsx +3 -3
  17. package/src/chat/ResumeView.tsx +1 -4
  18. package/src/chat/RewindView.tsx +2 -2
  19. package/src/chat/chatInputState.ts +1 -1
  20. package/src/chat/chatScreenUtils.ts +22 -11
  21. package/src/chat/chatSessionState.ts +2 -2
  22. package/src/chat/chatTurnOrchestrator.ts +16 -81
  23. package/src/chat/commands.ts +1 -1
  24. package/src/chat/textCursor.ts +1 -1
  25. package/src/chat/transcriptViewport.ts +2 -7
  26. package/src/cli/ResetConfirmView.tsx +1 -1
  27. package/src/cli/main.tsx +9 -3
  28. package/src/cli/preview.tsx +0 -5
  29. package/src/cli/updateNotice.ts +4 -2
  30. package/src/identity/continuity/editor.ts +7 -107
  31. package/src/identity/continuity/envelope.ts +1048 -40
  32. package/src/identity/continuity/history.ts +4 -4
  33. package/src/identity/continuity/localBackup.ts +249 -0
  34. package/src/identity/continuity/privateEdit/apply.ts +170 -0
  35. package/src/identity/continuity/privateEdit/diff.ts +82 -0
  36. package/src/identity/continuity/privateEdit/files.ts +23 -0
  37. package/src/identity/continuity/privateEdit/types.ts +28 -0
  38. package/src/identity/continuity/privateEdit.ts +10 -298
  39. package/src/identity/continuity/publicSkills.ts +8 -9
  40. package/src/identity/continuity/snapshots.ts +17 -6
  41. package/src/identity/continuity/storage/defaults.ts +111 -0
  42. package/src/identity/continuity/storage/files.ts +72 -0
  43. package/src/identity/continuity/storage/markdown.ts +81 -0
  44. package/src/identity/continuity/storage/paths.ts +24 -0
  45. package/src/identity/continuity/storage/scaffold.ts +124 -0
  46. package/src/identity/continuity/storage/status.ts +86 -0
  47. package/src/identity/continuity/storage/types.ts +27 -0
  48. package/src/identity/continuity/storage.ts +32 -507
  49. package/src/identity/continuity/zipWriter.ts +95 -0
  50. package/src/identity/crypto/backupEnvelope.ts +14 -247
  51. package/src/identity/crypto/eth.ts +7 -7
  52. package/src/identity/ens/agentRecords.ts +96 -0
  53. package/src/identity/ens/ensAutomation/contracts.ts +38 -0
  54. package/src/identity/ens/ensAutomation/delete.ts +80 -0
  55. package/src/identity/ens/ensAutomation/names.ts +14 -0
  56. package/src/identity/ens/ensAutomation/operators.ts +29 -0
  57. package/src/identity/ens/ensAutomation/read.ts +114 -0
  58. package/src/identity/ens/ensAutomation/root.ts +63 -0
  59. package/src/identity/ens/ensAutomation/setup.ts +284 -0
  60. package/src/identity/ens/ensAutomation/transactions.ts +107 -0
  61. package/src/identity/ens/ensAutomation/types.ts +126 -0
  62. package/src/identity/ens/ensAutomation.ts +29 -0
  63. package/src/identity/ens/ensLookup/client.ts +43 -0
  64. package/src/identity/ens/ensLookup/constants.ts +26 -0
  65. package/src/identity/ens/ensLookup/discovery.ts +70 -0
  66. package/src/identity/ens/ensLookup/names.ts +34 -0
  67. package/src/identity/ens/ensLookup/records.ts +45 -0
  68. package/src/identity/ens/ensLookup/resolve.ts +75 -0
  69. package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
  70. package/src/identity/ens/ensLookup/types.ts +38 -0
  71. package/src/identity/ens/ensLookup/validation.ts +72 -0
  72. package/src/identity/ens/ensLookup.ts +19 -0
  73. package/src/identity/ens/ensRegistration.ts +199 -0
  74. package/src/identity/ens/resolverDelegation.ts +48 -0
  75. package/src/identity/hub/IdentityHub.tsx +13 -817
  76. package/src/identity/hub/OperationalRoutes.tsx +370 -0
  77. package/src/identity/hub/Routes.tsx +361 -0
  78. package/src/identity/hub/advancedEnsValidation.ts +45 -0
  79. package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
  80. package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
  81. package/src/identity/hub/components/FlowTimeline.tsx +27 -0
  82. package/src/identity/hub/components/IdentitySummary.tsx +190 -0
  83. package/src/identity/hub/components/MenuScreen.tsx +237 -0
  84. package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
  85. package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
  86. package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
  87. package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
  88. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
  89. package/src/identity/hub/effects/create.ts +310 -0
  90. package/src/identity/hub/effects/ens/flows.ts +218 -0
  91. package/src/identity/hub/effects/ens/index.ts +11 -0
  92. package/src/identity/hub/effects/ens/transactions.ts +239 -0
  93. package/src/identity/hub/effects/index.ts +74 -0
  94. package/src/identity/hub/effects/profile/profileState.ts +173 -0
  95. package/src/identity/hub/effects/publicProfile/index.ts +5 -0
  96. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
  97. package/src/identity/hub/effects/rebackup/index.ts +7 -0
  98. package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
  99. package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
  100. package/src/identity/hub/effects/receipts.ts +46 -0
  101. package/src/identity/hub/effects/restore/apply.ts +112 -0
  102. package/src/identity/hub/effects/restore/auth.ts +159 -0
  103. package/src/identity/hub/effects/restore/discover.ts +86 -0
  104. package/src/identity/hub/effects/restore/envelopes.ts +21 -0
  105. package/src/identity/hub/effects/restore/fetch.ts +25 -0
  106. package/src/identity/hub/effects/restore/index.ts +22 -0
  107. package/src/identity/hub/effects/restore/recovery.ts +135 -0
  108. package/src/identity/hub/effects/restore/resolve.ts +102 -0
  109. package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
  110. package/src/identity/hub/effects/restore/shared.ts +91 -0
  111. package/src/identity/hub/effects/restoreAdmin.ts +93 -0
  112. package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
  113. package/src/identity/hub/effects/shared/snapshot.ts +336 -0
  114. package/src/identity/hub/effects/shared/sync.ts +190 -0
  115. package/src/identity/hub/effects/token-transfer/index.ts +6 -0
  116. package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
  117. package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
  118. package/src/identity/hub/effects/types.ts +53 -0
  119. package/src/identity/hub/effects/vault/preflight.ts +50 -0
  120. package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
  121. package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
  122. package/src/identity/hub/{screens → flows/continuity}/RecoveryConfirmScreen.tsx +28 -19
  123. package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
  124. package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
  125. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
  126. package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
  127. package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
  128. package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
  129. package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
  130. package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
  131. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
  132. package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
  133. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
  134. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
  135. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
  136. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
  137. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
  138. package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
  139. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
  140. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
  141. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
  142. package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
  143. package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
  144. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
  145. package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
  146. package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
  147. package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +23 -44
  148. package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
  149. package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
  150. package/src/identity/hub/identityHubReducer.ts +164 -99
  151. package/src/identity/hub/model/continuity.ts +94 -0
  152. package/src/identity/hub/model/copy.ts +35 -0
  153. package/src/identity/hub/model/custody.ts +54 -0
  154. package/src/identity/hub/model/ens.ts +49 -0
  155. package/src/identity/hub/model/errors.ts +140 -0
  156. package/src/identity/hub/model/format.ts +15 -0
  157. package/src/identity/hub/model/identity.ts +94 -0
  158. package/src/identity/hub/model/network.ts +32 -0
  159. package/src/identity/hub/model/transfer.ts +57 -0
  160. package/src/identity/hub/operatorWallets.ts +131 -0
  161. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
  162. package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
  163. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
  164. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
  165. package/src/identity/hub/reconciliation/index.ts +21 -0
  166. package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
  167. package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
  168. package/src/identity/hub/txGuard.ts +51 -0
  169. package/src/identity/hub/types.ts +17 -0
  170. package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
  171. package/src/identity/hub/useIdentityHubController.ts +396 -0
  172. package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
  173. package/src/identity/hub/utils.ts +79 -0
  174. package/src/identity/identityCompat.ts +34 -0
  175. package/src/identity/profile/agentIcon.ts +61 -0
  176. package/src/identity/profile/imagePicker.ts +12 -12
  177. package/src/identity/registry/erc8004/abi.ts +14 -0
  178. package/src/identity/registry/erc8004/chains.ts +150 -0
  179. package/src/identity/registry/erc8004/client.ts +11 -0
  180. package/src/identity/registry/erc8004/discovery.ts +511 -0
  181. package/src/identity/registry/erc8004/metadata.ts +335 -0
  182. package/src/identity/registry/erc8004/ownership.ts +121 -0
  183. package/src/identity/registry/erc8004/preflight.ts +123 -0
  184. package/src/identity/registry/erc8004/transactions.ts +77 -0
  185. package/src/identity/registry/erc8004/types.ts +88 -0
  186. package/src/identity/registry/erc8004/uri.ts +59 -0
  187. package/src/identity/registry/erc8004/utils.ts +58 -0
  188. package/src/identity/registry/erc8004.ts +53 -1106
  189. package/src/identity/registry/fieldParsers.ts +28 -0
  190. package/src/identity/registry/operatorVault/bytecode.ts +98 -0
  191. package/src/identity/registry/operatorVault/constants.ts +38 -0
  192. package/src/identity/registry/operatorVault/read.ts +246 -0
  193. package/src/identity/registry/operatorVault/transactions.ts +81 -0
  194. package/src/identity/registry/operatorVault.ts +44 -0
  195. package/src/identity/storage/ipfs.ts +26 -24
  196. package/src/identity/wallet/browserWallet/gas.ts +41 -0
  197. package/src/identity/wallet/browserWallet/html.ts +106 -0
  198. package/src/identity/wallet/browserWallet/http.ts +28 -0
  199. package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
  200. package/src/identity/wallet/browserWallet/requests.ts +191 -0
  201. package/src/identity/wallet/browserWallet/session.ts +325 -0
  202. package/src/identity/wallet/browserWallet/types.ts +192 -0
  203. package/src/identity/wallet/browserWallet/validation.ts +74 -0
  204. package/src/identity/wallet/browserWallet.ts +30 -393
  205. package/src/identity/wallet/page/constants.ts +5 -0
  206. package/src/identity/wallet/page/controller.ts +251 -0
  207. package/src/identity/wallet/page/copy.ts +340 -0
  208. package/src/identity/wallet/page/grainient.ts +278 -0
  209. package/src/identity/wallet/page/html.ts +28 -0
  210. package/src/identity/wallet/page/markup.ts +50 -0
  211. package/src/identity/wallet/page/state.ts +9 -0
  212. package/src/identity/wallet/page/styles/base.ts +259 -0
  213. package/src/identity/wallet/page/styles/components.ts +262 -0
  214. package/src/identity/wallet/page/styles/index.ts +5 -0
  215. package/src/identity/wallet/page/styles/responsive.ts +247 -0
  216. package/src/identity/wallet/page/types.ts +47 -0
  217. package/src/identity/wallet/page/view.ts +535 -0
  218. package/src/identity/wallet/page/walletProvider.ts +70 -0
  219. package/src/identity/wallet/page.tsx +38 -0
  220. package/src/identity/wallet/walletPurposeCompat.ts +27 -0
  221. package/src/mcp/manager.ts +0 -1
  222. package/src/models/ModelPicker.tsx +36 -30
  223. package/src/models/catalog.ts +5 -2
  224. package/src/models/huggingface.ts +9 -9
  225. package/src/models/llamacpp.ts +13 -13
  226. package/src/models/modelDisplay.ts +75 -0
  227. package/src/models/modelPickerOptions.ts +16 -3
  228. package/src/models/modelRecommendation.ts +0 -1
  229. package/src/providers/errors.ts +16 -0
  230. package/src/providers/gemini.ts +252 -39
  231. package/src/providers/registry.ts +2 -2
  232. package/src/providers/retry.ts +1 -1
  233. package/src/runtime/sessionMode.ts +1 -1
  234. package/src/runtime/systemPrompt.ts +2 -0
  235. package/src/runtime/toolExecution.ts +18 -22
  236. package/src/runtime/toolIntent.ts +0 -20
  237. package/src/runtime/turn.ts +0 -92
  238. package/src/storage/atomicWrite.ts +4 -1
  239. package/src/storage/config.ts +181 -5
  240. package/src/storage/identity.ts +9 -3
  241. package/src/storage/secrets.ts +2 -2
  242. package/src/tools/bashSafety.ts +8 -0
  243. package/src/tools/changeDirectoryTool.ts +1 -1
  244. package/src/tools/deleteFileTool.ts +4 -4
  245. package/src/tools/editTool.ts +4 -4
  246. package/src/tools/editUtils.ts +5 -5
  247. package/src/tools/privateContinuityEditTool.ts +4 -5
  248. package/src/tools/privateContinuityReadTool.ts +1 -2
  249. package/src/tools/registry.ts +30 -0
  250. package/src/tools/writeFileTool.ts +5 -5
  251. package/src/ui/BrandSplash.tsx +20 -85
  252. package/src/ui/ProgressBar.tsx +3 -5
  253. package/src/ui/Select.tsx +20 -8
  254. package/src/ui/Spinner.tsx +38 -3
  255. package/src/ui/Surface.tsx +2 -2
  256. package/src/ui/TextInput.tsx +63 -20
  257. package/src/ui/theme.ts +7 -34
  258. package/src/utils/openExternal.ts +21 -0
  259. package/src/utils/withRetry.ts +47 -3
  260. package/src/identity/hub/identityHubEffects.ts +0 -937
  261. package/src/identity/hub/identityHubModel.ts +0 -371
  262. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -156
  263. package/src/identity/hub/screens/EditProfileFlow.tsx +0 -146
  264. package/src/identity/hub/screens/IdentitySummary.tsx +0 -106
  265. package/src/identity/hub/screens/MenuScreen.tsx +0 -117
  266. package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
  267. package/src/identity/wallet/wallet-page/wallet.html +0 -1202
  268. /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'
2
2
  import { Box, Text } from 'ink'
3
3
  import { BrandSplash as Splash } from '../ui/BrandSplash.js'
4
4
  import { Select } from '../ui/Select.js'
5
+ import { Surface } from '../ui/Surface.js'
5
6
  import { TextInput } from '../ui/TextInput.js'
6
7
  import { theme } from '../ui/theme.js'
7
8
  import { ModelPicker, type ModelPickerSelection } from '../models/ModelPicker.js'
@@ -9,7 +10,8 @@ import { formatModelDisplayName } from '../models/modelDisplay.js'
9
10
  import { detectSpec, type SpecSnapshot } from '../models/runtimeDetection.js'
10
11
  import { FEATURED_HF_REPO_URL } from '../models/modelRecommendation.js'
11
12
  import {
12
- saveConfig,
13
+ loadConfig,
14
+ saveConfigWithMerge,
13
15
  normalizeConfig,
14
16
  defaultModelFor,
15
17
  defaultBaseUrlFor,
@@ -18,6 +20,7 @@ import {
18
20
  } from '../storage/config.js'
19
21
  import { setKey } from '../storage/secrets.js'
20
22
  import { IdentityHub, type IdentityHubResult } from '../identity/hub/IdentityHub.js'
23
+ import { FirstRunTimeline, firstRunStageNumber, type FirstRunStepKind } from './FirstRunTimeline.js'
21
24
 
22
25
  type Step =
23
26
  | { kind: 'detecting' }
@@ -39,18 +42,18 @@ type FirstRunProps = {
39
42
  onCancel: () => void
40
43
  }
41
44
 
42
- const STATUS: Record<string, string> = {
43
- 'detecting': 'first-run setup · inspecting machine',
44
- 'detect-error': 'first-run setup · detection failed',
45
- 'choose-path': 'first-run setup · choose how to run',
46
- 'hf-setup': 'first-run setup · local model',
47
- 'cloud-provider': 'first-run setup · pick a cloud provider',
48
- 'cloud-key': 'first-run setup · paste API key',
49
- 'cloud-key-saving': 'first-run setup · storing key',
50
- 'cloud-model': 'first-run setup · pick a model',
51
- 'saving': 'first-run setup · saving config',
52
- 'save-error': 'first-run setup · save failed',
53
- 'done': 'ready',
45
+ const TITLE: Record<string, string> = {
46
+ 'detecting': 'Inspecting Machine',
47
+ 'detect-error': 'Detection Failed',
48
+ 'choose-path': 'Choose How To Run',
49
+ 'hf-setup': 'Local Model',
50
+ 'cloud-provider': 'Pick A Cloud Provider',
51
+ 'cloud-key': 'Paste API Key',
52
+ 'cloud-key-saving': 'Storing Key',
53
+ 'cloud-model': 'Pick A Model',
54
+ 'saving': 'Saving Config',
55
+ 'save-error': 'Save Failed',
56
+ 'done': 'Ready',
54
57
  }
55
58
 
56
59
  const NAV_BACK = '↑↓ navigate · enter select · esc back'
@@ -60,6 +63,19 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
60
63
  const [step, setStep] = useState<Step>({ kind: 'detecting' })
61
64
  const [history, setHistory] = useState<Step[]>([])
62
65
  const [firstRunIdentity, setFirstRunIdentity] = useState<EthagentConfig['identity']>(undefined)
66
+ const [firstRunConfig, setFirstRunConfig] = useState<EthagentConfig | undefined>(undefined)
67
+
68
+ useEffect(() => {
69
+ let cancelled = false
70
+ loadConfig()
71
+ .then(loaded => {
72
+ if (cancelled || !loaded) return
73
+ setFirstRunConfig(loaded)
74
+ if (loaded.identity) setFirstRunIdentity(loaded.identity)
75
+ })
76
+ .catch(() => null)
77
+ return () => { cancelled = true }
78
+ }, [])
63
79
 
64
80
  const goTo = (next: Step): void => {
65
81
  setHistory(h => [...h, step])
@@ -93,17 +109,36 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
93
109
  useEffect(() => {
94
110
  if (step.kind !== 'saving') return
95
111
  let cancelled = false
96
- const config = normalizeConfig(step.config)
97
- saveConfig(config)
98
- .then(() => {
112
+ const incoming = normalizeConfig(step.config)
113
+ ;(async () => {
114
+ try {
115
+ const merged = await saveConfigWithMerge(current => {
116
+ if (!current) return incoming
117
+ const baseErc = current.erc8004
118
+ const incErc = incoming.erc8004
119
+ const next: EthagentConfig = { ...current, ...incoming }
120
+ if (baseErc || incErc) {
121
+ const mergedVaults = {
122
+ ...(baseErc?.operatorVaults ?? {}),
123
+ ...(incErc?.operatorVaults ?? {}),
124
+ }
125
+ const erc = {
126
+ ...(baseErc ?? {}),
127
+ ...(incErc ?? {}),
128
+ ...(Object.keys(mergedVaults).length > 0 ? { operatorVaults: mergedVaults } : {}),
129
+ } as NonNullable<EthagentConfig['erc8004']>
130
+ next.erc8004 = erc
131
+ }
132
+ return next
133
+ })
99
134
  if (cancelled) return
100
- setStep({ kind: 'done', config })
101
- onComplete(config)
102
- })
103
- .catch((err: unknown) => {
135
+ setStep({ kind: 'done', config: merged })
136
+ onComplete(merged)
137
+ } catch (err: unknown) {
104
138
  if (cancelled) return
105
- setStep({ kind: 'save-error', config, message: (err as Error).message })
106
- })
139
+ setStep({ kind: 'save-error', config: incoming, message: (err as Error).message })
140
+ }
141
+ })()
107
142
  return () => { cancelled = true }
108
143
  }, [step, onComplete])
109
144
 
@@ -114,6 +149,9 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
114
149
  if (step.result.kind === 'token') {
115
150
  setFirstRunIdentity(step.result.identity)
116
151
  }
152
+ const reloaded = await loadConfig().catch(() => null)
153
+ if (cancelled || !reloaded) return
154
+ setFirstRunConfig(reloaded)
117
155
  }
118
156
  persist()
119
157
  .then(() => {
@@ -124,35 +162,65 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
124
162
  if (cancelled) return
125
163
  setStep({
126
164
  kind: 'detect-error',
127
- message: `could not store identity: ${(err as Error).message}`,
165
+ message: `Could not store identity: ${(err as Error).message}`,
128
166
  })
129
167
  })
130
168
  return () => { cancelled = true }
131
169
  }, [step])
132
170
 
133
- const hint = (canBack: boolean): React.ReactElement => (
134
- <Box marginTop={1}>
135
- <Text color={theme.dim}>{canBack ? NAV_BACK : NAV_CANCEL}</Text>
171
+ const navHint = (canBack: boolean): string => canBack ? NAV_BACK : NAV_CANCEL
172
+
173
+ const withFirstRunIdentity = (config: EthagentConfig): EthagentConfig => {
174
+ const merged: EthagentConfig = firstRunConfig
175
+ ? {
176
+ ...firstRunConfig,
177
+ ...config,
178
+ ...(firstRunConfig.erc8004 ? { erc8004: { ...firstRunConfig.erc8004, ...(config.erc8004 ?? {}) } } : config.erc8004 ? { erc8004: config.erc8004 } : {}),
179
+ }
180
+ : config
181
+ return firstRunIdentity ? { ...merged, identity: firstRunIdentity } : merged
182
+ }
183
+
184
+ const renderShell = (
185
+ currentKind: FirstRunStepKind,
186
+ title: string,
187
+ body: React.ReactNode,
188
+ footer?: string,
189
+ ): React.ReactElement => (
190
+ <Box flexDirection="column" padding={1}>
191
+ <Splash />
192
+ <Box marginTop={1}>
193
+ <Surface
194
+ title={title}
195
+ subtitle={<FirstRunTimeline current={firstRunStageNumber(currentKind)} />}
196
+ footer={footer}
197
+ >
198
+ {body}
199
+ </Surface>
200
+ </Box>
136
201
  </Box>
137
202
  )
138
203
 
139
- const withFirstRunIdentity = (config: EthagentConfig): EthagentConfig =>
140
- firstRunIdentity ? { ...config, identity: firstRunIdentity } : config
204
+ const renderRaw = (currentKind: FirstRunStepKind, body: React.ReactNode): React.ReactElement => (
205
+ <Box flexDirection="column" padding={1}>
206
+ <Splash />
207
+ <Box marginTop={1} marginBottom={1}>
208
+ <FirstRunTimeline current={firstRunStageNumber(currentKind)} />
209
+ </Box>
210
+ {body}
211
+ </Box>
212
+ )
141
213
 
142
214
  if (step.kind === 'detecting') {
143
- return (
144
- <Box flexDirection="column" padding={1}>
145
- <Splash tipLine={STATUS['detecting']} />
146
- <Text color={theme.dim}>inspecting machine…</Text>
147
- </Box>
148
- )
215
+ return renderShell(step.kind, TITLE['detecting']!, (
216
+ <Text color={theme.dim}>Inspecting machine…</Text>
217
+ ))
149
218
  }
150
219
 
151
220
  if (step.kind === 'detect-error') {
152
- return (
153
- <Box flexDirection="column" padding={1}>
154
- <Splash tipLine={STATUS['detect-error']} />
155
- <Text color="#e87070">could not inspect machine: {step.message}</Text>
221
+ return renderShell(step.kind, TITLE['detect-error']!, (
222
+ <>
223
+ <Text color={theme.accentError}>Could not inspect machine: {step.message}</Text>
156
224
  <Box marginTop={1}>
157
225
  <Select<'quit'>
158
226
  options={[{ value: 'quit', label: 'quit' }]}
@@ -160,46 +228,41 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
160
228
  onCancel={onCancel}
161
229
  />
162
230
  </Box>
163
- </Box>
164
- )
231
+ </>
232
+ ))
165
233
  }
166
234
 
167
235
  if (step.kind === 'identity-start') {
168
- return (
169
- <Box flexDirection="column" padding={1}>
170
- <Splash tipLine="first-run setup · agent identity" />
171
- <IdentityHub
172
- mode="first-run"
173
- onComplete={result => {
174
- if (result.kind === 'cancel') {
175
- onCancel()
176
- return
177
- }
178
- if (result.kind === 'skip') {
179
- setStep({ kind: 'choose-path', spec: step.spec })
180
- return
181
- }
182
- setStep({ kind: 'identity-start-saving', spec: step.spec, result })
183
- }}
184
- />
185
- </Box>
186
- )
236
+ return renderRaw(step.kind, (
237
+ <IdentityHub
238
+ mode="first-run"
239
+ config={firstRunConfig}
240
+ onConfigChange={setFirstRunConfig}
241
+ onComplete={result => {
242
+ if (result.kind === 'cancel') {
243
+ onCancel()
244
+ return
245
+ }
246
+ if (result.kind === 'skip') {
247
+ setStep({ kind: 'choose-path', spec: step.spec })
248
+ return
249
+ }
250
+ setStep({ kind: 'identity-start-saving', spec: step.spec, result })
251
+ }}
252
+ />
253
+ ))
187
254
  }
188
255
 
189
256
  if (step.kind === 'identity-start-saving') {
190
- return (
191
- <Box flexDirection="column" padding={1}>
192
- <Splash tipLine="first-run setup · storing identity" />
193
- <Text color={theme.dim}>storing identity...</Text>
194
- </Box>
195
- )
257
+ return renderShell(step.kind, 'Storing Identity', (
258
+ <Text color={theme.dim}>Storing identity...</Text>
259
+ ))
196
260
  }
197
261
 
198
262
  if (step.kind === 'choose-path') {
199
263
  const { spec } = step
200
- return (
201
- <Box flexDirection="column" padding={1}>
202
- <Splash tipLine={STATUS['choose-path']} />
264
+ return renderShell(step.kind, TITLE['choose-path']!, (
265
+ <>
203
266
  <Box flexDirection="column" marginBottom={1}>
204
267
  <Text color={theme.dim}>
205
268
  detected {formatGB(spec.effectiveRamBytes)} RAM
@@ -208,10 +271,9 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
208
271
  </Text>
209
272
  </Box>
210
273
  <Select<'cloud' | 'hf'>
211
- label="how do you want to run?"
212
274
  options={[
213
- { value: 'cloud', label: 'cloud API', hint: 'anthropic, openai, or gemini' },
214
- { value: 'hf', label: 'local model', hint: 'download and run locally' },
275
+ { value: 'cloud', label: 'Cloud API', hint: 'OpenAI, Anthropic, or Gemini' },
276
+ { value: 'hf', label: 'Local Model', hint: 'Download and run locally' },
215
277
  ]}
216
278
  onSubmit={choice => {
217
279
  if (choice === 'cloud') goTo({ kind: 'cloud-provider' })
@@ -219,13 +281,10 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
219
281
  }}
220
282
  onCancel={onCancel}
221
283
  />
222
- {hint(false)}
223
- </Box>
224
- )
284
+ </>
285
+ ), navHint(false))
225
286
  }
226
287
 
227
-
228
-
229
288
  if (step.kind === 'hf-setup') {
230
289
  const modelPickerConfig: EthagentConfig = withFirstRunIdentity({
231
290
  version: 1,
@@ -234,57 +293,43 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
234
293
  baseUrl: defaultBaseUrlFor('llamacpp'),
235
294
  firstRunAt: new Date().toISOString(),
236
295
  })
237
- return (
238
- <Box flexDirection="column" padding={1}>
239
- <Splash tipLine={STATUS['hf-setup']} />
240
- <Box flexDirection="column" marginBottom={1}>
241
- <Text color={theme.dim}>featured: {FEATURED_HF_REPO_URL}</Text>
242
- </Box>
243
- <ModelPicker
244
- currentConfig={modelPickerConfig}
245
- currentProvider="llamacpp"
246
- currentModel={modelPickerConfig.model}
247
- featuredHfRepo={FEATURED_HF_REPO_URL}
248
- onPick={(selection: ModelPickerSelection) => {
249
- goTo({ kind: 'saving', config: configFromModelPickerSelection(selection, modelPickerConfig) })
250
- }}
251
- onCancel={goBack}
252
- />
253
- </Box>
254
- )
296
+ return renderRaw(step.kind, (
297
+ <ModelPicker
298
+ currentConfig={modelPickerConfig}
299
+ currentProvider="llamacpp"
300
+ currentModel={modelPickerConfig.model}
301
+ featuredHfRepo={FEATURED_HF_REPO_URL}
302
+ onPick={(selection: ModelPickerSelection) => {
303
+ goTo({ kind: 'saving', config: configFromModelPickerSelection(selection, modelPickerConfig) })
304
+ }}
305
+ onCancel={goBack}
306
+ />
307
+ ))
255
308
  }
256
309
 
257
310
  if (step.kind === 'cloud-provider') {
258
- return (
259
- <Box flexDirection="column" padding={1}>
260
- <Splash tipLine={STATUS['cloud-provider']} />
261
- <Text color={theme.accentSecondary} bold>pick a cloud provider</Text>
262
- <Box marginTop={1}>
263
- <Select<ProviderId>
264
- options={[
265
- { value: 'openai', label: 'openai' },
266
- { value: 'anthropic', label: 'anthropic' },
267
- { value: 'gemini', label: 'gemini' },
268
- ]}
269
- onSubmit={provider => goTo({ kind: 'cloud-key', provider })}
270
- onCancel={goBack}
271
- />
272
- </Box>
273
- {hint(true)}
274
- </Box>
275
- )
311
+ return renderShell(step.kind, TITLE['cloud-provider']!, (
312
+ <Select<ProviderId>
313
+ options={[
314
+ { value: 'openai', label: 'OpenAI' },
315
+ { value: 'anthropic', label: 'Anthropic' },
316
+ { value: 'gemini', label: 'Gemini' },
317
+ ]}
318
+ onSubmit={provider => goTo({ kind: 'cloud-key', provider })}
319
+ onCancel={goBack}
320
+ />
321
+ ), navHint(true))
276
322
  }
277
323
 
278
324
  if (step.kind === 'cloud-key' || step.kind === 'cloud-key-saving') {
279
325
  const provider = step.provider
280
326
  const saving = step.kind === 'cloud-key-saving'
281
327
  const error = step.kind === 'cloud-key' ? step.error : undefined
282
- return (
283
- <Box flexDirection="column" padding={1}>
284
- <Splash tipLine={saving ? STATUS['cloud-key-saving'] : STATUS['cloud-key']} />
285
- <Text color={theme.accentSecondary} bold>paste your {provider} API key</Text>
328
+ const keyTitle = `${TITLE[saving ? 'cloud-key-saving' : 'cloud-key']!} · ${providerDisplayName(provider)}`
329
+ return renderShell(step.kind, keyTitle, (
330
+ <>
286
331
  <Text color={theme.dim}>stored in your OS keyring when available; never written to config.json</Text>
287
- {error ? <Text color="#e87070">{error}</Text> : null}
332
+ {error ? <Text color={theme.accentError}>{error}</Text> : null}
288
333
  <Box marginTop={1}>
289
334
  <TextInput
290
335
  isSecret
@@ -310,18 +355,16 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
310
355
  onCancel={goBack}
311
356
  />
312
357
  </Box>
313
- {saving ? <Text color={theme.dim}>storing key…</Text> : hint(true)}
314
- </Box>
315
- )
358
+ {saving ? <Text color={theme.dim}>storing key…</Text> : null}
359
+ </>
360
+ ), saving ? undefined : navHint(true))
316
361
  }
317
362
 
318
363
  if (step.kind === 'cloud-model') {
319
364
  const { provider } = step
320
365
  const defaultModel = defaultModelFor(provider)
321
- return (
322
- <Box flexDirection="column" padding={1}>
323
- <Splash tipLine={STATUS['cloud-model']} />
324
- <Text color={theme.accentSecondary} bold>which model?</Text>
366
+ return renderShell(step.kind, TITLE['cloud-model']!, (
367
+ <>
325
368
  <Text color={theme.dim}>press enter to accept default: {defaultModel}</Text>
326
369
  <Box marginTop={1}>
327
370
  <TextInput
@@ -340,25 +383,20 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
340
383
  onCancel={goBack}
341
384
  />
342
385
  </Box>
343
- {hint(true)}
344
- </Box>
345
- )
386
+ </>
387
+ ), navHint(true))
346
388
  }
347
389
 
348
390
  if (step.kind === 'saving') {
349
- return (
350
- <Box flexDirection="column" padding={1}>
351
- <Splash tipLine={STATUS['saving']} />
352
- <Text color={theme.dim}>saving config…</Text>
353
- </Box>
354
- )
391
+ return renderShell(step.kind, TITLE['saving']!, (
392
+ <Text color={theme.dim}>saving config…</Text>
393
+ ))
355
394
  }
356
395
 
357
396
  if (step.kind === 'save-error') {
358
- return (
359
- <Box flexDirection="column" padding={1}>
360
- <Splash tipLine={STATUS['save-error']} />
361
- <Text color="#e87070">{step.message}</Text>
397
+ return renderShell(step.kind, TITLE['save-error']!, (
398
+ <>
399
+ <Text color={theme.accentError}>{step.message}</Text>
362
400
  <Box marginTop={1}>
363
401
  <Select<'retry' | 'back' | 'quit'>
364
402
  options={[
@@ -374,18 +412,16 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
374
412
  onCancel={goBack}
375
413
  />
376
414
  </Box>
377
- {hint(history.length > 0)}
378
- </Box>
379
- )
415
+ </>
416
+ ), navHint(history.length > 0))
380
417
  }
381
418
 
382
419
  if (step.kind === 'done') {
383
- return (
384
- <Box flexDirection="column" padding={1}>
385
- <Splash tipLine={`ready · ${step.config.provider} · ${formatModelDisplayName(step.config.provider, step.config.model, { maxLength: 48 })}`} />
386
- <Text color={theme.accentSecondary}>all set.</Text>
387
- </Box>
388
- )
420
+ return renderShell(step.kind, TITLE['done']!, (
421
+ <Text color={theme.accentPeriwinkle}>
422
+ Ready · {providerDisplayName(step.config.provider)} · {formatModelDisplayName(step.config.provider, step.config.model, { maxLength: 48 })}
423
+ </Text>
424
+ ))
389
425
  }
390
426
 
391
427
  return null
@@ -412,3 +448,11 @@ function formatGB(bytes: number): string {
412
448
  const gb = bytes / (1024 * 1024 * 1024)
413
449
  return gb < 10 ? `${gb.toFixed(1)}GB` : `${Math.round(gb)}GB`
414
450
  }
451
+
452
+ function providerDisplayName(provider: ProviderId): string {
453
+ if (provider === 'openai') return 'OpenAI'
454
+ if (provider === 'anthropic') return 'Anthropic'
455
+ if (provider === 'gemini') return 'Gemini'
456
+ if (provider === 'llamacpp') return 'llama.cpp'
457
+ return provider
458
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react'
2
+ import { FlowTimeline } from '../identity/hub/components/FlowTimeline.js'
3
+
4
+ export const FIRST_RUN_STAGES = ['Inspect', 'Identity', 'Model'] as const
5
+
6
+ export type FirstRunStepKind =
7
+ | 'detecting'
8
+ | 'detect-error'
9
+ | 'identity-start'
10
+ | 'identity-start-saving'
11
+ | 'choose-path'
12
+ | 'hf-setup'
13
+ | 'cloud-provider'
14
+ | 'cloud-key'
15
+ | 'cloud-key-saving'
16
+ | 'cloud-model'
17
+ | 'saving'
18
+ | 'save-error'
19
+ | 'done'
20
+
21
+ export function firstRunStageNumber(stepKind: FirstRunStepKind): number {
22
+ switch (stepKind) {
23
+ case 'detecting':
24
+ case 'detect-error':
25
+ return 1
26
+ case 'identity-start':
27
+ case 'identity-start-saving':
28
+ return 2
29
+ case 'choose-path':
30
+ case 'hf-setup':
31
+ case 'cloud-provider':
32
+ case 'cloud-key':
33
+ case 'cloud-key-saving':
34
+ case 'cloud-model':
35
+ return 3
36
+ case 'saving':
37
+ case 'save-error':
38
+ case 'done':
39
+ return 4
40
+ default:
41
+ return 0
42
+ }
43
+ }
44
+
45
+ export const FirstRunTimeline: React.FC<{ current: number }> = ({ current }) => (
46
+ <FlowTimeline steps={[...FIRST_RUN_STAGES]} current={current} />
47
+ )
@@ -105,7 +105,7 @@ export function useAppInput(
105
105
  options: { isActive?: boolean } = {},
106
106
  ): void {
107
107
  const ctx = useContext(AppInputContext)
108
- if (!ctx) throw new Error('useAppInput must be used inside AppInputProvider')
108
+ if (!ctx) throw new Error('Hook useAppInput must be used inside AppInputProvider')
109
109
 
110
110
  const handlerRef = useRef(handler)
111
111
  const isActiveRef = useRef(options.isActive !== false)
@@ -90,7 +90,7 @@ export const KeybindingProvider: React.FC<ProviderProps> = ({ bindings = DEFAULT
90
90
 
91
91
  export function useKeybindingContext(): KeybindingContextValue {
92
92
  const ctx = useContext(Ctx)
93
- if (!ctx) throw new Error('useKeybindingContext requires KeybindingProvider')
93
+ if (!ctx) throw new Error('Hook useKeybindingContext requires KeybindingProvider')
94
94
  return ctx
95
95
  }
96
96
 
@@ -203,7 +203,6 @@ export function ChatBottomPane({
203
203
  <IdentityHub
204
204
  mode="manage"
205
205
  config={config}
206
- cwd={cwd}
207
206
  initialAction={identityOverlay.initialAction}
208
207
  onComplete={handleIdentityResult}
209
208
  onConfigChange={onConfigChange}
@@ -495,14 +495,14 @@ export const ChatInput: React.FC<PromptInputProps> = ({
495
495
 
496
496
  const showPlaceholder = value.length === 0 && !disabled && placeholderHints && placeholderHints.length > 0
497
497
  const placeholder = showPlaceholder ? (placeholderHints[placeholderIdx] ?? placeholderHints[0] ?? '') : ''
498
- const promptColor = mode === 'bash' ? theme.accentWarm : (disabled ? theme.dim : theme.accentMint)
498
+ const promptColor = mode === 'bash' ? theme.accentPeriwinkle : (disabled ? theme.dim : theme.accentPeriwinkle)
499
499
  const promptChar = mode === 'bash' ? '!' : prefix
500
500
 
501
501
  const display = useMemo(
502
502
  () => renderWithCursor(value, cursor, !disabled, wrapWidth, maxVisibleInputLines),
503
503
  [value, cursor, disabled, wrapWidth, maxVisibleInputLines],
504
504
  )
505
- const borderColor = disabled ? theme.border : theme.accentMint
505
+ const borderColor = disabled ? theme.border : theme.accentPeriwinkle
506
506
 
507
507
  return (
508
508
  <Box flexDirection="column" width="100%">
@@ -544,7 +544,7 @@ export const ChatInput: React.FC<PromptInputProps> = ({
544
544
  {showingSlash && filteredSuggestions.length > 0 ? (
545
545
  <Box marginLeft={2} flexDirection="column">
546
546
  {filteredSuggestions.map((s, i) => (
547
- <Text key={s.name} color={i === suggestionIdx ? theme.accentPrimary : theme.dim}>
547
+ <Text key={s.name} color={i === suggestionIdx ? theme.accentPeriwinkle : theme.dim}>
548
548
  {i === suggestionIdx ? '\u203a ' : ' '}/{s.name}
549
549
  <Text color={theme.dim}> {s.summary}{i === suggestionIdx ? (s.executeOnEnter ? ' · enter runs' : ' · enter fills') : ''}</Text>
550
550
  </Text>
@@ -554,7 +554,7 @@ export const ChatInput: React.FC<PromptInputProps> = ({
554
554
  {showingFiles ? (
555
555
  <Box marginLeft={2} flexDirection="column">
556
556
  {fileSuggestions.slice(0, 8).map((s, i) => (
557
- <Text key={s.path} color={i === fileSuggestionIdx ? theme.accentPrimary : theme.dim}>
557
+ <Text key={s.path} color={i === fileSuggestionIdx ? theme.accentPeriwinkle : theme.dim}>
558
558
  {i === fileSuggestionIdx ? '\u203a ' : ' '}@{s.path}
559
559
  <Text color={theme.dim}> {i === fileSuggestionIdx ? 'tab/enter completes' : s.hint}</Text>
560
560
  </Text>
@@ -571,7 +571,7 @@ export const ChatInput: React.FC<PromptInputProps> = ({
571
571
  </Text>
572
572
  {queuedMessages.slice(0, 3).map((message, i) => (
573
573
  <Text key={`${i}-${message.slice(0, 24)}`}>
574
- <Text color={theme.accentMint}>{i === 0 ? '» ' : ' '}</Text>
574
+ <Text color={theme.accentPeriwinkle}>{i === 0 ? '» ' : ' '}</Text>
575
575
  <Text color={theme.textSubtle}>{summarizeQueuedMessage(message)}</Text>
576
576
  </Text>
577
577
  ))}
@@ -652,7 +652,7 @@ export function renderWithCursor(
652
652
  node: (
653
653
  <Text color={theme.text} wrap="wrap">
654
654
  {before}
655
- <Text backgroundColor={theme.accentMint} color="#08110c">{atChar}</Text>
655
+ <Text backgroundColor={theme.accentPeriwinkle} color="#0c0c1f">{atChar}</Text>
656
656
  {after}
657
657
  </Text>
658
658
  ),