type ChangeType = 'added' | 'modified' | 'deleted' | 'renamed' | 'copied'; type DiffLineType = 'context' | 'addition' | 'deletion'; interface DiffLine { type: DiffLineType; oldLineNumber: number | null; newLineNumber: number | null; content: string; } interface DiffHunk { header: string; oldStart: number; oldLines: number; newStart: number; newLines: number; lines: DiffLine[]; } interface DiffFile { oldPath: string; newPath: string; changeType: ChangeType; isBinary: boolean; hunks: DiffHunk[]; isUntracked?: boolean; contentLoaded?: boolean; /** Set when a safety budget stopped the loader reading this file; `hunks` is then empty and this is shown instead. */ omittedReason?: string; } type DiffSource = { type: 'git'; gitDiffArgs: string; repository: string; } | { type: 'directory'; sourcePath: string; } | { type: 'file'; sourcePath: string; } | { type: 'welcome'; } | { type: 'loading'; }; type ReviewSourceMode = 'git' | 'directory' | 'file' | 'remote'; /** * One side of a review's comparison, readable again later. A commit's SHA is resolved at load time. * `none` has no content; a read from `unknown` fails visibly rather than falling back to the working tree. */ type ReviewSourceSide = { kind: 'working-tree'; } | { kind: 'index'; } | { kind: 'commit'; sha: string; } | { kind: 'directory'; } | { kind: 'file'; path: string; } | { kind: 'none'; } | { kind: 'unknown'; reason: string; }; /** What a session reviews, recorded once at commit. Reads and path authorization resolve against this, never the cwd. */ interface ReviewSourceIdentity { mode: ReviewSourceMode; /** Physical directory (symlinks resolved) every reviewed path is relative to. */ sourceRoot: string; invocationCwd: string; gitDiffArgv: string[]; /** `/`-separated directory under `sourceRoot` that reviewed paths are relative to; empty except for `--relative` git reviews. */ pathPrefix: string; oldSide: ReviewSourceSide; newSide: ReviewSourceSide; } interface Suggestion { originalCode: string; proposedCode: string; } interface Attachment { id: string; fileName: string; mediaType: string; data?: ArrayBuffer; } interface LineRange { side: 'old' | 'new'; start: number; end: number; } /** * How consequential a finding is if it is real, most to least. * See SeverityEnum in self-review-v3.xsd for the semantics of each value. */ type CommentSeverity = 'critical' | 'major' | 'minor' | 'info'; /** * How sure the author is that a finding is real, most to least. * See ConfidenceEnum in self-review-v3.xsd for the semantics of each value. */ type CommentConfidence = 'high' | 'medium' | 'low'; /** * The forge hosting a remote pull/merge request. * See RemoteForgeEnum in self-review-v3.xsd. */ type RemoteForge = 'github' | 'gitlab'; /** * One turn in the conversation about a comment. * * A reply is deliberately thin. It carries no category, no severity, no * confidence and no suggestion: all four are properties of the finding, and * the finding is the root comment. A counter-proposal goes in `body` as a * fenced code block. * * Ordering is positional. The array order is the document order is the * conversation order, in all three directions. Nothing sorts replies. */ interface Reply { /** * In-memory render key only. Like `ReviewComment.id`, this is regenerated * on every parse and is never written to XML — the tree supplies parent * linkage and document order supplies ordering, so nothing needs naming. */ id: string; body: string; /** Absent means the human reviewer, present means a bot or LLM. */ author?: string; attachments?: Attachment[]; /** * Identifier of the remote comment this reply was fetched from, in the * forge's own id space (see `ReviewState.remoteForge`). Undefined for * replies authored locally. Never an ordering signal: array order stays * conversation order. */ remoteId?: string; } interface ReviewComment { id: string; filePath: string; lineRange: LineRange | null; body: string; category: string; suggestion: Suggestion | null; author?: string; severity?: CommentSeverity; confidence?: CommentConfidence; orphaned?: boolean; attachments?: Attachment[]; /** Ordered conversation turns on this comment. Undefined when there are none. */ replies?: Reply[]; /** * Identifier of the remote discussion thread or comment this comment was * fetched from, in the forge's own id space (see `ReviewState.remoteForge`). * Undefined for comments authored locally. */ remoteId?: string; } interface FileReviewState { path: string; changeType: ChangeType; viewed: boolean; comments: ReviewComment[]; } interface ReviewState { timestamp: string; source: DiffSource; files: FileReviewState[]; /** URL of the remote pull/merge request under review. */ remoteUrl?: string; /** Commit SHA the remote diff was computed from, for drift detection. */ remoteBaseSha?: string; /** Commit SHA of the remote PR/MR head at fetch time, for drift detection. */ remoteHeadSha?: string; /** Which forge hosts the remote PR/MR. Names the id space of remoteId values. */ remoteForge?: RemoteForge; } /** * Provenance and status of a remote PR/MR session, recorded at * materialization time. Rides on `DiffLoadPayload.remote` so the renderer * can display remote context; the main process injects the same provenance * into the submitted `ReviewState` before serialization, so the renderer * never needs to copy these fields back. */ interface RemoteSessionInfo { /** URL of the remote pull/merge request under review. */ remoteUrl: string; /** Resolved SHA of the PR/MR base branch tip. */ remoteBaseSha: string; /** Live SHA of the PR/MR head at materialization time. */ remoteHeadSha: string; /** Which forge hosts the PR/MR. */ remoteForge: RemoteForge; /** * False when the forge CLI was unavailable and discussion threads could * not be fetched; the review still opens fully (diff only). */ threadSyncAvailable: boolean; /** * True when the diff was materialized into a temporary clone the app * removes on exit, false when it reused a clone the user already owns. * * The distinction is a write boundary, not a detail: PRD Section 5.4.8 * bars applying a suggestion inside a temporary clone, so a front end * branches on this to ask for a destination directory first. */ temporaryClone: boolean; } /** * Drift comparison between a resumed document's recorded remote head SHA * and the live head SHA observed at materialization. Sent only when a * `--resume-from` document carries `remote-head-sha` in a remote session. */ interface RemoteDriftInfo { /** `remote-head-sha` recorded in the resumed document. */ recordedHeadSha: string; /** Live head SHA from this session's materialization. */ liveHeadSha: string; /** True when the two SHAs differ (the PR/MR moved since the review). */ drifted: boolean; } /** * Result of the `remote:open-url` channel (Renderer → Main, invoke). * On success the main process pushes `diff:load` (and `resume:load` when * threads were fetched) to the requesting window before resolving. */ type RemoteOpenUrlResult = { ok: true; } | { ok: false; error: string; }; /** * One file inside a guide group. The path must match a diff file * (repository-relative, same convention as review.xml); entries whose path * matches nothing in the diff are silently dropped by the consumer. */ interface GuideFileEntry { /** File path relative to the repository root. For renames, the new path. */ path: string; /** One-line description of the role this file plays in the change. */ description: string; } /** * A named set of related files in the walkthrough. Groups are labels for * orientation, never suppressions: every group and every file in it is * shown. Array position is reading order. */ interface GuideGroup { /** Short display name shown as the group heading, e.g. "Core change". */ name: string; /** One line explaining why these files form a group and what to look for. */ rationale: string; /** The files in this group, in reading order. At least one entry. */ files: GuideFileEntry[]; } /** * A parsed walkthrough guide. Files in the diff that no group mentions are * presented by the consumer in an implicit trailing "Everything else" * group; that group is derived at render time and never part of this state. */ interface ReviewGuide { /** * Review-level orientation prose shown before the first file. Markdown; * may include a fenced code block labelled "mermaid" for a diagram. * Absent when the guide has no overview. */ overview?: string; /** Ordered reading groups. Array position is reading order. */ groups: GuideGroup[]; } /** * One file inside a resolved display group. Unlike {@link GuideFileEntry}, * the description is optional: files swept into the implicit * "Everything else" group have no guide-authored one-liner. */ interface ResolvedGuideFile { /** File path relative to the repository root. For renames, the new path. */ path: string; /** One-line description from the guide; absent for implicit-group files. */ description?: string; } /** * A display group produced by reconciling a {@link ReviewGuide} with the * actual diff file list. Guide entries missing from the diff are dropped, * duplicate references keep only their first group, and diff files the * guide never mentions land in a terminal implicit group (in diff order) * marked with `implicit: true` so the UI can label it. */ interface ResolvedGuideGroup { /** Display name for the group heading. */ name: string; /** One-line rationale from the guide; absent for the implicit group. */ rationale?: string; /** True only for the derived trailing "Everything else" group. */ implicit: boolean; /** The files shown under this group, in display order. Never empty. */ files: ResolvedGuideFile[]; } interface CategoryDef { name: string; description: string; color: string; } interface AppConfig { theme: 'light' | 'dark' | 'system'; diffView: 'split' | 'unified'; fontSize: number; outputFormat: string; outputFile: string; ignore: string[]; categories: CategoryDef[]; defaultDiffArgs: string; showUntracked: boolean; showUntrackedExplicit: boolean; wordWrap: boolean; maxFiles: number; maxTotalLines: number; /** * Path to the walkthrough guide sidecar (`guide-file` YAML key). When * unset, the guide path is derived from the resolved output path as * `.guide.xml`. */ guideFile?: string; } interface DiffLoadPayload { files: DiffFile[]; source: DiffSource; isLargePayload?: boolean; /** What the loader could not show faithfully, one message each. Diagnostics with no files is a failed load, not "no changes". */ diagnostics?: string[]; /** * Present only for a remote PR/MR session. After materialization, remote * mode is git mode: `source` stays `type: 'git'` with `repository` set to * the materialized clone's root, and this field carries the remote * provenance and thread-sync status alongside it. */ remote?: RemoteSessionInfo; } interface ResumeLoadPayload { /** * Comments to restore. In a remote session this includes fetched forge * threads mapped to `ReviewComment`s; review-level threads (no file * anchor) keep the mapper's sentinel `filePath: ''` * (REVIEW_LEVEL_FILE_PATH). The renderer surfaces those under a synthetic * review-level entry and, on submit, keeps them in a `FileReviewState` * with `path: ''` so the serializer emits a legal ``. */ comments: ReviewComment[]; /** Paths the prior review marked as done. Absent when nothing was marked. */ viewedFiles?: string[]; /** * Remote head drift, present only when a resumed document recorded a * `remote-head-sha` in a remote session. See {@link RemoteDriftInfo}. */ remoteDrift?: RemoteDriftInfo; /** One line per resumed comment downgraded to file-level feedback (unusable anchor or unappliable suggestion). */ importDiagnostics?: string[]; } /** * Payload for the `guide:load` channel (Main → Renderer). Sent only when a * valid walkthrough guide sidecar was discovered; carries display-ready * data — the overview plus groups already reconciled against the parsed * diff — so the renderer stays free of tolerance logic. Metadata only * (paths, names, descriptions): safe to send in large-payload mode. */ interface GuideLoadPayload { /** Review-level orientation prose (Markdown, optionally Mermaid). */ overview?: string; /** Resolved display groups, in reading order. */ groups: ResolvedGuideGroup[]; } interface OutputPathInfo { resolvedOutputPath: string; outputPathWritable: boolean; } interface ExpandContextRequest { filePath: string; contextLines: number; } interface ExpandContextResponse { hunks: DiffHunk[]; totalLines: number; } interface FindInPageRequest { text: string; forward: boolean; findNext: boolean; } interface FindInPageResult { activeMatchOrdinal: number; matches: number; finalUpdate: boolean; } interface VersionUpdateInfo { latestVersion: string; releaseUrl: string; } type ImageLoadResult = { dataUri: string; } | { error: string; }; interface PayloadStats { fileCount: number; totalLines: number; exceedsFiles: boolean; exceedsLines: boolean; exceedsAny: boolean; } /** One apply attempt, as the UI knows it. The host names the destination. */ interface SuggestionApplyRequest { /** Path of the file to modify, relative to the review's source root. */ filePath: string; /** The comment's anchor. `null` (file-level) is always refused. */ lineRange: LineRange | null; suggestion: Suggestion; } interface SuggestionApplyApplied { status: 'applied'; /** Echoed from the request, so a caller can attribute the result. */ filePath: string; /** How many on-disk lines the proposal replaced. */ replacedLines: number; } interface SuggestionApplyRefused { status: 'refused'; filePath: string; /** Stable machine-readable reason, for logs and tests. Never file content. */ reason: string; /** One sentence naming the refusal, safe to show to the reviewer. */ detail: string; } type SuggestionApplyOutcome = SuggestionApplyApplied | SuggestionApplyRefused; /** * The destination directory an apply writes into was named by the user. * Only a temporary-clone review needs one; every other review already has * a destination the user controls. */ interface ApplyDestinationChosen { status: 'chosen'; /** Absolute directory every later apply in this session resolves against. */ destinationRoot: string; } /** The user dismissed the picker. Nothing changed. */ interface ApplyDestinationCancelled { status: 'cancelled'; } /** The picked directory cannot be written into. Nothing changed. */ interface ApplyDestinationRejected { status: 'rejected'; /** Stable machine-readable reason, for logs and tests. Never a file path. */ reason: string; /** One sentence naming the rejection, safe to show to the reviewer. */ detail: string; } type ApplyDestinationOutcome = ApplyDestinationChosen | ApplyDestinationCancelled | ApplyDestinationRejected; export type { AppConfig, ApplyDestinationCancelled, ApplyDestinationChosen, ApplyDestinationOutcome, ApplyDestinationRejected, Attachment, CategoryDef, ChangeType, CommentConfidence, CommentSeverity, DiffFile, DiffHunk, DiffLine, DiffLineType, DiffLoadPayload, DiffSource, ExpandContextRequest, ExpandContextResponse, FileReviewState, FindInPageRequest, FindInPageResult, GuideFileEntry, GuideGroup, GuideLoadPayload, ImageLoadResult, LineRange, OutputPathInfo, PayloadStats, RemoteDriftInfo, RemoteForge, RemoteOpenUrlResult, RemoteSessionInfo, Reply, ResolvedGuideFile, ResolvedGuideGroup, ResumeLoadPayload, ReviewComment, ReviewGuide, ReviewSourceIdentity, ReviewSourceMode, ReviewSourceSide, ReviewState, Suggestion, SuggestionApplyApplied, SuggestionApplyOutcome, SuggestionApplyRefused, SuggestionApplyRequest, VersionUpdateInfo };