ethagent 2.1.1 → 2.3.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 (177) hide show
  1. package/package.json +2 -1
  2. package/src/app/FirstRun.tsx +1 -7
  3. package/src/app/FirstRunTimeline.tsx +1 -1
  4. package/src/auth/openaiOAuth/credentials.ts +47 -0
  5. package/src/auth/openaiOAuth/crypto.ts +23 -0
  6. package/src/auth/openaiOAuth/index.ts +238 -0
  7. package/src/auth/openaiOAuth/landingPage.ts +125 -0
  8. package/src/auth/openaiOAuth/listener.ts +151 -0
  9. package/src/auth/openaiOAuth/refresh.ts +70 -0
  10. package/src/auth/openaiOAuth/shared.ts +115 -0
  11. package/src/chat/ChatBottomPane.tsx +20 -11
  12. package/src/chat/ChatScreen.tsx +160 -35
  13. package/src/chat/ConversationStack.tsx +1 -1
  14. package/src/chat/MessageList.tsx +185 -72
  15. package/src/chat/SessionStatus.tsx +3 -1
  16. package/src/chat/chatScreenUtils.ts +11 -15
  17. package/src/chat/chatSessionState.ts +3 -2
  18. package/src/chat/chatTurnOrchestrator.ts +1 -7
  19. package/src/chat/commands.ts +28 -27
  20. package/src/chat/display/DiffView.tsx +193 -0
  21. package/src/chat/display/SyntaxText.tsx +192 -0
  22. package/src/chat/display/toolCallDisplay.ts +103 -0
  23. package/src/chat/display/toolResultDisplay.ts +19 -0
  24. package/src/chat/{ChatInput.tsx → input/ChatInput.tsx} +36 -23
  25. package/src/chat/{TranscriptView.tsx → transcript/TranscriptView.tsx} +24 -50
  26. package/src/chat/{transcriptViewport.ts → transcript/transcriptViewport.ts} +12 -30
  27. package/src/chat/{ContextLimitView.tsx → views/ContextLimitView.tsx} +3 -3
  28. package/src/chat/{ContinuityEditReviewView.tsx → views/ContinuityEditReviewView.tsx} +11 -3
  29. package/src/chat/{CopyPicker.tsx → views/CopyPicker.tsx} +4 -5
  30. package/src/chat/{PermissionPrompt.tsx → views/PermissionPrompt.tsx} +16 -17
  31. package/src/chat/{PermissionsView.tsx → views/PermissionsView.tsx} +6 -6
  32. package/src/chat/{PlanApprovalView.tsx → views/PlanApprovalView.tsx} +4 -4
  33. package/src/chat/{ResumeView.tsx → views/ResumeView.tsx} +35 -35
  34. package/src/chat/views/RewindView.tsx +410 -0
  35. package/src/identity/continuity/privateEdit/diff.ts +2 -78
  36. package/src/identity/ens/agentRecords.ts +5 -19
  37. package/src/identity/ens/ensAutomation/setup.ts +0 -1
  38. package/src/identity/ens/ensAutomation/types.ts +0 -1
  39. package/src/identity/hub/OperationalRoutes.tsx +23 -32
  40. package/src/identity/hub/Routes.tsx +13 -13
  41. package/src/identity/hub/{flows/continuity → continuity}/ContinuityDashboardScreen.tsx +9 -9
  42. package/src/identity/hub/{flows/continuity → continuity}/RebackupStorageScreen.tsx +2 -2
  43. package/src/identity/hub/{flows/continuity → continuity}/RecoveryConfirmScreen.tsx +5 -5
  44. package/src/identity/hub/{flows/continuity → continuity}/SavePromptScreen.tsx +5 -5
  45. package/src/identity/hub/{effects/rebackup/runRebackup.ts → continuity/effects.ts} +19 -19
  46. package/src/identity/hub/{effects/rebackup → continuity}/index.ts +1 -1
  47. package/src/identity/hub/{effects/shared → continuity}/snapshot.ts +8 -8
  48. package/src/identity/hub/{effects/rebackup → continuity}/vault.ts +15 -15
  49. package/src/identity/hub/{flows/create → create}/CreateFlow.tsx +13 -13
  50. package/src/identity/hub/{effects/create.ts → create/effects.ts} +4 -4
  51. package/src/identity/hub/{flows/custody → custody}/CustodyEditFlow.tsx +10 -48
  52. package/src/identity/hub/{flows/custody/custodyFlowActions.ts → custody/actions.ts} +11 -9
  53. package/src/identity/hub/{flows/custody/custodyFlowHelpers.ts → custody/helpers.ts} +4 -4
  54. package/src/identity/hub/{effects/vault → custody}/preflight.ts +5 -5
  55. package/src/identity/hub/{flows/custody/custodyFlowRoutes.tsx → custody/routes.tsx} +8 -8
  56. package/src/identity/hub/{flows/custody/custodyEffects.ts → custody/transactions.ts} +9 -9
  57. package/src/identity/hub/{flows/custody/custodyFlowTypes.ts → custody/types.ts} +6 -6
  58. package/src/identity/hub/{flows/custody/custodyFlowEffects.ts → custody/useCustodyEffects.ts} +7 -7
  59. package/src/identity/hub/{flows/custody → custody}/useCustodyFlow.tsx +5 -5
  60. package/src/identity/hub/ens/EnsEditAdvancedScreens.tsx +241 -0
  61. package/src/identity/hub/{flows/ens → ens}/EnsEditFlow.tsx +27 -82
  62. package/src/identity/hub/{flows/ens → ens}/EnsEditMaintenanceScreens.tsx +25 -65
  63. package/src/identity/hub/{flows/ens → ens}/EnsEditReviewScreens.tsx +12 -30
  64. package/src/identity/hub/ens/EnsEditRunners.tsx +62 -0
  65. package/src/identity/hub/{flows/ens → ens}/EnsEditShared.tsx +15 -14
  66. package/src/identity/hub/{flows/ens → ens}/EnsEditSimpleScreens.tsx +68 -217
  67. package/src/identity/hub/{flows/ens/IdentityHubEnsFlow.tsx → ens/EnsFlow.tsx} +18 -11
  68. package/src/identity/hub/{flows/ens/OperatorWalletsScreen.tsx → ens/EnsOperatorWalletsScreen.tsx} +17 -48
  69. package/src/identity/hub/{advancedEnsValidation.ts → ens/advancedEnsValidation.ts} +2 -2
  70. package/src/identity/hub/{flows/ens/ensEditCopy.ts → ens/editCopy.ts} +4 -4
  71. package/src/identity/hub/{effects/ens/flows.ts → ens/effects.ts} +7 -7
  72. package/src/identity/hub/{effects/ens → ens}/index.ts +1 -1
  73. package/src/identity/hub/{model/ens.ts → ens/state.ts} +1 -1
  74. package/src/identity/hub/{effects/ens → ens}/transactions.ts +232 -232
  75. package/src/identity/hub/{flows/ens/ensEditTypes.ts → ens/types.ts} +12 -26
  76. package/src/identity/hub/identityHubReducer.ts +3 -3
  77. package/src/identity/hub/{flows/profile → profile}/EditProfileFlow.tsx +17 -10
  78. package/src/identity/hub/{effects/publicProfile/runPublicProfileSave.ts → profile/effects.ts} +55 -177
  79. package/src/identity/hub/{model → profile}/identity.ts +3 -3
  80. package/src/identity/hub/{effects/profile/profileState.ts → profile/state.ts} +181 -173
  81. package/src/identity/hub/{flows/restore → restore}/RestoreFlow.tsx +21 -21
  82. package/src/identity/hub/{effects/restore → restore}/apply.ts +10 -10
  83. package/src/identity/hub/{effects/restore → restore}/auth.ts +7 -7
  84. package/src/identity/hub/{effects/restore → restore}/discover.ts +6 -6
  85. package/src/identity/hub/{effects/restore → restore}/envelopes.ts +2 -2
  86. package/src/identity/hub/{effects/restore → restore}/fetch.ts +3 -3
  87. package/src/identity/hub/{effects/restore/shared.ts → restore/helpers.ts} +6 -6
  88. package/src/identity/hub/{effects/restore → restore}/recovery.ts +10 -10
  89. package/src/identity/hub/{effects/restore → restore}/resolve.ts +4 -4
  90. package/src/identity/hub/restore/restoreAdmin.ts +34 -0
  91. package/src/identity/hub/{flows/restore/useRestoreFlowEffects.ts → restore/useRestoreEffects.ts} +5 -5
  92. package/src/identity/hub/{flows/settings → settings}/StorageCredentialScreen.tsx +5 -5
  93. package/src/identity/hub/{components → shared/components}/BusyScreen.tsx +4 -4
  94. package/src/identity/hub/{components → shared/components}/DetailsScreen.tsx +4 -4
  95. package/src/identity/hub/{components → shared/components}/ErrorScreen.tsx +4 -4
  96. package/src/identity/hub/{components → shared/components}/FlowTimeline.tsx +1 -1
  97. package/src/identity/hub/{components → shared/components}/IdentitySummary.tsx +16 -11
  98. package/src/identity/hub/{components → shared/components}/MenuScreen.tsx +8 -9
  99. package/src/identity/hub/{components → shared/components}/NetworkScreen.tsx +4 -4
  100. package/src/identity/hub/{components → shared/components}/PinataJwtInput.tsx +4 -4
  101. package/src/identity/hub/{components → shared/components}/UnlinkedIdentityScreen.tsx +5 -5
  102. package/src/identity/hub/{components → shared/components}/WalletApprovalScreen.tsx +6 -6
  103. package/src/identity/hub/{components → shared/components}/menuFlagsFromReconciliation.ts +2 -4
  104. package/src/identity/hub/{effects/shared → shared/effects}/profilePrep.ts +1 -1
  105. package/src/identity/hub/{effects → shared/effects}/receipts.ts +2 -2
  106. package/src/identity/hub/{effects/shared → shared/effects}/sync.ts +6 -47
  107. package/src/identity/hub/{effects → shared/effects}/types.ts +3 -3
  108. package/src/identity/hub/{model → shared/model}/copy.ts +2 -2
  109. package/src/identity/hub/{model → shared/model}/errors.ts +5 -5
  110. package/src/identity/hub/{model → shared/model}/network.ts +3 -3
  111. package/src/identity/hub/{operatorWallets.ts → shared/operatorWallets.ts} +1 -1
  112. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/hook.ts +1 -2
  113. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/ownership.ts +2 -2
  114. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/run.ts +7 -40
  115. package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/types.ts +0 -4
  116. package/src/identity/hub/{reconciliation → shared/reconciliation}/index.ts +0 -7
  117. package/src/identity/hub/shared/reconciliation/walletSetup.ts +27 -0
  118. package/src/identity/hub/{utils.ts → shared/utils.ts} +5 -5
  119. package/src/identity/hub/{flows/token-transfer/IdentityHubTokenTransferFlow.tsx → transfer/TokenTransferFlow.tsx} +8 -8
  120. package/src/identity/hub/{flows/token-transfer → transfer}/TokenTransferScreens.tsx +14 -14
  121. package/src/identity/hub/{effects/token-transfer/runTokenTransfer.ts → transfer/effects.ts} +16 -16
  122. package/src/identity/hub/{effects/token-transfer → transfer}/progress.ts +1 -1
  123. package/src/identity/hub/useIdentityHubController.ts +11 -11
  124. package/src/identity/hub/useIdentityHubSideEffects.ts +11 -11
  125. package/src/identity/wallet/browserWallet/types.ts +0 -5
  126. package/src/identity/wallet/page/copy.ts +1 -31
  127. package/src/identity/wallet/walletPurposeCompat.ts +0 -2
  128. package/src/models/ModelPicker.tsx +248 -8
  129. package/src/models/catalog.ts +29 -1
  130. package/src/models/modelPickerOptions.ts +12 -10
  131. package/src/models/providerDisplay.ts +16 -0
  132. package/src/providers/errors.ts +6 -4
  133. package/src/providers/openai-chat.ts +2 -1
  134. package/src/providers/openai-responses-format.ts +156 -0
  135. package/src/providers/openai-responses.ts +276 -0
  136. package/src/providers/registry.ts +85 -8
  137. package/src/runtime/sessionMode.ts +1 -1
  138. package/src/runtime/systemPrompt.ts +4 -2
  139. package/src/runtime/toolExecution.ts +9 -6
  140. package/src/runtime/turn.ts +29 -1
  141. package/src/storage/rewind.ts +20 -0
  142. package/src/storage/secrets.ts +4 -1
  143. package/src/storage/sessions.ts +2 -1
  144. package/src/tools/bashSafety.ts +7 -3
  145. package/src/tools/bashTool.ts +1 -1
  146. package/src/tools/contracts.ts +3 -0
  147. package/src/tools/deleteFileTool.ts +8 -3
  148. package/src/tools/editTool.ts +10 -5
  149. package/src/tools/fileDiff.ts +261 -0
  150. package/src/tools/privateContinuityEditTool.ts +11 -1
  151. package/src/tools/writeFileTool.ts +8 -3
  152. package/src/ui/Spinner.tsx +25 -3
  153. package/src/ui/TextInput.tsx +2 -2
  154. package/src/ui/theme.ts +17 -0
  155. package/src/utils/clipboard.ts +10 -7
  156. package/src/utils/openExternal.ts +20 -10
  157. package/src/chat/RewindView.tsx +0 -386
  158. package/src/chat/toolResultDisplay.ts +0 -8
  159. package/src/identity/ens/ensRegistration.ts +0 -199
  160. package/src/identity/hub/effects/index.ts +0 -74
  161. package/src/identity/hub/effects/publicProfile/index.ts +0 -5
  162. package/src/identity/hub/effects/restore/restoreEffects.ts +0 -22
  163. package/src/identity/hub/effects/restoreAdmin.ts +0 -93
  164. package/src/identity/hub/effects/token-transfer/index.ts +0 -6
  165. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +0 -336
  166. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +0 -198
  167. package/src/identity/hub/reconciliation/walletSetup.ts +0 -220
  168. /package/src/chat/{chatInputState.ts → input/chatInputState.ts} +0 -0
  169. /package/src/chat/{chatPaste.ts → input/chatPaste.ts} +0 -0
  170. /package/src/chat/{textCursor.ts → input/textCursor.ts} +0 -0
  171. /package/src/identity/hub/{model/continuity.ts → continuity/state.ts} +0 -0
  172. /package/src/identity/hub/{model/custody.ts → custody/state.ts} +0 -0
  173. /package/src/identity/hub/{effects/restore → restore}/index.ts +0 -0
  174. /package/src/identity/hub/{model → shared/model}/format.ts +0 -0
  175. /package/src/identity/hub/{reconciliation → shared/reconciliation}/useAgentReconciliation.ts +0 -0
  176. /package/src/identity/hub/{txGuard.ts → shared/txGuard.ts} +0 -0
  177. /package/src/identity/hub/{model/transfer.ts → transfer/state.ts} +0 -0
