@shawnstack/quickforge 1.3.4 → 1.3.7
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 +348 -348
- package/dist/assets/{anthropic-BtDSt6Co.js → anthropic-67kzbrvt.js} +1 -1
- package/dist/assets/{azure-openai-responses-BiE5KBJr.js → azure-openai-responses-D2XquyCv.js} +1 -1
- package/dist/assets/{google-yYxzgw4O.js → google-CPViRPcU.js} +1 -1
- package/dist/assets/{google-gemini-cli-B8OJAX-P.js → google-gemini-cli-CNT4rz9y.js} +1 -1
- package/dist/assets/{google-vertex-Dmz1T75D.js → google-vertex-DOnrgFCc.js} +1 -1
- package/dist/assets/{index-BcbxYyM-.css → index-B0wkRg7T.css} +1 -1
- package/dist/assets/{index-Bc4Ghgsv.js → index-D3njc0Th.js} +454 -445
- package/dist/assets/{mistral-BJiBvfjD.js → mistral-Cz-xIri6.js} +1 -1
- package/dist/assets/{openai-codex-responses-rJJhq5x0.js → openai-codex-responses-CyA4Z3WI.js} +1 -1
- package/dist/assets/{openai-completions-CaCNPWQP.js → openai-completions-DSDeg9P2.js} +1 -1
- package/dist/assets/{openai-responses-D-zeSioM.js → openai-responses-jFa5zzTQ.js} +1 -1
- package/dist/assets/{openai-responses-shared-Bqg-VsV2.js → openai-responses-shared-BDx4vPct.js} +1 -1
- package/dist/index.html +2 -2
- package/node_modules/@aws-sdk/client-bedrock-runtime/dist-cjs/schemas/schemas_0.js +7 -4
- package/node_modules/@aws-sdk/client-bedrock-runtime/dist-es/schemas/schemas_0.js +7 -4
- package/node_modules/@aws-sdk/client-bedrock-runtime/package.json +2 -2
- package/node_modules/@aws-sdk/token-providers/package.json +1 -1
- package/package.json +1 -1
- package/server/agent-manager.mjs +2 -2
- package/server/ai-http-logger.mjs +208 -0
- package/server/conversation-compaction.mjs +3 -2
- package/server/index.mjs +2 -0
- package/server/utils/text-diff.mjs +215 -215
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
3
|
+
import { streamSimpleWithAiHttpLogging } from './ai-http-logger.mjs'
|
|
4
4
|
import { cacheDir } from './storage.mjs'
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_COMPACT_KEEP_TURNS = 0
|
|
@@ -253,7 +253,7 @@ export async function compactConversation({ messages, model, thinkingLevel, getA
|
|
|
253
253
|
const modelMaxTokens = Number(model.maxTokens) || 4096
|
|
254
254
|
const maxTokens = Math.max(512, Math.min(modelMaxTokens, 4096))
|
|
255
255
|
const apiKey = getApiKey ? await getApiKey(model.provider) : undefined
|
|
256
|
-
const stream =
|
|
256
|
+
const stream = streamSimpleWithAiHttpLogging(
|
|
257
257
|
model,
|
|
258
258
|
{
|
|
259
259
|
systemPrompt: COMPACT_SYSTEM_PROMPT,
|
|
@@ -266,6 +266,7 @@ export async function compactConversation({ messages, model, thinkingLevel, getA
|
|
|
266
266
|
temperature: 0.2,
|
|
267
267
|
reasoning: thinkingLevel === 'off' ? undefined : 'low',
|
|
268
268
|
maxRetryDelayMs: 60000,
|
|
269
|
+
metadata: { quickforgePurpose: 'compact' },
|
|
269
270
|
},
|
|
270
271
|
)
|
|
271
272
|
|
package/server/index.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { handleLanAccessApi, renderLanUnlockPage } from './routes/lan-access.mjs
|
|
|
25
25
|
import { handleMcpApi } from './routes/mcp.mjs'
|
|
26
26
|
import { serveStatic } from './routes/static.mjs'
|
|
27
27
|
import { logger, flushLogger } from './utils/logger.mjs'
|
|
28
|
+
import { installAiHttpLogger } from './ai-http-logger.mjs'
|
|
28
29
|
import { isLoopbackAddress, getLanUrls } from './utils/network.mjs'
|
|
29
30
|
import { parseCookies } from './share-store.mjs'
|
|
30
31
|
import { lanAccessCookieName, verifyLanAccessToken } from './lan-access-store.mjs'
|
|
@@ -50,6 +51,7 @@ const vitePort = Number(process.env.QUICKFORGE_VITE_PORT || 5176)
|
|
|
50
51
|
let restartInProgress = false
|
|
51
52
|
|
|
52
53
|
setDefaultWorkspaceRoot(process.env.QUICKFORGE_WORKSPACE_DIR || projectRoot)
|
|
54
|
+
installAiHttpLogger()
|
|
53
55
|
|
|
54
56
|
function getRestartSupport() {
|
|
55
57
|
return { supported: true, reason: null }
|
|
@@ -1,215 +1,215 @@
|
|
|
1
|
-
const DEFAULT_CONTEXT_LINES = 3
|
|
2
|
-
const DEFAULT_MAX_DIFF_CHARS = 60000
|
|
3
|
-
const DEFAULT_MAX_DIFF_LINES = 1200
|
|
4
|
-
const MAX_LCS_CELLS = 2_000_000
|
|
5
|
-
|
|
6
|
-
function splitTextLines(text) {
|
|
7
|
-
if (!text) return []
|
|
8
|
-
const normalized = String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
9
|
-
const lines = normalized.split('\n')
|
|
10
|
-
if (lines.at(-1) === '') lines.pop()
|
|
11
|
-
return lines
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function countCommonPrefix(oldLines, newLines) {
|
|
15
|
-
const limit = Math.min(oldLines.length, newLines.length)
|
|
16
|
-
let index = 0
|
|
17
|
-
while (index < limit && oldLines[index] === newLines[index]) index++
|
|
18
|
-
return index
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function countCommonSuffix(oldLines, newLines, prefixLength) {
|
|
22
|
-
const oldRemaining = oldLines.length - prefixLength
|
|
23
|
-
const newRemaining = newLines.length - prefixLength
|
|
24
|
-
const limit = Math.min(oldRemaining, newRemaining)
|
|
25
|
-
let count = 0
|
|
26
|
-
while (count < limit && oldLines[oldLines.length - 1 - count] === newLines[newLines.length - 1 - count]) count++
|
|
27
|
-
return count
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function diffMiddleLines(oldLines, newLines) {
|
|
31
|
-
const oldCount = oldLines.length
|
|
32
|
-
const newCount = newLines.length
|
|
33
|
-
|
|
34
|
-
if (oldCount === 0) return newLines.map((line) => ({ type: 'insert', line }))
|
|
35
|
-
if (newCount === 0) return oldLines.map((line) => ({ type: 'delete', line }))
|
|
36
|
-
|
|
37
|
-
if (oldCount * newCount > MAX_LCS_CELLS) {
|
|
38
|
-
return [
|
|
39
|
-
...oldLines.map((line) => ({ type: 'delete', line })),
|
|
40
|
-
...newLines.map((line) => ({ type: 'insert', line })),
|
|
41
|
-
]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const dp = Array.from({ length: oldCount + 1 }, () => new Uint32Array(newCount + 1))
|
|
45
|
-
for (let oldIndex = oldCount - 1; oldIndex >= 0; oldIndex--) {
|
|
46
|
-
for (let newIndex = newCount - 1; newIndex >= 0; newIndex--) {
|
|
47
|
-
dp[oldIndex][newIndex] = oldLines[oldIndex] === newLines[newIndex]
|
|
48
|
-
? dp[oldIndex + 1][newIndex + 1] + 1
|
|
49
|
-
: Math.max(dp[oldIndex + 1][newIndex], dp[oldIndex][newIndex + 1])
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const operations = []
|
|
54
|
-
let oldIndex = 0
|
|
55
|
-
let newIndex = 0
|
|
56
|
-
while (oldIndex < oldCount && newIndex < newCount) {
|
|
57
|
-
if (oldLines[oldIndex] === newLines[newIndex]) {
|
|
58
|
-
operations.push({ type: 'equal', line: oldLines[oldIndex] })
|
|
59
|
-
oldIndex++
|
|
60
|
-
newIndex++
|
|
61
|
-
} else if (dp[oldIndex + 1][newIndex] >= dp[oldIndex][newIndex + 1]) {
|
|
62
|
-
operations.push({ type: 'delete', line: oldLines[oldIndex] })
|
|
63
|
-
oldIndex++
|
|
64
|
-
} else {
|
|
65
|
-
operations.push({ type: 'insert', line: newLines[newIndex] })
|
|
66
|
-
newIndex++
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
while (oldIndex < oldCount) {
|
|
71
|
-
operations.push({ type: 'delete', line: oldLines[oldIndex] })
|
|
72
|
-
oldIndex++
|
|
73
|
-
}
|
|
74
|
-
while (newIndex < newCount) {
|
|
75
|
-
operations.push({ type: 'insert', line: newLines[newIndex] })
|
|
76
|
-
newIndex++
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return operations
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function diffLineOperations(oldLines, newLines) {
|
|
83
|
-
const prefixLength = countCommonPrefix(oldLines, newLines)
|
|
84
|
-
const suffixLength = countCommonSuffix(oldLines, newLines, prefixLength)
|
|
85
|
-
const oldMiddleEnd = oldLines.length - suffixLength
|
|
86
|
-
const newMiddleEnd = newLines.length - suffixLength
|
|
87
|
-
|
|
88
|
-
const operations = [
|
|
89
|
-
...oldLines.slice(0, prefixLength).map((line) => ({ type: 'equal', line })),
|
|
90
|
-
...diffMiddleLines(oldLines.slice(prefixLength, oldMiddleEnd), newLines.slice(prefixLength, newMiddleEnd)),
|
|
91
|
-
...oldLines.slice(oldMiddleEnd).map((line) => ({ type: 'equal', line })),
|
|
92
|
-
]
|
|
93
|
-
|
|
94
|
-
let oldLine = 1
|
|
95
|
-
let newLine = 1
|
|
96
|
-
for (const operation of operations) {
|
|
97
|
-
if (operation.type !== 'insert') {
|
|
98
|
-
operation.oldLine = oldLine
|
|
99
|
-
oldLine++
|
|
100
|
-
} else {
|
|
101
|
-
operation.oldLine = oldLine
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (operation.type !== 'delete') {
|
|
105
|
-
operation.newLine = newLine
|
|
106
|
-
newLine++
|
|
107
|
-
} else {
|
|
108
|
-
operation.newLine = newLine
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return operations
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function changedOperationRanges(operations, contextLines) {
|
|
116
|
-
const ranges = []
|
|
117
|
-
let index = 0
|
|
118
|
-
|
|
119
|
-
while (index < operations.length) {
|
|
120
|
-
while (index < operations.length && operations[index].type === 'equal') index++
|
|
121
|
-
if (index >= operations.length) break
|
|
122
|
-
|
|
123
|
-
const changeStart = index
|
|
124
|
-
while (index < operations.length && operations[index].type !== 'equal') index++
|
|
125
|
-
const changeEnd = index - 1
|
|
126
|
-
const start = Math.max(0, changeStart - contextLines)
|
|
127
|
-
const end = Math.min(operations.length, changeEnd + contextLines + 1)
|
|
128
|
-
|
|
129
|
-
const previous = ranges.at(-1)
|
|
130
|
-
if (previous && start <= previous.end) {
|
|
131
|
-
previous.end = end
|
|
132
|
-
} else {
|
|
133
|
-
ranges.push({ start, end })
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return ranges
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function formatRange(start, count) {
|
|
141
|
-
if (count === 0) return `${start},0`
|
|
142
|
-
if (count === 1) return String(start)
|
|
143
|
-
return `${start},${count}`
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function hunkHeader(operations, oldLineCount) {
|
|
147
|
-
const oldOperations = operations.filter((operation) => operation.type !== 'insert')
|
|
148
|
-
const newOperations = operations.filter((operation) => operation.type !== 'delete')
|
|
149
|
-
const firstOperation = operations[0]
|
|
150
|
-
const oldStart = oldOperations[0]?.oldLine ?? (oldLineCount === 0 ? 0 : firstOperation?.oldLine ?? 1)
|
|
151
|
-
const newStart = newOperations[0]?.newLine ?? (firstOperation?.newLine ?? 1)
|
|
152
|
-
|
|
153
|
-
return `@@ -${formatRange(oldStart, oldOperations.length)} +${formatRange(newStart, newOperations.length)} @@`
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function formatOperation(operation) {
|
|
157
|
-
if (operation.type === 'insert') return `+${operation.line}`
|
|
158
|
-
if (operation.type === 'delete') return `-${operation.line}`
|
|
159
|
-
return ` ${operation.line}`
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function truncateDiffText(text, maxChars, maxLines) {
|
|
163
|
-
let truncated = false
|
|
164
|
-
let output = text
|
|
165
|
-
|
|
166
|
-
const lines = output.split('\n')
|
|
167
|
-
if (lines.length > maxLines) {
|
|
168
|
-
output = lines.slice(0, maxLines).join('\n')
|
|
169
|
-
truncated = true
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (output.length > maxChars) {
|
|
173
|
-
output = output.slice(0, maxChars)
|
|
174
|
-
truncated = true
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (truncated) output = `${output}\n\n[diff truncated]`
|
|
178
|
-
return { text: output, truncated }
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function createTextDiff(oldText, newText, relativePath, options = {}) {
|
|
182
|
-
const oldLines = splitTextLines(oldText)
|
|
183
|
-
const newLines = splitTextLines(newText)
|
|
184
|
-
const operations = diffLineOperations(oldLines, newLines)
|
|
185
|
-
const addedLines = operations.filter((operation) => operation.type === 'insert').length
|
|
186
|
-
const removedLines = operations.filter((operation) => operation.type === 'delete').length
|
|
187
|
-
const contextLines = Math.max(0, Number(options.contextLines ?? DEFAULT_CONTEXT_LINES))
|
|
188
|
-
const ranges = changedOperationRanges(operations, contextLines)
|
|
189
|
-
const oldLabel = options.oldExists === false ? '/dev/null' : `a/${relativePath}`
|
|
190
|
-
const newLabel = `b/${relativePath}`
|
|
191
|
-
|
|
192
|
-
const diffLines = ranges.length > 0 ? [`--- ${oldLabel}`, `+++ ${newLabel}`] : []
|
|
193
|
-
for (const range of ranges) {
|
|
194
|
-
const hunkOperations = operations.slice(range.start, range.end)
|
|
195
|
-
diffLines.push(hunkHeader(hunkOperations, oldLines.length))
|
|
196
|
-
diffLines.push(...hunkOperations.map(formatOperation))
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const truncated = truncateDiffText(
|
|
200
|
-
diffLines.join('\n'),
|
|
201
|
-
Number(options.maxChars ?? DEFAULT_MAX_DIFF_CHARS),
|
|
202
|
-
Number(options.maxLines ?? DEFAULT_MAX_DIFF_LINES),
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
format: 'unified',
|
|
207
|
-
path: relativePath,
|
|
208
|
-
addedLines,
|
|
209
|
-
removedLines,
|
|
210
|
-
oldLineCount: oldLines.length,
|
|
211
|
-
newLineCount: newLines.length,
|
|
212
|
-
truncated: truncated.truncated,
|
|
213
|
-
text: truncated.text,
|
|
214
|
-
}
|
|
215
|
-
}
|
|
1
|
+
const DEFAULT_CONTEXT_LINES = 3
|
|
2
|
+
const DEFAULT_MAX_DIFF_CHARS = 60000
|
|
3
|
+
const DEFAULT_MAX_DIFF_LINES = 1200
|
|
4
|
+
const MAX_LCS_CELLS = 2_000_000
|
|
5
|
+
|
|
6
|
+
function splitTextLines(text) {
|
|
7
|
+
if (!text) return []
|
|
8
|
+
const normalized = String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
9
|
+
const lines = normalized.split('\n')
|
|
10
|
+
if (lines.at(-1) === '') lines.pop()
|
|
11
|
+
return lines
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function countCommonPrefix(oldLines, newLines) {
|
|
15
|
+
const limit = Math.min(oldLines.length, newLines.length)
|
|
16
|
+
let index = 0
|
|
17
|
+
while (index < limit && oldLines[index] === newLines[index]) index++
|
|
18
|
+
return index
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function countCommonSuffix(oldLines, newLines, prefixLength) {
|
|
22
|
+
const oldRemaining = oldLines.length - prefixLength
|
|
23
|
+
const newRemaining = newLines.length - prefixLength
|
|
24
|
+
const limit = Math.min(oldRemaining, newRemaining)
|
|
25
|
+
let count = 0
|
|
26
|
+
while (count < limit && oldLines[oldLines.length - 1 - count] === newLines[newLines.length - 1 - count]) count++
|
|
27
|
+
return count
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function diffMiddleLines(oldLines, newLines) {
|
|
31
|
+
const oldCount = oldLines.length
|
|
32
|
+
const newCount = newLines.length
|
|
33
|
+
|
|
34
|
+
if (oldCount === 0) return newLines.map((line) => ({ type: 'insert', line }))
|
|
35
|
+
if (newCount === 0) return oldLines.map((line) => ({ type: 'delete', line }))
|
|
36
|
+
|
|
37
|
+
if (oldCount * newCount > MAX_LCS_CELLS) {
|
|
38
|
+
return [
|
|
39
|
+
...oldLines.map((line) => ({ type: 'delete', line })),
|
|
40
|
+
...newLines.map((line) => ({ type: 'insert', line })),
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const dp = Array.from({ length: oldCount + 1 }, () => new Uint32Array(newCount + 1))
|
|
45
|
+
for (let oldIndex = oldCount - 1; oldIndex >= 0; oldIndex--) {
|
|
46
|
+
for (let newIndex = newCount - 1; newIndex >= 0; newIndex--) {
|
|
47
|
+
dp[oldIndex][newIndex] = oldLines[oldIndex] === newLines[newIndex]
|
|
48
|
+
? dp[oldIndex + 1][newIndex + 1] + 1
|
|
49
|
+
: Math.max(dp[oldIndex + 1][newIndex], dp[oldIndex][newIndex + 1])
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const operations = []
|
|
54
|
+
let oldIndex = 0
|
|
55
|
+
let newIndex = 0
|
|
56
|
+
while (oldIndex < oldCount && newIndex < newCount) {
|
|
57
|
+
if (oldLines[oldIndex] === newLines[newIndex]) {
|
|
58
|
+
operations.push({ type: 'equal', line: oldLines[oldIndex] })
|
|
59
|
+
oldIndex++
|
|
60
|
+
newIndex++
|
|
61
|
+
} else if (dp[oldIndex + 1][newIndex] >= dp[oldIndex][newIndex + 1]) {
|
|
62
|
+
operations.push({ type: 'delete', line: oldLines[oldIndex] })
|
|
63
|
+
oldIndex++
|
|
64
|
+
} else {
|
|
65
|
+
operations.push({ type: 'insert', line: newLines[newIndex] })
|
|
66
|
+
newIndex++
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
while (oldIndex < oldCount) {
|
|
71
|
+
operations.push({ type: 'delete', line: oldLines[oldIndex] })
|
|
72
|
+
oldIndex++
|
|
73
|
+
}
|
|
74
|
+
while (newIndex < newCount) {
|
|
75
|
+
operations.push({ type: 'insert', line: newLines[newIndex] })
|
|
76
|
+
newIndex++
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return operations
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function diffLineOperations(oldLines, newLines) {
|
|
83
|
+
const prefixLength = countCommonPrefix(oldLines, newLines)
|
|
84
|
+
const suffixLength = countCommonSuffix(oldLines, newLines, prefixLength)
|
|
85
|
+
const oldMiddleEnd = oldLines.length - suffixLength
|
|
86
|
+
const newMiddleEnd = newLines.length - suffixLength
|
|
87
|
+
|
|
88
|
+
const operations = [
|
|
89
|
+
...oldLines.slice(0, prefixLength).map((line) => ({ type: 'equal', line })),
|
|
90
|
+
...diffMiddleLines(oldLines.slice(prefixLength, oldMiddleEnd), newLines.slice(prefixLength, newMiddleEnd)),
|
|
91
|
+
...oldLines.slice(oldMiddleEnd).map((line) => ({ type: 'equal', line })),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
let oldLine = 1
|
|
95
|
+
let newLine = 1
|
|
96
|
+
for (const operation of operations) {
|
|
97
|
+
if (operation.type !== 'insert') {
|
|
98
|
+
operation.oldLine = oldLine
|
|
99
|
+
oldLine++
|
|
100
|
+
} else {
|
|
101
|
+
operation.oldLine = oldLine
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (operation.type !== 'delete') {
|
|
105
|
+
operation.newLine = newLine
|
|
106
|
+
newLine++
|
|
107
|
+
} else {
|
|
108
|
+
operation.newLine = newLine
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return operations
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function changedOperationRanges(operations, contextLines) {
|
|
116
|
+
const ranges = []
|
|
117
|
+
let index = 0
|
|
118
|
+
|
|
119
|
+
while (index < operations.length) {
|
|
120
|
+
while (index < operations.length && operations[index].type === 'equal') index++
|
|
121
|
+
if (index >= operations.length) break
|
|
122
|
+
|
|
123
|
+
const changeStart = index
|
|
124
|
+
while (index < operations.length && operations[index].type !== 'equal') index++
|
|
125
|
+
const changeEnd = index - 1
|
|
126
|
+
const start = Math.max(0, changeStart - contextLines)
|
|
127
|
+
const end = Math.min(operations.length, changeEnd + contextLines + 1)
|
|
128
|
+
|
|
129
|
+
const previous = ranges.at(-1)
|
|
130
|
+
if (previous && start <= previous.end) {
|
|
131
|
+
previous.end = end
|
|
132
|
+
} else {
|
|
133
|
+
ranges.push({ start, end })
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return ranges
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatRange(start, count) {
|
|
141
|
+
if (count === 0) return `${start},0`
|
|
142
|
+
if (count === 1) return String(start)
|
|
143
|
+
return `${start},${count}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function hunkHeader(operations, oldLineCount) {
|
|
147
|
+
const oldOperations = operations.filter((operation) => operation.type !== 'insert')
|
|
148
|
+
const newOperations = operations.filter((operation) => operation.type !== 'delete')
|
|
149
|
+
const firstOperation = operations[0]
|
|
150
|
+
const oldStart = oldOperations[0]?.oldLine ?? (oldLineCount === 0 ? 0 : firstOperation?.oldLine ?? 1)
|
|
151
|
+
const newStart = newOperations[0]?.newLine ?? (firstOperation?.newLine ?? 1)
|
|
152
|
+
|
|
153
|
+
return `@@ -${formatRange(oldStart, oldOperations.length)} +${formatRange(newStart, newOperations.length)} @@`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatOperation(operation) {
|
|
157
|
+
if (operation.type === 'insert') return `+${operation.line}`
|
|
158
|
+
if (operation.type === 'delete') return `-${operation.line}`
|
|
159
|
+
return ` ${operation.line}`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function truncateDiffText(text, maxChars, maxLines) {
|
|
163
|
+
let truncated = false
|
|
164
|
+
let output = text
|
|
165
|
+
|
|
166
|
+
const lines = output.split('\n')
|
|
167
|
+
if (lines.length > maxLines) {
|
|
168
|
+
output = lines.slice(0, maxLines).join('\n')
|
|
169
|
+
truncated = true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (output.length > maxChars) {
|
|
173
|
+
output = output.slice(0, maxChars)
|
|
174
|
+
truncated = true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (truncated) output = `${output}\n\n[diff truncated]`
|
|
178
|
+
return { text: output, truncated }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createTextDiff(oldText, newText, relativePath, options = {}) {
|
|
182
|
+
const oldLines = splitTextLines(oldText)
|
|
183
|
+
const newLines = splitTextLines(newText)
|
|
184
|
+
const operations = diffLineOperations(oldLines, newLines)
|
|
185
|
+
const addedLines = operations.filter((operation) => operation.type === 'insert').length
|
|
186
|
+
const removedLines = operations.filter((operation) => operation.type === 'delete').length
|
|
187
|
+
const contextLines = Math.max(0, Number(options.contextLines ?? DEFAULT_CONTEXT_LINES))
|
|
188
|
+
const ranges = changedOperationRanges(operations, contextLines)
|
|
189
|
+
const oldLabel = options.oldExists === false ? '/dev/null' : `a/${relativePath}`
|
|
190
|
+
const newLabel = `b/${relativePath}`
|
|
191
|
+
|
|
192
|
+
const diffLines = ranges.length > 0 ? [`--- ${oldLabel}`, `+++ ${newLabel}`] : []
|
|
193
|
+
for (const range of ranges) {
|
|
194
|
+
const hunkOperations = operations.slice(range.start, range.end)
|
|
195
|
+
diffLines.push(hunkHeader(hunkOperations, oldLines.length))
|
|
196
|
+
diffLines.push(...hunkOperations.map(formatOperation))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const truncated = truncateDiffText(
|
|
200
|
+
diffLines.join('\n'),
|
|
201
|
+
Number(options.maxChars ?? DEFAULT_MAX_DIFF_CHARS),
|
|
202
|
+
Number(options.maxLines ?? DEFAULT_MAX_DIFF_LINES),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
format: 'unified',
|
|
207
|
+
path: relativePath,
|
|
208
|
+
addedLines,
|
|
209
|
+
removedLines,
|
|
210
|
+
oldLineCount: oldLines.length,
|
|
211
|
+
newLineCount: newLines.length,
|
|
212
|
+
truncated: truncated.truncated,
|
|
213
|
+
text: truncated.text,
|
|
214
|
+
}
|
|
215
|
+
}
|