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
@@ -0,0 +1,190 @@
1
+ import React from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import { theme } from '../../../ui/theme.js'
4
+ import type { EthagentConfig, EthagentIdentity } from '../../../storage/config.js'
5
+ import {
6
+ displayCustodyMode,
7
+ identityOwnerAddress,
8
+ readCustodyMode,
9
+ readIdentityStateString,
10
+ } from '../model/custody.js'
11
+ import {
12
+ hasPendingPublish,
13
+ localChangeStatusView,
14
+ type LocalChangeStatusView,
15
+ } from '../model/continuity.js'
16
+ import { ensValidationReasonText, selectEnsStatus } from '../model/ens.js'
17
+ import { shortAddress } from '../model/format.js'
18
+ import { identitySummaryRows, lastBackupLabel } from '../model/identity.js'
19
+ import { transferSnapshotView, type TransferSnapshotView } from '../model/transfer.js'
20
+
21
+ import type { ContinuityWorkingTreeStatus } from '../../continuity/storage.js'
22
+
23
+ interface IdentitySummaryProps {
24
+ identity?: EthagentIdentity
25
+ config?: EthagentConfig
26
+ workingStatus?: ContinuityWorkingTreeStatus | null
27
+ hideLocalChanges?: boolean
28
+ tokenLinked?: boolean
29
+ onchainOwner?: string
30
+ }
31
+
32
+ export const IdentitySummary: React.FC<IdentitySummaryProps> = ({ identity, config, workingStatus, hideLocalChanges = false, tokenLinked = true, onchainOwner }) => {
33
+ if (!identity) {
34
+ return (
35
+ <Text color={theme.dim}>No agent yet. Create or load one.</Text>
36
+ )
37
+ }
38
+
39
+ const rows = identitySummaryRows(identity, config)
40
+ const lastBackup = lastBackupLabel(identity)
41
+ const stateName = readIdentityStateString(identity.state, 'name')
42
+
43
+ const row = (label: string) => rows.find(item => item.label === label)
44
+ const localChangeStatus = localChangeStatusView(workingStatus)
45
+
46
+ const ensStatus = selectEnsStatus(identity)
47
+ const custodyMode = readCustodyMode(identity.state)
48
+ const activeOperator = readIdentityStateString(identity.state, 'activeOperatorAddress')
49
+ const approvedOperatorCount = Array.isArray((identity.state as Record<string, unknown> | undefined)?.approvedOperatorWallets)
50
+ ? ((identity.state as Record<string, unknown>).approvedOperatorWallets as unknown[]).length
51
+ : 0
52
+ const ownerAddress = identityOwnerAddress(identity, onchainOwner)
53
+ const transferSnapshot = transferSnapshotView(identity)
54
+
55
+ const tokenValue = row('token')?.value ?? 'Not Created'
56
+ const networkValue = row('network')?.value ?? 'Unknown'
57
+ const tokenLine = identity.agentId
58
+ ? `${tokenValue} · ${displayValue(networkValue)}`
59
+ : displayValue(tokenValue)
60
+
61
+ return (
62
+ <Box flexDirection="column">
63
+ <Text color={theme.accentPeriwinkle} bold>{stateName || 'Active Agent'}</Text>
64
+ <Text color={identity.agentId ? theme.text : theme.dim} bold={Boolean(identity.agentId)}>{tokenLine}</Text>
65
+ <Text>
66
+ <Text color={theme.dim}>{'ENS'.padEnd(12)}</Text>
67
+ {ensStatus.kind === 'linked'
68
+ ? <Text color={theme.accentPeriwinkle}>{ensStatus.name}</Text>
69
+ : ensStatus.kind === 'issue'
70
+ ? <Text color={theme.accentError}>{ensStatus.name} ({ensValidationReasonText(ensStatus.reason)})</Text>
71
+ : <Text color={theme.dim}>Not Linked</Text>}
72
+ </Text>
73
+ {tokenLinked ? (
74
+ <Text>
75
+ <Text color={theme.dim}>{'Custody'.padEnd(12)}</Text>
76
+ <Text color={custodyMode ? theme.text : theme.dim}>{displayCustodyMode(custodyMode)}</Text>
77
+ </Text>
78
+ ) : null}
79
+ {ownerAddress ? (
80
+ <Text>
81
+ <Text color={theme.dim}>{'Owner'.padEnd(12)}</Text>
82
+ <Text color={theme.text}>{shortAddress(ownerAddress)}</Text>
83
+ </Text>
84
+ ) : null}
85
+ {(() => {
86
+ if (custodyMode !== 'advanced') return null
87
+ const vaultAddress = readIdentityStateString(identity.state, 'operatorVaultAddress')
88
+ if (!vaultAddress) return null
89
+ return (
90
+ <Text>
91
+ <Text color={theme.dim}>{'Agent Vault'.padEnd(12)}</Text>
92
+ <Text color={theme.text}>{shortAddress(vaultAddress)}</Text>
93
+ </Text>
94
+ )
95
+ })()}
96
+ {tokenLinked && custodyMode === 'advanced' ? (
97
+ <Text>
98
+ <Text color={theme.dim}>{'Operators'.padEnd(12)}</Text>
99
+ {approvedOperatorCount > 1 ? (
100
+ <Text color={theme.text}>{`${approvedOperatorCount} authorized${activeOperator ? ` (active ${shortAddress(activeOperator)})` : ''}`}</Text>
101
+ ) : activeOperator ? (
102
+ <Text color={theme.text}>{shortAddress(activeOperator)}</Text>
103
+ ) : (
104
+ <Text color={theme.dim}>None Authorized</Text>
105
+ )}
106
+ </Text>
107
+ ) : null}
108
+ <Text>
109
+ <Text color={theme.dim}>{'Last Saved'.padEnd(12)}</Text>
110
+ <Text color={lastBackup === 'never' ? theme.dim : theme.text}>{displayValue(lastBackup)}</Text>
111
+ </Text>
112
+ {hasPendingPublish(identity) ? (
113
+ <Text>
114
+ <Text color={theme.dim}>{'Pending'.padEnd(12)}</Text>
115
+ <Text color={theme.dim}>local snapshot ahead of chain, owner wallet rotates the pointer</Text>
116
+ </Text>
117
+ ) : null}
118
+ {transferSnapshot ? (
119
+ <Box marginTop={1}>
120
+ <TransferSnapshotStatus status={transferSnapshot} />
121
+ </Box>
122
+ ) : null}
123
+ {!hideLocalChanges && (
124
+ <Box marginTop={1}>
125
+ <LocalChangeStatusLine status={localChangeStatus} />
126
+ </Box>
127
+ )}
128
+ </Box>
129
+ )
130
+ }
131
+
132
+ const TransferSnapshotStatus: React.FC<{ status: NonNullable<TransferSnapshotView> }> = ({ status }) => {
133
+ const receiverLabel = status.receiverHandle && status.receiverHandle !== status.receiver
134
+ ? `${shortAddress(status.receiver)} (${status.receiverHandle})`
135
+ : shortAddress(status.receiver)
136
+ const title = status.kind === 'ready-to-transfer'
137
+ ? 'Transfer snapshot ready'
138
+ : 'Transfer snapshot received'
139
+ const detail = status.kind === 'ready-to-transfer'
140
+ ? 'sender can transfer externally'
141
+ : 'receiver can restore from this snapshot'
142
+ return (
143
+ <Box flexDirection="column">
144
+ <Text color={theme.accentPeriwinkle} bold>{title}</Text>
145
+ <Text>
146
+ <Text color={theme.dim}>{'Sender'.padEnd(12)}</Text>
147
+ <Text color={theme.text}>{shortAddress(status.sender)}</Text>
148
+ </Text>
149
+ <Text>
150
+ <Text color={theme.dim}>{'Receiver'.padEnd(12)}</Text>
151
+ <Text color={theme.text}>{receiverLabel}</Text>
152
+ </Text>
153
+ <Text color={theme.textSubtle}>{status.slotCount} decrypt slots · {detail}</Text>
154
+ </Box>
155
+ )
156
+ }
157
+
158
+ const LocalChangeStatusLine: React.FC<{ status: LocalChangeStatusView }> = ({ status }) => {
159
+ if (status.hasLocalChanges) {
160
+ return (
161
+ <Text color={theme.accentError} bold>
162
+ Local changes detected
163
+ {status.files.length > 0 ? `: ${status.files.join(', ')}` : ''}
164
+ </Text>
165
+ )
166
+ }
167
+
168
+ if (!status.detail) return null
169
+
170
+ const color = status.tone === 'ok' || status.tone === 'warn' ? theme.accentPeriwinkle : theme.dim
171
+ const label = status.detail === 'None detected' ? 'No local changes detected' : status.detail
172
+ return <Text color={color}>{label}</Text>
173
+ }
174
+
175
+ function displayValue(value: string): string {
176
+ const mapped = DISPLAY_VALUES[value]
177
+ return mapped ?? value
178
+ }
179
+
180
+ const DISPLAY_VALUES: Record<string, string> = {
181
+ 'not attached': 'Not Attached',
182
+ 'not connected': 'Not Connected',
183
+ 'not created': 'Not Created',
184
+ 'not saved': 'Not Saved',
185
+ 'not saved yet': 'Not Saved Yet',
186
+ 'never': 'Never',
187
+ 'unknown': 'Unknown',
188
+ 'ethereum mainnet': 'Ethereum Mainnet',
189
+ 'base': 'Base',
190
+ }
@@ -0,0 +1,237 @@
1
+ import React from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import { Surface } from '../../../ui/Surface.js'
4
+ import { Select, type SelectOption } from '../../../ui/Select.js'
5
+ import { theme } from '../../../ui/theme.js'
6
+ import type { EthagentConfig, EthagentIdentity } from '../../../storage/config.js'
7
+ import type { ContinuityWorkingTreeStatus } from '../../continuity/storage.js'
8
+ import { identityPerspective, readCustodyMode } from '../model/custody.js'
9
+ import { identityValuesCopyHint } from '../model/copy.js'
10
+ import { transferSnapshotView } from '../model/transfer.js'
11
+ import { IdentitySummary } from './IdentitySummary.js'
12
+ import type { AgentReconciliation } from '../reconciliation/index.js'
13
+ import { menuFlagsFromReconciliation } from './menuFlagsFromReconciliation.js'
14
+
15
+ type MenuScreenProps = {
16
+ mode: 'first-run' | 'manage'
17
+ config?: EthagentConfig
18
+ identity?: EthagentIdentity
19
+ workingStatus?: ContinuityWorkingTreeStatus | null
20
+ canRebackup: boolean
21
+ reconciliation?: AgentReconciliation
22
+ footer: React.ReactNode
23
+ onCreate: () => void
24
+ onLoad: () => void
25
+ onBackupNow: () => void
26
+ onRefetchLatest: () => void
27
+ onPublicProfile: () => void
28
+ onEnsName: () => void
29
+ onWalletSetup: () => void
30
+ onContinuity: () => void
31
+ onIdentityValues: () => void
32
+ onPrepareTransfer: () => void
33
+ onStorage: () => void
34
+ onSkip: () => void
35
+ onCancel: () => void
36
+ }
37
+
38
+ type Action =
39
+ | 'public-profile'
40
+ | 'ens-name'
41
+ | 'wallet-setup'
42
+ | 'continuity'
43
+ | 'backup'
44
+ | 'refetch'
45
+ | 'identity-values'
46
+ | 'prepare-transfer'
47
+ | 'storage'
48
+ | 'create'
49
+ | 'load'
50
+ | 'skip'
51
+ | 'cancel'
52
+
53
+ export const MenuScreen: React.FC<MenuScreenProps> = ({
54
+ mode,
55
+ config,
56
+ identity,
57
+ workingStatus,
58
+ canRebackup,
59
+ reconciliation,
60
+ footer,
61
+ onCreate,
62
+ onLoad,
63
+ onBackupNow,
64
+ onRefetchLatest,
65
+ onPublicProfile,
66
+ onEnsName,
67
+ onWalletSetup,
68
+ onContinuity,
69
+ onIdentityValues,
70
+ onPrepareTransfer,
71
+ onStorage,
72
+ onSkip,
73
+ onCancel,
74
+ }) => {
75
+ const title = mode === 'first-run' ? 'Set Up Agent Identity' : 'Identity Hub'
76
+ const subtitle = mode === 'first-run'
77
+ ? 'Create a portable agent or load one you already own.'
78
+ : 'Manage agent identity, custody, encrypted continuity, and recovery.'
79
+
80
+ const canRefetch = Boolean(canRebackup && identity?.backup?.cid)
81
+ const custodyMode = identity ? readCustodyMode(identity.state) : undefined
82
+
83
+ const perspective = identityPerspective(identity)
84
+ const flags = reconciliation
85
+ ? menuFlagsFromReconciliation(reconciliation, perspective)
86
+ : (perspective === 'operator'
87
+ ? menuFlagsFromReconciliation({
88
+ token: 'unknown', custody: 'unknown', agentUri: 'unknown', ensRecords: 'unknown',
89
+ vault: 'unknown', workingTree: 'unknown', rpc: 'reachable', driftCount: 0, lastCheckedAt: '',
90
+ }, perspective)
91
+ : null)
92
+
93
+ const walletSetupBaseHint = custodyMode === 'advanced'
94
+ ? 'Advanced. Owner wallet, agent vault, authorized operator wallets'
95
+ : 'Simple. Switch to Advanced to delegate URI rotation through a dedicated OperatorVault'
96
+
97
+ const walletSetupLabel = flags?.custodyAsterisk ? 'Custody Mode *' : 'Custody Mode'
98
+ const walletSetupHint = flags?.custodyModeReason ?? flags?.custodyHint ?? walletSetupBaseHint
99
+
100
+ const saveSnapshotLabel = flags?.saveSnapshotAsterisk ? 'Save Snapshot Now *' : 'Save Snapshot Now'
101
+ const saveSnapshotHint = flags?.saveSnapshotHint ?? 'Encrypt and publish latest snapshot'
102
+
103
+ const ensNameHint = flags?.ensNameReason ?? 'Public name or subdomain for this agent'
104
+
105
+ const prepareTransferHint = flags?.prepareTransferReason ?? 'Create transfer snapshot and handoff slots'
106
+
107
+ const tokenValuesHint = flags?.tokenValuesUnlinkedNote ?? identityValuesCopyHint(identity)
108
+
109
+ const options: Array<SelectOption<Action>> = identity
110
+ ? [
111
+ { value: 'public-profile', role: 'section', label: 'Public Identity' },
112
+ { value: 'public-profile', label: 'Public Profile', hint: 'Name, description, icon, and Agent Card' },
113
+ { value: 'ens-name', label: 'ENS Name', hint: ensNameHint, disabled: flags?.ensNameDisabled ?? false },
114
+ { value: 'continuity', role: 'section', label: 'Continuity' },
115
+ { value: 'continuity', label: 'Soul, Memory, Skills', hint: 'SOUL.md, MEMORY.md, skills.json on this device' },
116
+ { value: 'backup', label: saveSnapshotLabel, hint: saveSnapshotHint, disabled: !canRebackup || (flags?.saveSnapshotDisabled ?? false) },
117
+ { value: 'refetch', label: 'Refetch Latest', hint: 'Restore local files from latest saved snapshot', disabled: !canRefetch || (flags?.refetchLatestDisabled ?? false) },
118
+ { value: 'wallet-setup', role: 'section', label: 'Custody' },
119
+ { value: 'wallet-setup', label: walletSetupLabel, hint: walletSetupHint, disabled: !identity.agentId || (flags?.custodyModeDisabled ?? false) },
120
+ { value: 'prepare-transfer', label: 'Prepare Transfer', hint: prepareTransferHint, disabled: flags?.prepareTransferDisabled ?? false },
121
+ { value: 'identity-values', role: 'section', label: 'Token' },
122
+ { value: 'identity-values', label: 'Token Values', hint: tokenValuesHint },
123
+ { value: 'load', label: 'Load Agent', hint: 'Refresh this agent from chain, or load a different one' },
124
+ { value: 'create', label: 'New Agent', hint: 'Mint another token and make it active' },
125
+ { value: 'storage', role: 'section', label: 'Storage' },
126
+ { value: 'storage', label: 'IPFS Storage', hint: 'Publishing credentials for encrypted snapshots' },
127
+ { value: 'cancel', role: 'section', label: 'Exit' },
128
+ { value: 'cancel', label: 'Close Identity Hub', hint: 'Return to chat without changing identity', role: 'utility' },
129
+ ]
130
+ : [
131
+ { value: 'create', role: 'section', label: 'Setup' },
132
+ { value: 'create', label: 'Create New Agent', hint: 'Mint a wallet-owned token for this machine' },
133
+ { value: 'load', label: 'Load Existing Agent', hint: 'Find a token owned by this wallet or linked to it' },
134
+ { value: mode === 'first-run' ? 'skip' : 'cancel', role: 'section', label: 'Exit' },
135
+ ...(mode === 'first-run'
136
+ ? [
137
+ { value: 'skip' as Action, label: 'Skip For Now', hint: 'Continue now, use /identity later', role: 'utility' as const },
138
+ ]
139
+ : [
140
+ { value: 'cancel' as Action, label: 'Close Identity Hub', hint: 'Return to chat without changing identity', role: 'utility' as const },
141
+ ]),
142
+ ]
143
+
144
+ const reconciliationBanner = identity && reconciliation
145
+ ? renderReconciliationBanner(reconciliation, identity)
146
+ : null
147
+
148
+ return (
149
+ <Surface title={title} subtitle={subtitle} footer={footer}>
150
+ <IdentitySummary
151
+ identity={identity}
152
+ config={config}
153
+ workingStatus={workingStatus}
154
+ tokenLinked={reconciliation ? reconciliation.token === 'linked' : true}
155
+ {...(reconciliation?.onChainOwner ? { onchainOwner: reconciliation.onChainOwner } : {})}
156
+ />
157
+ {reconciliationBanner ? (
158
+ <Box marginTop={1} flexDirection="column">
159
+ {reconciliationBanner}
160
+ </Box>
161
+ ) : null}
162
+ <Box marginTop={1}>
163
+ <Select<Action>
164
+ options={options}
165
+ hintLayout="inline"
166
+ onSubmit={choice => {
167
+ if (choice === 'skip') return onSkip()
168
+ if (choice === 'cancel') return onCancel()
169
+ if (choice === 'public-profile') return onPublicProfile()
170
+ if (choice === 'ens-name') return onEnsName()
171
+ if (choice === 'wallet-setup') return onWalletSetup()
172
+ if (choice === 'continuity') return onContinuity()
173
+ if (choice === 'backup') return onBackupNow()
174
+ if (choice === 'refetch') return onRefetchLatest()
175
+ if (choice === 'identity-values') return onIdentityValues()
176
+ if (choice === 'prepare-transfer') return onPrepareTransfer()
177
+ if (choice === 'storage') return onStorage()
178
+ if (choice === 'load') return onLoad()
179
+ if (choice === 'create') return onCreate()
180
+ }}
181
+ onCancel={mode === 'first-run' ? undefined : onCancel}
182
+ />
183
+ </Box>
184
+ </Surface>
185
+ )
186
+ }
187
+
188
+ function renderReconciliationBanner(r: AgentReconciliation, identity: EthagentIdentity): React.ReactNode {
189
+ if (r.token === 'no-agent') return null
190
+ if (r.token === 'unlinked') {
191
+ const tokenLabel = r.tokenAgentId ? `Token #${r.tokenAgentId}` : 'Token'
192
+ const transferSnapshot = transferSnapshotView(identity)
193
+ if (transferSnapshot) {
194
+ return (
195
+ <>
196
+ <Text color={theme.accentError} bold>Agent Unlinked</Text>
197
+ <Text color={theme.textSubtle}>{tokenLabel} was transferred. Local SOUL.md, MEMORY.md, skills.json remain. Back them up before this directory is reused.</Text>
198
+ <Text color={theme.textSubtle}>Use Load Agent or New Agent to re-enable disabled actions.</Text>
199
+ </>
200
+ )
201
+ }
202
+ return (
203
+ <>
204
+ <Text color={theme.accentError} bold>Agent Unlinked</Text>
205
+ <Text color={theme.textSubtle}>{tokenLabel} left without Prepare Transfer. Back up local SOUL.md, MEMORY.md, skills.json before loading another agent.</Text>
206
+ <Text color={theme.textSubtle}>For continuity handoff: ask the new holder to return the token, then run Prepare Transfer before re-sending.</Text>
207
+ <Text color={theme.textSubtle}>Use Load Agent or New Agent to re-enable disabled actions.</Text>
208
+ </>
209
+ )
210
+ }
211
+ if (r.token === 'unknown') {
212
+ return (
213
+ <>
214
+ <Text color={theme.dim}>Ownership Check Failed (RPC?)</Text>
215
+ {r.tokenDetail ? <Text color={theme.dim}>{r.tokenDetail}</Text> : null}
216
+ </>
217
+ )
218
+ }
219
+ if (r.driftCount === 0) {
220
+ return null
221
+ }
222
+ const lines: string[] = []
223
+ if (r.custody === 'mid-flow-uri-pending') lines.push('Advanced setup pending. Open Custody Mode to finish.')
224
+ if (r.agentUri === 'local-newer') lines.push('Local state newer than chain. Save Snapshot Now to publish.')
225
+ if (r.agentUri === 'chain-newer') lines.push('Onchain agentURI is newer than local. Refetch Latest.')
226
+ if (r.ensRecords === 'drift') lines.push('ENS records out of sync. Open Custody Mode to Fix Records.')
227
+ if (r.vault === 'missing') lines.push('Recorded vault address has no contract at it. Open Custody Mode to redeploy.')
228
+ if (r.workingTree === 'dirty') lines.push('Local edits pending. Save Snapshot Now to publish.')
229
+ return (
230
+ <>
231
+ <Text color={theme.accentPeriwinkle} bold>Agent Linked. {lines.length} item{lines.length === 1 ? '' : 's'} need attention</Text>
232
+ {lines.map((line, i) => (
233
+ <Text key={i} color={theme.textSubtle}>· {line}</Text>
234
+ ))}
235
+ </>
236
+ )
237
+ }
@@ -3,7 +3,7 @@ import { Surface } from '../../../ui/Surface.js'
3
3
  import { Select, type SelectOption } from '../../../ui/Select.js'