@@ -0,0 +1,410 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import fs from 'node:fs/promises'
3
+ import path from 'node:path'
4
+ import { Box, Text } from 'ink'
5
+ import { Surface } from '../../ui/Surface.js'
6
+ import { Select, type SelectOption } from '../../ui/Select.js'
7
+ import { Spinner } from '../../ui/Spinner.js'
8
+ import { theme } from '../../ui/theme.js'
9
+ import { useAppInput } from '../../app/input/AppInputProvider.js'
10
+ import {
11
+ groupRewindEntriesByTurn,
12
+ rewindWorkspaceEditsByEntryIds,
13
+ type RewindEntry,
14
+ } from '../../storage/rewind.js'
15
+ import { loadSession } from '../../storage/sessions.js'
16
+ import { computeLineDiffStats } from '../../tools/fileDiff.js'
17
+
18
+ type RestoreAction = 'both' | 'conversation' | 'code' | 'summarize'
19
+ type ConfirmOption = RestoreAction | 'nevermind'
20
+
21
+ type RewindViewProps = {
22
+ cwd: string
23
+ currentSessionId: string
24
+ onRestoreConversation: (turnId: string, promptText?: string) => void
25
+ onSummarizeFromTurn: (turnId: string) => void | Promise<unknown>
26
+ onDone: (message: string, variant?: 'info' | 'error' | 'dim') => void
27
+ onCancel: () => void
28
+ }
29
+
30
+ type DiffStats = {
31
+ files: string[]
32
+ inserts: number
33
+ deletes: number
34
+ }
35
+
36
+ type PromptRow = {
37
+ turnId: string
38
+ text: string
39
+ createdAt: string
40
+ entries: RewindEntry[]
41
+ stats: DiffStats
42
+ }
43
+
44
+ type State =
45
+ | { kind: 'loading' }
46
+ | { kind: 'error'; message: string }
47
+ | { kind: 'picker' }
48
+ | { kind: 'confirm'; turnId: string; selectedAction: ConfirmOption }
49
+ | { kind: 'restoring' }
50
+
51
+ export const RewindView: React.FC<RewindViewProps> = ({
52
+ cwd,
53
+ currentSessionId,
54
+ onRestoreConversation,
55
+ onSummarizeFromTurn,
56
+ onDone,
57
+ onCancel,
58
+ }) => {
59
+ const [state, setState] = useState<State>({ kind: 'loading' })
60
+ const [rows, setRows] = useState<PromptRow[]>([])
61
+
62
+ useEffect(() => {
63
+ let cancelled = false
64
+ void (async () => {
65
+ try {
66
+ const [sessionMessages, grouped] = await Promise.all([
67
+ loadSession(currentSessionId),
68
+ groupRewindEntriesByTurn(cwd, currentSessionId),
69
+ ])
70
+ const prompts: PromptRow[] = []
71
+ for (const msg of sessionMessages) {
72
+ if (msg.role !== 'user') continue
73
+ if (msg.synthetic) continue
74
+ if (!msg.turnId) continue
75
+ const entries = grouped.get(msg.turnId) ?? []
76
+ const stats = await computeDiffStatsForEntries(entries)
77
+ prompts.push({
78
+ turnId: msg.turnId,
79
+ text: msg.content,
80
+ createdAt: msg.createdAt,
81
+ entries,
82
+ stats,
83
+ })
84
+ }
85
+ if (cancelled) return
86
+ setRows(prompts)
87
+ setState({ kind: 'picker' })
88
+ } catch (err: unknown) {
89
+ if (cancelled) return
90
+ setState({ kind: 'error', message: (err as Error).message })
91
+ }
92
+ })()
93
+ return () => { cancelled = true }
94
+ }, [cwd, currentSessionId])
95
+
96
+ const escActive =
97
+ state.kind === 'loading' ||
98
+ state.kind === 'error' ||
99
+ (state.kind === 'picker' && rows.length === 0)
100
+ useAppInput((_input, key) => {
101
+ if (key.escape) onCancel()
102
+ }, { isActive: escActive })
103
+
104
+ if (state.kind === 'loading') {
105
+ return (
106
+ <Surface title="Rewind" subtitle="Loading session history...">
107
+ <Spinner label="Loading rewind history..." />
108
+ </Surface>
109
+ )
110
+ }
111
+
112
+ if (state.kind === 'error') {
113
+ return (
114
+ <Surface title="Rewind" tone="muted" footer="Esc closes.">
115
+ <Text color={theme.dim}>{state.message}</Text>
116
+ </Surface>
117
+ )
118
+ }
119
+
120
+ if (state.kind === 'restoring') {
121
+ return (
122
+ <Surface title="Rewind" subtitle="Restoring..." footer="Restoring...">
123
+ <Spinner label="Restoring..." />
124
+ </Surface>
125
+ )
126
+ }
127
+
128
+ if (rows.length === 0) {
129
+ return (
130
+ <Surface title="Rewind" tone="muted" footer="Esc closes.">
131
+ <Text color={theme.dim}>Nothing to rewind to yet.</Text>
132
+ </Surface>
133
+ )
134
+ }
135
+
136
+ if (state.kind === 'confirm') {
137
+ const selectedRow = rows.find(row => row.turnId === state.turnId)
138
+ if (!selectedRow) {
139
+ setState({ kind: 'picker' })
140
+ return null
141
+ }
142
+ const canRestoreCode = selectedRow.entries.length > 0
143
+
144
+ const actionOptions: Array<SelectOption<ConfirmOption>> = [
145
+ { value: 'both', prefix: '1.', label: 'Restore code and conversation', disabled: !canRestoreCode },
146
+ { value: 'conversation', prefix: '2.', label: 'Restore conversation' },
147
+ { value: 'code', prefix: '3.', label: 'Restore code', disabled: !canRestoreCode },
148
+ { value: 'summarize', prefix: '4.', label: 'Summarize from here' },
149
+ { value: 'nevermind', prefix: '5.', label: 'Never mind' },
150
+ ]
151
+ const defaultValue: ConfirmOption = canRestoreCode ? 'both' : 'conversation'
152
+ const initialIndex = actionOptions.findIndex(option => option.value === defaultValue && !option.disabled)
153
+
154
+ const runRestore = async (action: RestoreAction) => {
155
+ if (action === 'summarize') {
156
+ try {
157
+ await onSummarizeFromTurn(state.turnId)
158
+ onDone('Summarizing the conversation from this point...', 'dim')
159
+ } catch (err: unknown) {
160
+ onDone(`Summarize failed: ${(err as Error).message}`, 'error')
161
+ }
162
+ return
163
+ }
164
+ setState({ kind: 'restoring' })
165
+ const codeIds = selectedRow.entries.map(entry => entry.id)
166
+ let codeError: Error | null = null
167
+ let codeFiles: string[] = []
168
+ let conversationError: Error | null = null
169
+
170
+ if (action === 'code' || action === 'both') {
171
+ try {
172
+ const result = await rewindWorkspaceEditsByEntryIds(cwd, codeIds)
173
+ codeFiles = result.files
174
+ if (result.reverted === 0) {
175
+ codeError = new Error('No matching rewind entries were found for this prompt.')
176
+ }
177
+ } catch (err: unknown) {
178
+ codeError = err as Error
179
+ }
180
+ }
181
+ if (action === 'conversation' || action === 'both') {
182
+ try {
183
+ onRestoreConversation(state.turnId, selectedRow.text)
184
+ } catch (err: unknown) {
185
+ conversationError = err as Error
186
+ }
187
+ }
188
+
189
+ if (codeError && conversationError) {
190
+ onDone(`Rewind failed: ${codeError.message}; ${conversationError.message}`, 'error')
191
+ return
192
+ }
193
+ if (codeError) {
194
+ onDone(`Code restore failed: ${codeError.message}`, 'error')
195
+ return
196
+ }
197
+ if (conversationError) {
198
+ onDone(`Conversation restore failed: ${conversationError.message}`, 'error')
199
+ return
200
+ }
201
+
202
+ if (action === 'both') {
203
+ const files = describeFileList(codeFiles, cwd)
204
+ onDone(`Restored code and conversation to before this prompt.${files ? ` Files: ${files}.` : ''}`, 'dim')
205
+ } else if (action === 'code') {
206
+ const files = describeFileList(codeFiles, cwd)
207
+ onDone(`Restored code to before this prompt.${files ? ` Files: ${files}.` : ''}`, 'dim')
208
+ } else {
209
+ onDone('Restored conversation to before this prompt.', 'dim')
210
+ }
211
+ }
212
+
213
+ return (
214
+ <Surface
215
+ title="Rewind"
216
+ subtitle="Confirm you want to restore to the point before you sent this message."
217
+ footer="Enter to restore. Esc to go back."
218
+ >
219
+ <Box flexDirection="column">
220
+ <Text color={theme.accentPeriwinkle}>{truncate(selectedRow.text, 280)}</Text>
221
+ <Text color={theme.dim}>
222
+ {formatTimestamp(selectedRow.createdAt)}
223
+ {describeStats(selectedRow.stats, cwd) ? ` · ${describeStats(selectedRow.stats, cwd)}` : ''}
224
+ </Text>
225
+ </Box>
226
+ <Box marginTop={1}>
227
+ <Select
228
+ options={actionOptions}
229
+ initialIndex={initialIndex < 0 ? 1 : initialIndex}
230
+ onSubmit={value => {
231
+ if (value === 'nevermind') {
232
+ setState({ kind: 'picker' })
233
+ return
234
+ }
235
+ void runRestore(value)
236
+ }}
237
+ onCancel={() => setState({ kind: 'picker' })}
238
+ onHighlight={value => {
239
+ setState(prev => prev.kind === 'confirm'
240
+ ? { ...prev, selectedAction: value }
241
+ : prev)
242
+ }}
243
+ />
244
+ </Box>
245
+ <Box marginTop={1} flexDirection="column">
246
+ <ConfirmDescription
247
+ action={state.selectedAction}
248
+ stats={selectedRow.stats}
249
+ cwd={cwd}
250
+ canRestoreCode={canRestoreCode}
251
+ />
252
+ </Box>
253
+ </Surface>
254
+ )
255
+ }
256
+
257
+ const pickerOptions = buildPickerOptions(rows, cwd)
258
+ const lastIndex = Math.max(0, rows.length - 1)
259
+
260
+ return (
261
+ <Surface
262
+ title="Rewind"
263
+ subtitle="Restore the code and/or conversation to the point before a prior prompt."
264
+ footer="Enter to continue. Esc to exit."
265
+ >
266
+ <Select
267
+ options={pickerOptions}
268
+ initialIndex={lastIndex}
269
+ maxVisible={7}
270
+ onSubmit={turnId => {
271
+ const row = rows.find(r => r.turnId === turnId)
272
+ const canRestoreCode = (row?.entries.length ?? 0) > 0
273
+ setState({
274
+ kind: 'confirm',
275
+ turnId,
276
+ selectedAction: canRestoreCode ? 'both' : 'conversation',
277
+ })
278
+ }}
279
+ onCancel={onCancel}
280
+ />
281
+ </Surface>
282
+ )
283
+ }
284
+
285
+ const ConfirmDescription: React.FC<{
286
+ action: ConfirmOption
287
+ stats: DiffStats
288
+ cwd: string
289
+ canRestoreCode: boolean
290
+ }> = ({ action, stats, cwd, canRestoreCode }) => {
291
+ if (action === 'nevermind') {
292
+ return <Text color={theme.textSubtle}>The conversation and code will be unchanged.</Text>
293
+ }
294
+ if (action === 'summarize') {
295
+ return (
296
+ <Text color={theme.textSubtle}>
297
+ Messages from this point forward will be summarized into a handoff. The earlier conversation will remain unchanged.
298
+ </Text>
299
+ )
300
+ }
301
+ const lines: React.ReactNode[] = []
302
+ if (action === 'both') {
303
+ lines.push(<Text key="0" color={theme.textSubtle}>The conversation will be forked.</Text>)
304
+ lines.push(<CodeRestoreLine key="1" stats={stats} cwd={cwd} canRestoreCode={canRestoreCode} />)
305
+ } else if (action === 'conversation') {
306
+ lines.push(<Text key="0" color={theme.textSubtle}>The conversation will be forked.</Text>)
307
+ lines.push(<Text key="1" color={theme.textSubtle}>The code will be unchanged.</Text>)
308
+ } else {
309
+ lines.push(<Text key="0" color={theme.textSubtle}>The conversation will be unchanged.</Text>)
310
+ lines.push(<CodeRestoreLine key="1" stats={stats} cwd={cwd} canRestoreCode={canRestoreCode} />)
311
+ }
312
+ if ((action === 'code' || action === 'both') && canRestoreCode) {
313
+ lines.push(
314
+ <Text key="footnote" color={theme.dim}>
315
+ Rewinding does not affect files edited manually or via bash.
316
+ </Text>,
317
+ )
318
+ }
319
+ return <>{lines}</>
320
+ }
321
+
322
+ const CodeRestoreLine: React.FC<{ stats: DiffStats; cwd: string; canRestoreCode: boolean }> = ({ stats, cwd, canRestoreCode }) => {
323
+ if (!canRestoreCode) {
324
+ return <Text color={theme.textSubtle}>The code has not changed (nothing will be restored).</Text>
325
+ }
326
+ if (stats.inserts === 0 && stats.deletes === 0) {
327
+ return <Text color={theme.textSubtle}>The code will be restored in {describeFiles(stats.files, cwd)}.</Text>
328
+ }
329
+ return (
330
+ <Text color={theme.textSubtle}>
331
+ The code will be restored{' '}
332
+ <Text color={theme.diffAdded}>+{stats.inserts} </Text>
333
+ <Text color={theme.diffRemoved}>-{stats.deletes}</Text>
334
+ {' '}in {describeFiles(stats.files, cwd)}.
335
+ </Text>
336
+ )
337
+ }
338
+
339
+ function buildPickerOptions(rows: PromptRow[], cwd: string): Array<SelectOption<string>> {
340
+ return rows.map(row => ({
341
+ value: row.turnId,
342
+ label: truncate(row.text, 80),
343
+ subtext: describeStats(row.stats, cwd) || 'No code changes',
344
+ hint: formatTimestamp(row.createdAt),
345
+ }))
346
+ }
347
+
348
+ async function computeDiffStatsForEntries(entries: RewindEntry[]): Promise<DiffStats> {
349
+ const files: string[] = []
350
+ const seen = new Set<string>()
351
+ let inserts = 0
352
+ let deletes = 0
353
+ for (const entry of entries) {
354
+ if (!seen.has(entry.filePath)) {
355
+ seen.add(entry.filePath)
356
+ files.push(entry.filePath)
357
+ }
358
+ let currentContent = ''
359
+ try {
360
+ currentContent = await fs.readFile(entry.filePath, 'utf8')
361
+ } catch (err: unknown) {
362
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err
363
+ }
364
+ const stats = computeLineDiffStats(entry.previousContent, currentContent)
365
+ inserts += stats.inserts
366
+ deletes += stats.deletes
367
+ }
368
+ return { files, inserts, deletes }
369
+ }
370
+
371
+ function describeStats(stats: DiffStats, cwd: string): string {
372
+ if (stats.files.length === 0) return ''
373
+ const fileLabel = describeFiles(stats.files, cwd)
374
+ if (stats.inserts === 0 && stats.deletes === 0) return fileLabel
375
+ return `${fileLabel} +${stats.inserts} -${stats.deletes}`
376
+ }
377
+
378
+ function describeFiles(files: string[], cwd: string): string {
379
+ if (files.length === 0) return ''
380
+ const formatted = files.map(file => formatRelative(file, cwd))
381
+ if (formatted.length === 1) return formatted[0]!
382
+ if (formatted.length === 2) return `${formatted[0]} and ${formatted[1]}`
383
+ return `${formatted[0]} and ${formatted.length - 1} other files`
384
+ }
385
+
386
+ function describeFileList(files: string[], cwd: string): string {
387
+ if (files.length === 0) return ''
388
+ return files.map(file => formatRelative(file, cwd)).join(', ')
389
+ }
390
+
391
+ function formatRelative(filePath: string, cwd: string): string {
392
+ try {
393
+ const rel = path.relative(cwd, filePath)
394
+ if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) return rel
395
+ } catch {
396
+ return path.basename(filePath)
397
+ }
398
+ return path.basename(filePath)
399
+ }
400
+
401
+ function formatTimestamp(iso: string): string {
402
+ const date = new Date(iso)
403
+ return Number.isNaN(date.getTime()) ? iso : date.toLocaleString()
404
+ }
405
+
406
+ function truncate(text: string, limit: number): string {
407
+ const normalized = text.replace(/\s+/g, ' ').trim()
408
+ if (normalized.length <= limit) return normalized
409
+ return `${normalized.slice(0, Math.max(0, limit - 3))}...`
410
+ }
@@ -1,82 +1,6 @@
1
1
  import type { PrivateContinuityFile } from '../storage.js'
