feat(coding-agent): added notebook-aware .ipynb edit/read path helpers

- Added `.ipynb` detection and editable-cell conversion utilities, including merge/serialize helpers in `edit/notebook`.
- Rerouted hashline, patch, and replace edit flows, plus read/write paths, through notebook-aware helpers before persistence.
- Removed the dedicated `notebook` tool, its schema flags, renderer, and built-in registration/settings checks.
- Updated notebook read behavior, docs, and tests so `.ipynb` reads return editable `# %%` cells and edits reserialize to JSON.
This commit is contained in:
can1357
2026-05-07 04:16:11 +02:00
parent 2c307e51f4
commit cbfdc0a212
15 changed files with 340 additions and 330 deletions
@@ -52,6 +52,7 @@ import {
HL_HASH_CAPTURE_RE_RAW,
} from "../line-hash";
import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "../normalize";
import { readEditFileText, serializeEditFileText } from "../read-file";
import type { EditToolDetails, LspBatchRequest } from "../renderer";
// ───────────────────────────────────────────────────────────────────────────
@@ -1656,11 +1657,14 @@ export function splitHashlineInputs(input: string, options: SplitHashlineOptions
// 13. Diff computation (for streaming preview)
// ───────────────────────────────────────────────────────────────────────────
async function readHashlineFileText(file: { text(): Promise<string> }, pathText: string): Promise<string> {
async function readHashlineFileText(
_file: { text(): Promise<string> },
absolutePath: string,
pathText: string,
): Promise<string> {
try {
return await file.text();
return await readEditFileText(absolutePath, pathText);
} catch (error) {
if (isEnoent(error)) throw new Error(`File not found: ${pathText}`);
const message = error instanceof Error ? error.message : String(error);
throw new Error(message || `Unable to read ${pathText}`);
}
@@ -1679,7 +1683,7 @@ export async function computeHashlineDiff(
const [section] = sections;
const absolutePath = resolveToCwd(section.path, cwd);
const rawContent = await readHashlineFileText(Bun.file(absolutePath), section.path);
const rawContent = await readHashlineFileText(Bun.file(absolutePath), absolutePath, section.path);
const { text: content } = stripBom(rawContent);
const normalized = normalizeToLF(content);
const result = applyHashlineEdits(normalized, parseHashline(section.diff), options);
@@ -1699,11 +1703,13 @@ interface ReadHashlineFileResult {
rawContent: string;
}
async function readHashlineFile(absolutePath: string): Promise<ReadHashlineFileResult> {
async function readHashlineFile(absolutePath: string, pathText: string): Promise<ReadHashlineFileResult> {
try {
return { exists: true, rawContent: await Bun.file(absolutePath).text() };
return { exists: true, rawContent: await readEditFileText(absolutePath, pathText) };
} catch (error) {
if (isEnoent(error)) return { exists: false, rawContent: "" };
if (error instanceof Error && error.message === `File not found: ${pathText}`)
return { exists: false, rawContent: "" };
throw error;
}
}
@@ -1746,7 +1752,7 @@ async function preflightHashlineSection(options: ExecuteHashlineSingleOptions &
const { edits } = parseHashlineWithWarnings(diff);
enforcePlanModeWrite(session, sectionPath, { op: "update" });
const source = await readHashlineFile(absolutePath);
const source = await readHashlineFile(absolutePath, sectionPath);
if (!source.exists && hasAnchorScopedEdit(edits)) throw new Error(`File not found: ${sectionPath}`);
if (source.exists) assertEditableFileContent(source.rawContent, sectionPath);
@@ -1773,7 +1779,7 @@ async function executeHashlineSection(
const { edits, warnings: parseWarnings } = parseHashlineWithWarnings(diff);
enforcePlanModeWrite(session, sourcePath, { op: "update" });
const source = await readHashlineFile(absolutePath);
const source = await readHashlineFile(absolutePath, sourcePath);
if (!source.exists && hasAnchorScopedEdit(edits)) throw new Error(`File not found: ${sourcePath}`);
if (source.exists) assertEditableFileContent(source.rawContent, sourcePath);
@@ -1789,7 +1795,11 @@ async function executeHashlineSection(
};
}
const finalContent = bom + restoreLineEndings(result.lines, originalEnding);
const finalContent = await serializeEditFileText(
absolutePath,
sourcePath,
bom + restoreLineEndings(result.lines, originalEnding),
);
const diagnostics = await writethrough(
absolutePath,
finalContent,
@@ -44,6 +44,7 @@ import {
restoreLineEndings,
stripBom,
} from "../normalize";
import { readEditFileText, serializeEditFileText } from "../read-file";
import type { EditToolDetails, LspBatchRequest } from "../renderer";
import {
type ContextLineResult,
@@ -104,13 +105,13 @@ export const defaultFileSystem: FileSystem = {
return Bun.file(path).exists();
},
async read(path: string): Promise<string> {
return Bun.file(path).text();
return readEditFileText(path, path);
},
async readBinary(path: string): Promise<Uint8Array> {
return fs.promises.readFile(path);
},
async write(path: string, content: string): Promise<void> {
await Bun.write(path, content);
await Bun.write(path, await serializeEditFileText(path, path, content));
},
async delete(path: string): Promise<void> {
await fs.promises.unlink(path);
@@ -1637,7 +1638,7 @@ class LspFileSystem implements FileSystem {
}
async read(path: string): Promise<string> {
return this.#getFile(path).text();
return readEditFileText(path, path);
}
async readBinary(path: string): Promise<Uint8Array> {
@@ -1647,10 +1648,11 @@ class LspFileSystem implements FileSystem {
async write(path: string, content: string): Promise<void> {
const file = this.#getFile(path);
const finalContent = await serializeEditFileText(path, path, content);
const deferredForPath = this.deferredForPath;
const result = await this.writethrough(
path,
content,
finalContent,
this.signal,
file,
this.batchRequest,
@@ -21,7 +21,7 @@ import {
restoreLineEndings,
stripBom,
} from "../normalize";
import { readEditFileText } from "../read-file";
import { readEditFileText, serializeEditFileText } from "../read-file";
import type { EditToolDetails, LspBatchRequest } from "../renderer";
export interface FuzzyMatch {
@@ -1065,7 +1065,11 @@ export async function executeReplaceSingle(
throw new Error(`Edits to ${path} resulted in no changes being made.`);
}
const finalContent = bom + restoreLineEndings(result.content, originalEnding);
const finalContent = await serializeEditFileText(
absolutePath,
path,
bom + restoreLineEndings(result.content, originalEnding),
);
const diagnostics = await writethrough(
absolutePath,
finalContent,