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/runtime/turn.ts
CHANGED
|
@@ -56,6 +56,7 @@ function normalize(event: StreamEvent): ProviderTurnEvent {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export const MAX_CONTINUATION_NUDGES = 3
|
|
59
|
+
export const MAX_TOOL_USES_PER_TURN = 25
|
|
59
60
|
|
|
60
61
|
export type ContinuationNudgeReason =
|
|
61
62
|
| 'continuation'
|
|
@@ -63,6 +64,7 @@ export type ContinuationNudgeReason =
|
|
|
63
64
|
| 'tool_state_claim'
|
|
64
65
|
| 'tool_protocol_fake'
|
|
65
66
|
| 'tool_delegation'
|
|
67
|
+
| 'tool_budget'
|
|
66
68
|
| 'private_continuity_tool'
|
|
67
69
|
| 'private_continuity_tool_repair'
|
|
68
70
|
| 'reasoning_only'
|
|
@@ -82,6 +84,9 @@ const TOOL_PROTOCOL_FAKE_NUDGE_TEXT =
|
|
|
82
84
|
const TOOL_DELEGATION_NUDGE_TEXT =
|
|
83
85
|
'Do not ask the user to run native tools. You have access to the tools in this environment. Make exactly one native tool call now.'
|
|
84
86
|
|
|
87
|
+
const TOOL_BUDGET_NUDGE_TEXT =
|
|
88
|
+
'You have reached the tool-call budget for this turn. Do not call any more tools. Produce your final answer now using only what you already know from earlier tool results.'
|
|
89
|
+
|
|
85
90
|
const PRIVATE_CONTINUITY_NUDGE_TEXT =
|
|
86
91
|
'SOUL.md and MEMORY.md are existing private identity-vault scaffold files. Do not search workspace folders, read plans/, create files, or overwrite them. If exact private text is needed for a surgical removal or targeted replacement, call read_private_continuity_file with {"file":"MEMORY.md"} or {"file":"SOUL.md"}. If the user wants private continuity changed, call propose_private_continuity_edit. For memory/preferences use {"file":"MEMORY.md","appendToSection":"Durable User Preferences","appendText":"- User preference or memory note."}. For persona use {"file":"SOUL.md","appendToSection":"Persona","appendText":"- Persona or standing behavior note."}.'
|
|
87
92
|
|
|
@@ -164,6 +169,7 @@ export async function* runRuntimeTurn(
|
|
|
164
169
|
let continuationNudges = 0
|
|
165
170
|
let iterationIndex = 0
|
|
166
171
|
let priorIterationHadTools = false
|
|
172
|
+
let cumulativeToolUseCount = 0
|
|
167
173
|
const toolEvidenceThisTurn: ToolEvidence[] = []
|
|
168
174
|
|
|
169
175
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -395,6 +401,29 @@ export async function* runRuntimeTurn(
|
|
|
395
401
|
return
|
|
396
402
|
}
|
|
397
403
|
|
|
404
|
+
if (cumulativeToolUseCount + pendingToolUses.length > MAX_TOOL_USES_PER_TURN) {
|
|
405
|
+
if (continuationNudges < maxContinuationNudges) {
|
|
406
|
+
continuationNudges += 1
|
|
407
|
+
yield {
|
|
408
|
+
type: 'continuation_nudge',
|
|
409
|
+
attempt: continuationNudges,
|
|
410
|
+
reason: 'tool_budget',
|
|
411
|
+
}
|
|
412
|
+
workingMessages = [
|
|
413
|
+
...await rebuildMessages(),
|
|
414
|
+
{ role: 'user', content: TOOL_BUDGET_NUDGE_TEXT },
|
|
415
|
+
]
|
|
416
|
+
continue
|
|
417
|
+
}
|
|
418
|
+
yield {
|
|
419
|
+
type: 'error',
|
|
420
|
+
message: `tool budget exceeded (${MAX_TOOL_USES_PER_TURN} max per turn); ask again with a narrower request`,
|
|
421
|
+
}
|
|
422
|
+
yield doneEvent(false, stopReason)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
cumulativeToolUseCount += pendingToolUses.length
|
|
426
|
+
|
|
398
427
|
const batch = await runToolBatch(pendingToolUses)
|
|
399
428
|
for (const completed of batch.completedTools) {
|
|
400
429
|
toolEvidenceThisTurn.push({
|
|
@@ -461,7 +490,6 @@ function nextToolResultRepairNudge(
|
|
|
461
490
|
completedTools: ExecutedToolUse[],
|
|
462
491
|
): string | null {
|
|
463
492
|
if (!provider.supportsTools) return null
|
|
464
|
-
if (provider.id !== 'llamacpp') return null
|
|
465
493
|
const failedPrivateEdit = completedTools.some(completed =>
|
|
466
494
|
completed.name === 'propose_private_continuity_edit'
|
|
467
495
|
&& !completed.result.ok
|
package/src/storage/rewind.ts
CHANGED
|
@@ -131,6 +131,26 @@ export async function listRewindEntries(
|
|
|
131
131
|
.slice(offset, offset + limit)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
export async function groupRewindEntriesByTurn(
|
|
135
|
+
workspaceRoot: string,
|
|
136
|
+
sessionId: string,
|
|
137
|
+
): Promise<Map<string, RewindEntry[]>> {
|
|
138
|
+
const normalizedWorkspaceRoot = path.resolve(workspaceRoot)
|
|
139
|
+
const snapshots = await loadSnapshots()
|
|
140
|
+
const grouped = new Map<string, RewindEntry[]>()
|
|
141
|
+
for (const snapshot of snapshots) {
|
|
142
|
+
if (isIdentityMarkdownSnapshot(snapshot)) continue
|
|
143
|
+
if (!isSnapshotWithinScope(snapshot, normalizedWorkspaceRoot)) continue
|
|
144
|
+
if (snapshot.sessionId !== sessionId) continue
|
|
145
|
+
if (!snapshot.turnId) continue
|
|
146
|
+
const entry = toEntry(snapshot)
|
|
147
|
+
const bucket = grouped.get(snapshot.turnId)
|
|
148
|
+
if (bucket) bucket.push(entry)
|
|
149
|
+
else grouped.set(snapshot.turnId, [entry])
|
|
150
|
+
}
|
|
151
|
+
return grouped
|
|
152
|
+
}
|
|
153
|
+
|
|
134
154
|
export async function rewindWorkspaceEditsByEntryIds(
|
|
135
155
|
workspaceRoot: string,
|
|
136
156
|
entryIds: string[],
|
package/src/storage/secrets.ts
CHANGED
|
@@ -129,8 +129,11 @@ export async function getSecret(account: string): Promise<string | null> {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
export async function setSecret(account: string, value: string): Promise<KeyBackend> {
|
|
132
|
+
if (typeof value !== 'string') {
|
|
133
|
+
throw new Error(`setSecret(${account}): value is ${typeof value}, expected string`)
|
|
134
|
+
}
|
|
132
135
|
const trimmed = value.trim()
|
|
133
|
-
if (!trimmed) throw new Error(
|
|
136
|
+
if (!trimmed) throw new Error(`setSecret(${account}): value is empty`)
|
|
134
137
|
const keytar = await loadKeytar()
|
|
135
138
|
if (keytar) {
|
|
136
139
|
await keytar.setPassword(KEYTAR_SERVICE, account, trimmed)
|
package/src/storage/sessions.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Message } from '../providers/contracts.js'
|
|
|
6
6
|
import { getCwd } from '../runtime/cwd.js'
|
|
7
7
|
import type { SessionMode } from '../runtime/sessionMode.js'
|
|
8
8
|
import { atomicWriteText } from './atomicWrite.js'
|
|
9
|
+
import { stripFileChangeResultDiff } from '../tools/fileDiff.js'
|
|
9
10
|
import {
|
|
10
11
|
isUserCorrectionOfToolState,
|
|
11
12
|
looksLikeToolStateClaim,
|
|
@@ -282,7 +283,7 @@ export function sessionMessagesToProviderMessages(
|
|
|
282
283
|
content: [{
|
|
283
284
|
type: 'tool_result',
|
|
284
285
|
toolUseId: message.toolUseId,
|
|
285
|
-
content: message.content,
|
|
286
|
+
content: stripFileChangeResultDiff(message.content),
|
|
286
287
|
isError: message.isError,
|
|
287
288
|
}],
|
|
288
289
|
})
|
package/src/tools/bashSafety.ts
CHANGED
|
@@ -98,16 +98,16 @@ export function assessBashCommand(command: string): BashSafetyAssessment {
|
|
|
98
98
|
const triggeredChecks = RISKY_PATTERN_CHECKS.filter(check => check.pattern.test(command)).map(check => check.message)
|
|
99
99
|
|
|
100
100
|
const warning = triggeredChecks.length > 0
|
|
101
|
-
? `
|
|
101
|
+
? `Warning: ${sentenceCase(triggeredChecks[0] ?? 'command is risky')}. Reusable approval is limited for this command.`
|
|
102
102
|
: highRisk
|
|
103
|
-
? `
|
|
103
|
+
? `Warning: ${sentenceCase(firstToken ?? '')} is a high-impact command. Reusable approval is limited for this command.`
|
|
104
104
|
: undefined
|
|
105
105
|
|
|
106
106
|
return {
|
|
107
107
|
warning,
|
|
108
108
|
canPersistExact: triggeredChecks.length === 0 && !nonPersistable,
|
|
109
109
|
canPersistPrefix: triggeredChecks.length === 0 && !highRisk && Boolean(firstToken),
|
|
110
|
-
commandPrefix: firstToken,
|
|
110
|
+
commandPrefix: firstToken ?? '',
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
@@ -180,3 +180,7 @@ function normalizeCommandToken(token: string): string {
|
|
|
180
180
|
.toLowerCase()
|
|
181
181
|
.replace(/[^a-z0-9_.:-]/g, '') ?? ''
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
function sentenceCase(value: string): string {
|
|
185
|
+
return value ? value[0]!.toUpperCase() + value.slice(1) : value
|
|
186
|
+
}
|
package/src/tools/bashTool.ts
CHANGED
|
@@ -39,7 +39,7 @@ export const bashTool: Tool<typeof schema> = {
|
|
|
39
39
|
command: input.command,
|
|
40
40
|
commandPrefix: safety.commandPrefix,
|
|
41
41
|
cwd,
|
|
42
|
-
title: '
|
|
42
|
+
title: 'Allow shell command?',
|
|
43
43
|
subtitle: `${input.command}\n${cwd}`,
|
|
44
44
|
warning: safety.warning,
|
|
45
45
|
canPersistExact: safety.canPersistExact,
|
package/src/tools/contracts.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type PermissionRequest =
|
|
|
23
23
|
subtitle: string
|
|
24
24
|
before: string
|
|
25
25
|
after: string
|
|
26
|
+
diff: string
|
|
26
27
|
changeSummary: string
|
|
27
28
|
}
|
|
28
29
|
| {
|
|
@@ -34,6 +35,7 @@ export type PermissionRequest =
|
|
|
34
35
|
subtitle: string
|
|
35
36
|
before: string
|
|
36
37
|
after: string
|
|
38
|
+
diff: string
|
|
37
39
|
changeSummary: string
|
|
38
40
|
}
|
|
39
41
|
| {
|
|
@@ -68,6 +70,7 @@ export type PermissionRequest =
|
|
|
68
70
|
subtitle: string
|
|
69
71
|
before: string
|
|
70
72
|
after: string
|
|
73
|
+
diff: string
|
|
71
74
|
changeSummary: string
|
|
72
75
|
}
|
|
73
76
|
| {
|
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import { recordRewindSnapshot } from '../storage/rewind.js'
|
|
5
5
|
import type { Tool } from './contracts.js'
|
|
6
|
+
import { formatFileChangeResult, renderUnifiedFileDiff } from './fileDiff.js'
|
|
6
7
|
import { resolveWorkspacePath } from './readTool.js'
|
|
7
8
|
|
|
8
9
|
const schema = z.object({
|
|
@@ -35,6 +36,7 @@ export const deleteFileTool: Tool<typeof schema> = {
|
|
|
35
36
|
subtitle: prepared.fullPath,
|
|
36
37
|
before: preview(prepared.before),
|
|
37
38
|
after: '(deleted)',
|
|
39
|
+
diff: renderUnifiedFileDiff({ filePath: prepared.relativePath, before: prepared.before, after: '' }),
|
|
38
40
|
changeSummary: `delete ${prepared.relativePath}`,
|
|
39
41
|
}
|
|
40
42
|
},
|
|
@@ -58,9 +60,12 @@ export const deleteFileTool: Tool<typeof schema> = {
|
|
|
58
60
|
return {
|
|
59
61
|
ok: true,
|
|
60
62
|
summary: `deleted ${prepared.relativePath}`,
|
|
61
|
-
content:
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
content: formatFileChangeResult(
|
|
64
|
+
rewindWarning
|
|
65
|
+
? `deleted ${prepared.fullPath}\nwarning: ${rewindWarning}`
|
|
66
|
+
: `deleted ${prepared.fullPath}`,
|
|
67
|
+
renderUnifiedFileDiff({ filePath: prepared.relativePath, before: prepared.before, after: '' }),
|
|
68
|
+
),
|
|
64
69
|
}
|
|
65
70
|
},
|
|
66
71
|
}
|
package/src/tools/editTool.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { recordRewindSnapshot } from '../storage/rewind.js'
|
|
|
5
5
|
import type { EthagentConfig } from '../storage/config.js'
|
|
6
6
|
import type { Tool } from './contracts.js'
|
|
7
7
|
import { applyRequestedEdit } from './editUtils.js'
|
|
8
|
+
import { formatFileChangeResult, renderUnifiedFileDiff } from './fileDiff.js'
|
|
8
9
|
import { resolveWorkspacePath } from './readTool.js'
|
|
9
10
|
|
|
10
11
|
const schema = z.object({
|
|
@@ -44,15 +45,16 @@ export const editTool: Tool<typeof schema> = {
|
|
|
44
45
|
subtitle: fullPath,
|
|
45
46
|
before: applied.previewBefore,
|
|
46
47
|
after: applied.previewAfter,
|
|
48
|
+
diff: renderUnifiedFileDiff({ filePath: relativePath, before: applied.before, after: applied.after }),
|
|
47
49
|
changeSummary: applied.summary,
|
|
48
50
|
}
|
|
49
51
|
},
|
|
50
52
|
async execute(input, context) {
|
|
51
|
-
const { fullPath, applied, existedBefore, before } = await prepareEdit(input, context)
|
|
53
|
+
const { fullPath, relativePath, applied, existedBefore, before } = await prepareEdit(input, context)
|
|
52
54
|
const rewindWarning = await tryRecordRewindSnapshot({
|
|
53
55
|
workspaceRoot: context.workspaceRoot,
|
|
54
56
|
filePath: fullPath,
|
|
55
|
-
relativePath
|
|
57
|
+
relativePath,
|
|
56
58
|
existedBefore,
|
|
57
59
|
previousContent: before,
|
|
58
60
|
changeSummary: applied.summary,
|
|
@@ -68,9 +70,12 @@ export const editTool: Tool<typeof schema> = {
|
|
|
68
70
|
return {
|
|
69
71
|
ok: true,
|
|
70
72
|
summary: applied.summary,
|
|
71
|
-
content:
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
content: formatFileChangeResult(
|
|
74
|
+
rewindWarning
|
|
75
|
+
? `updated ${fullPath}\nwarning: ${rewindWarning}`
|
|
76
|
+
: `updated ${fullPath}`,
|
|
77
|
+
renderUnifiedFileDiff({ filePath: relativePath, before: applied.before, after: applied.after }),
|
|
78
|
+
),
|
|
74
79
|
}
|
|
75
80
|
},
|
|
76
81
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
export const DEFAULT_DIFF_CONTEXT_LINES = 3
|
|
2
|
+
export const DEFAULT_DIFF_MAX_CHARS = 2400
|
|
3
|
+
export const FILE_DIFF_RESULT_MARKER = '\n\n<!-- ethagent:file-diff:v1 -->\n'
|
|
4
|
+
|
|
5
|
+
type DiffOp =
|
|
6
|
+
| { type: 'equal'; line: string }
|
|
7
|
+
| { type: 'delete'; line: string }
|
|
8
|
+
| { type: 'insert'; line: string }
|
|
9
|
+
|
|
10
|
+
type HunkRange = {
|
|
11
|
+
start: number
|
|
12
|
+
end: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type RenderUnifiedFileDiffInput = {
|
|
16
|
+
filePath: string
|
|
17
|
+
before: string
|
|
18
|
+
after: string
|
|
19
|
+
contextLines?: number
|
|
20
|
+
maxChars?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LARGE_DIFF_MATRIX_LIMIT = 500_000
|
|
24
|
+
|
|
25
|
+
export function renderUnifiedFileDiff(input: RenderUnifiedFileDiffInput): string {
|
|
26
|
+
const contextLines = input.contextLines ?? DEFAULT_DIFF_CONTEXT_LINES
|
|
27
|
+
const maxChars = input.maxChars ?? DEFAULT_DIFF_MAX_CHARS
|
|
28
|
+
const header = [`--- ${input.filePath}`, `+++ ${input.filePath}`]
|
|
29
|
+
|
|
30
|
+
if (input.before === input.after) {
|
|
31
|
+
return header.concat('(no changes)').join('\n')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const beforeLines = normalizedLines(input.before)
|
|
35
|
+
const afterLines = normalizedLines(input.after)
|
|
36
|
+
const ops = buildLineDiff(beforeLines, afterLines)
|
|
37
|
+
const hunks = buildHunkRanges(ops, contextLines)
|
|
38
|
+
|
|
39
|
+
if (hunks.length === 0) {
|
|
40
|
+
return header.concat('(only line ending changes)').join('\n')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const lines = [...header]
|
|
44
|
+
for (const hunk of hunks) {
|
|
45
|
+
const oldStart = countOldLines(ops, 0, hunk.start) + 1
|
|
46
|
+
const newStart = countNewLines(ops, 0, hunk.start) + 1
|
|
47
|
+
const oldCount = countOldLines(ops, hunk.start, hunk.end)
|
|
48
|
+
const newCount = countNewLines(ops, hunk.start, hunk.end)
|
|
49
|
+
lines.push(`@@ -${formatRange(oldStart, oldCount)} +${formatRange(newStart, newCount)} @@`)
|
|
50
|
+
for (const op of ops.slice(hunk.start, hunk.end)) {
|
|
51
|
+
if (op.type === 'equal') lines.push(` ${op.line}`)
|
|
52
|
+
else if (op.type === 'delete') lines.push(`-${op.line}`)
|
|
53
|
+
else lines.push(`+${op.line}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return truncateDiff(lines, maxChars)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatFileChangeResult(content: string, diff: string): string {
|
|
61
|
+
return `${content}${FILE_DIFF_RESULT_MARKER}${diff}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function splitFileChangeResult(content: string): { content: string; diff?: string } {
|
|
65
|
+
const markerIndex = content.indexOf(FILE_DIFF_RESULT_MARKER)
|
|
66
|
+
if (markerIndex === -1) return { content }
|
|
67
|
+
const diff = content.slice(markerIndex + FILE_DIFF_RESULT_MARKER.length).trimEnd()
|
|
68
|
+
return {
|
|
69
|
+
content: content.slice(0, markerIndex).trimEnd(),
|
|
70
|
+
diff: diff.length > 0 ? diff : undefined,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function stripFileChangeResultDiff(content: string): string {
|
|
75
|
+
return splitFileChangeResult(content).content
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function computeLineDiffStats(before: string, after: string): { inserts: number; deletes: number } {
|
|
79
|
+
if (before === after) return { inserts: 0, deletes: 0 }
|
|
80
|
+
const beforeLines = normalizedLines(before)
|
|
81
|
+
const afterLines = normalizedLines(after)
|
|
82
|
+
const ops = buildLineDiff(beforeLines, afterLines)
|
|
83
|
+
let inserts = 0
|
|
84
|
+
let deletes = 0
|
|
85
|
+
for (const op of ops) {
|
|
86
|
+
if (op.type === 'insert') inserts += 1
|
|
87
|
+
else if (op.type === 'delete') deletes += 1
|
|
88
|
+
}
|
|
89
|
+
return { inserts, deletes }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildLineDiff(beforeLines: string[], afterLines: string[]): DiffOp[] {
|
|
93
|
+
if (beforeLines.length * afterLines.length > LARGE_DIFF_MATRIX_LIMIT) {
|
|
94
|
+
return buildPrefixSuffixDiff(beforeLines, afterLines)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const lengths = lcsLengths(beforeLines, afterLines)
|
|
98
|
+
const ops: DiffOp[] = []
|
|
99
|
+
let beforeIndex = 0
|
|
100
|
+
let afterIndex = 0
|
|
101
|
+
|
|
102
|
+
while (beforeIndex < beforeLines.length && afterIndex < afterLines.length) {
|
|
103
|
+
const beforeLine = beforeLines[beforeIndex]!
|
|
104
|
+
const afterLine = afterLines[afterIndex]!
|
|
105
|
+
if (beforeLine === afterLine) {
|
|
106
|
+
ops.push({ type: 'equal', line: beforeLine })
|
|
107
|
+
beforeIndex += 1
|
|
108
|
+
afterIndex += 1
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const deleteScore = lengths[beforeIndex + 1]![afterIndex]!
|
|
113
|
+
const insertScore = lengths[beforeIndex]![afterIndex + 1]!
|
|
114
|
+
const deleteRevealsMatch = beforeLines[beforeIndex + 1] === afterLine
|
|
115
|
+
const insertRevealsMatch = beforeLine === afterLines[afterIndex + 1]
|
|
116
|
+
|
|
117
|
+
if (insertRevealsMatch && insertScore >= deleteScore) {
|
|
118
|
+
ops.push({ type: 'insert', line: afterLine })
|
|
119
|
+
afterIndex += 1
|
|
120
|
+
} else if (deleteRevealsMatch && deleteScore >= insertScore) {
|
|
121
|
+
ops.push({ type: 'delete', line: beforeLine })
|
|
122
|
+
beforeIndex += 1
|
|
123
|
+
} else if (deleteScore >= insertScore) {
|
|
124
|
+
ops.push({ type: 'delete', line: beforeLine })
|
|
125
|
+
beforeIndex += 1
|
|
126
|
+
} else {
|
|
127
|
+
ops.push({ type: 'insert', line: afterLine })
|
|
128
|
+
afterIndex += 1
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
while (beforeIndex < beforeLines.length) {
|
|
133
|
+
ops.push({ type: 'delete', line: beforeLines[beforeIndex]! })
|
|
134
|
+
beforeIndex += 1
|
|
135
|
+
}
|
|
136
|
+
while (afterIndex < afterLines.length) {
|
|
137
|
+
ops.push({ type: 'insert', line: afterLines[afterIndex]! })
|
|
138
|
+
afterIndex += 1
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return ops
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildPrefixSuffixDiff(beforeLines: string[], afterLines: string[]): DiffOp[] {
|
|
145
|
+
let prefixLength = 0
|
|
146
|
+
while (
|
|
147
|
+
prefixLength < beforeLines.length &&
|
|
148
|
+
prefixLength < afterLines.length &&
|
|
149
|
+
beforeLines[prefixLength] === afterLines[prefixLength]
|
|
150
|
+
) {
|
|
151
|
+
prefixLength += 1
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let beforeEnd = beforeLines.length
|
|
155
|
+
let afterEnd = afterLines.length
|
|
156
|
+
while (
|
|
157
|
+
beforeEnd > prefixLength &&
|
|
158
|
+
afterEnd > prefixLength &&
|
|
159
|
+
beforeLines[beforeEnd - 1] === afterLines[afterEnd - 1]
|
|
160
|
+
) {
|
|
161
|
+
beforeEnd -= 1
|
|
162
|
+
afterEnd -= 1
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const ops: DiffOp[] = []
|
|
166
|
+
for (let index = 0; index < prefixLength; index += 1) {
|
|
167
|
+
ops.push({ type: 'equal', line: beforeLines[index]! })
|
|
168
|
+
}
|
|
169
|
+
for (let index = prefixLength; index < beforeEnd; index += 1) {
|
|
170
|
+
ops.push({ type: 'delete', line: beforeLines[index]! })
|
|
171
|
+
}
|
|
172
|
+
for (let index = prefixLength; index < afterEnd; index += 1) {
|
|
173
|
+
ops.push({ type: 'insert', line: afterLines[index]! })
|
|
174
|
+
}
|
|
175
|
+
for (let index = beforeEnd; index < beforeLines.length; index += 1) {
|
|
176
|
+
ops.push({ type: 'equal', line: beforeLines[index]! })
|
|
177
|
+
}
|
|
178
|
+
return ops
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function lcsLengths(beforeLines: string[], afterLines: string[]): number[][] {
|
|
182
|
+
const lengths = Array.from(
|
|
183
|
+
{ length: beforeLines.length + 1 },
|
|
184
|
+
() => Array<number>(afterLines.length + 1).fill(0),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
for (let beforeIndex = beforeLines.length - 1; beforeIndex >= 0; beforeIndex -= 1) {
|
|
188
|
+
for (let afterIndex = afterLines.length - 1; afterIndex >= 0; afterIndex -= 1) {
|
|
189
|
+
lengths[beforeIndex]![afterIndex] = beforeLines[beforeIndex] === afterLines[afterIndex]
|
|
190
|
+
? lengths[beforeIndex + 1]![afterIndex + 1]! + 1
|
|
191
|
+
: Math.max(lengths[beforeIndex + 1]![afterIndex]!, lengths[beforeIndex]![afterIndex + 1]!)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return lengths
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildHunkRanges(ops: DiffOp[], contextLines: number): HunkRange[] {
|
|
199
|
+
const ranges: HunkRange[] = []
|
|
200
|
+
const context = Math.max(0, contextLines)
|
|
201
|
+
|
|
202
|
+
for (let index = 0; index < ops.length; index += 1) {
|
|
203
|
+
if (ops[index]?.type === 'equal') continue
|
|
204
|
+
const start = Math.max(0, index - context)
|
|
205
|
+
const end = Math.min(ops.length, index + context + 1)
|
|
206
|
+
const previous = ranges[ranges.length - 1]
|
|
207
|
+
if (previous && start <= previous.end) {
|
|
208
|
+
previous.end = Math.max(previous.end, end)
|
|
209
|
+
} else {
|
|
210
|
+
ranges.push({ start, end })
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return ranges
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function countOldLines(ops: DiffOp[], start: number, end: number): number {
|
|
218
|
+
let count = 0
|
|
219
|
+
for (let index = start; index < end; index += 1) {
|
|
220
|
+
const type = ops[index]?.type
|
|
221
|
+
if (type === 'equal' || type === 'delete') count += 1
|
|
222
|
+
}
|
|
223
|
+
return count
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function countNewLines(ops: DiffOp[], start: number, end: number): number {
|
|
227
|
+
let count = 0
|
|
228
|
+
for (let index = start; index < end; index += 1) {
|
|
229
|
+
const type = ops[index]?.type
|
|
230
|
+
if (type === 'equal' || type === 'insert') count += 1
|
|
231
|
+
}
|
|
232
|
+
return count
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function formatRange(start: number, count: number): string {
|
|
236
|
+
if (count === 0) return `${Math.max(0, start - 1)},0`
|
|
237
|
+
if (count === 1) return String(start)
|
|
238
|
+
return `${start},${count}`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizedLines(value: string): string[] {
|
|
242
|
+
if (value.length === 0) return []
|
|
243
|
+
const lines = value.replace(/\r\n?/g, '\n').split('\n')
|
|
244
|
+
if (lines[lines.length - 1] === '') lines.pop()
|
|
245
|
+
return lines
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function truncateDiff(lines: string[], maxChars: number): string {
|
|
249
|
+
const diff = lines.join('\n')
|
|
250
|
+
if (diff.length <= maxChars) return diff
|
|
251
|
+
|
|
252
|
+
const out: string[] = []
|
|
253
|
+
let length = 0
|
|
254
|
+
for (const line of lines) {
|
|
255
|
+
const nextLength = length + (out.length > 0 ? 1 : 0) + line.length
|
|
256
|
+
if (nextLength > maxChars) break
|
|
257
|
+
out.push(line)
|
|
258
|
+
length = nextLength
|
|
259
|
+
}
|
|
260
|
+
return out.join('\n')
|
|
261
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { recordPrivateContinuityHistorySnapshot } from '../identity/continuity/history.js'
|
|
7
7
|
import { readContinuityFiles, readPublicSkillsFile } from '../identity/continuity/storage.js'
|
|
8
8
|
import type { Tool } from './contracts.js'
|
|
9
|
+
import { formatFileChangeResult } from './fileDiff.js'
|
|
9
10
|
|
|
10
11
|
const schema = z.object({
|
|
11
12
|
file: z.preprocess(normalizePrivateContinuityFile, z.enum(['SOUL.md', 'MEMORY.md'])),
|
|
@@ -141,7 +142,10 @@ export const privateContinuityEditTool: Tool<typeof schema> = {
|
|
|
141
142
|
return {
|
|
142
143
|
ok: true,
|
|
143
144
|
summary: prepared.changeSummary,
|
|
144
|
-
content:
|
|
145
|
+
content: formatFileChangeResult(
|
|
146
|
+
formatPrivateContinuityEditResult(prepared.file, prepared.fullPath),
|
|
147
|
+
prepared.diff,
|
|
148
|
+
),
|
|
145
149
|
}
|
|
146
150
|
},
|
|
147
151
|
}
|
|
@@ -166,6 +170,12 @@ function normalizePrivateContinuityInput(input: Record<string, unknown>): Record
|
|
|
166
170
|
if (normalized.appendText === undefined) {
|
|
167
171
|
normalized.appendText = normalized.note ?? normalized.text ?? normalized.content
|
|
168
172
|
}
|
|
173
|
+
for (const key of ['oldText', 'newText', 'appendToSection', 'appendText'] as const) {
|
|
174
|
+
const value = normalized[key]
|
|
175
|
+
if (typeof value === 'string' && value.trim() === '') {
|
|
176
|
+
normalized[key] = undefined
|
|
177
|
+
}
|
|
178
|
+
}
|
|
169
179
|
return normalized
|
|
170
180
|
}
|
|
171
181
|
|
|
@@ -4,6 +4,7 @@ import { z } from 'zod'
|
|
|
4
4
|
import { recordRewindSnapshot } from '../storage/rewind.js'
|
|
5
5
|
import type { EthagentConfig } from '../storage/config.js'
|
|
6
6
|
import type { Tool } from './contracts.js'
|
|
7
|
+
import { formatFileChangeResult, renderUnifiedFileDiff } from './fileDiff.js'
|
|
7
8
|
import { resolveWorkspacePath } from './readTool.js'
|
|
8
9
|
|
|
9
10
|
const schema = z.object({
|
|
@@ -40,6 +41,7 @@ export const writeFileTool: Tool<typeof schema> = {
|
|
|
40
41
|
subtitle: prepared.fullPath,
|
|
41
42
|
before: previewText(prepared.before),
|
|
42
43
|
after: previewText(input.content),
|
|
44
|
+
diff: renderUnifiedFileDiff({ filePath: prepared.relativePath, before: prepared.before, after: input.content }),
|
|
43
45
|
changeSummary: prepared.existedBefore ? `replace entire ${prepared.relativePath}` : `create ${prepared.relativePath}`,
|
|
44
46
|
}
|
|
45
47
|
},
|
|
@@ -64,9 +66,12 @@ export const writeFileTool: Tool<typeof schema> = {
|
|
|
64
66
|
return {
|
|
65
67
|
ok: true,
|
|
66
68
|
summary: prepared.existedBefore ? `replace entire ${prepared.relativePath}` : `create ${prepared.relativePath}`,
|
|
67
|
-
content:
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
content: formatFileChangeResult(
|
|
70
|
+
rewindWarning
|
|
71
|
+
? `updated ${prepared.fullPath}\nwarning: ${rewindWarning}`
|
|
72
|
+
: `updated ${prepared.fullPath}`,
|
|
73
|
+
renderUnifiedFileDiff({ filePath: prepared.relativePath, before: prepared.before, after: input.content }),
|
|
74
|
+
),
|
|
70
75
|
}
|
|
71
76
|
},
|
|
72
77
|
}
|
package/src/ui/Spinner.tsx
CHANGED
|
@@ -226,6 +226,7 @@ export const Spinner: React.FC<SpinnerProps> = ({
|
|
|
226
226
|
const stickyVerbRef = useRef<string | null>(null)
|
|
227
227
|
const internalStartedAtRef = useRef<number>(Date.now())
|
|
228
228
|
const [frame, setFrame] = useState(0)
|
|
229
|
+
const [shimmerPos, setShimmerPos] = useState(0)
|
|
229
230
|
|
|
230
231
|
useEffect(() => {
|
|
231
232
|
if (!active) {
|
|
@@ -252,19 +253,40 @@ export const Spinner: React.FC<SpinnerProps> = ({
|
|
|
252
253
|
return () => clearInterval(timer)
|
|
253
254
|
}, [active])
|
|
254
255
|
|
|
255
|
-
if (!active) return null
|
|
256
|
-
|
|
257
256
|
const autoLabel = stickyVerbRef.current ?? verb ?? 'thinking'
|
|
258
257
|
const text = spinnerText(label ?? `${autoLabel}…`)
|
|
258
|
+
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (!active) return
|
|
261
|
+
const period = text.length + 12
|
|
262
|
+
const timer = setInterval(() => {
|
|
263
|
+
setShimmerPos(prev => (prev + 1) % period)
|
|
264
|
+
}, 90)
|
|
265
|
+
return () => clearInterval(timer)
|
|
266
|
+
}, [active, text.length])
|
|
267
|
+
|
|
268
|
+
if (!active) return null
|
|
269
|
+
|
|
259
270
|
const glyph = FRAMES[frame] ?? 'o'
|
|
260
271
|
const elapsed = showElapsed ? formatElapsedSeconds(Date.now() - (startedAt ?? internalStartedAtRef.current)) : null
|
|
261
272
|
const renderedHint = [rawHint, elapsed].filter(Boolean).join(' · ')
|
|
262
273
|
const hint = renderedHint ? spinnerHintText(renderedHint) : ''
|
|
263
274
|
|
|
275
|
+
const shimmerStart = shimmerPos - 1
|
|
276
|
+
const shimmerEnd = shimmerPos + 1
|
|
277
|
+
const visibleStart = Math.max(0, shimmerStart)
|
|
278
|
+
const visibleEnd = Math.min(text.length, shimmerEnd + 1)
|
|
279
|
+
const before = text.slice(0, visibleStart)
|
|
280
|
+
const shimmer = shimmerStart < text.length && shimmerEnd >= 0 ? text.slice(visibleStart, visibleEnd) : ''
|
|
281
|
+
const after = text.slice(visibleEnd)
|
|
282
|
+
|
|
264
283
|
return (
|
|
265
284
|
<Text>
|
|
266
285
|
<Text color={color}>{glyph}</Text>
|
|
267
|
-
<Text
|
|
286
|
+
<Text> </Text>
|
|
287
|
+
{before ? <Text color={theme.accentPeriwinkle}>{before}</Text> : null}
|
|
288
|
+
{shimmer ? <Text color={theme.accentWhite} bold>{shimmer}</Text> : null}
|
|
289
|
+
{after ? <Text color={theme.accentPeriwinkle}>{after}</Text> : null}
|
|
268
290
|
{hint ? <Text color={theme.dim}> · {hint}</Text> : null}
|
|
269
291
|
</Text>
|
|
270
292
|
)
|
package/src/ui/TextInput.tsx
CHANGED
|
@@ -2,11 +2,11 @@ import React, { useState, useRef, useEffect } from 'react'
|
|
|
2
2
|
import { Box, Text, useStdout } from 'ink'
|
|
3
3
|
import { theme } from './theme.js'
|
|
4
4
|
import { useAppInput } from '../app/input/AppInputProvider.js'
|
|
5
|
-
import { moveVerticalVisual } from '../chat/chatInputState.js'
|
|
5
|
+
import { moveVerticalVisual } from '../chat/input/chatInputState.js'
|
|
6
6
|
import {
|
|
7
7
|
getVisualLineIndex,
|
|
8
8
|
getVisualLines,
|
|
9
|
-
} from '../chat/textCursor.js'
|
|
9
|
+
} from '../chat/input/textCursor.js'
|
|
10
10
|
|
|
11
11
|
const DEFAULT_CHROME_WIDTH = 10
|
|
12
12
|
|