2
+ import { renderUnifiedFileDiff } from '../../../tools/fileDiff.js'
2
3
 
3
4
  export function renderPrivateContinuityDiff(file: PrivateContinuityFile, before: string, after: string): string {
4
- if (before === after) return '(no changes)'
5
- const changedLines = changedMarkdownLines(before, after)
6
- const lines = [
7
- `--- ${file}`,
8
- `+++ ${file}`,
9
- ...(changedLines.length > 0 ? changedLines : ['(only whitespace or line ending changes)']),
10
- ]
11
- const diff = lines.join('\n')
12
- return diff.length <= 2400 ? diff : `${diff.slice(0, 2397)}...`
13
- }
14
-
15
- function changedMarkdownLines(before: string, after: string): string[] {
16
- const beforeLines = markdownLines(before)
17
- const afterLines = markdownLines(after)
18
- const lengths = lcsLengths(beforeLines, afterLines)
19
- const changed: string[] = []
20
- let beforeIndex = 0
21
- let afterIndex = 0
22
-
23
- while (beforeIndex < beforeLines.length && afterIndex < afterLines.length) {
24
- if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
25
- beforeIndex += 1
26
- afterIndex += 1
27
- continue
28
- }
29
-
30
- const deleteScore = lengths[beforeIndex + 1]![afterIndex]!
31
- const insertScore = lengths[beforeIndex]![afterIndex + 1]!
32
- const deleteRevealsMatch = beforeLines[beforeIndex + 1] === afterLines[afterIndex]
33
- const insertRevealsMatch = beforeLines[beforeIndex] === afterLines[afterIndex + 1]
34
-
35
- if (insertRevealsMatch && insertScore >= deleteScore) {
36
- changed.push(`+${afterLines[afterIndex]}`)
37
- afterIndex += 1
38
- } else if (deleteRevealsMatch && deleteScore >= insertScore) {
39
- changed.push(`-${beforeLines[beforeIndex]}`)
40
- beforeIndex += 1
41
- } else if (deleteScore >= insertScore) {
42
- changed.push(`-${beforeLines[beforeIndex]}`)
43
- beforeIndex += 1
44
- } else {
45
- changed.push(`+${afterLines[afterIndex]}`)
46
- afterIndex += 1
47
- }
48
- }
49
-
50
- while (beforeIndex < beforeLines.length) {
51
- changed.push(`-${beforeLines[beforeIndex]}`)
52
- beforeIndex += 1
53
- }
54
- while (afterIndex < afterLines.length) {
55
- changed.push(`+${afterLines[afterIndex]}`)
56
- afterIndex += 1
57
- }
58
-
59
- return changed
60
- }
61
-
62
- function lcsLengths(beforeLines: string[], afterLines: string[]): number[][] {
63
- const lengths = Array.from(
64
- { length: beforeLines.length + 1 },
65
- () => Array<number>(afterLines.length + 1).fill(0),
66
- )
67
-
68
- for (let beforeIndex = beforeLines.length - 1; beforeIndex >= 0; beforeIndex -= 1) {
69
- for (let afterIndex = afterLines.length - 1; afterIndex >= 0; afterIndex -= 1) {
70
- lengths[beforeIndex]![afterIndex] = beforeLines[beforeIndex] === afterLines[afterIndex]
71
- ? lengths[beforeIndex + 1]![afterIndex + 1]! + 1
72
- : Math.max(lengths[beforeIndex + 1]![afterIndex]!, lengths[beforeIndex]![afterIndex + 1]!)
73
- }
74
- }
75
-
76
- return lengths
77
- }
78
-
79
- function markdownLines(value: string): string[] {
80
- const normalized = value.replace(/\r\n?/g, '\n')
81
- return normalized.split('\n')
5
+ return renderUnifiedFileDiff({ filePath: file, before, after })
82
6
  }
