refactor(chunk): restructured path parsing and edit operations for cleaner CRC handling

- Simplified chunk path parsing to extract selector separately from CRC field.
- Refactored chunk edit operations to conditionally strip CRC for auto-accepted batch operations.
- Enhanced chunk edit preview formatting to display operation types with target and line ranges.
- Improved chunk state rendering with clamped visible range and unified notice message format.
- Refactored error handling in chunk resolution from map_err to let-else pattern.
This commit is contained in:
can1357
2026-04-07 04:20:31 +02:00
parent 567197c34e
commit a30ee4a111
6 changed files with 86 additions and 39 deletions
+8 -3
View File
@@ -262,7 +262,10 @@ fn apply_replace(
});
let requires_checksum = operation.sel.is_some() || default_crc.is_some();
let batch_auto_accepted = ensure_batch_operation_target_current(scheduled, crc, touched_paths);
let resolved = resolve_chunk_with_crc(state, anchor_selector, crc, warnings)?;
// When auto-accepted, strip CRC so resolution finds by path only (CRC is stale
// from pre-batch).
let resolve_crc = if batch_auto_accepted { None } else { crc };
let resolved = resolve_chunk_with_crc(state, anchor_selector, resolve_crc, warnings)?;
if !batch_auto_accepted {
validate_batch_crc(resolved.chunk, resolved.crc.as_deref(), requires_checksum)?;
}
@@ -355,7 +358,8 @@ fn apply_delete(
});
let requires_checksum = operation.sel.is_some() || default_crc.is_some();
let batch_auto_accepted = ensure_batch_operation_target_current(scheduled, crc, touched_paths);
let resolved = resolve_chunk_with_crc(state, anchor_selector, crc, warnings)?;
let resolve_crc = if batch_auto_accepted { None } else { crc };
let resolved = resolve_chunk_with_crc(state, anchor_selector, resolve_crc, warnings)?;
if !batch_auto_accepted {
validate_batch_crc(resolved.chunk, resolved.crc.as_deref(), requires_checksum)?;
}
@@ -389,7 +393,8 @@ fn apply_insert(
}
});
let batch_auto_accepted = ensure_batch_operation_target_current(scheduled, crc, touched_paths);
let resolved = resolve_chunk_with_crc(state, anchor_selector, crc, warnings)?;
let resolve_crc = if batch_auto_accepted { None } else { crc };
let resolved = resolve_chunk_with_crc(state, anchor_selector, resolve_crc, warnings)?;
if !batch_auto_accepted {
validate_batch_crc(resolved.chunk, resolved.crc.as_deref(), resolved.crc.is_some())?;
}
+1 -6
View File
@@ -90,12 +90,7 @@ pub fn resolve_chunk_with_crc<'a>(
return Ok(ResolvedChunk { chunk, crc: Some(cleaned_crc) });
}
let chunk = resolve_chunk_selector_impl(
state,
cleaned_selector.as_deref(),
cleaned_crc.as_deref(),
warnings,
)?;
let chunk = resolve_chunk_selector_impl(state, cleaned_selector.as_deref(), None, warnings)?;
Ok(ResolvedChunk { chunk, crc: cleaned_crc })
}
+17 -10
View File
@@ -326,19 +326,20 @@ impl ChunkState {
});
}
let notice = if visible_range.start_line == visible_range.end_line {
format!("{}:L{}", params.display_path, visible_range.start_line)
} else {
format!(
"{}:L{}-L{}",
params.display_path, visible_range.start_line, visible_range.end_line
)
let clamped_range = VisibleLineRange {
start_line: visible_range.start_line,
end_line: visible_range.end_line.min(self.inner.tree().line_count),
};
let notice = format!(
"[Notice: chunk view scoped to requested lines L{}-L{}; non-overlapping lines \
omitted.]",
clamped_range.start_line, clamped_range.end_line
);
let text = self.render(RenderParams {
chunk_path: Some(root.path.clone()),
title: params.display_path.clone(),
language_tag: params.language_tag.clone(),
visible_range: Some(visible_range),
visible_range: Some(clamped_range),
render_children_only: true,
omit_checksum: params.omit_checksum,
anchor_style: params.anchor_style,
@@ -377,9 +378,15 @@ impl ChunkState {
}
let mut warnings = Vec::new();
let resolved =
let Ok(resolved) =
resolve_chunk_with_crc(self.inner(), selector.as_deref(), crc.as_deref(), &mut warnings)
.map_err(Error::from_reason)?;
else {
let sel = selector.unwrap_or_default();
return Ok(ReadResult {
text: format!("{}:{}\n\n[Chunk not found]", params.display_path, sel),
chunk: Some(ChunkReadTarget { status: ChunkReadStatus::NotFound, selector: sel }),
});
};
let chunk = resolved.chunk;
if let Some(absolute_line_range) = params.absolute_line_range {
+3 -1
View File
@@ -1,7 +1,6 @@
# Changelog
## [Unreleased]
### Added
- Added `anchorStyle` parameter to chunk read formatting to control chunk path display format (full, kind, or bare)
@@ -22,6 +21,9 @@
### Changed
- Updated streaming edit preview to display chunk edits alongside hashline edits with operation-specific formatting
- Improved chunk edit preview labels to show operation type (delete, append, prepend, insert, replace) with target and line ranges
- Simplified chunk read path parsing to extract only the selector string without redundant CRC field
- Agent sessions now hold a macOS power assertion for their lifetime so the host stays awake during active coding-agent runs
- 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
+56 -15
View File
@@ -22,7 +22,7 @@ import {
truncateDiffByHunk,
} from "../tools/render-utils";
import { Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
import type { HashlineToolEdit } from "./index";
import type { ChunkToolEdit, HashlineToolEdit } from "./index";
import type { DiffError, DiffResult, Operation } from "./types";
// ═══════════════════════════════════════════════════════════════════════════
@@ -83,8 +83,8 @@ interface EditRenderArgs {
* Computed preview diff (used when tool args don't include a diff, e.g. hashline mode).
*/
previewDiff?: string;
// Hashline mode fields
edits?: Partial<HashlineToolEdit>[];
// Hashline / chunk mode fields
edits?: Partial<HashlineToolEdit | ChunkToolEdit>[];
}
/** Extended context for edit tool rendering */
@@ -117,18 +117,24 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
return text;
}
function formatStreamingHashlineEdits(edits: Partial<HashlineToolEdit>[], uiTheme: Theme): string {
function formatStreamingHashlineEdits(edits: Partial<HashlineToolEdit | ChunkToolEdit>[], uiTheme: Theme): string {
const MAX_EDITS = 4;
const MAX_DST_LINES = 8;
let text = "\n\n";
text += uiTheme.fg("dim", `[${edits.length} hashline edit${edits.length === 1 ? "" : "s"}]`);
// Detect whether these are chunk edits (target field) or hashline edits (loc field)
const isChunk = edits.length > 0 && "target" in edits[0];
const label = isChunk ? "chunk edit" : "hashline edit";
text += uiTheme.fg("dim", `[${edits.length} ${label}${edits.length === 1 ? "" : "s"}]`);
text += "\n";
let shownEdits = 0;
let shownDstLines = 0;
for (const edit of edits) {
shownEdits++;
if (shownEdits > MAX_EDITS) break;
const formatted = formatHashlineEdit(edit);
const formatted = isChunk
? formatChunkEdit(edit as Partial<ChunkToolEdit>)
: formatHashlineEdit(edit as Partial<HashlineToolEdit>);
text += uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(formatted.srcLabel), 120));
text += "\n";
if (formatted.dst === "") {
@@ -145,39 +151,74 @@ function formatStreamingHashlineEdits(edits: Partial<HashlineToolEdit>[], uiThem
if (shownDstLines > MAX_DST_LINES) break;
}
if (edits.length > MAX_EDITS) {
text += uiTheme.fg("dim", `… (${edits.length - MAX_EDITS} more edits)`);
text += uiTheme.fg("dim", `\u2026 (${edits.length - MAX_EDITS} more edits)`);
}
if (shownDstLines > MAX_DST_LINES) {
text += uiTheme.fg("dim", `\n… (${shownDstLines - MAX_DST_LINES} more dst lines)`);
text += uiTheme.fg("dim", `\n\u2026 (${shownDstLines - MAX_DST_LINES} more dst lines)`);
}
return text.trimEnd();
function formatHashlineEdit(edit: Partial<HashlineToolEdit>): { srcLabel: string; dst: string } {
if (typeof edit !== "object" || !edit) {
return { srcLabel: "• (incomplete edit)", dst: "" };
return { srcLabel: "\u2022 (incomplete edit)", dst: "" };
}
const contentLines = Array.isArray(edit.content) ? (edit.content as string[]).join("\n") : "";
const loc = edit.loc;
if (loc === "append" || loc === "prepend") {
return { srcLabel: `• ${loc} (file-level)`, dst: contentLines };
return { srcLabel: `\u2022 ${loc} (file-level)`, dst: contentLines };
}
if (typeof loc === "object" && loc) {
if ("range" in loc && typeof loc.range === "object" && loc.range) {
return { srcLabel: `• range ${loc.range.pos ?? "?"}…${loc.range.end ?? "?"}`, dst: contentLines };
return { srcLabel: `\u2022 range ${loc.range.pos ?? "?"}\u2026${loc.range.end ?? "?"}`, dst: contentLines };
}
if ("line" in loc) {
return { srcLabel: `• line ${(loc as { line: string }).line}`, dst: contentLines };
return { srcLabel: `\u2022 line ${(loc as { line: string }).line}`, dst: contentLines };
}
if ("append" in loc) {
return { srcLabel: `• append ${(loc as { append: string }).append}`, dst: contentLines };
return { srcLabel: `\u2022 append ${(loc as { append: string }).append}`, dst: contentLines };
}
if ("prepend" in loc) {
return { srcLabel: `• prepend ${(loc as { prepend: string }).prepend}`, dst: contentLines };
return { srcLabel: `\u2022 prepend ${(loc as { prepend: string }).prepend}`, dst: contentLines };
}
}
return { srcLabel: "• (unknown edit)", dst: contentLines };
return { srcLabel: "\u2022 (unknown edit)", dst: contentLines };
}
function formatChunkEdit(edit: Partial<ChunkToolEdit>): { srcLabel: string; dst: string } {
if (typeof edit !== "object" || !edit) {
return { srcLabel: "\u2022 (incomplete edit)", dst: "" };
}
const contentLines = Array.isArray(edit.content)
? (edit.content as string[]).join("\n")
: typeof edit.content === "string"
? edit.content
: "";
const target = edit.target ?? "?";
if (edit.delete) {
return { srcLabel: `\u2022 delete ${target}`, dst: "" };
}
if (edit.append) {
return { srcLabel: `\u2022 append child ${target}`, dst: contentLines };
}
if (edit.prepend) {
return { srcLabel: `\u2022 prepend child ${target}`, dst: contentLines };
}
if (edit.after) {
return { srcLabel: `\u2022 insert after ${target}.${edit.after}`, dst: contentLines };
}
if (edit.before) {
return { srcLabel: `\u2022 insert before ${target}.${edit.before}`, dst: contentLines };
}
if (edit.line != null) {
const range = edit.end_line != null ? `${edit.line}\u2026${edit.end_line}` : `${edit.line}`;
return { srcLabel: `\u2022 replace ${target} L${range}`, dst: contentLines };
}
return { srcLabel: `\u2022 replace ${target}`, dst: contentLines };
}
}
function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
@@ -129,12 +129,9 @@ 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: rawSelector,
crc: parsedSelector.crc,
selector: parseChunkSelector(readPath.slice(colonIndex + 1) || undefined).selector,
};
}