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
@@ -52,26 +52,37 @@ export function buildBaseMessages(
52
52
 
53
53
  export function sessionMessagesToRows(messages: SessionMessage[], nextRowId: () => string): MessageRow[] {
54
54
  const restored: MessageRow[] = []
55
+ const toolCallByUseId = new Map<string, Extract<MessageRow, { role: 'tool_call' }>>()
55
56
  for (const msg of messages) {
56
57
  if (msg.role === 'user') restored.push({ role: 'user', id: nextRowId(), content: msg.content })
57
58
  else if (msg.role === 'assistant') restored.push({ role: 'assistant', id: nextRowId(), content: msg.content })
58
59
  else if (msg.role === 'tool_use') {
59
- restored.push({
60
- role: 'tool_use',
60
+ const row: Extract<MessageRow, { role: 'tool_call' }> = {
61
+ role: 'tool_call',
61
62
  id: nextRowId(),
62
63
  name: msg.name,
63
64
  summary: msg.name,
64
65
  input: summarizeToolInput(msg.input),
65
- })
66
+ }
67
+ restored.push(row)
68
+ toolCallByUseId.set(msg.toolUseId, row)
66
69
  } else if (msg.role === 'tool_result') {
67
- restored.push({
68
- role: 'tool_result',
69
- id: nextRowId(),
70
- name: msg.name,
71
- summary: msg.isError ? `${msg.name} failed` : `${msg.name} completed`,
72
- content: toolResultContentForRow(msg.name, msg.content, msg.isError),
73
- isError: msg.isError,
74
- })
70
+ const isError = Boolean(msg.isError)
71
+ const summary = isError ? `${msg.name} failed` : `${msg.name} completed`
72
+ const content = toolResultContentForRow(msg.name, msg.content, msg.isError)
73
+ const existing = toolCallByUseId.get(msg.toolUseId)
74
+ if (existing) {
75
+ existing.result = { content, summary, isError }
76
+ existing.name = msg.name
77
+ } else {
78
+ restored.push({
79
+ role: 'tool_call',
80
+ id: nextRowId(),
81
+ name: msg.name,
82
+ summary: msg.name,
83
+ result: { content, summary, isError },
84
+ })
85
+ }
75
86
  }
76
87
  }
77
88
  return restored
@@ -49,7 +49,7 @@ export function resolveModelSelection(
49
49
  model: selection.model,
50
50
  baseUrl,
51
51
  },
52
- notice: `local Hugging Face model ready. now using ${formatModelDisplayName('llamacpp', selection.model, { maxLength: 64 })}.`,
52
+ notice: `Local Hugging Face model ready. Now using ${formatModelDisplayName('llamacpp', selection.model, { maxLength: 64 })}.`,
53
53
  tone: 'info',
54
54
  }
55
55
  }
@@ -69,7 +69,7 @@ export function resolveModelSelection(
69
69
  return {
70
70
  kind: 'switch',
71
71
  config: nextConfig,
72
- notice: `${selection.keyJustSet ? `${selection.provider} key saved.` : `${selection.provider} ready.`} now using ${nextConfig.provider} · ${formatModelDisplayName(nextConfig.provider, nextConfig.model, { maxLength: 64 })}.`,
72
+ notice: `${selection.keyJustSet ? `${selection.provider} key saved.` : `${selection.provider} ready.`} Now using ${nextConfig.provider} · ${formatModelDisplayName(nextConfig.provider, nextConfig.model, { maxLength: 64 })}.`,
73
73
  tone: 'dim',
74
74
  }
75
75
  }
@@ -60,18 +60,6 @@ export type StreamingTurnResult = {
60
60
  cancelled: boolean
61
61
  }
62
62
 