4
4
  import type { SelectableNetwork } from '../../../storage/config.js'
5
5
  import { SELECTABLE_NETWORKS } from '../../../storage/config.js'
6
- import { networkLabel, networkSubtitle } from '../identityHubModel.js'
6
+ import { networkLabel, networkSubtitle } from '../model/network.js'
7
7
 
8
8
  type NetworkScreenProps = {
9
9
  subtitle: string
@@ -14,9 +14,9 @@ type NetworkScreenProps = {
14
14
 
15
15
  export const NetworkScreen: React.FC<NetworkScreenProps> = ({ subtitle, footer, onSelect, onCancel }) => {
16
16
  const options: Array<SelectOption<SelectableNetwork>> = [
17
- { value: 'mainnet', role: 'section', prefix: '--', label: 'Main Network' },
17
+ { value: 'mainnet', role: 'section', label: 'High Security' },
18
18
  networkOption('mainnet'),
19
- { value: 'arbitrum', role: 'section', prefix: '--', label: 'Lower-Fee Networks' },
19
+ { value: 'base', role: 'section', label: 'Lower Cost' },
20
20
  ...SELECTABLE_NETWORKS.filter(network => network !== 'mainnet').map(networkOption),
21
21
  ]
22
22
 
@@ -4,34 +4,38 @@ import { Surface } from '../../../ui/Surface.js'
4
4
  import { TextInput } from '../../../ui/TextInput.js'
5
5
  import { theme } from '../../../ui/theme.js'
6
6
  import { extractPinataJwt } from '../../storage/ipfs.js'
7
- import type { Step } from '../identityHubReducer.js'
8
7
 
9
8
  const PINATA_API_KEYS_URL = 'https://app.pinata.cloud/developers/api-keys'
10
9
 
11
- type RebackupStorageScreenProps = {
12
- step: Extract<Step, { kind: 'rebackup-storage' | 'public-profile-storage' }>
10
+ type PinataJwtInputProps = {
11
+ inputKey: string
12
+ title?: string
13
+ subtitle?: string
13
14
  footer: React.ReactNode
14
15
  onSubmit: (input: string) => void
15
16
  onCancel: () => void
16
17
  }
17
18
 
18
- export const RebackupStorageScreen: React.FC<RebackupStorageScreenProps> = ({ step, footer, onSubmit, onCancel }) => {
19
- const publicOnly = step.kind === 'public-profile-storage'
20
- return (
21
- <Surface
22
- title="Connect IPFS Storage"
23
- subtitle={step.error ?? (publicOnly
24
- ? 'Save a Pinata JWT so ethagent can pin public profile metadata to IPFS.'
25
- : 'Save a Pinata JWT so ethagent can pin encrypted state to IPFS.')}
26
- footer={footer}
27
- >
19
+ export const PinataJwtInput: React.FC<PinataJwtInputProps> = ({
20
+ inputKey,
21
+ title,
22
+ subtitle,
23
+ footer,
24
+ onSubmit,
25
+ onCancel,
26
+ }) => (
27
+ <Surface
28
+ title={title ?? 'Connect IPFS Storage'}
29
+ subtitle={subtitle ?? 'Save a Pinata JWT so ethagent can pin encrypted state to IPFS.'}
30
+ footer={footer}
31
+ >
28
32
  <Text>
29
33
  <Text color={theme.dim}>Paste your Pinata JWT. Get one at </Text>
30
- <Text color={theme.accentPrimary} underline>{PINATA_API_KEYS_URL}</Text>
34
+ <Text color={theme.accentPeriwinkle} underline>{PINATA_API_KEYS_URL}</Text>
31
35
  </Text>
32
36
  <Text color={theme.dim}>Saved encrypted on this device · used only for IPFS pinning</Text>
33
37
  <TextInput
34
- key="rebackup-storage"
38
+ key={inputKey}
35
39
  isSecret
36
40
  placeholder="Pinata JWT"
37
41
  validate={v => {
@@ -45,6 +49,5 @@ export const RebackupStorageScreen: React.FC<RebackupStorageScreenProps> = ({ st
45
49
  onSubmit={onSubmit}
46
50
  onCancel={onCancel}
47
51
  />
48
- </Surface>
49
- )
50
- }
52
+ </Surface>
53
+ )
@@ -0,0 +1,76 @@
1
+ import React from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import { Surface } from '../../../ui/Surface.js'
4
+ import { Select } from '../../../ui/Select.js'
5
+ import { theme } from '../../../ui/theme.js'
6
+ import { transferSnapshotView } from '../model/transfer.js'
7
+ import type { EthagentIdentity } from '../../../storage/config.js'
8
+
9
+ type UnlinkedIdentityScreenProps = {
10
+ identity?: EthagentIdentity
11
+ agentId?: string
12
+ onLoadAgent: () => void
13
+ onOpenMenu: () => void
14
+ onRetry?: () => void
15
+ onCancel: () => void
16
+ }
17
+
18
+ type Action = 'load-agent' | 'open-menu' | 'retry'
19
+
20
+ export const UnlinkedIdentityScreen: React.FC<UnlinkedIdentityScreenProps> = ({
21
+ identity,
22
+ agentId,
23
+ onLoadAgent,
24
+ onOpenMenu,
25
+ onRetry,
26
+ onCancel,
27
+ }) => {
28
+ const options: Array<{ value: Action; label: string; hint?: string; role?: 'section' | 'utility' }> = [
29
+ { value: 'load-agent', role: 'section', label: 'Load Agent' },
30
+ { value: 'load-agent', label: 'Load Agent', hint: 'Reconnect this token by signing with the current owner wallet, or load a different one' },
31
+ { value: 'open-menu', role: 'section', label: 'Identity Hub' },
32
+ { value: 'open-menu', label: 'Open Identity Hub', hint: 'Browse local identity, continuity files, and settings without reconnecting' },
33
+ ]
34
+ if (onRetry) {
35
+ options.push({ value: 'retry', role: 'section', label: 'Recheck' })
36
+ options.push({ value: 'retry', label: 'Retry Ownership Check', hint: 'Re-query the chain to confirm the current owner', role: 'utility' })
37
+ }
38
+
39
+ const tokenLabel = agentId ? `Token #${agentId}` : 'Token'
40
+ const transferSnapshot = transferSnapshotView(identity)
41
+
42
+ return (
43
+ <Surface
44
+ title="No Linked Agent"
45
+ subtitle="The agent token recorded locally is not currently owned by your wallet."
46
+ footer={<Text color={theme.dim}>enter selects, esc back</Text>}
47
+ >
48
+ <Box flexDirection="column">
49
+ {transferSnapshot ? (
50
+ <Text color={theme.textSubtle}>
51
+ {tokenLabel} was transferred. Local SOUL.md, MEMORY.md, skills.json remain. Back them up before this directory is reused.
52
+ </Text>
53
+ ) : (
54
+ <>
55
+ <Text color={theme.accentPeriwinkle}>{tokenLabel} left this wallet without Prepare Transfer, so the new holder has no continuity handoff.</Text>
56
+ <Text color={theme.textSubtle}>
57
+ Local SOUL.md, MEMORY.md, skills.json remain. Back them up before this directory is reused.
58
+ </Text>
59
+ </>
60
+ )}
61
+ </Box>
62
+ <Box marginTop={1}>
63
+ <Select<Action>
64
+ options={options}
65
+ hintLayout="inline"
66
+ onSubmit={choice => {
67
+ if (choice === 'load-agent') return onLoadAgent()
68
+ if (choice === 'open-menu') return onOpenMenu()
69
+ if (choice === 'retry') return onRetry?.()
70
+ }}
71
+ onCancel={onCancel}
72
+ />
73
+ </Box>
74
+ </Surface>
75
+ )
76
+ }
@@ -1,26 +1,28 @@
1
1
  import React from 'react'
2
2
  import { Box, Text } from 'ink'
3
- import { exec } from 'node:child_process'
4
3
  import { Surface } from '../../../ui/Surface.js'
5
4
  import { Spinner } from '../../../ui/Spinner.js'
6
5
  import { theme } from '../../../ui/theme.js'
7
6
  import { useAppInput } from '../../../app/input/AppInputProvider.js'
7
+ import { openExternalUrl } from '../../../utils/openExternal.js'
8
8
  import type { BrowserWalletReady } from '../../wallet/browserWallet.js'
9
9
 
10
10
  type WalletApprovalScreenProps = {
11
11
  title: string
12
- subtitle: string
12
+ subtitle: React.ReactNode
13
13
  walletSession: BrowserWalletReady | null
14
14
  label: string
15
15
  onCancel?: () => void
16
16
  }
17
17
 
18
+ export const OPEN_BROWSER_HINT = 'Press enter to open in browser...'
19
+ const PREPARING_WALLET_REQUEST_LABEL = 'preparing wallet request...'
20
+
18
21
  export const WalletApprovalScreen: React.FC<WalletApprovalScreenProps> = ({ title, subtitle, walletSession, label, onCancel }) => {
19
22
  useAppInput((_input, key) => {
20
23
  if (key.escape && onCancel) onCancel()
21
24
  if (key.return && walletSession?.url) {
22
- const command = process.platform === 'win32' ? 'start ""' : process.platform === 'darwin' ? 'open' : 'xdg-open'
23
- exec(`${command} "${walletSession.url}"`).on('error', () => {})
25
+ openExternalUrl(walletSession.url)
24
26
  }
25
27
  }, { isActive: Boolean(onCancel) || Boolean(walletSession) })
26
28
  const footer = onCancel ? <Text color={theme.dim}>esc cancels</Text> : undefined
@@ -28,15 +30,14 @@ export const WalletApprovalScreen: React.FC<WalletApprovalScreenProps> = ({ titl
28
30
  <Surface title={title} subtitle={subtitle} footer={footer}>
29
31
  {walletSession ? (
30
32
  <Box flexDirection="column">
31
- <Text color={theme.dim}>open this approval page</Text>
32
- <Text color={theme.accentPrimary}>{walletSession.url}</Text>
33
- <Text color={theme.dim}>press enter to open in browser...</Text>
33
+ <Text color={theme.accentBlue} underline>{walletSession.url}</Text>
34
+ <Text color={theme.dim}>{OPEN_BROWSER_HINT}</Text>
34
35
  <Box marginTop={1}>
35
36
  <Spinner label={label} />
36
37
  </Box>
37
38
  </Box>
38
39
  ) : (
39
- <Spinner label="preparing wallet approval..." />
40
+ <Spinner label={PREPARING_WALLET_REQUEST_LABEL} />
40
41
  )}
41
42
  </Surface>
42
43
  )