@bubblebrain-ai/bubble 0.0.16 → 0.0.18
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/dist/agent/internal-reminder-sanitizer.d.ts +2 -0
- package/dist/agent/internal-reminder-sanitizer.js +27 -0
- package/dist/agent/tool-intent.js +0 -1
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +148 -23
- package/dist/context/budget.js +15 -0
- package/dist/context/prune.d.ts +1 -0
- package/dist/context/prune.js +32 -0
- package/dist/debug-trace.js +14 -0
- package/dist/feishu/agent-host/run-driver.js +2 -2
- package/dist/feishu/card/run-state.js +1 -0
- package/dist/feishu/serve.js +1 -0
- package/dist/main.js +13 -9
- package/dist/model-catalog.d.ts +3 -0
- package/dist/model-catalog.js +38 -0
- package/dist/model-config.d.ts +3 -0
- package/dist/model-config.js +3 -0
- package/dist/model-pricing.js +2 -1
- package/dist/model-selection.d.ts +7 -0
- package/dist/model-selection.js +9 -0
- package/dist/network/chatgpt-transport.js +1 -0
- package/dist/orchestrator/default-hooks.js +1 -1
- package/dist/prompt/compose.js +1 -1
- package/dist/prompt/environment.js +1 -3
- package/dist/prompt/reminders.js +3 -3
- package/dist/prompt/runtime.js +2 -1
- package/dist/provider-anthropic.d.ts +89 -0
- package/dist/provider-anthropic.js +597 -0
- package/dist/provider-openai-codex.js +3 -1
- package/dist/provider-registry.d.ts +2 -0
- package/dist/provider-registry.js +29 -3
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +14 -0
- package/dist/provider.d.ts +4 -1
- package/dist/provider.js +120 -41
- package/dist/session-log.js +14 -2
- package/dist/session-title.js +3 -6
- package/dist/slash-commands/commands.js +8 -2
- package/dist/stats/usage.d.ts +1 -0
- package/dist/stats/usage.js +28 -3
- package/dist/tools/edit.js +75 -1
- package/dist/tools/glob.js +77 -12
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +1 -3
- package/dist/tools/prompt-metadata.d.ts +3 -0
- package/dist/tools/prompt-metadata.js +17 -0
- package/dist/tools/write.js +14 -0
- package/dist/tui/paste-placeholder.d.ts +10 -0
- package/dist/tui/paste-placeholder.js +45 -0
- package/dist/tui/run.js +23 -0
- package/dist/tui-ink/app.js +2 -0
- package/dist/tui-ink/input-box.d.ts +1 -8
- package/dist/tui-ink/input-box.js +8 -38
- package/dist/tui-opentui/app.js +2 -0
- package/dist/tui-opentui/input-box.d.ts +1 -3
- package/dist/tui-opentui/input-box.js +17 -26
- package/dist/types.d.ts +22 -0
- package/package.json +7 -3
- package/dist/tools/apply-patch.d.ts +0 -9
- package/dist/tools/apply-patch.js +0 -330
- package/dist/tools/patch-apply.d.ts +0 -41
- package/dist/tools/patch-apply.js +0 -312
|
@@ -14,21 +14,10 @@ import { useTheme } from "./theme.js";
|
|
|
14
14
|
import { filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
|
|
15
15
|
import { ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
|
|
16
16
|
import { appendHistoryEntry, loadHistorySync, stepHistory, } from "./input-history.js";
|
|
17
|
+
export { createPastedContentMarker, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
|
|
18
|
+
import { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
|
|
17
19
|
const PROMPT = " > ";
|
|
18
|
-
const LONG_PASTE_CHAR_THRESHOLD = 1000;
|
|
19
|
-
const LONG_PASTE_LINE_THRESHOLD = 20;
|
|
20
20
|
const MAX_VISIBLE_SUGGESTIONS = 8;
|
|
21
|
-
export function shouldCollapsePastedContent(text) {
|
|
22
|
-
if (text.length >= LONG_PASTE_CHAR_THRESHOLD)
|
|
23
|
-
return true;
|
|
24
|
-
const lines = text.split("\n").length;
|
|
25
|
-
return lines >= LONG_PASTE_LINE_THRESHOLD;
|
|
26
|
-
}
|
|
27
|
-
export function createPastedContentMarker(content) {
|
|
28
|
-
const lineCount = content.split("\n").length;
|
|
29
|
-
const wordCount = content.trim().split(/\s+/).length;
|
|
30
|
-
return `[Pasted ${lineCount} lines · ${wordCount} words]`;
|
|
31
|
-
}
|
|
32
21
|
export function isCtrlCInput(input, key) {
|
|
33
22
|
return input === "\x03" || (key.ctrl === true && input.toLowerCase() === "c");
|
|
34
23
|
}
|
|
@@ -37,7 +26,7 @@ export function InputBox({ onSubmit, onPasteNotice, disabled = false, cursorRese
|
|
|
37
26
|
const [buffer, setBuffer] = useState("");
|
|
38
27
|
const [cursor, setCursor] = useState(0);
|
|
39
28
|
const [images, setImages] = useState([]);
|
|
40
|
-
const [pastedRefs, setPastedRefs] = useState(
|
|
29
|
+
const [pastedRefs, setPastedRefs] = useState([]);
|
|
41
30
|
const [history] = useState(() => loadHistorySync());
|
|
42
31
|
const [historyIndex, setHistoryIndex] = useState(null);
|
|
43
32
|
const [suggestions, setSuggestions] = useState([]);
|
|
@@ -55,6 +44,7 @@ export function InputBox({ onSubmit, onPasteNotice, disabled = false, cursorRese
|
|
|
55
44
|
const suggestionIndexRef = useRef(suggestionIndex);
|
|
56
45
|
const suggestionKindRef = useRef(suggestionKind);
|
|
57
46
|
const historyIndexRef = useRef(historyIndex);
|
|
47
|
+
const nextPastedContentIndexRef = useRef(1);
|
|
58
48
|
bufferRef.current = buffer;
|
|
59
49
|
cursorRef.current = cursor;
|
|
60
50
|
imagesRef.current = images;
|
|
@@ -70,7 +60,8 @@ export function InputBox({ onSubmit, onPasteNotice, disabled = false, cursorRese
|
|
|
70
60
|
setBuffer("");
|
|
71
61
|
setCursor(0);
|
|
72
62
|
setImages([]);
|
|
73
|
-
setPastedRefs(
|
|
63
|
+
setPastedRefs([]);
|
|
64
|
+
nextPastedContentIndexRef.current = 1;
|
|
74
65
|
setSuggestions([]);
|
|
75
66
|
setSuggestionKind(null);
|
|
76
67
|
setHistoryIndex(null);
|
|
@@ -81,6 +72,8 @@ export function InputBox({ onSubmit, onPasteNotice, disabled = false, cursorRese
|
|
|
81
72
|
return;
|
|
82
73
|
setBuffer(draftText);
|
|
83
74
|
setCursor(draftText.length);
|
|
75
|
+
setPastedRefs([]);
|
|
76
|
+
nextPastedContentIndexRef.current = 1;
|
|
84
77
|
onDraftApplied?.();
|
|
85
78
|
}, [draftText, draftEpoch, onDraftApplied]);
|
|
86
79
|
const updateSuggestions = useCallback(async (text, cursorPos) => {
|
|
@@ -168,10 +161,7 @@ export function InputBox({ onSubmit, onPasteNotice, disabled = false, cursorRese
|
|
|
168
161
|
const refs = pastedRefsRef.current;
|
|
169
162
|
if (!b.trim() && imgs.length === 0)
|
|
170
163
|
return;
|
|
171
|
-
|
|
172
|
-
for (const [marker, content] of refs) {
|
|
173
|
-
expanded = expanded.split(marker).join(content);
|
|
174
|
-
}
|
|
164
|
+
const expanded = expandPastedContentMarkers(b, refs);
|
|
175
165
|
const payload = {
|
|
176
166
|
text: expanded,
|
|
177
167
|
displayText: expanded !== b ? b : undefined,
|
|
@@ -183,7 +173,8 @@ export function InputBox({ onSubmit, onPasteNotice, disabled = false, cursorRese
|
|
|
183
173
|
setBuffer("");
|
|
184
174
|
setCursor(0);
|
|
185
175
|
setImages([]);
|
|
186
|
-
setPastedRefs(
|
|
176
|
+
setPastedRefs([]);
|
|
177
|
+
nextPastedContentIndexRef.current = 1;
|
|
187
178
|
setSuggestions([]);
|
|
188
179
|
setSuggestionKind(null);
|
|
189
180
|
setHistoryIndex(null);
|
|
@@ -211,12 +202,8 @@ export function InputBox({ onSubmit, onPasteNotice, disabled = false, cursorRese
|
|
|
211
202
|
}
|
|
212
203
|
// Plain text: collapse if long, otherwise insert at cursor.
|
|
213
204
|
if (shouldCollapsePastedContent(text)) {
|
|
214
|
-
const marker = createPastedContentMarker(text);
|
|
215
|
-
setPastedRefs((prev) => {
|
|
216
|
-
const next = new Map(prev);
|
|
217
|
-
next.set(marker, text);
|
|
218
|
-
return next;
|
|
219
|
-
});
|
|
205
|
+
const marker = createPastedContentMarker(text, nextPastedContentIndexRef.current++);
|
|
206
|
+
setPastedRefs((prev) => [...prev, { marker, content: text }]);
|
|
220
207
|
insertAtCursor(marker);
|
|
221
208
|
}
|
|
222
209
|
else {
|
|
@@ -306,6 +293,8 @@ export function InputBox({ onSubmit, onPasteNotice, disabled = false, cursorRese
|
|
|
306
293
|
setBuffer(next.text);
|
|
307
294
|
setCursor(next.text.length);
|
|
308
295
|
setHistoryIndex(next.index);
|
|
296
|
+
setPastedRefs([]);
|
|
297
|
+
nextPastedContentIndexRef.current = 1;
|
|
309
298
|
}
|
|
310
299
|
return;
|
|
311
300
|
}
|
|
@@ -324,6 +313,8 @@ export function InputBox({ onSubmit, onPasteNotice, disabled = false, cursorRese
|
|
|
324
313
|
setBuffer(next.text);
|
|
325
314
|
setCursor(next.text.length);
|
|
326
315
|
setHistoryIndex(next.index);
|
|
316
|
+
setPastedRefs([]);
|
|
317
|
+
nextPastedContentIndexRef.current = 1;
|
|
327
318
|
return;
|
|
328
319
|
}
|
|
329
320
|
const lineEnd = b.indexOf("\n", c);
|
package/dist/types.d.ts
CHANGED
|
@@ -14,6 +14,14 @@ export interface ImageContent {
|
|
|
14
14
|
export type ContentPart = TextContent | ImageContent;
|
|
15
15
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
16
16
|
export type ReasoningEffort = ThinkingLevel;
|
|
17
|
+
export type ProviderRawContentBlock = Record<string, unknown> & {
|
|
18
|
+
type: string;
|
|
19
|
+
};
|
|
20
|
+
export interface AssistantProviderMetadata {
|
|
21
|
+
anthropic?: {
|
|
22
|
+
contentBlocks?: ProviderRawContentBlock[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
17
25
|
export interface UserMessage {
|
|
18
26
|
role: "user";
|
|
19
27
|
content: string | ContentPart[];
|
|
@@ -23,6 +31,7 @@ export interface AssistantMessage {
|
|
|
23
31
|
content: string;
|
|
24
32
|
reasoning?: string;
|
|
25
33
|
toolCalls?: ToolCall[];
|
|
34
|
+
providerMetadata?: AssistantProviderMetadata;
|
|
26
35
|
/** Model metadata captured for local usage statistics. */
|
|
27
36
|
model?: string;
|
|
28
37
|
providerId?: string;
|
|
@@ -78,6 +87,7 @@ export interface ToolDefinition {
|
|
|
78
87
|
description: string;
|
|
79
88
|
parameters: ToolSchema;
|
|
80
89
|
}
|
|
90
|
+
export type ToolChoiceMode = "auto" | "none";
|
|
81
91
|
export interface ToolCall {
|
|
82
92
|
id: string;
|
|
83
93
|
name: string;
|
|
@@ -194,6 +204,12 @@ export interface ToolContext {
|
|
|
194
204
|
}
|
|
195
205
|
export interface ToolRegistryEntry extends ToolDefinition {
|
|
196
206
|
execute: ToolExecutor;
|
|
207
|
+
/** Optional one-line summary for the Available tools section. */
|
|
208
|
+
promptSnippet?: string;
|
|
209
|
+
/** Optional tool-specific rules appended to the system prompt when this tool is active. */
|
|
210
|
+
promptGuidelines?: string[];
|
|
211
|
+
/** Optional compatibility shim for provider-specific argument shapes. */
|
|
212
|
+
prepareArguments?: (args: Record<string, any>) => Record<string, any>;
|
|
197
213
|
/** Whether this tool is allowed in plan mode. Defaults to false (treated as write-capable). */
|
|
198
214
|
readOnly?: boolean;
|
|
199
215
|
/** Capability classification used by subagent profiles. Defaults to "unknown". */
|
|
@@ -239,6 +255,10 @@ export type StreamChunk = {
|
|
|
239
255
|
} | {
|
|
240
256
|
type: "reasoning_delta";
|
|
241
257
|
content: string;
|
|
258
|
+
} | {
|
|
259
|
+
type: "provider_content_block";
|
|
260
|
+
provider: "anthropic";
|
|
261
|
+
block: ProviderRawContentBlock;
|
|
242
262
|
} | {
|
|
243
263
|
type: "tool_call";
|
|
244
264
|
id: string;
|
|
@@ -259,6 +279,7 @@ export interface TokenUsage {
|
|
|
259
279
|
completionTokens: number;
|
|
260
280
|
promptCacheHitTokens?: number;
|
|
261
281
|
promptCacheMissTokens?: number;
|
|
282
|
+
cacheCreationTokens?: number;
|
|
262
283
|
reasoningTokens?: number;
|
|
263
284
|
totalTokens?: number;
|
|
264
285
|
}
|
|
@@ -266,6 +287,7 @@ export interface Provider {
|
|
|
266
287
|
streamChat(messages: ProviderMessage[], options: {
|
|
267
288
|
model: string;
|
|
268
289
|
tools?: ToolDefinition[];
|
|
290
|
+
toolChoice?: ToolChoiceMode;
|
|
269
291
|
temperature?: number;
|
|
270
292
|
thinkingLevel?: ThinkingLevel;
|
|
271
293
|
abortSignal?: AbortSignal;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bubblebrain-ai/bubble",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "A terminal coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@vue/language-server": "^3.2.7",
|
|
34
34
|
"better-sqlite3": "^12.9.0",
|
|
35
35
|
"chalk": "^5.3.0",
|
|
36
|
-
"diff": "^
|
|
36
|
+
"diff": "^9.0.0",
|
|
37
37
|
"ink": "^7.0.3",
|
|
38
38
|
"js-tiktoken": "^1.0.21",
|
|
39
39
|
"openai": "^4.77.0",
|
|
@@ -49,12 +49,16 @@
|
|
|
49
49
|
"vscode-langservers-extracted": "^4.10.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@types/diff": "^7.0.0",
|
|
53
52
|
"@types/node": "^22.0.0",
|
|
54
53
|
"@types/picomatch": "^4.0.3",
|
|
55
54
|
"@types/qrcode-terminal": "^0.12.2",
|
|
56
55
|
"@vitest/coverage-v8": "^4.1.4",
|
|
57
56
|
"typescript": "^5.7.0",
|
|
58
57
|
"vitest": "^4.1.4"
|
|
58
|
+
},
|
|
59
|
+
"overrides": {
|
|
60
|
+
"axios": "^1.17.0",
|
|
61
|
+
"postcss": "^8.5.15",
|
|
62
|
+
"ws": "^8.21.0"
|
|
59
63
|
}
|
|
60
64
|
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { ApprovalController } from "../approval/types.js";
|
|
2
|
-
import { type LspService } from "../lsp/index.js";
|
|
3
|
-
import type { ToolRegistryEntry } from "../types.js";
|
|
4
|
-
import { type FileStateTracker } from "./file-state.js";
|
|
5
|
-
export interface ApplyPatchArgs {
|
|
6
|
-
patch?: string;
|
|
7
|
-
patchText?: string;
|
|
8
|
-
}
|
|
9
|
-
export declare function createApplyPatchTool(cwd: string, approval?: ApprovalController, lsp?: LspService, fileState?: FileStateTracker): ToolRegistryEntry;
|
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
import { constants } from "node:fs";
|
|
2
|
-
import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
import { createTwoFilesPatch } from "diff";
|
|
5
|
-
import { gateToolAction } from "../approval/tool-helper.js";
|
|
6
|
-
import { countUnifiedDiffChanges } from "../diff-stats.js";
|
|
7
|
-
import { formatDiagnosticBlocks } from "../lsp/index.js";
|
|
8
|
-
import { isWithinWorkspace } from "./file-state.js";
|
|
9
|
-
import { withFileMutationQueues } from "./file-mutation-queue.js";
|
|
10
|
-
import { resolveToolPath } from "./path-utils.js";
|
|
11
|
-
import { applyPatchChunks, buildAddedFileContent, parseApplyPatch, PatchApplyError, } from "./patch-apply.js";
|
|
12
|
-
export function createApplyPatchTool(cwd, approval, lsp, fileState) {
|
|
13
|
-
return {
|
|
14
|
-
name: "apply_patch",
|
|
15
|
-
effect: "write_patch",
|
|
16
|
-
requiresApproval: true,
|
|
17
|
-
description: "Apply a structured patch for multi-file or larger changes. Use edit for small targeted replacements, write for full-file generation, and apply_patch for related adds/updates/deletes/moves.",
|
|
18
|
-
parameters: {
|
|
19
|
-
type: "object",
|
|
20
|
-
properties: {
|
|
21
|
-
patch: {
|
|
22
|
-
type: "string",
|
|
23
|
-
description: "Patch text using *** Begin Patch / *** Add File / *** Update File / *** Delete File / *** End Patch markers.",
|
|
24
|
-
},
|
|
25
|
-
patchText: {
|
|
26
|
-
type: "string",
|
|
27
|
-
description: "Alias for patch; accepted for compatibility with other coding agents.",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
async execute(args) {
|
|
32
|
-
const patchText = typeof args.patch === "string"
|
|
33
|
-
? args.patch
|
|
34
|
-
: typeof args.patchText === "string"
|
|
35
|
-
? args.patchText
|
|
36
|
-
: "";
|
|
37
|
-
if (!patchText.trim()) {
|
|
38
|
-
return {
|
|
39
|
-
content: "Error: apply_patch requires a non-empty patch string.",
|
|
40
|
-
isError: true,
|
|
41
|
-
status: "blocked",
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
let operations;
|
|
45
|
-
try {
|
|
46
|
-
operations = parseApplyPatch(patchText).operations;
|
|
47
|
-
}
|
|
48
|
-
catch (err) {
|
|
49
|
-
if (err instanceof PatchApplyError) {
|
|
50
|
-
return { content: err.message, isError: true, status: err.status };
|
|
51
|
-
}
|
|
52
|
-
throw err;
|
|
53
|
-
}
|
|
54
|
-
const lockPaths = collectOperationPaths(cwd, operations);
|
|
55
|
-
return withFileMutationQueues(lockPaths, async () => {
|
|
56
|
-
let plan;
|
|
57
|
-
try {
|
|
58
|
-
plan = await buildPatchPlan(cwd, operations);
|
|
59
|
-
}
|
|
60
|
-
catch (err) {
|
|
61
|
-
if (err instanceof PatchApplyError) {
|
|
62
|
-
return { content: err.message, isError: true, status: err.status };
|
|
63
|
-
}
|
|
64
|
-
throw err;
|
|
65
|
-
}
|
|
66
|
-
const gate = await gateToolAction(approval, {
|
|
67
|
-
type: "patch",
|
|
68
|
-
path: summarizePaths(plan.paths),
|
|
69
|
-
paths: plan.paths,
|
|
70
|
-
files: plan.changes.map((change) => ({ path: change.path, kind: change.kind })),
|
|
71
|
-
diff: plan.diff,
|
|
72
|
-
});
|
|
73
|
-
if (!gate.approved)
|
|
74
|
-
return gate.result;
|
|
75
|
-
const stale = await checkPlanFresh(plan);
|
|
76
|
-
if (stale)
|
|
77
|
-
return stale;
|
|
78
|
-
try {
|
|
79
|
-
await writePatchPlan(plan);
|
|
80
|
-
}
|
|
81
|
-
catch (err) {
|
|
82
|
-
await rollbackPatchPlan(plan).catch(() => undefined);
|
|
83
|
-
return {
|
|
84
|
-
content: `Error: apply_patch failed while writing files: ${err instanceof Error ? err.message : String(err)}`,
|
|
85
|
-
isError: true,
|
|
86
|
-
status: "partial",
|
|
87
|
-
metadata: {
|
|
88
|
-
kind: "patch",
|
|
89
|
-
paths: plan.paths,
|
|
90
|
-
diff: plan.diff,
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
await observePatchPlan(fileState, plan);
|
|
95
|
-
let output = `Applied patch to ${plan.changes.length} file${plan.changes.length === 1 ? "" : "s"}.`;
|
|
96
|
-
if (plan.fallbackCount > 0) {
|
|
97
|
-
output += ` ${plan.fallbackCount} hunk${plan.fallbackCount === 1 ? "" : "s"} used normalized matching.`;
|
|
98
|
-
}
|
|
99
|
-
if (lsp) {
|
|
100
|
-
for (const change of plan.changes) {
|
|
101
|
-
if (change.newContent === undefined)
|
|
102
|
-
continue;
|
|
103
|
-
try {
|
|
104
|
-
await lsp.touchFile(change.path, "document");
|
|
105
|
-
output += formatDiagnosticBlocks(cwd, change.path, lsp.diagnostics());
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
// LSP diagnostics should not turn a successful patch into a failed tool call.
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
const totals = plan.changes.reduce((acc, change) => ({
|
|
113
|
-
added: acc.added + change.addedLines,
|
|
114
|
-
removed: acc.removed + change.removedLines,
|
|
115
|
-
}), { added: 0, removed: 0 });
|
|
116
|
-
return {
|
|
117
|
-
content: output,
|
|
118
|
-
status: "success",
|
|
119
|
-
metadata: {
|
|
120
|
-
kind: "patch",
|
|
121
|
-
paths: plan.paths,
|
|
122
|
-
diff: plan.diff,
|
|
123
|
-
addedLines: totals.added,
|
|
124
|
-
removedLines: totals.removed,
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
});
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
async function buildPatchPlan(cwd, operations) {
|
|
132
|
-
const states = new Map();
|
|
133
|
-
let fallbackCount = 0;
|
|
134
|
-
const stateFor = async (path) => {
|
|
135
|
-
const absolutePath = resolveToolPath(cwd, path);
|
|
136
|
-
assertWorkspacePath(cwd, absolutePath);
|
|
137
|
-
const existing = states.get(absolutePath);
|
|
138
|
-
if (existing)
|
|
139
|
-
return existing;
|
|
140
|
-
const originalContent = await readExistingFile(absolutePath);
|
|
141
|
-
const state = {
|
|
142
|
-
path: absolutePath,
|
|
143
|
-
originalContent,
|
|
144
|
-
currentContent: originalContent,
|
|
145
|
-
};
|
|
146
|
-
states.set(absolutePath, state);
|
|
147
|
-
return state;
|
|
148
|
-
};
|
|
149
|
-
for (const operation of operations) {
|
|
150
|
-
if (operation.type === "add") {
|
|
151
|
-
const state = await stateFor(operation.path);
|
|
152
|
-
if (state.currentContent !== undefined) {
|
|
153
|
-
throw new PatchApplyError(`Error: Cannot add ${operation.path}; file already exists.`, "blocked");
|
|
154
|
-
}
|
|
155
|
-
state.currentContent = buildAddedFileContent(operation.lines);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (operation.type === "delete") {
|
|
159
|
-
const state = await stateFor(operation.path);
|
|
160
|
-
if (state.currentContent === undefined) {
|
|
161
|
-
throw new PatchApplyError(`Error: Cannot delete ${operation.path}; file does not exist.`, "blocked");
|
|
162
|
-
}
|
|
163
|
-
state.currentContent = undefined;
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
const source = await stateFor(operation.path);
|
|
167
|
-
if (source.currentContent === undefined) {
|
|
168
|
-
throw new PatchApplyError(`Error: Cannot update ${operation.path}; file does not exist.`, "blocked");
|
|
169
|
-
}
|
|
170
|
-
let nextContent = source.currentContent;
|
|
171
|
-
if (operation.chunks.length > 0) {
|
|
172
|
-
const patched = applyPatchChunks(source.currentContent, operation.chunks, operation.path);
|
|
173
|
-
nextContent = patched.content;
|
|
174
|
-
if (patched.usedFallback)
|
|
175
|
-
fallbackCount++;
|
|
176
|
-
}
|
|
177
|
-
if (operation.movePath) {
|
|
178
|
-
const target = await stateFor(operation.movePath);
|
|
179
|
-
if (target.path === source.path) {
|
|
180
|
-
throw new PatchApplyError(`Error: Cannot move ${operation.path} to itself.`, "blocked");
|
|
181
|
-
}
|
|
182
|
-
if (target.currentContent !== undefined) {
|
|
183
|
-
throw new PatchApplyError(`Error: Cannot move ${operation.path} to ${operation.movePath}; target already exists.`, "blocked");
|
|
184
|
-
}
|
|
185
|
-
source.currentContent = undefined;
|
|
186
|
-
target.currentContent = nextContent;
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
source.currentContent = nextContent;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
const changes = [...states.values()]
|
|
193
|
-
.filter((state) => state.originalContent !== state.currentContent)
|
|
194
|
-
.map((state) => fileStateToChange(state));
|
|
195
|
-
if (changes.length === 0) {
|
|
196
|
-
throw new PatchApplyError("Error: Patch produced no file changes.", "blocked");
|
|
197
|
-
}
|
|
198
|
-
const diff = changes.map((change) => change.diff).join("\n");
|
|
199
|
-
return {
|
|
200
|
-
changes,
|
|
201
|
-
diff,
|
|
202
|
-
paths: changes.map((change) => change.path),
|
|
203
|
-
fallbackCount,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
function fileStateToChange(state) {
|
|
207
|
-
const oldContent = state.originalContent;
|
|
208
|
-
const newContent = state.currentContent;
|
|
209
|
-
const kind = oldContent === undefined
|
|
210
|
-
? "add"
|
|
211
|
-
: newContent === undefined
|
|
212
|
-
? "delete"
|
|
213
|
-
: "update";
|
|
214
|
-
const diff = createTwoFilesPatch(state.path, state.path, oldContent ?? "", newContent ?? "", "original", "modified", { context: 3 });
|
|
215
|
-
const stats = countUnifiedDiffChanges(diff);
|
|
216
|
-
return {
|
|
217
|
-
path: state.path,
|
|
218
|
-
kind,
|
|
219
|
-
oldContent,
|
|
220
|
-
newContent,
|
|
221
|
-
diff,
|
|
222
|
-
addedLines: stats.added,
|
|
223
|
-
removedLines: stats.removed,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
async function readExistingFile(path) {
|
|
227
|
-
try {
|
|
228
|
-
const info = await stat(path);
|
|
229
|
-
if (info.isDirectory()) {
|
|
230
|
-
throw new PatchApplyError(`Error: Cannot patch directory: ${path}`, "blocked");
|
|
231
|
-
}
|
|
232
|
-
await access(path, constants.R_OK | constants.W_OK);
|
|
233
|
-
return await readFile(path, "utf-8");
|
|
234
|
-
}
|
|
235
|
-
catch (err) {
|
|
236
|
-
if (err instanceof PatchApplyError)
|
|
237
|
-
throw err;
|
|
238
|
-
if (isMissingPathError(err))
|
|
239
|
-
return undefined;
|
|
240
|
-
throw new PatchApplyError(`Error: Cannot read/write file for patch: ${path}`, "blocked");
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
async function checkPlanFresh(plan) {
|
|
244
|
-
for (const change of plan.changes) {
|
|
245
|
-
let current;
|
|
246
|
-
try {
|
|
247
|
-
current = await readExistingFile(change.path);
|
|
248
|
-
}
|
|
249
|
-
catch (err) {
|
|
250
|
-
if (err instanceof PatchApplyError) {
|
|
251
|
-
return { content: err.message, isError: true, status: err.status };
|
|
252
|
-
}
|
|
253
|
-
throw err;
|
|
254
|
-
}
|
|
255
|
-
if (current !== change.oldContent) {
|
|
256
|
-
return {
|
|
257
|
-
content: `Error: Cannot safely apply patch because ${change.path} changed after the patch was prepared.\n\n`
|
|
258
|
-
+ "Re-read the affected file and regenerate the patch against the latest content.",
|
|
259
|
-
isError: true,
|
|
260
|
-
status: "blocked",
|
|
261
|
-
metadata: {
|
|
262
|
-
kind: "patch",
|
|
263
|
-
path: change.path,
|
|
264
|
-
paths: plan.paths,
|
|
265
|
-
reason: "changed",
|
|
266
|
-
},
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return undefined;
|
|
271
|
-
}
|
|
272
|
-
async function writePatchPlan(plan) {
|
|
273
|
-
for (const change of plan.changes) {
|
|
274
|
-
if (change.newContent === undefined)
|
|
275
|
-
continue;
|
|
276
|
-
await mkdir(dirname(change.path), { recursive: true });
|
|
277
|
-
await writeFile(change.path, change.newContent, "utf-8");
|
|
278
|
-
}
|
|
279
|
-
for (const change of plan.changes) {
|
|
280
|
-
if (change.newContent !== undefined)
|
|
281
|
-
continue;
|
|
282
|
-
await rm(change.path, { force: true });
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
async function rollbackPatchPlan(plan) {
|
|
286
|
-
for (const change of [...plan.changes].reverse()) {
|
|
287
|
-
if (change.oldContent === undefined) {
|
|
288
|
-
await rm(change.path, { force: true });
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
await mkdir(dirname(change.path), { recursive: true });
|
|
292
|
-
await writeFile(change.path, change.oldContent, "utf-8");
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
async function observePatchPlan(fileState, plan) {
|
|
297
|
-
if (!fileState)
|
|
298
|
-
return;
|
|
299
|
-
await Promise.all(plan.changes.map(async (change) => {
|
|
300
|
-
if (change.newContent === undefined)
|
|
301
|
-
return;
|
|
302
|
-
await fileState.observe(change.path, "edit", change.newContent).catch(() => undefined);
|
|
303
|
-
}));
|
|
304
|
-
}
|
|
305
|
-
function collectOperationPaths(cwd, operations) {
|
|
306
|
-
const paths = [];
|
|
307
|
-
for (const operation of operations) {
|
|
308
|
-
paths.push(resolveToolPath(cwd, operation.path));
|
|
309
|
-
if (operation.type === "update" && operation.movePath) {
|
|
310
|
-
paths.push(resolveToolPath(cwd, operation.movePath));
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
return paths;
|
|
314
|
-
}
|
|
315
|
-
function assertWorkspacePath(cwd, filePath) {
|
|
316
|
-
if (!isWithinWorkspace(cwd, filePath)) {
|
|
317
|
-
throw new PatchApplyError(`Error: Patch path is outside the workspace: ${filePath}`, "blocked");
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
function summarizePaths(paths) {
|
|
321
|
-
if (paths.length === 1)
|
|
322
|
-
return paths[0];
|
|
323
|
-
return `${paths[0]} (+${paths.length - 1} more)`;
|
|
324
|
-
}
|
|
325
|
-
function isMissingPathError(error) {
|
|
326
|
-
return (typeof error === "object"
|
|
327
|
-
&& error !== null
|
|
328
|
-
&& "code" in error
|
|
329
|
-
&& (error.code === "ENOENT" || error.code === "ENOTDIR"));
|
|
330
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
export type PatchFileOperation = {
|
|
2
|
-
type: "add";
|
|
3
|
-
path: string;
|
|
4
|
-
lines: string[];
|
|
5
|
-
} | {
|
|
6
|
-
type: "delete";
|
|
7
|
-
path: string;
|
|
8
|
-
} | {
|
|
9
|
-
type: "update";
|
|
10
|
-
path: string;
|
|
11
|
-
movePath?: string;
|
|
12
|
-
chunks: PatchChunk[];
|
|
13
|
-
};
|
|
14
|
-
export interface PatchChunk {
|
|
15
|
-
header: string;
|
|
16
|
-
lines: PatchLine[];
|
|
17
|
-
}
|
|
18
|
-
export type PatchLine = {
|
|
19
|
-
kind: "context";
|
|
20
|
-
text: string;
|
|
21
|
-
} | {
|
|
22
|
-
kind: "remove";
|
|
23
|
-
text: string;
|
|
24
|
-
} | {
|
|
25
|
-
kind: "add";
|
|
26
|
-
text: string;
|
|
27
|
-
};
|
|
28
|
-
export interface ParsedApplyPatch {
|
|
29
|
-
operations: PatchFileOperation[];
|
|
30
|
-
}
|
|
31
|
-
export interface PatchedContentResult {
|
|
32
|
-
content: string;
|
|
33
|
-
usedFallback: boolean;
|
|
34
|
-
}
|
|
35
|
-
export declare class PatchApplyError extends Error {
|
|
36
|
-
readonly status: "no_match" | "blocked";
|
|
37
|
-
constructor(message: string, status?: "no_match" | "blocked");
|
|
38
|
-
}
|
|
39
|
-
export declare function parseApplyPatch(patchText: string): ParsedApplyPatch;
|
|
40
|
-
export declare function buildAddedFileContent(lines: string[]): string;
|
|
41
|
-
export declare function applyPatchChunks(rawContent: string, chunks: PatchChunk[], path: string): PatchedContentResult;
|