@@ -1,20 +1,17 @@
1
1
  export const AGENT_RECORD_KEYS = {
2
- token: 'org.ethagent.token',
3
- profile: 'org.ethagent.profile',
2
+ token: 'org.ethagent.token',
4
3
  } as const
5
4
 
6
5
  type AgentRecordKey = typeof AGENT_RECORD_KEYS[keyof typeof AGENT_RECORD_KEYS]
7
6
 
8
7
  export const AGENT_RECORD_KEY_LIST: readonly AgentRecordKey[] = [
9
8
  AGENT_RECORD_KEYS.token,
10
- AGENT_RECORD_KEYS.profile,
11
9
  ] as const
12
10
 
13
11
  export const AGENT_RECORD_READ_KEY_LIST: readonly string[] = AGENT_RECORD_KEY_LIST
14
12
 
15
13
  export type AgentEnsRecords = {
16
14
  token?: string
17
- profile?: string
18
15
  }
19
16
 
20
17
  export type AgentEnsRecordState = AgentEnsRecords
@@ -28,19 +25,16 @@ export type AgentRecordDiff = {
28
25
  }
29
26
 
30
27
  const FIELD_FOR_KEY: Record<AgentRecordKey, keyof AgentEnsRecords> = {
31
- [AGENT_RECORD_KEYS.token]: 'token',
32
- [AGENT_RECORD_KEYS.profile]: 'profile',
28
+ [AGENT_RECORD_KEYS.token]: 'token',
33
29
  }
