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.
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +1 -7
- package/src/app/FirstRunTimeline.tsx +1 -1
- package/src/auth/openaiOAuth/credentials.ts +47 -0
- package/src/auth/openaiOAuth/crypto.ts +23 -0
- package/src/auth/openaiOAuth/index.ts +238 -0
- package/src/auth/openaiOAuth/landingPage.ts +125 -0
- package/src/auth/openaiOAuth/listener.ts +151 -0
- package/src/auth/openaiOAuth/refresh.ts +70 -0
- package/src/auth/openaiOAuth/shared.ts +115 -0
- package/src/chat/ChatBottomPane.tsx +20 -11
- package/src/chat/ChatScreen.tsx +160 -35
- package/src/chat/ConversationStack.tsx +1 -1
- package/src/chat/MessageList.tsx +185 -72
- package/src/chat/SessionStatus.tsx +3 -1
- package/src/chat/chatScreenUtils.ts +11 -15
- package/src/chat/chatSessionState.ts +3 -2
- package/src/chat/chatTurnOrchestrator.ts +1 -7
- package/src/chat/commands.ts +28 -27
- package/src/chat/display/DiffView.tsx +193 -0
- package/src/chat/display/SyntaxText.tsx +192 -0
- package/src/chat/display/toolCallDisplay.ts +103 -0
- package/src/chat/display/toolResultDisplay.ts +19 -0
- package/src/chat/{ChatInput.tsx → input/ChatInput.tsx} +36 -23
- package/src/chat/{TranscriptView.tsx → transcript/TranscriptView.tsx} +24 -50
- package/src/chat/{transcriptViewport.ts → transcript/transcriptViewport.ts} +12 -30
- package/src/chat/{ContextLimitView.tsx → views/ContextLimitView.tsx} +3 -3
- package/src/chat/{ContinuityEditReviewView.tsx → views/ContinuityEditReviewView.tsx} +11 -3
- package/src/chat/{CopyPicker.tsx → views/CopyPicker.tsx} +4 -5
- package/src/chat/{PermissionPrompt.tsx → views/PermissionPrompt.tsx} +16 -17
- package/src/chat/{PermissionsView.tsx → views/PermissionsView.tsx} +6 -6
- package/src/chat/{PlanApprovalView.tsx → views/PlanApprovalView.tsx} +4 -4
- package/src/chat/{ResumeView.tsx → views/ResumeView.tsx} +35 -35
- package/src/chat/views/RewindView.tsx +410 -0
- package/src/identity/continuity/privateEdit/diff.ts +2 -78
- package/src/identity/ens/agentRecords.ts +5 -19
- package/src/identity/ens/ensAutomation/setup.ts +0 -1
- package/src/identity/ens/ensAutomation/types.ts +0 -1
- package/src/identity/hub/OperationalRoutes.tsx +23 -32
- package/src/identity/hub/Routes.tsx +13 -13
- package/src/identity/hub/{flows/continuity → continuity}/ContinuityDashboardScreen.tsx +9 -9
- package/src/identity/hub/{flows/continuity → continuity}/RebackupStorageScreen.tsx +2 -2
- package/src/identity/hub/{flows/continuity → continuity}/RecoveryConfirmScreen.tsx +5 -5
- package/src/identity/hub/{flows/continuity → continuity}/SavePromptScreen.tsx +5 -5
- package/src/identity/hub/{effects/rebackup/runRebackup.ts → continuity/effects.ts} +19 -19
- package/src/identity/hub/{effects/rebackup → continuity}/index.ts +1 -1
- package/src/identity/hub/{effects/shared → continuity}/snapshot.ts +8 -8
- package/src/identity/hub/{effects/rebackup → continuity}/vault.ts +15 -15
- package/src/identity/hub/{flows/create → create}/CreateFlow.tsx +13 -13
- package/src/identity/hub/{effects/create.ts → create/effects.ts} +4 -4
- package/src/identity/hub/{flows/custody → custody}/CustodyEditFlow.tsx +10 -48
- package/src/identity/hub/{flows/custody/custodyFlowActions.ts → custody/actions.ts} +11 -9
- package/src/identity/hub/{flows/custody/custodyFlowHelpers.ts → custody/helpers.ts} +4 -4
- package/src/identity/hub/{effects/vault → custody}/preflight.ts +5 -5
- package/src/identity/hub/{flows/custody/custodyFlowRoutes.tsx → custody/routes.tsx} +8 -8
- package/src/identity/hub/{flows/custody/custodyEffects.ts → custody/transactions.ts} +9 -9
- package/src/identity/hub/{flows/custody/custodyFlowTypes.ts → custody/types.ts} +6 -6
- package/src/identity/hub/{flows/custody/custodyFlowEffects.ts → custody/useCustodyEffects.ts} +7 -7
- package/src/identity/hub/{flows/custody → custody}/useCustodyFlow.tsx +5 -5
- package/src/identity/hub/ens/EnsEditAdvancedScreens.tsx +241 -0
- package/src/identity/hub/{flows/ens → ens}/EnsEditFlow.tsx +27 -82
- package/src/identity/hub/{flows/ens → ens}/EnsEditMaintenanceScreens.tsx +25 -65
- package/src/identity/hub/{flows/ens → ens}/EnsEditReviewScreens.tsx +12 -30
- package/src/identity/hub/ens/EnsEditRunners.tsx +62 -0
- package/src/identity/hub/{flows/ens → ens}/EnsEditShared.tsx +15 -14
- package/src/identity/hub/{flows/ens → ens}/EnsEditSimpleScreens.tsx +68 -217
- package/src/identity/hub/{flows/ens/IdentityHubEnsFlow.tsx → ens/EnsFlow.tsx} +18 -11
- package/src/identity/hub/{flows/ens/OperatorWalletsScreen.tsx → ens/EnsOperatorWalletsScreen.tsx} +17 -48
- package/src/identity/hub/{advancedEnsValidation.ts → ens/advancedEnsValidation.ts} +2 -2
- package/src/identity/hub/{flows/ens/ensEditCopy.ts → ens/editCopy.ts} +4 -4
- package/src/identity/hub/{effects/ens/flows.ts → ens/effects.ts} +7 -7
- package/src/identity/hub/{effects/ens → ens}/index.ts +1 -1
- package/src/identity/hub/{model/ens.ts → ens/state.ts} +1 -1
- package/src/identity/hub/{effects/ens → ens}/transactions.ts +232 -232
- package/src/identity/hub/{flows/ens/ensEditTypes.ts → ens/types.ts} +12 -26
- package/src/identity/hub/identityHubReducer.ts +3 -3
- package/src/identity/hub/{flows/profile → profile}/EditProfileFlow.tsx +17 -10
- package/src/identity/hub/{effects/publicProfile/runPublicProfileSave.ts → profile/effects.ts} +55 -177
- package/src/identity/hub/{model → profile}/identity.ts +3 -3
- package/src/identity/hub/{effects/profile/profileState.ts → profile/state.ts} +181 -173
- package/src/identity/hub/{flows/restore → restore}/RestoreFlow.tsx +21 -21
- package/src/identity/hub/{effects/restore → restore}/apply.ts +10 -10
- package/src/identity/hub/{effects/restore → restore}/auth.ts +7 -7
- package/src/identity/hub/{effects/restore → restore}/discover.ts +6 -6
- package/src/identity/hub/{effects/restore → restore}/envelopes.ts +2 -2
- package/src/identity/hub/{effects/restore → restore}/fetch.ts +3 -3
- package/src/identity/hub/{effects/restore/shared.ts → restore/helpers.ts} +6 -6
- package/src/identity/hub/{effects/restore → restore}/recovery.ts +10 -10
- package/src/identity/hub/{effects/restore → restore}/resolve.ts +4 -4
- package/src/identity/hub/restore/restoreAdmin.ts +34 -0
- package/src/identity/hub/{flows/restore/useRestoreFlowEffects.ts → restore/useRestoreEffects.ts} +5 -5
- package/src/identity/hub/{flows/settings → settings}/StorageCredentialScreen.tsx +5 -5
- package/src/identity/hub/{components → shared/components}/BusyScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/DetailsScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/ErrorScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/FlowTimeline.tsx +1 -1
- package/src/identity/hub/{components → shared/components}/IdentitySummary.tsx +16 -11
- package/src/identity/hub/{components → shared/components}/MenuScreen.tsx +8 -9
- package/src/identity/hub/{components → shared/components}/NetworkScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/PinataJwtInput.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/UnlinkedIdentityScreen.tsx +5 -5
- package/src/identity/hub/{components → shared/components}/WalletApprovalScreen.tsx +6 -6
- package/src/identity/hub/{components → shared/components}/menuFlagsFromReconciliation.ts +2 -4
- package/src/identity/hub/{effects/shared → shared/effects}/profilePrep.ts +1 -1
- package/src/identity/hub/{effects → shared/effects}/receipts.ts +2 -2
- package/src/identity/hub/{effects/shared → shared/effects}/sync.ts +6 -47
- package/src/identity/hub/{effects → shared/effects}/types.ts +3 -3
- package/src/identity/hub/{model → shared/model}/copy.ts +2 -2
- package/src/identity/hub/{model → shared/model}/errors.ts +5 -5
- package/src/identity/hub/{model → shared/model}/network.ts +3 -3
- package/src/identity/hub/{operatorWallets.ts → shared/operatorWallets.ts} +1 -1
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/hook.ts +1 -2
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/ownership.ts +2 -2
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/run.ts +7 -40
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/types.ts +0 -4
- package/src/identity/hub/{reconciliation → shared/reconciliation}/index.ts +0 -7
- package/src/identity/hub/shared/reconciliation/walletSetup.ts +27 -0
- package/src/identity/hub/{utils.ts → shared/utils.ts} +5 -5
- package/src/identity/hub/{flows/token-transfer/IdentityHubTokenTransferFlow.tsx → transfer/TokenTransferFlow.tsx} +8 -8
- package/src/identity/hub/{flows/token-transfer → transfer}/TokenTransferScreens.tsx +14 -14
- package/src/identity/hub/{effects/token-transfer/runTokenTransfer.ts → transfer/effects.ts} +16 -16
- package/src/identity/hub/{effects/token-transfer → transfer}/progress.ts +1 -1
- package/src/identity/hub/useIdentityHubController.ts +11 -11
- package/src/identity/hub/useIdentityHubSideEffects.ts +11 -11
- package/src/identity/wallet/browserWallet/types.ts +0 -5
- package/src/identity/wallet/page/copy.ts +1 -31
- package/src/identity/wallet/walletPurposeCompat.ts +0 -2
- package/src/models/ModelPicker.tsx +248 -8
- package/src/models/catalog.ts +29 -1
- package/src/models/modelPickerOptions.ts +12 -10
- package/src/models/providerDisplay.ts +16 -0
- package/src/providers/errors.ts +6 -4
- package/src/providers/openai-chat.ts +2 -1
- package/src/providers/openai-responses-format.ts +156 -0
- package/src/providers/openai-responses.ts +276 -0
- package/src/providers/registry.ts +85 -8
- package/src/runtime/sessionMode.ts +1 -1
- package/src/runtime/systemPrompt.ts +4 -2
- package/src/runtime/toolExecution.ts +9 -6
- package/src/runtime/turn.ts +29 -1
- package/src/storage/rewind.ts +20 -0
- package/src/storage/secrets.ts +4 -1
- package/src/storage/sessions.ts +2 -1
- package/src/tools/bashSafety.ts +7 -3
- package/src/tools/bashTool.ts +1 -1
- package/src/tools/contracts.ts +3 -0
- package/src/tools/deleteFileTool.ts +8 -3
- package/src/tools/editTool.ts +10 -5
- package/src/tools/fileDiff.ts +261 -0
- package/src/tools/privateContinuityEditTool.ts +11 -1
- package/src/tools/writeFileTool.ts +8 -3
- package/src/ui/Spinner.tsx +25 -3
- package/src/ui/TextInput.tsx +2 -2
- package/src/ui/theme.ts +17 -0
- package/src/utils/clipboard.ts +10 -7
- package/src/utils/openExternal.ts +20 -10
- package/src/chat/RewindView.tsx +0 -386
- package/src/chat/toolResultDisplay.ts +0 -8
- package/src/identity/ens/ensRegistration.ts +0 -199
- package/src/identity/hub/effects/index.ts +0 -74
- package/src/identity/hub/effects/publicProfile/index.ts +0 -5
- package/src/identity/hub/effects/restore/restoreEffects.ts +0 -22
- package/src/identity/hub/effects/restoreAdmin.ts +0 -93
- package/src/identity/hub/effects/token-transfer/index.ts +0 -6
- package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +0 -336
- package/src/identity/hub/flows/ens/EnsEditRunners.tsx +0 -198
- package/src/identity/hub/reconciliation/walletSetup.ts +0 -220
- /package/src/chat/{chatInputState.ts → input/chatInputState.ts} +0 -0
- /package/src/chat/{chatPaste.ts → input/chatPaste.ts} +0 -0
- /package/src/chat/{textCursor.ts → input/textCursor.ts} +0 -0
- /package/src/identity/hub/{model/continuity.ts → continuity/state.ts} +0 -0
- /package/src/identity/hub/{model/custody.ts → custody/state.ts} +0 -0
- /package/src/identity/hub/{effects/restore → restore}/index.ts +0 -0
- /package/src/identity/hub/{model → shared/model}/format.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/useAgentReconciliation.ts +0 -0
- /package/src/identity/hub/{txGuard.ts → shared/txGuard.ts} +0 -0
- /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',
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
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<
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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()
|
package/src/chat/RewindView.tsx
DELETED
|
@@ -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
|
-
}
|