ethagent 1.1.2 → 2.0.1

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 +124 -32
  3. package/package.json +8 -3
  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
@@ -3,7 +3,6 @@ import { z } from 'zod'
3
3
  import {
4
4
  continuityVaultRef,
5
5
  ensureContinuityFiles,
6
- type PrivateContinuityFile,
7
6
  } from '../identity/continuity/storage.js'
8
7
  import type { Tool } from './contracts.js'
9
8
 
@@ -71,7 +70,7 @@ function preparePrivateContinuityRead(
71
70
  ) {
72
71
  const identity = config?.identity
73
72
  if (!identity) {
74
- throw new Error('no active identity; create or load an identity before reading private continuity files')
73
+ throw new Error('No active identity; create or load an identity before reading private continuity files')
75
74
  }
76
75
  const ref = continuityVaultRef(identity)
77
76
  const fullPath = input.file === 'SOUL.md' ? ref.soulPath : ref.memoryPath
@@ -1,4 +1,5 @@
1
1
  import type { AnthropicToolDefinition } from '../providers/anthropic.js'
2
+ import type { GeminiToolDefinition } from '../providers/gemini.js'
2
3
  import type { OpenAIToolDefinition } from '../providers/openai-chat.js'
3
4
  import type { Tool } from './contracts.js'
4
5
  import { modePolicy, type SessionMode } from '../runtime/sessionMode.js'
@@ -65,3 +66,32 @@ export function openAITools(mode: SessionMode = 'chat', context: ToolAvailabilit
65
66
  },
66
67
  }))
67
68
  }
