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
@@ -1,7 +1,14 @@
1
- import React, { useState } from 'react'
2
- import { Box, Text } from 'ink'
1
+ import React, { useState, useRef, useEffect } from 'react'
2
+ import { Box, Text, useStdout } from 'ink'
3
3
  import { theme } from './theme.js'
4
4
  import { useAppInput } from '../app/input/AppInputProvider.js'
5
+ import { moveVerticalVisual } from '../chat/chatInputState.js'
6
+ import {
7
+ getVisualLineIndex,
8
+ getVisualLines,
9
+ } from '../chat/textCursor.js'
10
+
11
+ const DEFAULT_CHROME_WIDTH = 10
5
12
 
6
13
  type TextInputProps = {
7
14
  label?: string
@@ -9,10 +16,19 @@ type TextInputProps = {
9
16
  isSecret?: boolean
10
17
  initialValue?: string
11
18
  allowEmpty?: boolean
19
+ multiline?: boolean
20
+ chromeWidth?: number
12
21
  maxLength?: number
13
22
  validate?: (value: string) => string | null
14
23
  onSubmit: (value: string) => void
15
24
  onCancel?: () => void
25
+ onNavigateLeft?: () => void
26
+ onNavigateRight?: (value: string) => void
27
+ }
28
+
29
+ type RenderedTextInputLine = {
30
+ visualLineIndex: number
31
+ node: React.ReactNode
16
32
  }
17
33
 
18
34
  export function TextInput({
@@ -21,50 +37,126 @@ export function TextInput({
21
37
  isSecret,
22
38
  initialValue = '',
23
39
  allowEmpty = false,
40
+ multiline = false,
41
+ chromeWidth = DEFAULT_CHROME_WIDTH,
24
42
  maxLength = 4096,
25
43
  validate,
26
44
  onSubmit,
27
45
  onCancel,
46
+ onNavigateLeft,
47
+ onNavigateRight,
28
48
  }: TextInputProps) {
49
+ const { stdout } = useStdout()
29
50
  const [value, setValue] = useState(initialValue)
51
+ const [cursor, setCursor] = useState(initialValue.length)
52
+ const [preferredColumn, setPreferredColumn] = useState<number | null>(null)
30
53
  const [error, setError] = useState<string | null>(null)
31
54
 
55
+ const [columns, setColumns] = useState<number>(() => Math.floor(stdout?.columns ?? 80))
56
+ useEffect(() => {
57
+ if (!stdout) return
58
+ const handleResize = () => setColumns(Math.floor(stdout.columns ?? 80))
59
+ stdout.on('resize', handleResize)
60
+ return () => { stdout.off('resize', handleResize) }
61
+ }, [stdout])
62
+
63
+ const wrapWidth = textInputWrapWidth(columns, chromeWidth)
64
+
65
+ const stateRef = useRef({ value, cursor, preferredColumn, wrapWidth })
66
+ stateRef.current = { value, cursor, preferredColumn, wrapWidth }
67
+
32
68
  useAppInput((input, key) => {
33
- if (key.return) {
34
- if (!allowEmpty && value.trim().length === 0) {
69
+ const { value: val, cursor: cur, preferredColumn: prefCol, wrapWidth: ww } = stateRef.current
70
+
71
+ const submitValue = (submit: (value: string) => void) => {
72
+ if (!allowEmpty && val.trim().length === 0) {
35
73
  setError('value cannot be empty')
36
- return
74
+ return false
37
75
  }
38
- const validationError = validate?.(value) ?? null
76
+ const validationError = validate?.(val) ?? null
39
77
  if (validationError) {
40
78
  setError(validationError)
41
- return
79
+ return false
42
80
  }
43
81
  setError(null)
44
- onSubmit(value)
82
+ submit(val)
83
+ return true
84
+ }
85
+
86
+ if (multiline && isTextInputSoftBreak(key)) {
87
+ const next = insertTextInputText(val, cur, '\n', maxLength)
88
+ setValue(next.value)
89
+ setCursor(next.cursor)
90
+ setPreferredColumn(null)
91
+ if (error) setError(null)
92
+ return
93
+ }
94
+ if (key.return) {
95
+ submitValue(onSubmit)
45
96
  return
46
97
  }
47
- if (key.escape) {
98
+ if (key.escape || (key.ctrl && input === 'c')) {
48
99
  onCancel?.()
49
100
  return
50
101
  }
102
+ if (key.leftArrow) {
103
+ if (onNavigateLeft && cur === 0) {
104
+ onNavigateLeft()
105
+ return
106
+ }
107
+ setCursor(Math.max(0, cur - 1))
108
+ setPreferredColumn(null)
109
+ return
110
+ }
111
+ if (key.rightArrow) {
112
+ if (onNavigateRight && cur === val.length) {
113
+ submitValue(onNavigateRight)
114
+ return
115
+ }
116
+ setCursor(Math.min(val.length, cur + 1))
117
+ setPreferredColumn(null)
118
+ return
119
+ }
120
+ if (multiline && (key.upArrow || key.downArrow)) {
121
+ const result = moveVerticalVisual(val, cur, key.upArrow ? -1 : 1, ww, prefCol)
122
+ if (result.kind === 'moved') setCursor(result.cursor)
123
+ setPreferredColumn(result.preferredColumn)
124
+ return
125
+ }
51
126
  if (key.backspace || key.delete) {
52
- setValue(v => v.slice(0, -1))
127
+ if (cur === 0) return
128
+ setValue(val.slice(0, cur - 1) + val.slice(cur))
129
+ setCursor(cur - 1)
130
+ setPreferredColumn(null)
53
131
  if (error) setError(null)
54
132
  return
55
133
  }
56
134
  if (key.ctrl && input === 'u') {
57
- setValue('')
135
+ const lineStart = val.lastIndexOf('\n', cur - 1) + 1
136
+ if (lineStart === cur) {
137
+ if (!multiline || cur === 0) return
138
+ setValue(val.slice(0, cur - 1) + val.slice(cur))
139
+ setCursor(cur - 1)
140
+ } else {
141
+ setValue(val.slice(0, lineStart) + val.slice(cur))
142
+ setCursor(lineStart)
143
+ }
144
+ setPreferredColumn(null)
58
145
  if (error) setError(null)
59
146
  return
60
147
  }
61
- if (key.ctrl || key.meta || key.leftArrow || key.rightArrow || key.upArrow || key.downArrow || key.tab) {
148
+ if (key.ctrl || key.meta || key.upArrow || key.downArrow || key.tab) {
62
149
  return
63
150
  }
64
151
  if (input) {
65
- const clean = input.replace(/[\r\n]/g, '')
152
+ const clean = multiline
153
+ ? input.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
154
+ : input.replace(/[\r\n]/g, '')
66
155
  if (clean) {
67
- setValue(v => (v + clean).slice(0, maxLength))
156
+ const next = insertTextInputText(val, cur, clean, maxLength)
157
+ setValue(next.value)
158
+ setCursor(next.cursor)
159
+ setPreferredColumn(null)
68
160
  if (error) setError(null)
69
161
  }
70
162
  }
@@ -72,26 +164,96 @@ export function TextInput({
72
164
 
73
165
  const display = isSecret ? '*'.repeat(value.length) : value
74
166
  const showPlaceholder = value.length === 0 && placeholder
167
+ const renderedLines = multiline
168
+ ? renderTextInputLines(display, cursor, true, wrapWidth)
169
+ : []
75
170
 
76
171
  return (
77
172
  <Box flexDirection="column">
78
173
  {label ? <Text color={theme.dim}>{label}</Text> : null}
79
- <Box flexDirection="row">
80
- <Text color={theme.accentPrimary}>{'> '}</Text>
81
- {showPlaceholder ? (
82
- <>
83
- <Text color={theme.accentPrimary}>|</Text>
84
- <Text color={theme.dim}>{placeholder}</Text>
85
- </>
86
- ) : (
87
- <>
88
- <Text color={theme.text}>{display}</Text>
89
- <Text color={theme.accentPrimary}>|</Text>
90
- </>
91
- )}
92
- </Box>
93
- {error ? <Text color="#e87070">{error}</Text> : null}
174
+ {multiline && !showPlaceholder ? (
175
+ <Box flexDirection="column">
176
+ {renderedLines.map(line => (
177
+ <Box key={line.visualLineIndex} flexDirection="row">
178
+ <Text color={line.visualLineIndex === 0 ? theme.accentPeriwinkle : theme.dim}>
179
+ {line.visualLineIndex === 0 ? '> ' : ' '}
180
+ </Text>
181
+ <Box width={wrapWidth}>{line.node}</Box>
182
+ </Box>
183
+ ))}
184
+ </Box>
185
+ ) : (
186
+ <Box flexDirection="row">
187
+ <Text color={theme.accentPeriwinkle}>{'> '}</Text>
188
+ <Box width={wrapWidth}>
189
+ {showPlaceholder ? (
190
+ <Text wrap={multiline ? 'wrap' : 'truncate-end'}>
191
+ <Text backgroundColor={theme.accentPeriwinkle} color="#0c0c1f">{' '}</Text>
192
+ <Text color={theme.dim}>{placeholder}</Text>
193
+ </Text>
194
+ ) : (
195
+ <Text color={theme.text} wrap="truncate-end">
196
+ {display.slice(0, cursor)}
197
+ <Text backgroundColor={theme.accentPeriwinkle} color="#0c0c1f">{display[cursor] ?? ' '}</Text>
198
+ {display.slice(cursor + 1)}
199
+ </Text>
200
+ )}
201
+ </Box>
202
+ </Box>
203
+ )}
204
+ {error ? <Text color={theme.accentError}>{error}</Text> : null}
94
205
  </Box>
95
206
  )
96
207
  }
97
208
 
209
+ export function textInputWrapWidth(columns: number, chromeWidth = DEFAULT_CHROME_WIDTH): number {
210
+ return Math.max(1, Math.floor(columns) - Math.max(0, Math.floor(chromeWidth)))
211
+ }
212
+
213
+ export function insertTextInputText(value: string, cursor: number, input: string, maxLength = 4096): { value: string; cursor: number } {
214
+ const cleanCursor = Math.max(0, Math.min(cursor, value.length))
215
+ const next = (value.slice(0, cleanCursor) + input + value.slice(cleanCursor)).slice(0, maxLength)
216
+ return {
217
+ value: next,
218
+ cursor: Math.min(cleanCursor + input.length, next.length),
219
+ }
220
+ }
221
+
222
+ export function isTextInputSoftBreak(key: { return: boolean; shift?: boolean; meta?: boolean }): boolean {
223
+ return key.return && Boolean(key.shift || key.meta)
224
+ }
225
+
226
+ export function renderTextInputLines(
227
+ value: string,
228
+ cursor: number,
229
+ showCursor: boolean,
230
+ wrapWidth: number,
231
+ ): RenderedTextInputLine[] {
232
+ const lines = getVisualLines(value, wrapWidth)
233
+ const cursorLine = getVisualLineIndex(lines, cursor)
234
+
235
+ return lines.map((line, visualLineIndex) => {
236
+ const text = value.slice(line.start, line.end)
237
+ if (!showCursor || visualLineIndex !== cursorLine) {
238
+ return {
239
+ visualLineIndex,
240
+ node: <Text color={theme.text} wrap="wrap">{text || ' '}</Text>,
241
+ }
242
+ }
243
+
244
+ const column = Math.max(0, Math.min(cursor - line.start, text.length))
245
+ const before = text.slice(0, column)
246
+ const atChar = text[column] ?? ' '
247
+ const after = text.slice(column + 1)
248
+ return {
249
+ visualLineIndex,
250
+ node: (
251
+ <Text color={theme.text} wrap="wrap">
252
+ {before}
253
+ <Text backgroundColor={theme.accentPeriwinkle} color="#0c0c1f">{atChar}</Text>
254
+ {after}
255
+ </Text>
256
+ ),
257
+ }
258
+ })
259
+ }
package/src/ui/theme.ts CHANGED
@@ -1,29 +1,16 @@
1
1
  export const palette: Array<[number, number, number]> = [
2
2
  [0xff, 0xff, 0xff],
3
- [0xf2, 0xf9, 0xf4],
4
- [0xe7, 0xf4, 0xec],
5
- [0xd4, 0xee, 0xdd],
6
- [0xe7, 0xf4, 0xec],
3
+ [0xd8, 0xdc, 0xfa],
4
+ [0xe8, 0xee, 0xfd],
5
+ [0xd8, 0xdc, 0xfa],
7
6
  [0xff, 0xff, 0xff],
8
7
  ]
9
8
 
10
- export const eyePalette: Array<[number, number, number]> = [
11
- [0xf5, 0xd8, 0xd8],
12
- [0xf5, 0xe7, 0xcf],
13
- [0xf5, 0xf0, 0xd4],
14
- [0xd4, 0xee, 0xdd],
15
- [0xd4, 0xe6, 0xf5],
16
- ] as const
17
-
18
9
  export const theme = {
19
- accentPrimary: '#d4eedd',
20
- accentWarm: '#d8cda8',
21
- accentNeutral: '#e4e3b5',
22
- accentSecondary: '#c0e3cb',
23
- accentMint: '#e7f4ec',
24
- accentPeach: '#e7cdb7',
25
- accentLavender: '#d9cae8',
26
- accentInfo: '#90b8e8',
10
+ accentPeriwinkle: '#d8dcfa',
11
+ accentBlue: '#e8eefd',
12
+ accentWhite: '#f5f8ff',
13
+ accentError: '#d99898',
27
14
  border: '#555555',
28
15
  dim: '#777777',
29
16
  text: '#f1f1f1',
@@ -43,17 +30,3 @@ export function gradientColor(t: number): string {
43
30
  const b = Math.round(b1 + (b2 - b1) * f)
44
31
  return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
45
32
  }
46
-
47
- export function eyeGradientColor(t: number): string {
48
- const s = Math.max(0, Math.min(1, t)) * (eyePalette.length - 1)
49
- const i = Math.min(Math.floor(s), eyePalette.length - 2)
50
- const f = s - i
51
- const lo = eyePalette[i] ?? eyePalette[0]!
52
- const hi = eyePalette[i + 1] ?? eyePalette[eyePalette.length - 1]!
53
- const [r1, g1, b1] = lo
54
- const [r2, g2, b2] = hi
55
- const r = Math.round(r1 + (r2 - r1) * f)
56
- const g = Math.round(g1 + (g2 - g1) * f)
57
- const b = Math.round(b1 + (b2 - b1) * f)
58
- return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
59
- }
@@ -0,0 +1,21 @@
1
+ import { spawn } from 'node:child_process'
2
+
3
+ export function openExternalUrl(url: string): void {
4
+ const target = url.trim()
5
+ if (!target) return
6
+ const command = process.platform === 'win32'
7
+ ? 'cmd'
8
+ : process.platform === 'darwin'
9
+ ? 'open'
10
+ : 'xdg-open'
11
+ const args = process.platform === 'win32'
12
+ ? ['/c', 'start', '', target]
13
+ : [target]
14
+ const child = spawn(command, args, {
15
+ detached: true,
16
+ stdio: 'ignore',
17
+ windowsHide: true,
18
+ })
19
+ child.on('error', () => {})
20
+ child.unref()
21
+ }
@@ -6,6 +6,12 @@ export type RetryClassification = {
6
6
  code?: string
7
7
  }
8
8
 
9
+ export type RetryBodyHint = {
10
+ retryAfterMs?: number
11
+ fatal?: boolean
12
+ reason?: string
13
+ }
14
+
9
15
  export type RetryPolicy = {
10
16
  maxRetries: number
11
17
  baseDelayMs: number
@@ -179,6 +185,7 @@ export type FetchWithRetryOptions = {
179
185
  retryAfterCapMs?: number
180
186
  jitterRatio?: number
181
187
  rateLimitResetProvider?: RateLimitResetProvider
188
+ parseRetryHintFromBody?: (body: string) => RetryBodyHint | undefined
182
189
  signal?: AbortSignal
183
190
  fetchImpl?: typeof fetch
184
191
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>
@@ -205,10 +212,36 @@ export async function fetchWithRetry(
205
212
  const response = await fetchImpl(input, { ...init, signal: options.signal })
206
213
  if (response.ok) return response
207
214
 
208
- const classification = classifyRetryableProviderResponse(response, now(), options.rateLimitResetProvider)
209
- if (!classification.retryable || attempt > policy.maxRetries) return response
215
+ let classification = classifyRetryableProviderResponse(response, now(), options.rateLimitResetProvider)
216
+ let bufferedResponse: Response | undefined
217
+ if (
218
+ classification.retryable
219
+ && response.status === 429
220
+ && options.parseRetryHintFromBody
221
+ ) {
222
+ let bodyText = ''
223
+ try { bodyText = await response.text() } catch { /* ignore */ }
224
+ bufferedResponse = new Response(bodyText, {
225
+ status: response.status,
226
+ statusText: response.statusText,
227
+ headers: response.headers,
228
+ })
229
+ const hint = bodyText ? safeParseRetryHint(bodyText, options.parseRetryHintFromBody) : undefined
230
+ if (hint?.fatal) {
231
+ return bufferedResponse
232
+ }
233
+ if (hint?.retryAfterMs !== undefined) {
234
+ classification = { ...classification, retryAfterMs: hint.retryAfterMs }
235
+ }
236
+ if (hint?.reason) {
237
+ classification = { ...classification, reason: `${classification.reason} ${hint.reason}` }
238
+ }
239
+ }
240
+ if (!classification.retryable || attempt > policy.maxRetries) return bufferedResponse ?? response
210
241
 
211
- try { await response.body?.cancel() } catch { /* ignore */ }
242
+ if (!bufferedResponse) {
243
+ try { await response.body?.cancel() } catch { /* ignore */ }
244
+ }
212
245
  const delayMs = computeBackoffMs(
213
246
  attempt,
214
247
  classification.retryAfterMs,
@@ -243,6 +276,17 @@ export async function fetchWithRetry(
243
276
  throw lastError ?? new Error('fetchWithRetry exhausted')
244
277
  }
245
278
 
279
+ function safeParseRetryHint(
280
+ body: string,
281
+ parser: (body: string) => RetryBodyHint | undefined,
282
+ ): RetryBodyHint | undefined {
283
+ try {
284
+ return parser(body)
285
+ } catch {
286
+ return undefined
287
+ }
288
+ }
289
+
246
290
  function retryEvent(
247
291
  attempt: number,
248
292
  maxRetries: number,