34
30
 
35
31
  const LABEL_FOR_FIELD: Record<keyof AgentEnsRecordState, string> = {
36
- token: 'Agent token',
37
- profile: 'Agent profile',
32
+ token: 'Agent token',
38
33
  }
39
34
 
40
35
  export function recordsFromTextMap(text: Record<string, string>): AgentEnsRecordState {
41
36
  return {
42
- token: text[AGENT_RECORD_KEYS.token] ?? '',
43
- profile: text[AGENT_RECORD_KEYS.profile] ?? '',
37
+ token: text[AGENT_RECORD_KEYS.token] ?? '',
44
38
  }
45
39
  }
46
40
 
@@ -71,11 +65,7 @@ export function recordLabel(field: keyof AgentEnsRecordState): string {
71
65
  return LABEL_FOR_FIELD[field]
72
66
  }
73
67
 
74
- export function formatRecordValue(field: keyof AgentEnsRecordState, value: string): string {
75
- if (field === 'profile' && value.startsWith('ipfs://')) {
76
- const cid = value.slice('ipfs://'.length)
77
- return cid.length > 18 ? `ipfs://${cid.slice(0, 10)}...${cid.slice(-6)}` : value
78
- }
68
+ export function formatRecordValue(_field: keyof AgentEnsRecordState, value: string): string {
79
69
  return value
80
70
  }
81
71
 
@@ -83,14 +73,10 @@ export function buildAgentEnsRecords(args: {
83
73
  chainId: number
84
74
  identityRegistryAddress: string
85
75
  agentId: string | undefined
86
- agentCardCid: string | undefined
87
76
  }): AgentEnsRecords {
88
77
  const records: AgentEnsRecords = {}
89
78
  if (args.agentId) {
90
79
  records.token = `eip155:${args.chainId}:${args.identityRegistryAddress.toLowerCase()}:${args.agentId}`
91
80
  }
92
- if (args.agentCardCid) {
93
- records.profile = `ipfs://${args.agentCardCid}`
94
- }
95
81
  return records
96
82
  }
@@ -230,7 +230,6 @@ export async function preflightEnsSetup(args: EnsSetupPreflightArgs): Promise<En
230
230
  chainId: args.registry.chainId,
231
231
  identityRegistryAddress: args.registry.identityRegistryAddress,
232
232
  agentId: String(args.agentId),
233
- agentCardCid: args.agentCardCid,
234
233
  })
