import type { ConflictBlock, ConflictEntry, ConflictScope } from "@oh-my-pi/pi-tui/tools/conflict-detect"; /** * 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.js"; /** * 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[]; /** Result of {@link scanFileForConflicts}. */ export interface ConflictFileScan { blocks: ConflictBlock[]; /** First line the byte cap left unscanned (possibly partial); unset when the whole file was scanned. */ truncatedAtLine?: number; } /** * 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 the line the scan * stopped at; 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; /** * Note for a scan the byte cap stopped at `line`: `:conflicts` cannot list or * register blocks from there on, but a ranged read that shows them registers them. */ export declare function formatScanCapNote(displayPath: string, line: number): string; /** Per-session log of conflict regions surfaced by `read`. */ export declare class ConflictHistory { #private; /** * Reuse an id only for the same block at the same position. Reconciliation * relocates unchanged blocks first; a changed block never inherits an old id. */ register(input: Omit): ConflictEntry; /** * Refresh only already-registered blocks, without registering unseen ones. * Match unchanged positions before relocations so identical distinct blocks * keep separate ids. A capped scan cannot disprove a block crossing or beyond * its unscanned boundary. */ reconcilePath(absolutePath: string, scan: ConflictFileScan): void; 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; } /** * Re-check registered ids against the live file contents (all files, or only * `absolutePath`): unchanged blocks follow line shifts, changed or vanished * ones are retired, and a deleted file retires all of its ids. */ export declare function reconcileRegisteredConflicts(history: ConflictHistory, absolutePath?: string): Promise; /** Lazily attach a `ConflictHistory` to the session and return it. */ export declare function getConflictHistory(session: ToolSession): ConflictHistory; /** Parsed `conflict://` / `conflict:///` / `conflict://*` URI. */ export interface ParsedConflictUri { /** `"*"` selects every currently-registered conflict (bulk write only). */ id: number | "*"; scope?: ConflictScope; } /** * Tool-entry normalization for a `:conflict://…` target, which is not a * URL the router can route. Returns the bare `conflict://…` URL plus a note for * the model; any other input passes through unchanged without a note. */ export declare function recoverConflictUriPrefix(raw: string): { path: string; note?: 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; /** Result of {@link spliceConflict}: the new file text plus any boundary-echo repair applied. */ export interface ConflictSplice { text: string; /** Replacement lines dropped because they duplicated the context directly above the region. */ trimmedLeading: number; /** Replacement lines dropped because they duplicated the context directly below the region. */ trimmedTrailing: number; } /** * 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. * * Boundary-echo repair (same philosophy as the edit tool's hashline * keeper repair): models frequently paste the "whole resolved function" * including the lines that live directly before/after the marker block, * which the verbatim splice would duplicate. Replacement lines that * exactly echo the adjacent context are dropped when the echo is * unambiguous — two or more consecutive lines, or a single line whose * removal fixes a delimiter-balance mismatch against the recorded sides. */ export declare function spliceConflict(originalText: string, entry: ConflictEntry, replacement: string): ConflictSplice; /** * True when two blocks record the same marker-block content (labels and all * sides), independent of source coordinates. */ export declare function conflictRegionsEqual(a: ConflictBlock, b: ConflictBlock): boolean; /** * 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; /** * 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; /** Line where the underlying file scan hit its byte cap, if it did. */ truncatedAtLine?: number; } 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; truncatedAtLine?: number; }): string;