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:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user