235
234
  if (currentRecords.token && nextRecords.token && currentRecords.token !== nextRecords.token) {
236
235
  return manual(args, {
@@ -82,7 +82,6 @@ export type EnsSetupPreflightArgs = {
82
82
  allowSameOwnerOperator?: boolean
83
83
  registry: Erc8004RegistryConfig
84
84
  agentId: string | bigint | undefined
85
- agentCardCid?: string
86
85
  ensClient?: EnsAutomationReadClient
87
86
  tokenPublicClient?: TokenOwnerReadClient
88
87
  }
@@ -1,36 +1,33 @@
1
1
  import React from 'react'
2
- import { hasPendingPublish } from './model/continuity.js'
2
+ import { hasPendingPublish } from './continuity/state.js'
3
3
  import type { ProfileUpdates } from './identityHubReducer.js'
4
4
  import { clearPinataJwt, savePinataJwt } from '../storage/pinataJwt.js'
5
- import {
6
- runFixRecordsSubmit,
7
- } from './effects/restoreAdmin.js'
8
5
  import {
9
6
  runRebackupStorageSubmit,
10
- } from './effects/rebackup/runRebackup.js'
7
+ } from './continuity/effects.js'
11
8
  import {
12
9
  runPublicProfileStorageSubmit,
13
- } from './effects/publicProfile/runPublicProfileSave.js'
14
- import { resolveVaultAddress } from './flows/custody/custodyEffects.js'
15
- import { WalletApprovalScreen } from './components/WalletApprovalScreen.js'
16
- import { RebackupStorageScreen } from './flows/continuity/RebackupStorageScreen.js'
17
- import { BusyScreen } from './components/BusyScreen.js'
18
- import { StorageCredentialScreen } from './flows/settings/StorageCredentialScreen.js'
10
+ } from './profile/effects.js'
11
+ import { resolveVaultAddress } from './custody/transactions.js'
12
+ import { WalletApprovalScreen } from './shared/components/WalletApprovalScreen.js'
13
+ import { RebackupStorageScreen } from './continuity/RebackupStorageScreen.js'
14
+ import { BusyScreen } from './shared/components/BusyScreen.js'
15
+ import { StorageCredentialScreen } from './settings/StorageCredentialScreen.js'
19
16
  import {
20
17
  PrivateContinuityScreen,
21
18
  PublicProfileScreen,
22
- } from './flows/continuity/ContinuityDashboardScreen.js'
23
- import { RecoveryConfirmScreen } from './flows/continuity/RecoveryConfirmScreen.js'
24
- import { SavePromptScreen } from './flows/continuity/SavePromptScreen.js'
25
- import { ErrorScreen } from './components/ErrorScreen.js'
26
- import { UnlinkedIdentityScreen } from './components/UnlinkedIdentityScreen.js'
27
- import { invalidateOwnershipCache } from './reconciliation/index.js'
19
+ } from './continuity/ContinuityDashboardScreen.js'
20
+ import { RecoveryConfirmScreen } from './continuity/RecoveryConfirmScreen.js'
21
+ import { SavePromptScreen } from './continuity/SavePromptScreen.js'
22
+ import { ErrorScreen } from './shared/components/ErrorScreen.js'
23
+ import { UnlinkedIdentityScreen } from './shared/components/UnlinkedIdentityScreen.js'
24
+ import { invalidateOwnershipCache } from './shared/reconciliation/index.js'
28
25
  import {
29
- IdentityHubEnsFlow,
30
- isIdentityHubEnsStep,
31
- } from './flows/ens/IdentityHubEnsFlow.js'
32
- import { CustodyEditFlow, isCustodyEditStep } from './flows/custody/CustodyEditFlow.js'
33
- import { rebackupWalletApprovalView } from './utils.js'
26
+ EnsFlow,
27
+ isEnsStep,
28
+ } from './ens/EnsFlow.js'
29
+ import { CustodyEditFlow, isCustodyEditStep } from './custody/CustodyEditFlow.js'
30
+ import { rebackupWalletApprovalView } from './shared/utils.js'
34
31
  import type { IdentityHubController } from './useIdentityHubController.js'