69
+
70
+ const GEMINI_DROP_KEYS = new Set([
71
+ 'additionalProperties',
72
+ '$schema',
73
+ '$ref',
74
+ '$defs',
75
+ 'definitions',
76
+ ])
77
+
78
+ function sanitizeForGemini(schema: unknown): unknown {
79
+ if (Array.isArray(schema)) return schema.map(sanitizeForGemini)
80
+ if (schema && typeof schema === 'object') {
81
+ const out: Record<string, unknown> = {}
82
+ for (const [k, v] of Object.entries(schema as Record<string, unknown>)) {
83
+ if (GEMINI_DROP_KEYS.has(k)) continue
84
+ out[k] = sanitizeForGemini(v)
85
+ }
86
+ return out
87
+ }
88
+ return schema
89
+ }
90
+
91
+ export function geminiTools(mode: SessionMode = 'chat', context: ToolAvailabilityContext = {}): GeminiToolDefinition[] {
92
+ return toolsForMode(mode, context).map(tool => ({
93
+ name: tool.name,
94
+ description: tool.description,
95
+ parameters: sanitizeForGemini(tool.inputSchemaJson) as GeminiToolDefinition['parameters'],
96
+ }))
97
+ }
@@ -78,7 +78,7 @@ async function prepareWrite(
78
78
  assertSafeWritePath(input.path)
79
79
  assertNotPrivateContinuityWorkspacePath(input.path, context.config, 'write_file')
80
80
  if (input.content.length === 0) {
81
- throw new Error('write_file content is empty; provide non-empty file contents')
81
+ throw new Error('Tool write_file content is empty; provide non-empty file contents')
82
82
  }
83
83
 
84
84
  const fullPath = resolveWorkspacePath(context.workspaceRoot, input.path)
@@ -91,7 +91,7 @@ async function prepareWrite(
91
91
  async function readExistingFile(fullPath: string): Promise<{ before: string; existedBefore: boolean }> {
92
92
  try {
93
93
  const stats = await fs.stat(fullPath)
94
- if (stats.isDirectory()) throw new Error('write_file path points to a directory; provide a file path')
94
+ if (stats.isDirectory()) throw new Error('Tool write_file path points to a directory; provide a file path')
95
95
  return { before: await fs.readFile(fullPath, 'utf8'), existedBefore: true }
96
96
  } catch (error: unknown) {
97
97
  if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { before: '', existedBefore: false }
@@ -113,13 +113,13 @@ async function tryRecordRewindSnapshot(
113
113
  function assertSafeWritePath(requestedPath: string): void {
114
114
  const trimmed = requestedPath.trim()
115
115
  if (trimmed !== requestedPath || trimmed.length === 0) {
116
- throw new Error('write_file path must be a clean workspace-relative file path')
116
+ throw new Error('Tool write_file path must be a clean workspace-relative file path')
117
117
  }
118
118
  if (/[|;&<>`]/.test(trimmed)) {
119
- throw new Error('write_file path must not contain shell operators')
119
+ throw new Error('Tool write_file path must not contain shell operators')
120
120
  }
121
121
  if (/^(?:rm|del|erase|rmdir|remove-item|mkdir|type|cat|echo|copy|move|mv|cp)\b/i.test(trimmed)) {
122
- throw new Error('write_file path looks like a shell command; pass only the file path')
122
+ throw new Error('Tool write_file path looks like a shell command; pass only the file path')
123
123
  }
124
124
  }
125
125
 
@@ -1,56 +1,14 @@
1
1
  import React, { useEffect, useState } from 'react'
2
2
  import { Text, Box } from 'ink'
3
- import { eyeGradientColor, theme } from './theme.js'
3
+ import { theme } from './theme.js'
4
4
 
5
5
  const glyphs = {
6
- ethagent: {
7
- eth: `░░░░░░░╗░░░░░░░░╗░░╗ ░░╗
8
- ░░╔════╝╚══░░╔══╝░░║ ░░║
9
- ░░░░░╗ ░░║ ░░░░░░░║
10
- ░░╔══╝ ░░║ ░░╔══░░║
11
- ░░░░░░░╗ ░░║ ░░║ ░░║
12
- ╚══════╝ ╚═╝ ╚═╝ ╚═╝`,
13
- a: [
14
- ` █████╗ `,
15
- `██╔══██╗`,
16
- `███████║`,
17
- `██╔══██║`,
18
- `██║ ██║`,
19
- `╚═╝ ╚═╝`,
20
- ].join('\n'),
21
- g: [
22
- ` ██████╗ `,
23
- `██╔════╝ `,
24
- `██║ ███╗`,
25
- `██║ ██║`,
26
- `╚██████╔╝`,
27
- ` ╚═════╝ `,
28
- ].join('\n'),
29
- e: [
30
- `███████╗`,
31
- `██╔════╝`,
32
- `█████╗ `,
33
- `██╔══╝ `,
34
- `███████╗`,
35
- `╚══════╝`,
36
- ].join('\n'),
37
- n: [
38
- `███╗ ██╗`,
39
- `████╗ ██║`,
40
- `██╔██╗ ██║`,
41
- `██║╚██╗██║`,
42
- `██║ ╚████║`,
43
- `╚═╝ ╚═══╝`,
44
- ].join('\n'),
45
- t: [
46
- `████████╗`,
47
- `╚══██╔══╝`,
48
- ` ██║ `,
49
- ` ██║ `,
50
- ` ██║ `,
51
- ` ╚═╝ `,
52
- ].join('\n'),
53
- },
6
+ ethagent: `░░░░░░░╗░░░░░░░░╗░░╗ ░░╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗
7
+ ░░╔════╝╚══░░╔══╝░░║ ░░║██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝
8
+ ░░░░░╗ ░░║ ░░░░░░░║███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║
9
+ ░░╔══╝ ░░║ ░░╔══░░║██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║
10
+ ░░░░░░░╗ ░░║ ░░║ ░░║██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║
11
+ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ `,
54
12
  eyes: `
55
13
  -+:
56
14
  :=- -%@@@%.
@@ -79,33 +37,13 @@ const glyphs = {
79
37
  },
80
38
  } as const
81
39
 
82
- const ethagentGlyphOrder = ['eth', 'a', 'g', 'e', 'n', 't'] as const
83
-
84
40
  const Eyes = () => {
85
41
  const lines = glyphs.eyes.split('\n')
86
42
  return (
87
43
  <Box flexDirection="column">
88
- {lines.map((line, li) => {
89
- const glyphPositions = [...line]
90
- .map((char, index) => ({ char, index }))
91
- .filter(entry => entry.char.trim().length > 0)
92
- .map(entry => entry.index)
93
- const firstGlyph = glyphPositions[0] ?? 0
94
- const lastGlyph = glyphPositions[glyphPositions.length - 1] ?? firstGlyph
95
- const span = Math.max(lastGlyph - firstGlyph, 1)
96
-
97
- return (
98
- <Text key={li}>
99
- {[...line].map((char, ci) => {
100
- if (!char.trim()) {
101
- return <Text key={ci}>{char}</Text>
102
- }
103
- const t = (ci - firstGlyph) / span
104
- return <Text key={ci} color={eyeGradientColor(t)}>{char}</Text>
105
- })}
106
- </Text>
107
- )
108
- })}
44
+ {lines.map((line, li) => (
45
+ <Text key={li} color={theme.text}>{line}</Text>
46
+ ))}
109
47
  </Box>
110
48
  )
111
49
  }
@@ -135,19 +73,18 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
135
73
  return (
136
74
  <Box flexDirection="column" alignSelf="flex-start" padding={1}>
137
75
  <Eyes />
138
- <Text bold color={theme.accentPrimary}>ethagent</Text>
139
- <Text color={theme.dim}>{glyphs.tagline.trim()}</Text>
76
+ <Text bold color={theme.accentWhite}>ethagent</Text>
77
+ <Text color={theme.dim}>privacy-first AI agent with a portable <Text color={theme.accentPeriwinkle}>Ethereum</Text> identity</Text>
140
78
  {contextLine ? <Text color={theme.dim}>{contextLine}</Text> : null}
141
79
  {tipLine ? <Text color={theme.dim}>{tipLine}</Text> : null}
142
- {updateNotice ? <Text color={theme.accentPeach}>{updateNotice}</Text> : null}
80
+ {updateNotice ? <Text color={theme.accentPeriwinkle}>{updateNotice}</Text> : null}
143
81
  </Box>
144
82
  )
145
83
  }
146
84
 
147
- const logoLines = ethagentGlyphOrder.map(key => glyphs.ethagent[key].split('\n'))
148
- const rowCount = Math.max(...logoLines.map(lines => lines.length))
149
-
150
85
  const w = 69
86
+ const logoLines = glyphs.ethagent.split('\n').map(line => line.padEnd(w, ' '))
87
+
151
88
  const topPad = Math.max(0, w - glyphs.tagline.length - 1)
152
89
 
153
90
  const bottomInline = contextLine ? ` ${truncateToFit(contextLine, w - 4)} ` : ''
@@ -158,22 +95,20 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
158
95
  <Eyes />
159
96
  <Text>
160
97
  <Text color={theme.border}>{glyphs.frame.topLeft}</Text>
161
- <Text color={theme.dim}>{glyphs.tagline}</Text>
98
+ <Text color={theme.dim}>{' privacy-first AI agent with a portable '}<Text color={theme.accentPeriwinkle}>Ethereum</Text>{' identity '}</Text>
162
99
  <Text color={theme.border}>{glyphs.frame.horizontal.repeat(topPad)}{glyphs.frame.topRight}</Text>
163
100
  </Text>
164
- {Array.from({ length: rowCount }, (_, i) => (
101
+ {logoLines.map((line, i) => (
165
102
  <Box key={i}>
166
103
  <Text color={theme.border}>{glyphs.frame.side}</Text>
167
- {logoLines.map((lines, index) => (
168
- <Text key={ethagentGlyphOrder[index]} color={theme.border}>{lines[i] ?? ''}</Text>
169
- ))}
104
+ <Text color={theme.border}>{line}</Text>
170
105
  <Text color={theme.border}>{glyphs.frame.side}</Text>
171
106
  </Box>
172
107
  ))}
173
108
  {bottomInline ? (
174
109
  <Text>
175
110
  <Text color={theme.border}>{glyphs.frame.bottomLeft}</Text>
176
- <Text color={theme.accentMint}>{bottomInline}</Text>
111
+ <Text color={theme.accentPeriwinkle}>{bottomInline}</Text>
177
112
  <Text color={theme.border}>{glyphs.frame.horizontal.repeat(bottomPad)}{glyphs.frame.bottomRight}</Text>
178
113
  </Text>
179
114
  ) : (
@@ -182,7 +117,7 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
182
117
  {tipLine || updateNotice ? (
183
118
  <Box marginTop={1} flexDirection="column">
184
119
  {tipLine ? <Text color={theme.dim}>{tipLine}</Text> : null}
185
- {updateNotice ? <Text color={theme.accentPeach}>{updateNotice}</Text> : null}
120
+ {updateNotice ? <Text color={theme.accentPeriwinkle}>{updateNotice}</Text> : null}
186
121
  </Box>
187
122
  ) : null}
188
123
  </Box>
@@ -1,24 +1,22 @@
1
1
  import React from 'react'
2
2
  import { Box, Text } from 'ink'
3
- import { theme, gradientColor, eyeGradientColor } from './theme.js'
3
+ import { theme, gradientColor } from './theme.js'
4
4
 
5
5
  type ProgressBarProps = {
6
6
  progress: number
7
7
  width?: number
8
8
  label?: string
9
9
  suffix?: string
10
- variant?: 'default' | 'rainbow'
11
10
  }
12
11
 
13
- export const ProgressBar: React.FC<ProgressBarProps> = ({ progress, width = 40, label, suffix, variant = 'default' }) => {
12
+ export const ProgressBar: React.FC<ProgressBarProps> = ({ progress, width = 40, label, suffix }) => {
14
13
  const p = Math.max(0, Math.min(1, progress))
15
14
  const filled = Math.round(p * width)
16
15
  const empty = Math.max(0, width - filled)
17
- const colorFor = variant === 'rainbow' ? eyeGradientColor : gradientColor
18
16
  const cells: React.ReactElement[] = []
19
17
  for (let i = 0; i < filled; i++) {
20
18
  cells.push(
21
- <Text key={`f-${i}`} color={colorFor(i / Math.max(width - 1, 1))}>█</Text>,
19
+ <Text key={`f-${i}`} color={gradientColor(i / Math.max(width - 1, 1))}>█</Text>,
22
20
  )
23
21
  }
24
22
  for (let i = 0; i < empty; i++) {
package/src/ui/Select.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react'
1
+ import React, { useEffect, useMemo, useState } from 'react'
2
2
  import { Box, Text } from 'ink'
3
3
  import { theme } from './theme.js'
4
4
  import { useAppInput } from '../app/input/AppInputProvider.js'
@@ -39,9 +39,18 @@ export function Select<T>({
39
39
  onCancel,
40
40
  onHighlight,
41
41
  }: SelectProps<T>) {
42
+ const optionsSignature = useMemo(
43
+ () => options.map(option => `${String(option.value)}:${option.disabled ? 'disabled' : 'enabled'}:${option.role ?? 'option'}`).join('|'),
44
+ [options],
45
+ )
42
46
  const firstEnabled = Math.max(0, options.findIndex(isSelectableOption))
43
47
  const start = isSelectableOption(options[initialIndex]) ? initialIndex : firstEnabled
44
48
  const [index, setIndex] = useState(start === -1 ? 0 : start)
49
+
50
+ useEffect(() => {
51
+ setIndex(start === -1 ? 0 : start)
52
+ }, [optionsSignature, start])
53
+
45
54
  const visibleCount = Math.max(1, maxVisible ?? options.length)
46
55
  const windowStart = Math.max(0, Math.min(
47
56
  index - Math.floor(visibleCount / 2),
@@ -88,24 +97,27 @@ export function Select<T>({
88
97
  const absoluteIndex = windowStart + visibleIndex
89
98
  const isActive = absoluteIndex === index
90
99
  const selectable = isSelectableOption(option)
100
+ const disabled = !!option.disabled
91
101
  const cursor = !selectable ? ' ' : isActive ? '>' : ' '
92
102
  const isSection = option.role === 'section' || option.role === 'group'
93
103
  const prefix = option.prefix && !isSection ? `${option.prefix} ` : ''
94
104
  const rowIndent = option.indent ?? (usesInlineSections ? isSection ? 1 : 3 : 0)
95
- const prefixColor = option.disabled
105
+ const prefixColor = disabled
96
106
  ? option.labelColor ?? theme.border
97
107
  : isActive && selectable
98
- ? theme.accentPrimary
108
+ ? theme.accentPeriwinkle
99
109
  : option.labelColor ?? theme.dim
100
110
  const labelColor = isSection
101
- ? option.labelColor ?? theme.dim
111
+ ? option.labelColor ?? theme.textSubtle
102
112
  : isActive && selectable
103
- ? theme.accentPrimary
104
- : option.labelColor ?? (option.disabled ? theme.dim : theme.text)
113
+ ? theme.accentPeriwinkle
114
+ : option.labelColor ?? (disabled ? theme.dim : theme.text)
105
115
  const hintColor = isActive && selectable
106
116
  ? theme.textSubtle
107
- : option.hintColor ?? theme.dim
108
- const subtextColor = option.subtextColor ?? theme.dim
117
+ : disabled
118
+ ? theme.border
119
+ : option.hintColor ?? theme.dim
120
+ const subtextColor = disabled ? theme.border : option.subtextColor ?? theme.dim
109
121
  const bold = option.bold ?? (isSection || (isActive && selectable))
110
122
  const inlineHint = Boolean(option.hint && hintLayout === 'inline' && !isSection)
111
123
  const belowHint = Boolean(option.hint && (!inlineHint || isSection))
@@ -193,6 +193,15 @@ export function pickVerb(): string {
193
193
  return SPINNER_VERBS[idx] ?? 'thinking'
194
194
  }
195
195
 
196
+ export function spinnerText(value: string): string {
197
+ const text = restoreSpinnerTerms(value.toLowerCase())
198
+ return text.replace(/^(\s*)([a-z])/, (_match, prefix: string, letter: string) => `${prefix}${letter.toUpperCase()}`)
199
+ }
200
+
201
+ export function spinnerHintText(value: string): string {
202
+ return restoreSpinnerTerms(value.toLowerCase())
203
+ }
204
+
196
205
  type SpinnerProps = {
197
206
  active?: boolean
198
207
  hint?: string
@@ -210,7 +219,7 @@ export const Spinner: React.FC<SpinnerProps> = ({
210
219
  hint: rawHint,
211
220
  label,
212
221
  verb,
213
- color = theme.accentSecondary,
222
+ color = theme.accentPeriwinkle,
214
223
  startedAt,
215
224
  showElapsed = true,
216
225
  }) => {
@@ -246,11 +255,11 @@ export const Spinner: React.FC<SpinnerProps> = ({
246
255
  if (!active) return null
247
256
 
248
257
  const autoLabel = stickyVerbRef.current ?? verb ?? 'thinking'
249
- const text = label ?? `${autoLabel}…`
258
+ const text = spinnerText(label ?? `${autoLabel}…`)
250
259
  const glyph = FRAMES[frame] ?? 'o'
251
260
  const elapsed = showElapsed ? formatElapsedSeconds(Date.now() - (startedAt ?? internalStartedAtRef.current)) : null
252
261
  const renderedHint = [rawHint, elapsed].filter(Boolean).join(' · ')
253
- const hint = renderedHint
262
+ const hint = renderedHint ? spinnerHintText(renderedHint) : ''
254
263
 
255
264
  return (
256
265
  <Text>
@@ -267,3 +276,29 @@ function formatElapsedSeconds(milliseconds: number): string {
267
276
  const minutes = Math.floor(seconds / 60)
268
277
  return `${minutes}:${(seconds % 60).toString().padStart(2, '0')}`
269
278
  }
279
+
280
+ function restoreSpinnerTerms(value: string): string {
281
+ return value
282
+ .replace(/\bapi\b/g, 'API')
283
+ .replace(/\bens\b/g, 'ENS')
284
+ .replace(/\berc-8004\b/g, 'ERC-8004')
285
+ .replace(/\bgguf\b/g, 'GGUF')
286
+ .replace(/\bhugging face\b/g, 'Hugging Face')
287
+ .replace(/\bipfs\b/g, 'IPFS')
288
+ .replace(/\bjson\b/g, 'JSON')
289
+ .replace(/\bjwt\b/g, 'JWT')
290
+ .replace(/\bmemory\.md\b/g, 'MEMORY.md')
291
+ .replace(/\bopenai\b/g, 'OpenAI')
292
+ .replace(/\banthropic\b/g, 'Anthropic')
293
+ .replace(/\bgemini\b/g, 'Gemini')
294
+ .replace(/\bos\b/g, 'OS')
295
+ .replace(/\brpc\b/g, 'RPC')
296
+ .replace(/\bsoul\.md\b/g, 'SOUL.md')
297
+ .replace(/\buri\b/g, 'URI')
298
+ .replace(/\burl\b/g, 'URL')
299
+ .replace(/\bbase\b/g, 'Base')
300
+ .replace(/\bethereum mainnet\b/g, 'Ethereum Mainnet')
301
+ .replace(/\bethereum\b/g, 'Ethereum')
302
+ .replace(/\bsepolia\b/g, 'Sepolia')
303
+ .replace(/\bbase sepolia\b/g, 'Base Sepolia')
304
+ }
@@ -13,9 +13,9 @@ type SurfaceProps = {
13
13
  }
14
14
 
15
15
  const toneColor: Record<SurfaceTone, string> = {
16
- primary: theme.accentPrimary,
16
+ primary: theme.accentPeriwinkle,
17
17
  muted: theme.border,
18
- error: '#e87070',
18
+ error: theme.accentError,
19
19
  }
20
20
 
21
21
  export const Surface: React.FC<SurfaceProps> = ({
@@ -8,7 +8,6 @@ import {
8
8
  getVisualLines,
9
9
  } from '../chat/textCursor.js'
10
10
 
11
- // ConversationStack padding=1 (2) + Surface border (2) + Surface paddingX=2 (4) + '> ' prefix (2) = 10
12
11
  const DEFAULT_CHROME_WIDTH = 10
13
12
 
14
13
  type TextInputProps = {
@@ -23,6 +22,8 @@ type TextInputProps = {
23
22
  validate?: (value: string) => string | null
24
23
  onSubmit: (value: string) => void
25
24
  onCancel?: () => void
25
+ onNavigateLeft?: () => void
26
+ onNavigateRight?: (value: string) => void
26
27
  }
27
28
 
28
29
  type RenderedTextInputLine = {
@@ -42,6 +43,8 @@ export function TextInput({
42
43
  validate,
43
44
  onSubmit,
44
45
  onCancel,
46
+ onNavigateLeft,
47
+ onNavigateRight,
45
48
  }: TextInputProps) {
46
49
  const { stdout } = useStdout()
47
50
  const [value, setValue] = useState(initialValue)
@@ -49,7 +52,6 @@ export function TextInput({
49
52
  const [preferredColumn, setPreferredColumn] = useState<number | null>(null)
50
53
  const [error, setError] = useState<string | null>(null)
51
54
 
52
- // Keep a columns state updated via resize, matching ChatInput's pattern exactly
53
55
  const [columns, setColumns] = useState<number>(() => Math.floor(stdout?.columns ?? 80))
54
56
  useEffect(() => {
55
57
  if (!stdout) return
@@ -60,26 +62,37 @@ export function TextInput({
60
62
 
61
63
  const wrapWidth = textInputWrapWidth(columns, chromeWidth)
62
64
 
63
- // Sync refs during render so the input handler always reads fresh values,
64
- // even if AppInputProvider fires before the next useEffect cycle updates handlerRef.
65
65
  const stateRef = useRef({ value, cursor, preferredColumn, wrapWidth })
66
66
  stateRef.current = { value, cursor, preferredColumn, wrapWidth }
67
67
 
68
68
  useAppInput((input, key) => {
69
69
  const { value: val, cursor: cur, preferredColumn: prefCol, wrapWidth: ww } = stateRef.current
70
70
 
71
- if (key.return) {
71
+ const submitValue = (submit: (value: string) => void) => {
72
72
  if (!allowEmpty && val.trim().length === 0) {
73
73
  setError('value cannot be empty')
74
- return
74
+ return false
75
75
  }
76
76
  const validationError = validate?.(val) ?? null
77
77
  if (validationError) {
78
78
  setError(validationError)
79
- return
79
+ return false
80
80
  }
81
81
  setError(null)
82
- onSubmit(val)
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)
83
96
  return
84
97
  }
85
98
  if (key.escape || (key.ctrl && input === 'c')) {
@@ -87,11 +100,19 @@ export function TextInput({
87
100
  return
88
101
  }
89
102
  if (key.leftArrow) {
103
+ if (onNavigateLeft && cur === 0) {
104
+ onNavigateLeft()
105
+ return
106
+ }
90
107
  setCursor(Math.max(0, cur - 1))
91
108
  setPreferredColumn(null)
92
109
  return
93
110
  }
94
111
  if (key.rightArrow) {
112
+ if (onNavigateRight && cur === val.length) {
113
+ submitValue(onNavigateRight)
114
+ return
115
+ }
95
116
  setCursor(Math.min(val.length, cur + 1))
96
117
  setPreferredColumn(null)
97
118
  return
@@ -111,8 +132,15 @@ export function TextInput({
111
132
  return
112
133
  }
113
134
  if (key.ctrl && input === 'u') {
114
- setValue('')
115
- setCursor(0)
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
+ }
116
144
  setPreferredColumn(null)
117
145
  if (error) setError(null)
118
146
  return
@@ -121,11 +149,13 @@ export function TextInput({
121
149
  return
122
150
  }
123
151
  if (input) {
124
- 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, '')
125
155
  if (clean) {
126
- const next = (val.slice(0, cur) + clean + val.slice(cur)).slice(0, maxLength)
127
- setValue(next)
128
- setCursor(Math.min(cur + clean.length, maxLength))
156
+ const next = insertTextInputText(val, cur, clean, maxLength)
157
+ setValue(next.value)
158
+ setCursor(next.cursor)
129
159
  setPreferredColumn(null)
130
160
  if (error) setError(null)
131
161
  }
@@ -145,7 +175,7 @@ export function TextInput({
145
175
  <Box flexDirection="column">
146
176
  {renderedLines.map(line => (
147
177
  <Box key={line.visualLineIndex} flexDirection="row">
148
- <Text color={line.visualLineIndex === 0 ? theme.accentPrimary : theme.dim}>
178
+ <Text color={line.visualLineIndex === 0 ? theme.accentPeriwinkle : theme.dim}>
149
179
  {line.visualLineIndex === 0 ? '> ' : ' '}
150
180
  </Text>
151
181
  <Box width={wrapWidth}>{line.node}</Box>
@@ -154,24 +184,24 @@ export function TextInput({
154
184
  </Box>
155
185
  ) : (
156
186
  <Box flexDirection="row">
157
- <Text color={theme.accentPrimary}>{'> '}</Text>
187
+ <Text color={theme.accentPeriwinkle}>{'> '}</Text>
158
188
  <Box width={wrapWidth}>
159
189
  {showPlaceholder ? (
160
190
  <Text wrap={multiline ? 'wrap' : 'truncate-end'}>
161
- <Text backgroundColor={theme.accentMint} color="#08110c">{' '}</Text>
191
+ <Text backgroundColor={theme.accentPeriwinkle} color="#0c0c1f">{' '}</Text>
162
192
  <Text color={theme.dim}>{placeholder}</Text>
163
193
  </Text>
164
194
  ) : (
165
195
  <Text color={theme.text} wrap="truncate-end">
166
196
  {display.slice(0, cursor)}
167
- <Text backgroundColor={theme.accentMint} color="#08110c">{display[cursor] ?? ' '}</Text>
197
+ <Text backgroundColor={theme.accentPeriwinkle} color="#0c0c1f">{display[cursor] ?? ' '}</Text>
168
198
  {display.slice(cursor + 1)}
169
199
  </Text>
170
200
  )}
171
201
  </Box>
172
202
  </Box>
173
203
  )}
174
- {error ? <Text color="#e87070">{error}</Text> : null}
204
+ {error ? <Text color={theme.accentError}>{error}</Text> : null}
175
205
  </Box>
176
206
  )
177
207
  }
@@ -180,6 +210,19 @@ export function textInputWrapWidth(columns: number, chromeWidth = DEFAULT_CHROME
180
210
  return Math.max(1, Math.floor(columns) - Math.max(0, Math.floor(chromeWidth)))
181
211
  }
182
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
+
183
226
  export function renderTextInputLines(
184
227
  value: string,
185
228
  cursor: number,
@@ -207,7 +250,7 @@ export function renderTextInputLines(
207
250
  node: (
208
251
  <Text color={theme.text} wrap="wrap">
209
252
  {before}
210
- <Text backgroundColor={theme.accentMint} color="#08110c">{atChar}</Text>
253
+ <Text backgroundColor={theme.accentPeriwinkle} color="#0c0c1f">{atChar}</Text>
211
254
  {after}
212
255
  </Text>
213
256
  ),