/** * Detect and resolve unresolved git merge conflicts that surface in `read` * output. * * Workflow: * 1. `read` collects lines from disk as usual. * 2. `scanConflictLines` inspects those lines (no extra I/O) for * well-formed `<<<<<<<` / `=======` / `>>>>>>>` blocks. * 3. Each completed block is registered with the session's * `ConflictHistory`, which assigns it a stable id. * 4. The read output is returned verbatim with a short footer naming * every conflict id surfaced, and the agent calls * `write({ path: "conflict://", content })` to splice the * recorded region with the chosen content. * * Marker shape is strict: only column-0 markers of the exact prefix length * followed by either EOL or a single space + label count. Lines that * merely start with `<` or `=` never match. */ import type { ToolSession } from "./index"; export interface ConflictBlock { /** 1-indexed line of the `<<<<<<<` marker. */ startLine: number; /** 1-indexed line of the `=======` separator. */ separatorLine: number; /** 1-indexed line of the `>>>>>>>` marker. */ endLine: number; /** 1-indexed line of the `|||||||` base marker (diff3 only). */ baseLine?: number; oursLabel?: string; baseLabel?: string; theirsLabel?: string; oursLines: string[]; baseLines?: string[]; theirsLines: string[]; } /** * Scan an already-collected array of file lines for completed conflict * blocks. `firstLineNumber` is the 1-indexed line number of `lines[0]` * (so a windowed read starting at line 200 passes `firstLineNumber: 200`). * * Only fully-closed blocks (opener + separator + closer all present in * the window) are returned. A block whose closer is past the window's * tail is dropped — the agent will see the open marker and can widen * the read. */ export declare function scanConflictLines(lines: readonly string[], firstLineNumber: number): ConflictBlock[]; /** * Scan a whole file for unresolved conflict blocks. * * Reads at most `maxBytes` (default 10 MB) so this stays cheap on * pathological files. Files truncated by the cap report * `scanTruncated: true`; only complete blocks within the scanned prefix * are returned, so trailing partial markers never invent fake blocks. */ export declare function scanFileForConflicts(absolutePath: string, options?: { maxBytes?: number; }): Promise<{ blocks: ConflictBlock[]; scanTruncated: boolean; }>; /** * Recorded conflict block keyed by a session-stable id. The history is * append-only; ids stay valid even after later writes resolve other * blocks in the same file, so retries don't depend on re-reading. */ export interface ConflictEntry extends ConflictBlock { id: number; absolutePath: string; displayPath: string; } /** Per-session log of conflict regions surfaced by `read`. */ export declare class ConflictHistory { #private; /** * Register a conflict block. Returns the (possibly pre-existing) entry * — if the same `absolutePath`+`startLine` was registered before, the * earlier id is reused so a re-read does not inflate the counter or * orphan the prior id. The recorded region is overwritten on re-read * so the splice always reflects the current marker positions on disk. */ register(input: Omit): ConflictEntry; get(id: number): ConflictEntry | undefined; /** Snapshot every registered entry in insertion (id) order. */ entries(): ConflictEntry[]; /** Drop a single entry by id. Used after a successful resolve. */ invalidate(id: number): void; /** Drop every entry referencing `absolutePath`. Used after a successful resolve. */ invalidatePath(absolutePath: string): void; } /** Lazily attach a `ConflictHistory` to the session and return it. */ export declare function getConflictHistory(session: ToolSession): ConflictHistory; /** A side of a conflict block that the `read` tool can render via `conflict://N/`. */ export type ConflictScope = "ours" | "theirs" | "base"; /** Parsed `conflict://` / `conflict:///` / `conflict://*` URI. */ export interface ParsedConflictUri { /** `"*"` selects every currently-registered conflict (bulk write only). */ id: number | "*"; scope?: ConflictScope; /** * When `raw` was a malformed `:conflict://…` path, the * stripped prefix is preserved here so callers can surface a gentle * "you don't need the file path" note. `undefined` for clean URIs. */ recoveredPrefix?: string; } /** * Parse a `conflict://`, `conflict:///`, or `conflict://*` URI. * * Returns `null` for non-conflict paths; throws `ToolError` for a * well-formed scheme with an invalid id or scope so the agent gets a * clear actionable message rather than a confusing "not found" later. * * `*` is the bulk-write wildcard — only valid as `conflict://*` (no * scope segment). Use it with `write({ path: "conflict://*", content })` * to apply `content` (with optional `@ours` / `@theirs` / `@base` / * `@both` shorthand) to every currently-registered conflict in one shot. */ export declare function parseConflictUri(raw: string): ParsedConflictUri | null; /** * Splice the conflict region recorded in `entry` out of `originalText` * and replace it with `replacement` (markers and all sides included). * * Works like the edit tool's patch infra: locates the recorded marker * block by content (anchored to `entry.startLine` as the preferred * match), so out-of-band edits earlier in the file that shift line * numbers don't break resolution. Throws clearly when the marker block * has actually been altered or removed. */ export declare function spliceConflict(originalText: string, entry: ConflictEntry, replacement: string): string; /** * Expand `@ours` / `@theirs` / `@base` / `@both` line tokens against the * recorded sections of `entry`. A token only triggers when it is the * entire content of a line (after CRLF normalisation), so `@ours` inside * actual code is left alone. Other lines pass through verbatim. * * - `@ours` → expands to the recorded `oursLines` (in order). * - `@theirs` → expands to the recorded `theirsLines` (in order). * - `@base` → expands to `baseLines`; throws if no base section was * recorded (i.e. the conflict was 2-way, not diff3). * - `@both` → expands to `oursLines` then `theirsLines`. */ export declare function expandContentTokens(content: string, entry: ConflictEntry): string; /** * Materialise a conflict block for `conflict://` reads (and their * `/ours` / `/theirs` / `/base` scopes). * * Returns: * - `lines`: the lines to render, ordered top-to-bottom. * - `startLine`: the 1-indexed file line number `lines[0]` corresponds * to, so the read formatter can label hashline anchors with the * original file positions. * * Bare (no scope) returns the full block including marker lines. A * scoped view returns only that side's body — `base` throws when the * recorded conflict is a 2-way merge with no base section. */ export declare function renderConflictRegion(entry: ConflictEntry, scope: ConflictScope | undefined): { lines: string[]; startLine: number; }; /** * Build a compact diff-style footer describing the conflicts registered * during a read. Designed to be appended after the file content. * * Format: * * ⚠ N unresolved conflicts detected * - ours = HEAD * - theirs = feature/x * NOTICE: … * * ──── #1 L42-48 ──── * <<< ours * …ours body… * === base ≡ ours * >>> theirs * …theirs body… * * Labels are aggregated once at the top from the first entry that has * them; when a section body equals another section's body the redundant * body is collapsed to `≡ `. */ export interface FormatConflictWarningOptions { /** * Total number of conflicts in the underlying file. If greater than * `entries.length` the header notes how many are visible vs the total * and points at `:conflicts` for the compact list. */ totalInFile?: number; /** Display path used inside the `:conflicts` hint. */ displayPath?: string; /** Whether the underlying file scan hit its byte cap. */ scanTruncated?: boolean; } export declare function formatConflictWarning(entries: readonly ConflictEntry[], options?: FormatConflictWarningOptions): string; /** * Render a single-line-per-block index of every conflict in a file. * Used by the `:conflicts` read selector to give the agent a cheap overview * of a heavily-conflicted file without dumping every body. */ export declare function formatConflictSummary(entries: readonly ConflictEntry[], options?: { displayPath: string; scanTruncated?: boolean; }): string;