ethagent 2.2.0 → 2.4.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/README.md +11 -0
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +3 -7
- package/src/app/FirstRunTimeline.tsx +1 -1
- package/src/chat/ChatBottomPane.tsx +29 -11
- package/src/chat/ChatScreen.tsx +169 -38
- 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 +5 -2
- package/src/chat/chatTurnOrchestrator.ts +7 -9
- package/src/chat/commands.ts +26 -26
- 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} +61 -25
- package/src/chat/input/imageRefs.ts +30 -0
- 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} +50 -41
- package/src/chat/views/RewindView.tsx +410 -0
- package/src/identity/continuity/privateEdit/diff.ts +2 -78
- package/src/identity/hub/OperationalRoutes.tsx +21 -21
- 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} +17 -17
- 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 +9 -9
- package/src/identity/hub/{flows/custody/custodyFlowActions.ts → custody/actions.ts} +6 -6
- 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} +5 -5
- 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/{flows/ens → ens}/EnsEditAdvancedScreens.tsx +13 -13
- package/src/identity/hub/{flows/ens → ens}/EnsEditFlow.tsx +7 -7
- package/src/identity/hub/{flows/ens → ens}/EnsEditMaintenanceScreens.tsx +10 -10
- package/src/identity/hub/{flows/ens → ens}/EnsEditReviewScreens.tsx +12 -12
- package/src/identity/hub/{flows/ens → ens}/EnsEditRunners.tsx +5 -5
- package/src/identity/hub/{flows/ens → ens}/EnsEditShared.tsx +10 -10
- package/src/identity/hub/{flows/ens → ens}/EnsEditSimpleScreens.tsx +14 -14
- package/src/identity/hub/{flows/ens/IdentityHubEnsFlow.tsx → ens/EnsFlow.tsx} +12 -12
- package/src/identity/hub/{flows/ens/OperatorWalletsScreen.tsx → ens/EnsOperatorWalletsScreen.tsx} +17 -17
- package/src/identity/hub/{advancedEnsValidation.ts → ens/advancedEnsValidation.ts} +2 -2
- package/src/identity/hub/{flows/ens/ensEditCopy.ts → ens/editCopy.ts} +3 -3
- 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 +239 -239
- package/src/identity/hub/{flows/ens/ensEditTypes.ts → ens/types.ts} +7 -7
- package/src/identity/hub/identityHubReducer.ts +3 -3
- package/src/identity/hub/{flows/profile → profile}/EditProfileFlow.tsx +11 -11
- package/src/identity/hub/{effects/publicProfile/runPublicProfileSave.ts → profile/effects.ts} +18 -18
- package/src/identity/hub/{model → profile}/identity.ts +3 -3
- package/src/identity/hub/{effects/profile/profileState.ts → profile/state.ts} +181 -181
- package/src/identity/hub/{flows/restore → restore}/RestoreFlow.tsx +16 -16
- 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/{effects → restore}/restoreAdmin.ts +1 -1
- 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 +8 -8
- package/src/identity/hub/{components → shared/components}/MenuScreen.tsx +7 -7
- 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 +1 -1
- 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 +4 -4
- 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 -1
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/ownership.ts +2 -2
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/run.ts +6 -6
- 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/models/ModelPicker.tsx +143 -9
- package/src/models/catalog.ts +2 -1
- package/src/models/huggingface.ts +180 -2
- package/src/models/llamacpp.ts +110 -15
- package/src/models/llamacppPreflight.ts +30 -11
- package/src/models/modelPickerOptions.ts +16 -15
- package/src/models/providerDisplay.ts +16 -0
- package/src/providers/anthropic.ts +36 -5
- package/src/providers/contracts.ts +9 -1
- package/src/providers/errors.ts +6 -4
- package/src/providers/gemini.ts +29 -3
- package/src/providers/openai-chat.ts +83 -3
- package/src/providers/openai-responses-format.ts +29 -8
- package/src/providers/openai-responses.ts +22 -7
- package/src/providers/registry.ts +1 -0
- package/src/runtime/sessionMode.ts +1 -1
- package/src/runtime/systemPrompt.ts +3 -1
- package/src/runtime/toolExecution.ts +9 -6
- package/src/runtime/turn.ts +29 -0
- package/src/storage/config.ts +1 -0
- package/src/storage/rewind.ts +20 -0
- package/src/storage/sessions.ts +16 -3
- 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 +5 -1
- package/src/tools/writeFileTool.ts +8 -3
- package/src/ui/Spinner.tsx +39 -5
- package/src/ui/TextInput.tsx +2 -2
- package/src/ui/theme.ts +19 -0
- package/src/utils/clipboard.ts +10 -7
- package/src/utils/images.ts +140 -0
- package/src/utils/messages.ts +2 -0
- package/src/chat/RewindView.tsx +0 -386
- package/src/chat/toolResultDisplay.ts +0 -8
- package/src/identity/hub/effects/index.ts +0 -73
- 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/token-transfer/index.ts +0 -6
- /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}/agentReconciliation/types.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/index.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/useAgentReconciliation.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/walletSetup.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
|
@@ -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
|
}
|
|
@@ -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
|
)
|
|
@@ -273,8 +295,20 @@ export const Spinner: React.FC<SpinnerProps> = ({
|
|
|
273
295
|
function formatElapsedSeconds(milliseconds: number): string {
|
|
274
296
|
const seconds = Math.max(0, Math.floor(milliseconds / 1000))
|
|
275
297
|
if (seconds < 60) return `${seconds}s`
|
|
276
|
-
|
|
277
|
-
|
|
298
|
+
|
|
299
|
+
const hours = Math.floor(seconds / 3600)
|
|
300
|
+
const minutes = Math.floor((seconds % 3600) / 60)
|
|
301
|
+
const remainingSeconds = seconds % 60
|
|
302
|
+
|
|
303
|
+
if (hours > 0) {
|
|
304
|
+
return remainingSeconds > 0
|
|
305
|
+
? `${hours}h ${minutes}min ${remainingSeconds}s`
|
|
306
|
+
: `${hours}h ${minutes}min`
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return remainingSeconds > 0
|
|
310
|
+
? `${minutes}min ${remainingSeconds}s`
|
|
311
|
+
: `${minutes}min`
|
|
278
312
|
}
|
|
279
313
|
|
|
280
314
|
function restoreSpinnerTerms(value: string): string {
|
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
|
|
package/src/ui/theme.ts
CHANGED
|
@@ -11,6 +11,25 @@ export const theme = {
|
|
|
11
11
|
accentBlue: '#e8eefd',
|
|
12
12
|
accentWhite: '#f5f8ff',
|
|
13
13
|
accentError: '#d99898',
|
|
14
|
+
modePlan: '#f0c7a8',
|
|
15
|
+
modeAcceptEdits: '#c7b6f2',
|
|
16
|
+
diffAdded: '#8fd49d',
|
|
17
|
+
diffRemoved: '#d99898',
|
|
18
|
+
diffAddedBackground: '#16351f',
|
|
19
|
+
diffRemovedBackground: '#3a1717',
|
|
20
|
+
blockBackground: '#0b0d12',
|
|
21
|
+
codeKeyword: '#e8eefd',
|
|
22
|
+
codeString: '#c9e08f',
|
|
23
|
+
codeNumber: '#d8dcfa',
|
|
24
|
+
codeComment: '#777777',
|
|
25
|
+
codeFunction: '#8fc7ff',
|
|
26
|
+
codeType: '#f2d087',
|
|
27
|
+
codeBuiltin: '#b8a7ff',
|
|
28
|
+
codeProperty: '#91dcc0',
|
|
29
|
+
codeOperator: '#d8dcfa',
|
|
30
|
+
codePunctuation: '#aeb4c8',
|
|
31
|
+
codeTag: '#ffb3b3',
|
|
32
|
+
codeAttribute: '#f2d087',
|
|
14
33
|
border: '#555555',
|
|
15
34
|
dim: '#777777',
|
|
16
35
|
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 {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { ImageBlock, Message, MessageContentBlock } from '../providers/contracts.js'
|
|
4
|
+
|
|
5
|
+
const IMAGE_MARKER_RE = /\[image:\s*([^\]]+?)\]/gi
|
|
6
|
+
const PLACEHOLDER_RE = /^([<{[].*[>}\]]|#\d+)$/
|
|
7
|
+
|
|
8
|
+
export class ImageLoadError extends Error {
|
|
9
|
+
readonly imagePath: string
|
|
10
|
+
constructor(imagePath: string, message: string) {
|
|
11
|
+
super(message)
|
|
12
|
+
this.name = 'ImageLoadError'
|
|
13
|
+
this.imagePath = imagePath
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function collapseImagePathsToRefs(text: string): string {
|
|
18
|
+
let counter = 0
|
|
19
|
+
return text.replace(IMAGE_MARKER_RE, (full, raw: string) => {
|
|
20
|
+
const trimmed = raw.trim()
|
|
21
|
+
if (!trimmed || PLACEHOLDER_RE.test(trimmed)) return full
|
|
22
|
+
counter += 1
|
|
23
|
+
return `[Image #${counter}]`
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function modelSupportsImages(
|
|
28
|
+
provider: string,
|
|
29
|
+
model: string,
|
|
30
|
+
extra?: { mmprojPath?: string },
|
|
31
|
+
): boolean {
|
|
32
|
+
const normalized = model.toLowerCase()
|
|
33
|
+
switch (provider) {
|
|
34
|
+
case 'anthropic':
|
|
35
|
+
return /claude-3|claude-sonnet-4|claude-opus-4|claude-haiku-4/.test(normalized)
|
|
36
|
+
case 'gemini':
|
|
37
|
+
return /gemini-1\.5|gemini-2\.0|gemini-2\.5/.test(normalized)
|
|
38
|
+
case 'openai':
|
|
39
|
+
if (normalized.includes('gpt-3.5')) return false
|
|
40
|
+
return /gpt-4o|gpt-4\.1|gpt-4-turbo|gpt-4-vision|gpt-5|o1|o3|o4|chatgpt-4/.test(normalized)
|
|
41
|
+
case 'llamacpp':
|
|
42
|
+
return Boolean(extra?.mmprojPath)
|
|
43
|
+
default:
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hasImageBlocks(messages: Message[]): boolean {
|
|
49
|
+
return messages.some(message => Array.isArray(message.content) && message.content.some(block => block.type === 'image'))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function userTextToContentBlocks(text: string): string | MessageContentBlock[] {
|
|
53
|
+
const blocks = parseImageMarkers(text)
|
|
54
|
+
return blocks.length === 1 && blocks[0]?.type === 'text' ? blocks[0].text : blocks
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseImageMarkers(text: string): MessageContentBlock[] {
|
|
58
|
+
const out: MessageContentBlock[] = []
|
|
59
|
+
let lastIndex = 0
|
|
60
|
+
let match: RegExpExecArray | null
|
|
61
|
+
|
|
62
|
+
while ((match = IMAGE_MARKER_RE.exec(text)) !== null) {
|
|
63
|
+
const full = match[0]
|
|
64
|
+
const rawPath = match[1]?.trim() ?? ''
|
|
65
|
+
if (match.index > lastIndex) {
|
|
66
|
+
const prefix = text.slice(lastIndex, match.index)
|
|
67
|
+
if (prefix) out.push({ type: 'text', text: prefix })
|
|
68
|
+
}
|
|
69
|
+
if (rawPath && !PLACEHOLDER_RE.test(rawPath)) {
|
|
70
|
+
out.push({ type: 'image', path: rawPath })
|
|
71
|
+
} else {
|
|
72
|
+
out.push({ type: 'text', text: full })
|
|
73
|
+
}
|
|
74
|
+
lastIndex = match.index + full.length
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (lastIndex < text.length) {
|
|
78
|
+
const suffix = text.slice(lastIndex)
|
|
79
|
+
if (suffix) out.push({ type: 'text', text: suffix })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (out.length === 0) return text ? [{ type: 'text', text }] : []
|
|
83
|
+
return mergeAdjacentTextBlocks(out)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function loadImageBlock(block: ImageBlock): Promise<ImageBlock> {
|
|
87
|
+
if (block.dataBase64 && block.mimeType) return block
|
|
88
|
+
if (block.url) return block
|
|
89
|
+
const rawPath = block.path?.trim() ?? ''
|
|
90
|
+
if (!rawPath) throw new ImageLoadError(rawPath, 'image path is empty')
|
|
91
|
+
if (PLACEHOLDER_RE.test(rawPath)) {
|
|
92
|
+
throw new ImageLoadError(rawPath, `image path looks like a placeholder, not a real file: ${rawPath}`)
|
|
93
|
+
}
|
|
94
|
+
let file: Buffer
|
|
95
|
+
try {
|
|
96
|
+
file = await fs.readFile(rawPath)
|
|
97
|
+
} catch (err: unknown) {
|
|
98
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
99
|
+
if (code === 'ENOENT') {
|
|
100
|
+
throw new ImageLoadError(rawPath, `image file not found: ${rawPath}`)
|
|
101
|
+
}
|
|
102
|
+
throw new ImageLoadError(rawPath, `could not read image at ${rawPath}: ${(err as Error).message}`)
|
|
103
|
+
}
|
|
104
|
+
const mimeType = block.mimeType ?? mimeTypeForPath(rawPath)
|
|
105
|
+
return {
|
|
106
|
+
...block,
|
|
107
|
+
path: rawPath,
|
|
108
|
+
mimeType,
|
|
109
|
+
dataBase64: file.toString('base64'),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function imagePlaceholder(pathValue: string): string {
|
|
114
|
+
return `[image: ${path.basename(pathValue)}]`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function mergeAdjacentTextBlocks(blocks: MessageContentBlock[]): MessageContentBlock[] {
|
|
118
|
+
const out: MessageContentBlock[] = []
|
|
119
|
+
for (const block of blocks) {
|
|
120
|
+
const prev = out[out.length - 1]
|
|
121
|
+
if (block.type === 'text' && prev?.type === 'text') {
|
|
122
|
+
prev.text += block.text
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
out.push(block)
|
|
126
|
+
}
|
|
127
|
+
return out
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function mimeTypeForPath(filePath: string): string {
|
|
131
|
+
switch (path.extname(filePath).toLowerCase()) {
|
|
132
|
+
case '.png': return 'image/png'
|
|
133
|
+
case '.jpg':
|
|
134
|
+
case '.jpeg': return 'image/jpeg'
|
|
135
|
+
case '.webp': return 'image/webp'
|
|
136
|
+
case '.gif': return 'image/gif'
|
|
137
|
+
case '.bmp': return 'image/bmp'
|
|
138
|
+
default: return 'application/octet-stream'
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/utils/messages.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
1
2
|
import type { Message, MessageContentBlock } from '../providers/contracts.js'
|
|
2
3
|
|
|
3
4
|
export function systemMessage(content: string): Message {
|
|
@@ -20,6 +21,7 @@ export function blocksToText(blocks: MessageContentBlock[]): string {
|
|
|
20
21
|
return blocks
|
|
21
22
|
.map(block => {
|
|
22
23
|
if (block.type === 'text') return block.text
|
|
24
|
+
if (block.type === 'image') return `[image attached: ${path.basename(block.path)}]`
|
|
23
25
|
if (block.type === 'tool_use') return `[tool use: ${block.name}]`
|
|
24
26
|
return block.isError
|
|
25
27
|
? `[tool error: ${block.content}]`
|