63
- /**
64
- * runStreamingTurn - the UI adapter over runRuntimeTurn.
65
- *
66
- * Responsibilities (UI-only; logic lives in runtime/turn.ts):
67
- * - translate runtime events into Ink MessageRow updates,
68
- * - flush streaming text to rows on a debounce,
69
- * - persist SessionMessages on commit boundaries,
70
- * - drive the tool batch (permission prompts, row pushes, persistence),
71
- * - surface plan-mode output to the caller,
72
- * - return a summary of what happened (finishedNormally / cancelled)
73
- * for the caller to act on.
74
- */
75
63
  export async function runStreamingTurn(
76
64
  context: TurnOrchestratorContext,
77
65
  ): Promise<StreamingTurnResult> {
@@ -86,7 +74,6 @@ export async function runStreamingTurn(
86
74
  nowIso,
87
75
  getConfig,
88
76
  getCwd,
89
- getSessionMessages,
90
77
  setActiveCheckpoint,
91
78
  setStreaming,
92
79
  updateRows,
@@ -135,8 +122,6 @@ export async function runStreamingTurn(
135
122
  ]
136
123
  }
137
124
 
138
- // Per-iteration UI scratch. These are reset each time the runtime loop
139
- // re-enters streaming (new provider call = new assistant row, new accumulator).
140
125
  let accumulated = ''
141
126
  let thinkingContent = ''
142
127
  let thinkingRowId: string | null = null
@@ -207,9 +192,6 @@ export async function runStreamingTurn(
207
192
  flushStreamRows(true)
208
193
  updateRows(prev => {
209
194
  let next = finalizeStreamingRowsById(prev, assistantId, thinkingRowId, accumulated, thinkingContent)
210
- // If we emitted tool_uses, strip the empty assistant text row - tool_use
211
- // rows replace it. If the assistant emitted no text at all (pure tool
212
- // turn), drop the empty row.
213
195
  if (assistantId && (hasPendingToolUse || accumulated.length === 0)) {
214
196
  next = next.filter(r => r.id !== assistantId)
215
197
  }
@@ -235,13 +217,6 @@ export async function runStreamingTurn(
235
217
  name: string
236
218
  input: Record<string, unknown>
237
219
  }>) => {
238
- // Persist the assistant tool_use blocks into the session before execution
239
- // so microcompact / rebuild has them on the next provider call. The actual
240
- // Message[] sent to the provider is rebuilt from session messages.
241
- // (This mirrors the pre-Wave-2 behavior, just routed from the event loop.)
242
- // NOTE: some provider loops keep tool_use blocks inside the assistant
243
- // message; ethagent stores them as discrete tool_use SessionMessages via
244
- // runPendingToolUses, which keeps the microcompact model simpler.
245
220
  const step = await runPendingToolUses({
246
221
  pendingToolUses,
247
222
  nextRowId,
@@ -351,10 +326,6 @@ export async function runStreamingTurn(
351
326
  }
352
327
  }
353
328
 
354
- // ---------------------------------------------------------------------------
355
- // Event handling: per-event UI translation
356
- // ---------------------------------------------------------------------------
357
-
358
329
  type EventHandlerContext = {
359
330
  ensureAssistantRow: () => string
360
331
  flushStreamRows: (immediate?: boolean) => void
@@ -391,10 +362,6 @@ function isCancelledEvent(ev: TurnEvent): boolean {
391
362
  async function handleEvent(ev: TurnEvent, ctx: EventHandlerContext): Promise<void> {
392
363
  switch (ev.type) {
393
364
  case 'iteration_start': {
394
- // Reset per-iteration scratch so each provider call gets a fresh
395
- // assistant row, accumulator, and hasPendingToolUse flag. Iteration 0
396
- // is the initial stream - resetting before anything runs is a no-op,
397
- // which is fine.
398
365
  ctx.resetIteration()
399
366
  return
400
367
  }
@@ -433,7 +400,6 @@ async function handleEvent(ev: TurnEvent, ctx: EventHandlerContext): Promise<voi
433
400
  return
434
401
  }
435
402
  case 'retry': {
436
- ctx.pushNote(formatRetryStatus(ev), 'dim')
437
403
  return
438
404
  }
439
405
  case 'tool_use_stop': {
@@ -442,8 +408,6 @@ async function handleEvent(ev: TurnEvent, ctx: EventHandlerContext): Promise<voi
442
408
  return
443
409
  }
444
410
  case 'assistant_message_committed': {
445
- // End of a streaming round with no tool_use - finalize rows, persist
446
- // the assistant text, and hand it to the plan hook if in plan mode.
447
411
  ctx.finalizeStreamingRows()
448
412
  if (ev.text) {
449
413
  await ctx.persistTurnMessage({
@@ -458,22 +422,14 @@ async function handleEvent(ev: TurnEvent, ctx: EventHandlerContext): Promise<voi
458
422
  return
459
423
  }
460
424
  case 'tool_executed': {
461
- // Row + session persistence happened inside runPendingToolUses; the
462
- // event is informational for observers that need it (tests, future
463
- // instrumentation). No UI side-effect here.
464
425
  return
465
426
  }
466
427
  case 'local_tool_recovery': {
467
- // The runtime recovered tool calls from local model text output.
468
- // Discard the streamed assistant rows that contained the JSON blob
469
- // so they are not persisted or displayed as prose.
470
428
  ctx.discardStreamingRows()
471
429
  ctx.markPendingToolUse()
472
430
  return
473
431
  }
474
432
  case 'continuation_nudge': {
475
- // Clean break between provider calls. Corrective nudges suppress the
476
- // unverified assistant row so it cannot become durable context.
477
433
  if (
478
434
  ev.reason === 'tool_state_claim' ||
479
435
  ev.reason === 'tool_capability' ||
@@ -501,9 +457,6 @@ async function handleEvent(ev: TurnEvent, ctx: EventHandlerContext): Promise<voi
501
457
  return
502
458
  }
503
459
  case 'done': {
504
- // If we ended mid-iteration (no assistant_message_committed yet) the
505
- // finalize call from error/cancelled already ran. If we ended after a
506
- // tool_executed batch, finalize here so the UI settles.
507
460
  ctx.finalizeStreamingRows()
508
461
  if (ev.finishedNormally) ctx.onFinishedNormally()
509
462
  return
@@ -514,21 +467,6 @@ async function handleEvent(ev: TurnEvent, ctx: EventHandlerContext): Promise<voi
514
467
  }
515
468
  }
516
469
 
517
- function formatRetryStatus(ev: Extract<TurnEvent, { type: 'retry' }>): string {
518
- const totalAttempts = ev.maxRetries + 1
519
- const reason = ev.status !== undefined ? `HTTP ${ev.status}` : ev.code ?? ev.reason
520
- return `provider retry ${ev.nextAttempt}/${totalAttempts} in ${formatRetryDelay(ev.delayMs)} (${reason})`
521
- }
522
-
523
- function formatRetryDelay(delayMs: number): string {
524
- if (delayMs < 1000) return `${Math.max(0, Math.round(delayMs))}ms`
525
- const seconds = delayMs / 1000
526
- if (seconds < 10) return `${seconds.toFixed(1).replace(/\.0$/, '')}s`
527
- if (seconds < 60) return `${Math.round(seconds)}s`
528
- const minutes = seconds / 60
529
- return `${minutes.toFixed(minutes < 10 ? 1 : 0).replace(/\.0$/, '')}m`
530
- }
531
-
532
470
  function updateStreamingRows(
533
471
  rows: MessageRow[],
534
472
  assistantId: string | null,
@@ -592,10 +530,6 @@ function findRowIndexById(rows: MessageRow[], id: string): number {
592
530
  return -1
593
531
  }
594
532
 
595
- // ---------------------------------------------------------------------------
596
- // File-mention context (unchanged from pre-Wave-2 - pure helper)
597
- // ---------------------------------------------------------------------------
598
-
599
533
  async function buildFileMentionContextMessages(
600
534
  userText: string,
601
535
  cwd: string,
@@ -641,23 +575,24 @@ export async function buildIdentityContinuityContextMessages(
641
575
 
642
576
  try {
643
577
  const privateFiles = await readContinuityFiles(identity)
578
+ const parts: string[] = [
579
+ '<identity_continuity_files>',
580
+ 'The active identity continuity files have been loaded automatically for this turn.',
581
+ 'SOUL.md is private owner continuity and is the authoritative persona, voice, and standing-behavior layer for this active identity.',
582
+ 'MEMORY.md is private owner continuity for durable preferences, facts, and project context.',
583
+ 'Apply SOUL.md and MEMORY.md over generic ethagent identity/style unless they conflict with safety, tool correctness, developer instructions, or the user\'s latest explicit request. Do not quote private continuity unless necessary.',
584
+ '<SOUL.md visibility="private">',
585
+ privateFiles['SOUL.md'].trimEnd(),
586
+ '</SOUL.md>',
587
+ '',
588
+ '<MEMORY.md visibility="private">',
589
+ privateFiles['MEMORY.md'].trimEnd(),
590
+ '</MEMORY.md>',
591
+ '</identity_continuity_files>',
592
+ ]
644
593
  return [{
645
594
  role: 'system',
646
- content: [
647
- '<identity_continuity_files>',
648
- 'The active identity continuity files have been loaded automatically for this turn.',
649
- 'SOUL.md is private owner continuity and is the authoritative persona, voice, and standing-behavior layer for this active identity.',
650
- 'MEMORY.md is private owner continuity for durable preferences, facts, and project context.',
651
- 'Apply SOUL.md and MEMORY.md over generic ethagent identity/style unless they conflict with safety, tool correctness, developer instructions, or the user\'s latest explicit request. Do not quote private continuity unless necessary.',
652
- '<SOUL.md visibility="private">',
653
- privateFiles['SOUL.md'].trimEnd(),
654
- '</SOUL.md>',
655
- '',
656
- '<MEMORY.md visibility="private">',
657
- privateFiles['MEMORY.md'].trimEnd(),
658
- '</MEMORY.md>',
659
- '</identity_continuity_files>',
660
- ].join('\n'),
595
+ content: parts.join('\n'),
661
596
  }]
662
597
  } catch (err: unknown) {
663
598
  return [{
@@ -210,7 +210,7 @@ const COMMANDS: CommandSpec[] = [
210
210
  }
211
211
  await saveConfig(next)
212
212
  ctx.onReplaceConfig(next)
213
- return { kind: 'note', text: `now using ${next.provider} · ${formatModelDisplayName(next.provider, name, { maxLength: 64 })}.` }
213
+ return { kind: 'note', text: `Now using ${next.provider} · ${formatModelDisplayName(next.provider, name, { maxLength: 64 })}.` }
214
214
  },
215
215
  },
216
216
  {
@@ -119,7 +119,7 @@ export function moveVerticalVisualCursor(
119
119
  }
120
120
  }
121
121
 
122
- export function cursorOnLastLine(value: string, offset: number): TextCursor {
122
+ export function cursorOnLastLine(value: string, _offset: number): TextCursor {
123
123
  const lines = getLogicalLines(value)
124
124
  const lastLine = Math.max(0, lines.length - 1)
125
125
  return normalizeCursor(value, offsetFromPosition(lines, lastLine, 0))
@@ -1,5 +1,4 @@
1
1
  import type { MessageRow } from './MessageList.js'
2
- import { hidesSuccessfulToolResultContent } from './toolResultDisplay.js'
3
2
 
4
3
  export type TranscriptAnchor = {
5
4
  rowId: string
@@ -219,12 +218,8 @@ export function estimateMessageRowHeight(row: MessageRow, columns = 80): number
219
218
  return row.expanded
220
219
  ? 3 + wrappedLineCount([row.content, row.liveTail ?? ''].filter(Boolean).join('\n'), contentWidth)
221
220
  : 3 + wrappedLineCount(reasoningPreview(row), contentWidth)
222
- case 'tool_use':
223
- return 3 + (row.input ? wrappedLineCount(row.input, contentWidth) : 0)
224
- case 'tool_result':
225
- return hidesSuccessfulToolResultContent(row.name, row.isError)
226
- ? 3
227
- : 3 + wrappedLineCount(row.content, contentWidth)
221
+ case 'tool_call':
222
+ return 1
228
223
  case 'note':
229
224
  return 1 + wrappedLineCount(row.content, contentWidth)
230
225
  case 'progress':
@@ -47,7 +47,7 @@ export const ResetConfirmView: React.FC<{
47
47
 
48
48
  const Section: React.FC<{ title: string; lines: string[] }> = ({ title, lines }) => (
49
49
  <Box flexDirection="column" marginBottom={1}>
50
- <Text color={theme.accentMint}>{title}</Text>
50
+ <Text color={theme.accentPeriwinkle}>{title}</Text>
51
51
  {lines.map(line => (
52
52
  <Text key={line} color={theme.textSubtle}>- {line}</Text>
53
53
  ))}
package/src/cli/main.tsx CHANGED
@@ -103,13 +103,19 @@ const AppRoot: React.FC<{ setExitCode: (code: number) => void; currentVersion: s
103
103
 
104
104
  useAppInput((input, key) => {
105
105
  if (phase.kind === 'ready') return
106
- if (key.ctrl && (input === 'c' || input === 'd')) exit()
106
+ if (key.ctrl && (input === 'c' || input === 'd')) {
107
+ if (phase.kind === 'setup') {
108
+ setPhase({ kind: 'cancelled' })
109
+ } else {
110
+ exit()
111
+ }
112
+ }
107
113
  })
108
114
 
109
115
  if (phase.kind === 'loading') {
110
116
  return (
111
117
  <Box padding={1}>
112
- <Spinner label="Starting ethagent..." showElapsed={false} />
118
+ <Spinner label="starting ethagent..." showElapsed={false} />
113
119
  </Box>
114
120
  )
115
121
  }
@@ -131,7 +137,7 @@ const AppRoot: React.FC<{ setExitCode: (code: number) => void; currentVersion: s
131
137
  if (phase.kind === 'error') {
132
138
  return (
133
139
  <Box padding={1}>
134
- <Text color="#e87070">Error: {phase.message}</Text>
140
+ <Text color={theme.accentError}>Error: {phase.message}</Text>
135
141
  </Box>
136
142
  )
137
143
  }
@@ -2,17 +2,12 @@ import React from 'react'
2
2
  import { Box, render } from 'ink'
3
3
  import { BrandSplash } from '../ui/BrandSplash.js'
4
4
 
5
- /**
6
- * `ethagent preview` — renders the brand splash with only the tagline
7
- * in the top border, no technical details at the bottom, and exits.
8
- */
9
5
  export async function runPreviewCommand(): Promise<number> {
10
6
  const instance = render(
11
7
  <Box flexDirection="column" marginY={1}>
12
8
  <BrandSplash />
13
9
  </Box>,
14
10
  )
15
- // Give Ink one tick to paint, then unmount cleanly.
16
11
  await new Promise<void>(resolve => setTimeout(resolve, 50))
17
12
  instance.unmount()
18
13
  return 0
@@ -12,8 +12,10 @@ export function compareVersions(left: string, right: string): number {
12
12
  const b = parseVersion(right)
13
13
  if (!a || !b) return 0
14
14
  for (let i = 0; i < 3; i++) {
15
- if (a[i] > b[i]) return 1
16
- if (a[i] < b[i]) return -1
15
+ const av = a[i as 0 | 1 | 2]
16
+ const bv = b[i as 0 | 1 | 2]
17
+ if (av > bv) return 1
18
+ if (av < bv) return -1
17
19
  }
18
20
  return 0
19
21
  }
@@ -24,7 +26,7 @@ export function isNewerVersion(candidate: string, current: string): boolean {
24
26
 
25
27
  export function formatUpdateNotice(currentVersion: string, latestVersion: string): string | null {
26
28
  if (!isNewerVersion(latestVersion, currentVersion)) return null
27
- return `update available: ethagent ${currentVersion} -> ${latestVersion}; run npm i -g ethagent`
29
+ return `✨ update available: ethagent ${currentVersion} -> ${latestVersion} · run npm i -g ethagent@latest`
28
30
  }
29
31
 
30
32
  export async function checkForUpdates(
@@ -1,12 +1,10 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
1
  import { spawn } from 'node:child_process'
4
2
 
5
- export type EditorOpenResult =
3
+ type EditorOpenResult =
6
4
  | { ok: true; method: string; waited: boolean }
7
5
  | { ok: false; error: string }
8
6
 
9
- export type EditorCommand = {
7
+ type EditorCommand = {
10
8
  cmd: string
11
9
  args: string[]
12
10
  method: string
@@ -14,59 +12,10 @@ export type EditorCommand = {
14
12
  shell?: boolean
15
13
  }
16
14
 
17
- export type EditorResolutionOptions = {
18
- platform?: NodeJS.Platform
19
- commandExists?: (command: string) => string | null
20
- }
21
-
22
- const IDE_CANDIDATES = ['code', 'cursor', 'windsurf'] as const
23
-
24
- export function openFileInEditor(file: string, env: NodeJS.ProcessEnv = process.env): Promise<EditorOpenResult> {
25
- const command = resolveEditorCommand(file, env)
26
- if (command) return openEditorCommand(command)
27
- return openDefaultEditor(file)
28
- }
29
-
30
- export function resolveEditorCommand(
31
- file: string,
32
- env: NodeJS.ProcessEnv = process.env,
33
- options: EditorResolutionOptions = {},
34
- ): EditorCommand | null {
35
- const platform = options.platform ?? process.platform
36
- const commandExists = options.commandExists ?? (command => findExecutable(command, env, platform))
37
-
38
- const ethagentEditor = env.ETHAGENT_EDITOR?.trim()
39
- if (ethagentEditor) return configuredCommand(ethagentEditor, file, true, platform)
40
-
41
- for (const candidate of IDE_CANDIDATES) {
42
- const executable = commandExists(candidate)
43
- if (executable) {
44
- return {
45
- cmd: executable,
46
- args: [file],
47
- method: candidate,
48
- waited: false,
49
- shell: platform === 'win32' && /\.(?:cmd|bat)$/i.test(executable),
50
- }
51
- }
52
- }
53
-
54
- const configured = env.VISUAL?.trim() || env.EDITOR?.trim()
55
- if (configured) return configuredCommand(configured, file, true, platform)
56
-
57
- return defaultEditorCommand(file, platform)
58
- }
59
-
60
- function configuredCommand(commandLine: string, file: string, waited: boolean, platform: NodeJS.Platform): EditorCommand | null {
61
- const [cmd, ...args] = splitCommand(commandLine)
62
- if (!cmd) return null
63
- return {
64
- cmd,
65
- args: [...args, file],
66
- method: path.basename(cmd),
67
- waited,
68
- shell: platform === 'win32' && /\.(?:cmd|bat)$/i.test(cmd),
69
- }
15
+ export function openFileInEditor(file: string): Promise<EditorOpenResult> {
16
+ const command = defaultEditorCommand(file)
17
+ if (!command) return Promise.resolve({ ok: false, error: 'no default open command for this platform' })
18
+ return openEditorCommand(command)
70
19
  }
71
20
 
72
21
  function openEditorCommand(command: EditorCommand): Promise<EditorOpenResult> {
@@ -93,57 +42,8 @@ function openEditorCommand(command: EditorCommand): Promise<EditorOpenResult> {
93
42
  })
94
43
  }
95
44
 
96
- function openDefaultEditor(file: string): Promise<EditorOpenResult> {
97
- const command = defaultEditorCommand(file)
98
- if (!command) return Promise.resolve({ ok: false, error: 'no default editor command for this platform' })
99
- return openEditorCommand(command)
100
- }
101
-
102
- function defaultEditorCommand(file: string, platform: NodeJS.Platform = process.platform): EditorCommand | null {
45
+ export function defaultEditorCommand(file: string, platform: NodeJS.Platform = process.platform): EditorCommand | null {
103
46
  if (platform === 'win32') return { cmd: 'cmd', args: ['/c', 'start', '', file], method: 'cmd', waited: false }
104
47
  if (platform === 'darwin') return { cmd: 'open', args: [file], method: 'open', waited: false }
105
48
  return { cmd: 'xdg-open', args: [file], method: 'xdg-open', waited: false }
106
49
  }
107
-
108
- function splitCommand(commandLine: string): string[] {
109
- return commandLine.match(/"[^"]+"|'[^']+'|\S+/g)?.map(part => {
110
- if ((part.startsWith('"') && part.endsWith('"')) || (part.startsWith("'") && part.endsWith("'"))) {
111
- return part.slice(1, -1)
112
- }
113
- return part
114
- }) ?? []
115
- }
116
-
117
- function findExecutable(command: string, env: NodeJS.ProcessEnv, platform: NodeJS.Platform): string | null {
118
- const hasPathSeparator = command.includes('/') || command.includes('\\')
119
- if (hasPathSeparator || path.isAbsolute(command)) {
120
- return canAccessExecutable(command) ? command : null
121
- }
122
-
123
- const pathValue = env.PATH ?? ''
124
- const pathParts = pathValue.split(path.delimiter).filter(Boolean)
125
- const extensions = platform === 'win32'
126
- ? (env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
127
- : ['']
128
-
129
- for (const dir of pathParts) {
130
- for (const ext of extensions) {
131
- const candidate = path.join(dir, platform === 'win32' && path.extname(command) === '' ? `${command}${ext.toLowerCase()}` : command)
132
- if (canAccessExecutable(candidate)) return candidate
133
- if (platform === 'win32') {
134
- const upperCandidate = path.join(dir, path.extname(command) === '' ? `${command}${ext.toUpperCase()}` : command)
135
- if (canAccessExecutable(upperCandidate)) return upperCandidate
136
- }
137
- }
138
- }
139
- return null
140
- }
141
-
142
- function canAccessExecutable(file: string): boolean {
143
- try {
144
- fs.accessSync(file, fs.constants.X_OK)
145
- return true
146
- } catch {
147
- return false
148
- }
149
- }