refactor(chunk): restructured selector resolution and state lookup with unified classification

- Refactored chunk selector resolution to extract and preserve raw selector strings separately from parsed CRC checksums.
- Reorganized chunk state lookup infrastructure with dedicated maps for checksum, leaf, and suffix-based chunk resolution.
- Simplified batch operation CRC validation to auto-accept stale checksums for subsequent operations on the same chunk.
- Unified TypeScript interface and Nix attrset chunk classification with consistent naming and nested expression handling.
- Extracted clipboard utility functions to dedicated module with improved OSC 52 and Termux support.
- Updated chunk read formatting with anchorStyle parameter and ellipsis standardization for improved consistency.
This commit is contained in:
can1357
2026-04-07 03:36:57 +02:00
parent 3944b0f7f5
commit ad4c5917a4
12 changed files with 761 additions and 351 deletions
+4
View File
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
### Added
- Added `anchorStyle` parameter to chunk read formatting to control chunk path display format (full, kind, or bare)
@@ -21,6 +22,9 @@
### Changed
- Updated chunk edit prompt documentation to use ellipsis (…) instead of ellipsis (...) for consistency in operation examples
- Modified chunk path parsing to preserve raw selector strings and extract CRC separately, enabling accurate chunk reference round-tripping in read/edit workflows
- Changed chunk edit behavior to auto-accept stale CRC checksums for subsequent operations on the same chunk within a batch, improving usability when applying multiple edits to the same target
- Moved `copyToClipboard` and `readImageFromClipboard` functions to new `utils/clipboard.ts` module with improved OSC 52 support and Termux compatibility
- Updated grep output mode to use `GrepOutputMode` enum from pi-natives instead of string literals
- Changed macOS appearance observer to use `MacAppearanceObserver.start()` class method with error-first callback signature
@@ -8,7 +8,7 @@ Successful edit responses include the updated chunk tree with checksums. Do not
<operations>
**Choosing the right edit shape:**
- To rewrite an entire chunk → `{ "target": "chunk#CRC", "content": "..." }`
- To rewrite an entire chunk → `{ "target": "chunk#CRC", "content": "…" }`
- To fix a single line → add `"line": 13`
- To fix a contiguous range → add `"line": 13, "end_line": 17`
- To delete a chunk → `{ "target": "chunk#CRC", "delete": true }`
@@ -17,15 +17,14 @@ Successful edit responses include the updated chunk tree with checksums. Do not
|shape|effect|
|---|---|
|`{ "target": "chunk#CRC", "content": "..." }`|replace the target chunk|
|`{ "target": "chunk#CRC", "line": 13, "content": "..." }`|replace one line within the target chunk|
|`{ "target": "chunk#CRC", "line": 13, "end_line": 17, "content": "..." }`|replace an inclusive line range within the target chunk|
|`{ "target": "chunk#CRC", "content": "…" }`|replace the target chunk|
|`{ "target": "chunk#CRC", "line": 13, "content": "…" }`|replace one line within the target chunk|
|`{ "target": "chunk#CRC", "line": 13, "end_line": 17, "content": "…" }`|replace an inclusive line range within the target chunk|
|`{ "target": "chunk#CRC", "delete": true }`|delete the target chunk|
|`{ "target": "chunk", "append": true, "content": "..." }`|append as last child of the target chunk|
|`{ "target": "chunk", "prepend": true, "content": "..." }`|prepend as first child of the target chunk|
|`{ "target": "parent", "after": "child", "content": "..." }`|insert after the named child within `parent`|
|`{ "target": "parent", "before": "child", "content": "..." }`|insert before the named child within `parent`|
|`{ "target": "chunk", "append": true, "content": "…" }`|append as last child of the target chunk|
|`{ "target": "chunk", "prepend": true, "content": "…" }`|prepend as first child of the target chunk|
|`{ "target": "parent", "after": "child", "content": "…" }`|insert after the named child within `parent`|
|`{ "target": "parent", "before": "child", "content": "…" }`|insert before the named child within `parent`|
- `line`/`end_line` are **absolute file line numbers** from the `read` gutter. `line` alone = single line. `line` + `end_line` = inclusive range. `line` with `end_line` = `line`-1 = zero-width insert.
- `path` is always just the file path. Do not embed `:chunk` selectors in `path`.
- `target` is the chunk path, optionally followed by `#CRC` copied from the anchor. Example: `"class_Server.fn_start#HTST"`.
@@ -130,4 +129,4 @@ All examples reference this `read` output:
- For line-scoped replace edits, use file line numbers from the `read` gutter.
- Multiple line-scoped edits on the same chunk in one batch are fine — the engine auto-updates the checksum between operations.
</critical>
</output>
</output>
@@ -62,6 +62,7 @@ export type ChunkEditResult = {
export type ParsedChunkReadPath = {
filePath: string;
selector?: string;
crc?: string;
};
type ChunkCacheEntry = {
@@ -128,9 +129,12 @@ export function parseChunkReadPath(readPath: string): ParsedChunkReadPath {
if (colonIndex === -1) {
return { filePath: readPath };
}
const rawSelector = readPath.slice(colonIndex + 1) || undefined;
const parsedSelector = parseChunkSelector(rawSelector);
return {
filePath: readPath.slice(0, colonIndex),
selector: parseChunkSelector(readPath.slice(colonIndex + 1) || undefined).selector,
selector: rawSelector,
crc: parsedSelector.crc,
};
}
+4 -2
View File
@@ -771,6 +771,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
const pathSelectorParsed = chunkMode ? parseSel(parsedReadPath.selector) : { kind: "none" as const };
const pathChunkSelector = pathSelectorParsed.kind === "chunk" ? pathSelectorParsed.selector : undefined;
const selectorInput = sel ?? parsedReadPath.selector;
const rawSelectorInput = sel ?? parsedReadPath.selector;
const parsed = parseSel(selectorInput);
const archivePath = await this.#resolveArchiveReadPath(localReadPath, signal);
@@ -837,10 +838,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
: undefined;
// sel= wins over path:chunk when both are provided (explicit param > embedded path).
const effectiveSelector = sel ? selectorInput : (pathChunkSelector ?? selectorInput);
const rawEffectiveSelector = sel ? selectorInput : (rawSelectorInput ?? effectiveSelector);
const chunkReadPath =
parsed.kind === "chunk" || (pathChunkSelector && !sel)
? effectiveSelector
? `${localReadPath}:${effectiveSelector}`
? rawEffectiveSelector
? `${localReadPath}:${rawEffectiveSelector}`
: localReadPath
: parsed.kind === "lines"
? parsed.endLine !== undefined
@@ -465,24 +465,24 @@ describe("edit safety invariants", () => {
});
}
test("rejects a second same-path replace in one batch when the checksum stays stale", () => {
test("auto-accepts stale CRC for a second same-path replace in one batch", () => {
const checksum = getChecksum(testSource, runChunkPath);
expect(() =>
edit([
{
op: "replace",
sel: runChunkPath,
crc: checksum,
content: '\trun(): void {\n\t\tconsole.log("first");\n\t}',
},
{
op: "replace",
sel: runChunkPath,
crc: checksum,
content: '\trun(): void {\n\t\tconsole.log("second");\n\t}',
},
]),
).toThrow(/changed by an earlier batch operation/);
const result = edit([
{
op: "replace",
sel: runChunkPath,
crc: checksum,
content: '\trun(): void {\n\t\tconsole.log("first");\n\t}',
},
{
op: "replace",
sel: runChunkPath,
crc: checksum,
content: '\trun(): void {\n\t\tconsole.log("second");\n\t}',
},
]);
expect(result.diffSourceAfter).toContain('console.log("second")');
expect(result.diffSourceAfter).not.toContain('console.log("first")');
});
test("applies two same-path replaces in one batch when the second checksum matches the post-first state", () => {
@@ -506,28 +506,28 @@ describe("edit safety invariants", () => {
expect(result.diffSourceAfter).not.toContain('console.log("first")');
});
test("rejects a second same-path splice in one batch when the checksum stays stale", () => {
test("auto-accepts stale CRC for a second same-path splice in one batch", () => {
const checksum = getChecksum(testSource, runChunkPath);
expect(() =>
edit([
{
op: "replace",
sel: runChunkPath,
crc: checksum,
line: 6,
endLine: 6,
content: '\trun(task = "default"): void {',
},
{
op: "replace",
sel: runChunkPath,
crc: checksum,
line: 7,
endLine: 7,
content: "\t\tconsole.log(task);",
},
]),
).toThrow(/changed by an earlier batch operation/);
const result = edit([
{
op: "replace",
sel: runChunkPath,
crc: checksum,
line: 6,
endLine: 6,
content: '\trun(task = "default"): void {',
},
{
op: "replace",
sel: runChunkPath,
crc: checksum,
line: 7,
endLine: 7,
content: "\t\tconsole.log(task);",
},
]);
expect(result.diffSourceAfter).toContain('run(task = "default")');
expect(result.diffSourceAfter).toContain("console.log(task)");
});
test("applies two same-path splices in one batch when the second checksum matches the post-first state", () => {
@@ -447,7 +447,7 @@ describe("chunk mode tools", () => {
expect(await Bun.file(filePath).text()).toBe(originalSource);
});
it("reports stale mixed batches against the listed splice operation", async () => {
it("auto-accepts stale CRC in mixed batches on the same chunk", async () => {
const filePath = path.join(tmpDir, "server.ts");
const originalSource = buildLargeTypescriptFixture();
await Bun.write(filePath, originalSource);
@@ -455,27 +455,24 @@ describe("chunk mode tools", () => {
const editTool = new EditTool(session);
const checksum = getChunkChecksum(originalSource, "typescript", "class_Server.fn_handleError");
await expect(
editTool.execute("chunk-edit-stale-mixed-batch", {
path: filePath,
edits: [
{
target: `class_Server.fn_handleError#${checksum}`,
content: " private handleError(err: Error): string {\n return err.message;\n }",
},
{
target: `class_Server.fn_handleError#${checksum}`,
line: 63,
end_line: 63,
content: " return err.message.toUpperCase();",
},
],
}),
).rejects.toThrow(
/Edit operation 2\/2 failed \(replace on "class_Server\.fn_handleError"\): Chunk "class_Server\.fn_handleError" was changed by an earlier batch operation/,
);
const _result = await editTool.execute("chunk-edit-stale-mixed-batch", {
path: filePath,
edits: [
{
target: `class_Server.fn_handleError#${checksum}`,
content: " private handleError(err: Error): string {\n return err.message;\n }",
},
{
target: `class_Server.fn_handleError#${checksum}`,
line: 3,
end_line: 3,
content: " return err.message.toUpperCase();",
},
],
});
expect(await Bun.file(filePath).text()).toBe(originalSource);
const updatedSource = await Bun.file(filePath).text();
expect(updatedSource).toContain("toUpperCase");
});
it("rejects missing CRC", async () => {