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
package/src/ui/theme.ts CHANGED
@@ -11,6 +11,23 @@ export const theme = {
11
11
  accentBlue: '#e8eefd',
12
12
  accentWhite: '#f5f8ff',
13
13
  accentError: '#d99898',
14
+ diffAdded: '#8fd49d',
15
+ diffRemoved: '#d99898',
16
+ diffAddedBackground: '#16351f',
17
+ diffRemovedBackground: '#3a1717',
18
+ blockBackground: '#0b0d12',
19
+ codeKeyword: '#e8eefd',
20
+ codeString: '#c9e08f',
21
+ codeNumber: '#d8dcfa',
22
+ codeComment: '#777777',
23
+ codeFunction: '#8fc7ff',
24
+ codeType: '#f2d087',
25
+ codeBuiltin: '#b8a7ff',
26
+ codeProperty: '#91dcc0',
27
+ codeOperator: '#d8dcfa',
28
+ codePunctuation: '#aeb4c8',
29
+ codeTag: '#ffb3b3',
30
+ codeAttribute: '#f2d087',
14
31
  border: '#555555',
15
32
  dim: '#777777',
16
33
  text: '#f1f1f1',
@@ -3,26 +3,29 @@ import { mkdir, stat } from 'node:fs/promises'
3
3
  import os from 'node:os'
4
4
  import path from 'node:path'
5
5
 
6
- export type CopyResult = { ok: true; method: string } | { ok: false; error: string }
6
+ export type CopyResult = { ok: true; method: string; chars: number } | { ok: false; error: string }
7
7
  export type ReadResult = { ok: true; text: string; method: string } | { ok: false; error: string }
8
8
  export type ReadImageResult = { ok: true; path: string; method: string } | { ok: false; error: string }
9
9
 
10
+ type CopyAttempt = { ok: true; method: string } | { ok: false; error: string }
11
+
10
12
  export async function copyToClipboard(text: string): Promise<CopyResult> {
13
+ const chars = text.length
11
14
  const native = await tryNative(text)
12
- if (native.ok) return native
15
+ if (native.ok) return { ...native, chars }
13
16
 
14
17
  const tmux = await tryTmux(text)
15
- if (tmux.ok) return tmux
18
+ if (tmux.ok) return { ...tmux, chars }
16
19
 
17
20
  try {
18
21
  process.stdout.write(osc52(text))
19
- return { ok: true, method: 'osc52' }
22
+ return { ok: true, method: 'osc52', chars }
20
23
  } catch (err: unknown) {
21
24
  return { ok: false, error: (err as Error).message || 'osc52 write failed' }
22
25
  }
23
26
  }
24
27
 
25
- async function tryNative(text: string): Promise<CopyResult> {
28
+ async function tryNative(text: string): Promise<CopyAttempt> {
26
29
  if (process.platform === 'darwin') {
27
30
  return pipeTo('pbcopy', [], text, 'pbcopy')
28
31
  }
@@ -38,12 +41,12 @@ async function tryNative(text: string): Promise<CopyResult> {
38
41
  return { ok: false, error: 'no native clipboard tool found' }
39
42
  }
40
43
 
41
- async function tryTmux(text: string): Promise<CopyResult> {
44
+ async function tryTmux(text: string): Promise<CopyAttempt> {
42
45
  if (!process.env['TMUX']) return { ok: false, error: 'not in tmux' }
43
46
  return pipeTo('tmux', ['load-buffer', '-w', '-'], text, 'tmux load-buffer')
44
47
  }
45
48
 
46
- function pipeTo(cmd: string, args: string[], text: string, method: string): Promise<CopyResult> {
49
+ function pipeTo(cmd: string, args: string[], text: string, method: string): Promise<CopyAttempt> {
47
50
  return new Promise(resolve => {
48
51
  let child
49
52
  try {
@@ -3,18 +3,28 @@ import { spawn } from 'node:child_process'
3
3
  export function openExternalUrl(url: string): void {
4
4
  const target = url.trim()
5
5
  if (!target) return
6
- const command = process.platform === 'win32'
7
- ? 'cmd'
8
- : process.platform === 'darwin'
9
- ? 'open'
10
- : 'xdg-open'
11
- const args = process.platform === 'win32'
12
- ? ['/c', 'start', '', target]
13
- : [target]
14
- const child = spawn(command, args, {
6
+
7
+ if (process.platform === 'win32') {
8
+ const safe = target.replace(/"/g, '%22')
9
+ const child = spawn(
10
+ 'cmd.exe',
11
+ ['/s', '/c', `start "" "${safe}"`],
12
+ {
13
+ detached: true,
14
+ stdio: 'ignore',
15
+ windowsHide: true,
16
+ windowsVerbatimArguments: true,
17
+ },
18
+ )
19
+ child.on('error', () => {})
20
+ child.unref()
21
+ return
22
+ }
23
+
24
+ const command = process.platform === 'darwin' ? 'open' : 'xdg-open'
25
+ const child = spawn(command, [target], {
15
26
  detached: true,
16
27
  stdio: 'ignore',
17
- windowsHide: true,
18
28
  })
19
29
  child.on('error', () => {})
20
30
  child.unref()
@@ -1,386 +0,0 @@
1
- import React, { useEffect, useState } from 'react'
2
- import { Box, Text } from 'ink'
3
- import { Surface } from '../ui/Surface.js'
4
- import { Select, type SelectOption } from '../ui/Select.js'
5
- import { Spinner } from '../ui/Spinner.js'
6
- import { theme } from '../ui/theme.js'
7
- import { useAppInput } from '../app/input/AppInputProvider.js'
8
- import {
9
- listRewindEntries,
10
- rewindWorkspaceEditsByEntryIds,
11
- type RewindEntry,
12
- } from '../storage/rewind.js'
13
-
14
- type RestoreAction = 'both' | 'code' | 'conversation'
15
-
16
- type RewindViewProps = {
17
- cwd: string
18
- currentSessionId: string
19
- onRestoreConversation: (turnId: string) => void
20
- onDone: (message: string, variant?: 'info' | 'error' | 'dim') => void
21
- onCancel: () => void
22
- }
23
-
24
- type ReadyState = {
25
- kind: 'ready'
26
- entries: RewindEntry[]
27
- offset: number
28
- pageSize: number
29
- hasMore: boolean
30
- selectedFilePath: string | null
31
- selectedId: string | null
32
- selectedAction: RestoreAction
33
- stage: 'files' | 'entries' | 'actions'
34
- restoring: boolean
35
- }
36
-
37
- type State =
38
- | { kind: 'loading' }
39
- | { kind: 'error'; message: string }
40
- | ReadyState
41
-
42
- export const RewindView: React.FC<RewindViewProps> = ({
43
- cwd,
44
- currentSessionId,
45
- onRestoreConversation,
46
- onDone,
47
- onCancel,
48
- }) => {
49
- const [state, setState] = useState<State>({ kind: 'loading' })
50
- const pageSize = 12
51
-
52
- const escActive = state.kind === 'loading' || state.kind === 'error' || (state.kind === 'ready' && state.entries.length === 0)
53
- useAppInput((_input, key) => {
54
- if (key.escape) onCancel()
55
- }, { isActive: escActive })
56
-
57
- useEffect(() => {
58
- let cancelled = false
59
- void (async () => {
60
- try {
61
- const entries = await listRewindEntries(cwd, { limit: pageSize, offset: 0 })
62
- if (cancelled) return
63
- const firstFilePath = entries[0]?.filePath ?? null
64
- const firstEntryId = entries.find(entry => entry.filePath === firstFilePath)?.id ?? null
65
- setState({
66
- kind: 'ready',
67
- entries,
68
- offset: entries.length,
69
- pageSize,
70
- hasMore: entries.length === pageSize,
71
- selectedFilePath: firstFilePath,
72
- selectedId: firstEntryId,
73
- selectedAction: 'code',
74
- stage: 'files',
75
- restoring: false,
76
- })
77
- } catch (err: unknown) {
78
- if (cancelled) return
79
- setState({ kind: 'error', message: (err as Error).message })
80
- }
81
- })()
82
- return () => { cancelled = true }
83
- }, [cwd, pageSize])
84
-
85
- if (state.kind === 'loading') {
86
- return (
87
- <Surface title="Rewind" subtitle="loading checkpoints...">
88
- <Spinner label="loading rewind history..." />
89
- </Surface>
90
- )
91
- }
92
-
93
- if (state.kind === 'error') {
94
- return (
95
- <Surface title="Rewind" tone="muted" footer="esc closes">
96
- <Text color={theme.dim}>{state.message}</Text>
97
- </Surface>
98
- )
99
- }
100
-
101
- if (state.entries.length === 0) {
102
- return (
103
- <Surface title="Rewind" tone="muted" footer="esc closes">
104
- <Text color={theme.dim}>No managed edits are available to rewind in this workspace.</Text>
105
- </Surface>
106
- )
107
- }
108
-
109
- const fileEntries = dedupeFiles(state.entries)
110
- const scopedEntries = state.selectedFilePath
111
- ? state.entries.filter(entry => entry.filePath === state.selectedFilePath)
112
- : []
113
- const selectedFile = fileEntries.find(entry => entry.filePath === state.selectedFilePath) ?? fileEntries[0]!
114
- const selectedEntry = scopedEntries.find(entry => entry.id === state.selectedId) ?? scopedEntries[0] ?? selectedFile
115
- const canRestoreConversation = Boolean(selectedEntry.turnId && selectedEntry.sessionId === currentSessionId)
116
-
117
- const executeRestore = async (action: RestoreAction) => {
118
- setState(prev => prev.kind === 'ready' ? { ...prev, restoring: true, selectedAction: action } : prev)
119
- try {
120
- if (action === 'conversation') {
121
- if (!selectedEntry.turnId || !canRestoreConversation) {
122
- onDone('conversation restore is not available for this checkpoint.', 'error')
123
- return
124
- }
125
- onRestoreConversation(selectedEntry.turnId)
126
- onDone(`restored conversation to before: ${selectedEntry.checkpointLabel}`, 'dim')
127
- return
128
- }
129
-
130
- const result = await rewindWorkspaceEditsByEntryIds(cwd, [selectedEntry.id])
131
- if (result.reverted === 0) {
132
- onDone('no matching rewind entry was found.', 'error')
133
- return
134
- }
135
-
136
- if (action === 'both') {
137
- if (!selectedEntry.turnId || !canRestoreConversation) {
138
- onDone('conversation restore is not available for this checkpoint.', 'error')
139
- return
140
- }
141
- onRestoreConversation(selectedEntry.turnId)
142
- }
143
-
144
- const fileList = result.files.map(file => file.split(/[\\/]/).at(-1) ?? file).join(', ')
145
- const prefix = action === 'both' ? 'restored code and conversation' : 'restored code'
146
- onDone(`${prefix}: ${fileList}`, 'dim')
147
- } catch (err: unknown) {
148
- onDone(`rewind failed: ${(err as Error).message}`, 'error')
149
- }
150
- }
151
-
152
- const loadMoreEntries = async () => {
153
- if (state.kind !== 'ready' || !state.hasMore || state.restoring) return
154
- try {
155
- const nextEntries = await listRewindEntries(cwd, { limit: state.pageSize, offset: state.offset })
156
- setState(prev => {
157
- if (prev.kind !== 'ready') return prev
158
- const merged = dedupeEntryIds([...prev.entries, ...nextEntries])
159
- const selectedFilePath = prev.selectedFilePath ?? merged[0]?.filePath ?? null
160
- const selectedId =
161
- prev.selectedId && merged.some(entry => entry.id === prev.selectedId)
162
- ? prev.selectedId
163
- : merged.find(entry => entry.filePath === selectedFilePath)?.id ?? merged[0]?.id ?? null
164
- return {
165
- ...prev,
166
- entries: merged,
167
- offset: prev.offset + nextEntries.length,
168
- hasMore: nextEntries.length === prev.pageSize,
169
- selectedFilePath,
170
- selectedId,
171
- }
172
- })
173
- } catch (err: unknown) {
174
- onDone(`failed to load older checkpoints: ${(err as Error).message}`, 'error')
175
- }
176
- }
177
-
178
- const handleCancel = () => {
179
- if (state.stage === 'actions') {
180
- setState(prev => prev.kind === 'ready' ? { ...prev, stage: 'entries' } : prev)
181
- return
182
- }
183
- if (state.stage === 'entries') {
184
- setState(prev => prev.kind === 'ready' ? { ...prev, stage: 'files' } : prev)
185
- return
186
- }
187
- onCancel()
188
- }
189
-
190
- return (
191
- <Surface
192
- title="Rewind"
193
- subtitle={buildSubtitle(state.stage, selectedEntry.relativePath)}
194
- footer={buildFooter(state.stage, state.restoring)}
195
- >
196
- {state.stage === 'files' ? (
197
- <>
198
- <Select
199
- options={buildFileOptions(fileEntries, state.hasMore)}
200
- onSubmit={filePath => {
201
- if (filePath === LOAD_MORE_VALUE) {
202
- void loadMoreEntries()
203
- return
204
- }
205
- const firstEntry = state.entries.find(entry => entry.filePath === filePath) ?? null
206
- setState(prev => prev.kind === 'ready'
207
- ? {
208
- ...prev,
209
- selectedFilePath: filePath,
210
- selectedId: firstEntry?.id ?? null,
211
- stage: 'entries',
212
- }
213
- : prev)
214
- }}
215
- onCancel={handleCancel}
216
- onHighlight={value => {
217
- if (value === LOAD_MORE_VALUE) return
218
- setState(prev => prev.kind === 'ready'
219
- ? {
220
- ...prev,
221
- selectedFilePath: value,
222
- selectedId: prev.entries.find(entry => entry.filePath === value)?.id ?? null,
223
- }
224
- : prev)
225
- }}
226
- />
227
- <CompactPreview entry={selectedFile} />
228
- </>
229
- ) : state.stage === 'entries' ? (
230
- <>
231
- <Select
232
- options={buildEntryOptions(scopedEntries, state.hasMore)}
233
- onSubmit={entryId => {
234
- if (entryId === LOAD_MORE_VALUE) {
235
- void loadMoreEntries()
236
- return
237
- }
238
- setState(prev => prev.kind === 'ready'
239
- ? { ...prev, selectedId: entryId, stage: 'actions' }
240
- : prev)
241
- }}
242
- onCancel={handleCancel}
243
- onHighlight={value => {
244
- if (value === LOAD_MORE_VALUE) return
245
- setState(prev => prev.kind === 'ready'
246
- ? { ...prev, selectedId: value }
247
- : prev)
248
- }}
249
- />
250
- <CompactPreview entry={selectedEntry} />
251
- </>
252
- ) : (
253
- <>
254
- <Select
255
- options={buildActionOptions(canRestoreConversation)}
256
- onSubmit={value => { void executeRestore(value) }}
257
- onCancel={handleCancel}
258
- onHighlight={value => setState(prev => prev.kind === 'ready'
259
- ? { ...prev, selectedAction: value }
260
- : prev)}
261
- />
262
- <ActionPreview entry={selectedEntry} selectedAction={state.selectedAction} canRestoreConversation={canRestoreConversation} />
263
- </>
264
- )}
265
- </Surface>
266
- )
267
- }
268
-
269
- const LOAD_MORE_VALUE = '__load_more__'
270
-
271
- function buildFileOptions(entries: RewindEntry[], hasMore: boolean): Array<SelectOption<string>> {
272
- const options = entries.map(entry => ({
273
- value: entry.filePath,
274
- label: entry.relativePath,
275
- hint: formatTimestamp(entry.createdAt),
276
- }))
277
- if (hasMore) {
278
- options.push({
279
- value: LOAD_MORE_VALUE,
280
- label: 'show older checkpoints',
281
- hint: 'load more file history from this directory',
282
- })
283
- }
284
- return options
285
- }
286
-
287
- function buildEntryOptions(entries: RewindEntry[], hasMore: boolean): Array<SelectOption<string>> {
288
- const options = entries.map(entry => ({
289
- value: entry.id,
290
- label: entry.checkpointLabel || 'checkpoint',
291
- hint: `${formatTimestamp(entry.createdAt)} · ${entry.changeSummary}`,
292
- }))
293
- if (hasMore) {
294
- options.push({
295
- value: LOAD_MORE_VALUE,
296
- label: 'show older checkpoints',
297
- hint: 'load more checkpoints for this directory',
298
- })
299
- }
300
- return options
301
- }
302
-
303
- function buildActionOptions(canRestoreConversation: boolean): Array<SelectOption<RestoreAction>> {
304
- const options: Array<SelectOption<RestoreAction>> = []
305
- if (canRestoreConversation) {
306
- options.push({ value: 'both', label: 'restore code and conversation', hint: 'full rewind' })
307
- options.push({ value: 'conversation', label: 'restore conversation only', hint: 'keep current files unchanged' })
308
- }
309
- options.push({ value: 'code', label: 'restore code only', hint: 'revert the selected file checkpoint only' })
310
- return options
311
- }
312
-
313
- const CompactPreview: React.FC<{ entry: RewindEntry }> = ({ entry }) => (
314
- <Box flexDirection="column" marginTop={1}>
315
- <Text color={theme.accentPeriwinkle}>{entry.relativePath}</Text>
316
- <Text color={theme.dim}>{entry.promptSnippet || '(prompt snippet unavailable for older checkpoints)'}</Text>
317
- </Box>
318
- )
319
-
320
- const ActionPreview: React.FC<{
321
- entry: RewindEntry
322
- selectedAction: RestoreAction
323
- canRestoreConversation: boolean
324
- }> = ({ entry, selectedAction, canRestoreConversation }) => (
325
- <Box flexDirection="column" marginTop={1}>
326
- <Text color={theme.accentPeriwinkle}>{entry.relativePath}</Text>
327
- <Text color={theme.dim}>{formatTimestamp(entry.createdAt)} · {entry.changeSummary}</Text>
328
- <Text color={theme.textSubtle}>
329
- {selectedAction === 'both'
330
- ? 'restore the selected file checkpoint and roll the current conversation back to before this prompt.'
331
- : selectedAction === 'conversation'
332
- ? 'restore only the conversation state to before this prompt.'
333
- : 'restore only the selected file checkpoint.'}
334
- </Text>
335
- {!canRestoreConversation ? (
336
- <Text color={theme.dim}>Conversation restore is only available for checkpoints from the current session.</Text>
337
- ) : null}
338
- <Text color={theme.textSubtle}>{previewContent(entry.previousContent)}</Text>
339
- </Box>
340
- )
341
-
342
- function dedupeFiles(entries: RewindEntry[]): RewindEntry[] {
343
- const seen = new Set<string>()
344
- const out: RewindEntry[] = []
345
- for (const entry of entries) {
346
- if (seen.has(entry.filePath)) continue
347
- seen.add(entry.filePath)
348
- out.push(entry)
349
- }
350
- return out
351
- }
352
-
353
- function dedupeEntryIds(entries: RewindEntry[]): RewindEntry[] {
354
- const seen = new Set<string>()
355
- const out: RewindEntry[] = []
356
- for (const entry of entries) {
357
- if (seen.has(entry.id)) continue
358
- seen.add(entry.id)
359
- out.push(entry)
360
- }
361
- return out
362
- }
363
-
364
- function previewContent(text: string): string {
365
- if (!text.trim()) return '(empty before this edit)'
366
- const normalized = text.replace(/\s+$/g, '')
367
- return normalized.length <= 140 ? normalized : `${normalized.slice(0, 137)}...`
368
- }
369
-
370
- function formatTimestamp(iso: string): string {
371
- const date = new Date(iso)
372
- return Number.isNaN(date.getTime()) ? iso : date.toLocaleString()
373
- }
374
-
375
- function buildSubtitle(stage: ReadyState['stage'], relativePath: string): string {
376
- if (stage === 'files') return 'choose a file with saved checkpoints.'
377
- if (stage === 'entries') return `checkpoints for ${relativePath}`
378
- return `choose how to restore ${relativePath}`
379
- }
380
-
381
- function buildFooter(stage: ReadyState['stage'], restoring: boolean): string {
382
- if (restoring) return 'restoring...'
383
- if (stage === 'files') return 'enter selects a file · esc closes'
384
- if (stage === 'entries') return 'enter chooses a checkpoint · esc back'
385
- return 'enter restores · esc back'
386
- }
@@ -1,8 +0,0 @@
1
- const COMPACT_SUCCESS_TOOL_RESULTS = new Set([
2
- 'read_file',
3
- 'read_private_continuity_file',
4
- ])
5
-
6
- export function hidesSuccessfulToolResultContent(name: string, isError?: boolean): boolean {
7
- return !isError && COMPACT_SUCCESS_TOOL_RESULTS.has(name)
8
- }