35
32
 
36
33
  type IdentityHubOperationalRoutesProps = {
@@ -198,16 +195,18 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
198
195
  )
199
196
  }
200
197
 
201
- if (isIdentityHubEnsStep(step)) {
198
+ if (isEnsStep(step)) {
202
199
  return (
203
- <IdentityHubEnsFlow
200
+ <EnsFlow
204
201
  step={step}
205
202
  walletSession={walletSession}
203
+ reconciliation={reconciliation}
206
204
  onSetStep={setStep}
207
205
  onBack={back}
208
206
  onWalletReady={setWalletSession}
209
207
  onTriggerRebackup={triggerRebackup}
210
208
  onTriggerPublicProfileSave={triggerPublicProfileSave}
209
+ onWithdrawTokenForEns={currentStep => custodyFlow.beginWithdrawToken(currentStep, currentStep, 'ens')}
211
210
  />
212
211
  )
213
212
  }
@@ -240,14 +239,6 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
240
239
  onManageOperatorWallets={() => {
241
240
  setStep({ kind: 'manage-ens-operators', identity: step.identity, registry: step.registry, returnTo: step })
242
241
  }}
243
- onFixRecords={async plan => {
244
- try {
245
- await runFixRecordsSubmit({ identity: step.identity, registry: step.registry, plan, callbacks })
246
- setStep({ ...step })
247
- } catch (err: unknown) {
248
- handleStepError(err, step)
249
- }
250
- }}
251
242
  onPrepareTransfer={openTokenTransferFlow}
252
243
  onBack={back}
253
244
  />