mergie-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +91 -0
  3. package/bin/mergie-dev +18 -0
  4. package/bin/mergie.ts +44 -0
  5. package/dist/web/assets/index-4bq8mkyV.css +1 -0
  6. package/dist/web/assets/index-Cp_Gdwf2.js +50 -0
  7. package/dist/web/favicon.svg +12 -0
  8. package/dist/web/index.html +14 -0
  9. package/package.json +68 -0
  10. package/src/cli/args.ts +63 -0
  11. package/src/cli/constants.ts +32 -0
  12. package/src/cli/daemonClient.ts +42 -0
  13. package/src/cli/openBrowser.ts +11 -0
  14. package/src/cli/run.ts +76 -0
  15. package/src/daemon/aiReviewTracker.ts +71 -0
  16. package/src/daemon/allComments.ts +119 -0
  17. package/src/daemon/artifactCapture.ts +47 -0
  18. package/src/daemon/buildUi.ts +54 -0
  19. package/src/daemon/chatPrompt.ts +35 -0
  20. package/src/daemon/chatSocket.ts +78 -0
  21. package/src/daemon/createRegistry.ts +569 -0
  22. package/src/daemon/githubThreads.ts +92 -0
  23. package/src/daemon/inflight.ts +49 -0
  24. package/src/daemon/postMapping.ts +94 -0
  25. package/src/daemon/registry.ts +263 -0
  26. package/src/daemon/reviewPrompt.ts +22 -0
  27. package/src/daemon/reviewService.ts +243 -0
  28. package/src/daemon/router.ts +287 -0
  29. package/src/daemon/server.ts +106 -0
  30. package/src/daemon/splitView.ts +63 -0
  31. package/src/daemon/trpc.ts +20 -0
  32. package/src/db/migrate.ts +24 -0
  33. package/src/db/repositories/aiReviews.ts +113 -0
  34. package/src/db/repositories/artifacts.ts +101 -0
  35. package/src/db/repositories/chatSessions.ts +197 -0
  36. package/src/db/repositories/comments.ts +195 -0
  37. package/src/db/repositories/githubComments.ts +166 -0
  38. package/src/db/repositories/hunkViews.ts +36 -0
  39. package/src/db/repositories/reviewedRanges.ts +75 -0
  40. package/src/db/schema.ts +97 -0
  41. package/src/domain/config.ts +137 -0
  42. package/src/domain/context.ts +32 -0
  43. package/src/domain/diff.ts +178 -0
  44. package/src/domain/entities.ts +92 -0
  45. package/src/domain/fuzzy.ts +43 -0
  46. package/src/domain/generated.ts +33 -0
  47. package/src/domain/hash.ts +0 -0
  48. package/src/domain/lockfiles.ts +20 -0
  49. package/src/domain/paths.ts +59 -0
  50. package/src/domain/ranges.ts +81 -0
  51. package/src/domain/references.ts +36 -0
  52. package/src/domain/url.ts +50 -0
  53. package/src/domain/wordDiff.ts +174 -0
  54. package/src/services/ai.ts +137 -0
  55. package/src/services/exec.ts +44 -0
  56. package/src/services/ghPr.ts +102 -0
  57. package/src/services/ghSearch.ts +135 -0
  58. package/src/services/git.ts +297 -0
  59. package/src/services/github.ts +300 -0
  60. package/src/services/symbols.ts +364 -0
  61. package/src/web/App.tsx +32 -0
  62. package/src/web/components/AiReviewIndicator.tsx +63 -0
  63. package/src/web/components/AiReviewModal.tsx +101 -0
  64. package/src/web/components/AiReviewsPanel.tsx +64 -0
  65. package/src/web/components/ChatPanel.tsx +142 -0
  66. package/src/web/components/CodePreview.tsx +63 -0
  67. package/src/web/components/CommentComposer.tsx +40 -0
  68. package/src/web/components/CommentItem.tsx +59 -0
  69. package/src/web/components/CommentsPanel.tsx +309 -0
  70. package/src/web/components/CommitRail.tsx +64 -0
  71. package/src/web/components/ConfirmButton.tsx +32 -0
  72. package/src/web/components/CopyButton.tsx +33 -0
  73. package/src/web/components/CopyIconButton.tsx +38 -0
  74. package/src/web/components/DiffFrame.tsx +133 -0
  75. package/src/web/components/DiffLines.tsx +149 -0
  76. package/src/web/components/FileFrame.tsx +45 -0
  77. package/src/web/components/FileNavigator.tsx +195 -0
  78. package/src/web/components/FileTree.tsx +45 -0
  79. package/src/web/components/FileView.tsx +53 -0
  80. package/src/web/components/GithubThreadView.tsx +56 -0
  81. package/src/web/components/HunkCard.tsx +121 -0
  82. package/src/web/components/Icons.tsx +215 -0
  83. package/src/web/components/IdentifierMenuPortal.tsx +33 -0
  84. package/src/web/components/PostMenu.tsx +63 -0
  85. package/src/web/components/PrDescription.tsx +29 -0
  86. package/src/web/components/PrLoading.tsx +45 -0
  87. package/src/web/components/PrPicker.tsx +201 -0
  88. package/src/web/components/RangeSelector.tsx +141 -0
  89. package/src/web/components/ResultsList.tsx +159 -0
  90. package/src/web/components/ReviewView.tsx +308 -0
  91. package/src/web/components/RightRail.tsx +146 -0
  92. package/src/web/components/SearchRailPanel.tsx +127 -0
  93. package/src/web/components/Switch.tsx +31 -0
  94. package/src/web/components/SwitchPrModal.tsx +28 -0
  95. package/src/web/components/SymbolLookupMenu.tsx +35 -0
  96. package/src/web/components/Toolbar.tsx +79 -0
  97. package/src/web/components/Tooltip.tsx +72 -0
  98. package/src/web/index.html +13 -0
  99. package/src/web/lib/aiReviewIndicator.ts +29 -0
  100. package/src/web/lib/anchorRow.ts +25 -0
  101. package/src/web/lib/codeSearchFetch.ts +46 -0
  102. package/src/web/lib/commentFilters.ts +36 -0
  103. package/src/web/lib/commentVisibility.ts +90 -0
  104. package/src/web/lib/composerKeys.ts +24 -0
  105. package/src/web/lib/dedupeResults.ts +37 -0
  106. package/src/web/lib/diffMarks.ts +81 -0
  107. package/src/web/lib/fileStatus.ts +12 -0
  108. package/src/web/lib/filterCodeResults.ts +42 -0
  109. package/src/web/lib/filterPrs.ts +38 -0
  110. package/src/web/lib/highlight.ts +40 -0
  111. package/src/web/lib/identifierMenu.ts +41 -0
  112. package/src/web/lib/lineSelection.ts +48 -0
  113. package/src/web/lib/navRouting.ts +40 -0
  114. package/src/web/lib/navStack.ts +109 -0
  115. package/src/web/lib/persistedFlag.ts +43 -0
  116. package/src/web/lib/prPickerModel.ts +27 -0
  117. package/src/web/lib/railState.ts +19 -0
  118. package/src/web/lib/rangeCoverage.ts +27 -0
  119. package/src/web/lib/rangeMap.ts +42 -0
  120. package/src/web/lib/resultCountLabel.ts +11 -0
  121. package/src/web/lib/reviewProgress.ts +26 -0
  122. package/src/web/lib/searchInputsKey.ts +35 -0
  123. package/src/web/lib/searchToken.ts +25 -0
  124. package/src/web/lib/splitSide.ts +13 -0
  125. package/src/web/lib/time.ts +9 -0
  126. package/src/web/lib/togglePrefs.ts +81 -0
  127. package/src/web/lib/useEscToClose.ts +17 -0
  128. package/src/web/lib/useIdentifierMenu.ts +77 -0
  129. package/src/web/lib/useNavLookup.ts +74 -0
  130. package/src/web/lib/usePageTitle.ts +15 -0
  131. package/src/web/lib/visibleFiles.ts +52 -0
  132. package/src/web/main.tsx +24 -0
  133. package/src/web/public/favicon.svg +12 -0
  134. package/src/web/state/useChat.ts +181 -0
  135. package/src/web/state/useCodeSearch.ts +198 -0
  136. package/src/web/state/useReview.ts +138 -0
  137. package/src/web/styles.css +1020 -0
  138. package/src/web/trpc.ts +5 -0
  139. package/tsconfig.json +27 -0
  140. package/vite.config.ts +29 -0
@@ -0,0 +1,92 @@
1
+ import type { DiffSide } from "@/domain/hash.ts";
2
+
3
+ /** A cached GitHub comment row enriched with its computed html URL. */
4
+ export interface GithubThreadRow {
5
+ /** GitHub's stable comment id. */
6
+ githubId: string;
7
+ /** File path the comment is on (null if GitHub omitted it). */
8
+ path: string | null;
9
+ /** Raw side string from GitHub (`'LEFT'`/`'RIGHT'`). */
10
+ side: string | null;
11
+ /** Line number the comment anchors to (side-relative, 1-based). */
12
+ line: number | null;
13
+ /** Markdown body. */
14
+ body: string;
15
+ /** Author login (null if unknown). */
16
+ author: string | null;
17
+ /** Creation time as a Unix ms timestamp (null if unknown). */
18
+ createdAt: number | null;
19
+ /** Parent comment id if this is a reply; null for roots. */
20
+ inReplyTo: string | null;
21
+ /** Full URL to the comment on GitHub. */
22
+ htmlUrl: string;
23
+ }
24
+
25
+ /** A single comment as presented within a thread. */
26
+ export interface ThreadComment {
27
+ /** GitHub comment id. */
28
+ githubId: string;
29
+ /** Author login (empty string if unknown). */
30
+ author: string;
31
+ /** Markdown body. */
32
+ body: string;
33
+ /** Creation time (Unix ms) or null. */
34
+ createdAt: number | null;
35
+ /** Link to the comment on GitHub. */
36
+ htmlUrl: string;
37
+ }
38
+
39
+ /** A GitHub inline comment thread: a root comment plus ordered replies. */
40
+ export interface GithubThread {
41
+ /** File the thread is on. */
42
+ path: string | null;
43
+ /** Diff side the thread anchors to. */
44
+ side: DiffSide;
45
+ /** Side-relative line number the thread anchors to. */
46
+ line: number | null;
47
+ /** The root (non-reply) comment. */
48
+ root: ThreadComment;
49
+ /** Replies, oldest-first. */
50
+ replies: ThreadComment[];
51
+ }
52
+
53
+ /** Normalise GitHub's side string to the typed union (defaulting to RIGHT). */
54
+ function toSide(side: string | null): DiffSide {
55
+ return side === "LEFT" ? "LEFT" : "RIGHT";
56
+ }
57
+
58
+ /** Project a row to its thread-comment view. */
59
+ function toComment(r: GithubThreadRow): ThreadComment {
60
+ return { githubId: r.githubId, author: r.author ?? "", body: r.body, createdAt: r.createdAt, htmlUrl: r.htmlUrl };
61
+ }
62
+
63
+ /**
64
+ * Group flat GitHub inline comments into threads: each root (no `inReplyTo`)
65
+ * gets its replies attached oldest-first. Replies whose parent is missing from
66
+ * the set become their own single-comment threads so nothing is dropped.
67
+ */
68
+ export function groupGithubThreads(rows: GithubThreadRow[]): GithubThread[] {
69
+ const byId = new Map<string, GithubThreadRow>(rows.map((r) => [r.githubId, r]));
70
+ const replies = new Map<string, GithubThreadRow[]>();
71
+ const roots: GithubThreadRow[] = [];
72
+
73
+ for (const r of rows) {
74
+ if (r.inReplyTo !== null && byId.has(r.inReplyTo)) {
75
+ const bucket = replies.get(r.inReplyTo) ?? [];
76
+ bucket.push(r);
77
+ replies.set(r.inReplyTo, bucket);
78
+ } else {
79
+ roots.push(r);
80
+ }
81
+ }
82
+
83
+ return roots.map((root) => ({
84
+ path: root.path,
85
+ side: toSide(root.side),
86
+ line: root.line,
87
+ root: toComment(root),
88
+ replies: (replies.get(root.githubId) ?? [])
89
+ .sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0))
90
+ .map(toComment),
91
+ }));
92
+ }
@@ -0,0 +1,49 @@
1
+ /** Tracks in-flight long-running operations so shutdown can drain them. */
2
+ export interface Inflight {
3
+ /** Mark an operation started; returns an idempotent `done` callback. */
4
+ begin(): () => void;
5
+ /** Number of currently-active operations. */
6
+ active(): number;
7
+ /**
8
+ * Resolve `true` once no operations are in flight, or `false` if `timeoutMs`
9
+ * elapses first. Resolves `true` immediately when already idle.
10
+ */
11
+ idle(timeoutMs: number): Promise<boolean>;
12
+ }
13
+
14
+ /** Create an {@link Inflight} tracker. */
15
+ export function createInflight(): Inflight {
16
+ let count = 0;
17
+ let waiters: Array<() => void> = [];
18
+
19
+ const settleIfIdle = (): void => {
20
+ if (count === 0) {
21
+ const pending = waiters;
22
+ waiters = [];
23
+ for (const w of pending) w();
24
+ }
25
+ };
26
+
27
+ return {
28
+ begin() {
29
+ count += 1;
30
+ let called = false;
31
+ return () => {
32
+ if (called) return;
33
+ called = true;
34
+ count = Math.max(0, count - 1);
35
+ settleIfIdle();
36
+ };
37
+ },
38
+ active: () => count,
39
+ idle(timeoutMs: number) {
40
+ if (count === 0) return Promise.resolve(true);
41
+ return new Promise<boolean>((resolve) => {
42
+ let done = false;
43
+ const finish = (v: boolean): void => { if (!done) { done = true; resolve(v); } };
44
+ waiters.push(() => finish(true));
45
+ setTimeout(() => finish(false), timeoutMs);
46
+ });
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,94 @@
1
+ import { commentAnchorHash, type DiffSide } from "@/domain/hash.ts";
2
+ import type { DiffLine } from "@/domain/diff.ts";
3
+ import type { PostCommentInput } from "@/services/github.ts";
4
+
5
+ /** A resolved line-number span on one diff side (both bounds inclusive). */
6
+ export interface LineSpan {
7
+ /** First (smallest) 1-based line number on the side. */
8
+ startNo: number;
9
+ /** Last 1-based line number on the side. */
10
+ endNo: number;
11
+ }
12
+
13
+ /** Indices of lines that participate on a given diff side, in reading order. */
14
+ function sideIndices(lines: DiffLine[], side: DiffSide): number[] {
15
+ const out: number[] = [];
16
+ lines.forEach((line, i) => {
17
+ const onSide: boolean = side === "RIGHT"
18
+ ? line.kind === "add" || line.kind === "ctx"
19
+ : line.kind === "del" || line.kind === "ctx";
20
+ if (onSide) out.push(i);
21
+ });
22
+ return out;
23
+ }
24
+
25
+ /** The side line number of a line (new-side for RIGHT, old-side for LEFT). */
26
+ function lineNo(line: DiffLine, side: DiffSide): number | undefined {
27
+ return side === "RIGHT" ? line.newNo : line.oldNo;
28
+ }
29
+
30
+ /**
31
+ * Locate a contiguous window of `span` same-side lines whose joined text hashes
32
+ * to `anchorHash`, and return its first/last side line numbers — or null when
33
+ * the block is not present on that side.
34
+ */
35
+ export function locateLineComment(
36
+ path: string,
37
+ lines: DiffLine[],
38
+ side: DiffSide,
39
+ span: number,
40
+ anchorHash: string,
41
+ ): LineSpan | null {
42
+ const idx: number[] = sideIndices(lines, side);
43
+ for (let s = 0; s + span <= idx.length; s++) {
44
+ const window: number[] = idx.slice(s, s + span);
45
+ const text: string = window.map((i) => lines[i]?.text ?? "").join("\n");
46
+ if (commentAnchorHash(path, side, text) !== anchorHash) continue;
47
+ const startNo = lineNo(lines[window[0] ?? 0]!, side);
48
+ const endNo = lineNo(lines[window[window.length - 1] ?? 0]!, side);
49
+ if (startNo === undefined || endNo === undefined) return null;
50
+ return { startNo, endNo };
51
+ }
52
+ return null;
53
+ }
54
+
55
+ /**
56
+ * The side line-number span covering a hunk's changed lines (additions for
57
+ * RIGHT, deletions for LEFT). Null when the hunk changes nothing on that side.
58
+ */
59
+ export function hunkChangedSpan(lines: DiffLine[], side: DiffSide): LineSpan | null {
60
+ const changedKind: DiffLine["kind"] = side === "RIGHT" ? "add" : "del";
61
+ const nums: number[] = lines
62
+ .filter((l) => l.kind === changedKind)
63
+ .map((l) => lineNo(l, side))
64
+ .filter((n): n is number => n !== undefined);
65
+ if (nums.length === 0) return null;
66
+ return { startNo: Math.min(...nums), endNo: Math.max(...nums) };
67
+ }
68
+
69
+ /** Fields needed to build a GitHub inline-comment post request. */
70
+ export interface ToPostInputArgs extends LineSpan {
71
+ /** Repo-relative file path. */
72
+ path: string;
73
+ /** Diff side to post on. */
74
+ side: DiffSide;
75
+ /** Markdown body. */
76
+ body: string;
77
+ /** Commit SHA to anchor the comment to. */
78
+ commitId: string;
79
+ }
80
+
81
+ /**
82
+ * Build a {@link PostCommentInput}: a single-line comment when start and end
83
+ * coincide, otherwise a multi-line span from `startNo` to `endNo`.
84
+ */
85
+ export function toPostInput(args: ToPostInputArgs): PostCommentInput {
86
+ const base: PostCommentInput = {
87
+ body: args.body,
88
+ commitId: args.commitId,
89
+ path: args.path,
90
+ side: args.side,
91
+ line: args.endNo,
92
+ };
93
+ return args.startNo < args.endNo ? { ...base, startLine: args.startNo } : base;
94
+ }
@@ -0,0 +1,263 @@
1
+ import type { CommitInfo } from "@/services/git.ts";
2
+ import type { CodeResult } from "@/services/symbols.ts";
3
+ import type { ChatMessageRow, ChatScopeKind, ChatSessionRow } from "@/db/repositories/chatSessions.ts";
4
+ import type { ArtifactRow } from "@/db/repositories/artifacts.ts";
5
+ import type { AiReviewRow } from "@/db/repositories/aiReviews.ts";
6
+ import type { AiReviewStatus } from "./aiReviewTracker.ts";
7
+ import type { ModelChoice, ReviewTemplate } from "@/domain/config.ts";
8
+
9
+ /** Options for running an AI review of a commit range. */
10
+ export interface AiReviewOptions {
11
+ /** Model id to run. */
12
+ model: string;
13
+ /** Optional config template id to base the review on. */
14
+ templateId?: string;
15
+ /** Optional user prompt to focus the review. */
16
+ prompt?: string;
17
+ }
18
+
19
+ /** A commit range an AI turn / artifact is linked to. */
20
+ export interface ChatRange {
21
+ /** Range baseline SHA. */
22
+ start: string;
23
+ /** Range end SHA. */
24
+ end: string;
25
+ }
26
+
27
+ /**
28
+ * A live event streamed during a chat turn:
29
+ * - `delta`: an incremental chunk of the assistant's reply text.
30
+ * - `activity`: a human-readable note that the agent is using a tool.
31
+ */
32
+ export interface LiveChatEvent {
33
+ /** Which kind of live event this is. */
34
+ kind: "delta" | "activity";
35
+ /** The delta text, or the activity description. */
36
+ text: string;
37
+ }
38
+ import type { Range } from "@/domain/ranges.ts";
39
+ import type { ReviewedRangeRow } from "@/db/repositories/reviewedRanges.ts";
40
+ import type { CommentRow, CommentSide, CommentKind } from "@/db/repositories/comments.ts";
41
+ import type { AllCommentEntry } from "./allComments.ts";
42
+ import type { FileView } from "./reviewService.ts";
43
+ import type { SplitRow } from "./splitView.ts";
44
+
45
+ /** Input to create a new comment (server derives the anchor hash). */
46
+ export interface NewComment {
47
+ /** Whether the comment targets a line or the whole hunk. */
48
+ kind: CommentKind;
49
+ /** Which diff side it applies to. */
50
+ side: CommentSide;
51
+ /** Repo-relative file path. */
52
+ path: string;
53
+ /** Markdown body. */
54
+ body: string;
55
+ /** The end commit of the range the comment was made in. */
56
+ madeAtSha: string;
57
+ /** For `lines`: the exact text of the commented line. */
58
+ lineText?: string;
59
+ /** For `lines`: the first line number on the comment's side. */
60
+ lineNo?: number;
61
+ /** For `lines`: the last line number on the comment's side (defaults to lineNo). */
62
+ endLineNo?: number;
63
+ /** For `hunk`: the hunk's content hash. */
64
+ hunkHash?: string;
65
+ }
66
+
67
+ /**
68
+ * Which commit a comment should be anchored to when posting to GitHub.
69
+ *
70
+ * - `'end'` — the range's end commit (pins to exactly what was reviewed).
71
+ * - `'head'` — the PR's head commit (the comment stays "live").
72
+ */
73
+ export type PostTarget = "end" | "head";
74
+
75
+ /**
76
+ * The result of resolving where a comment would land if posted to GitHub.
77
+ * Used both to preview (warn before posting) and to drive the actual post.
78
+ */
79
+ export interface PostPreview {
80
+ /** Whether the comment can be posted at the chosen target. */
81
+ canPost: boolean;
82
+ /** The commit SHA the comment would be anchored to. */
83
+ commitId: string;
84
+ /** The diff side the comment would post on. */
85
+ side: CommentSide;
86
+ /** The last (or only) line number, or null when it cannot be resolved. */
87
+ line: number | null;
88
+ /** First line of a multi-line span, or null for a single-line comment. */
89
+ startLine: number | null;
90
+ /** A human-readable reason the comment cannot be posted; null when postable. */
91
+ warning: string | null;
92
+ }
93
+
94
+ /** A PR's commit topology for the range selector. */
95
+ export interface CommitsWithBaseline {
96
+ /** The "before-PR" baseline SHA (merge-base with the target branch). */
97
+ baselineSha: string;
98
+ /** PR commits, oldest → newest. */
99
+ commits: CommitInfo[];
100
+ }
101
+
102
+ /** Per-PR review operations (backed by the PR's clone + database). */
103
+ export interface Workspace {
104
+ /** The public PR summary. */
105
+ pr: LoadedPr;
106
+ /** The selectable models and review templates from config. */
107
+ config(): { models: ModelChoice[]; templates: ReviewTemplate[] };
108
+ /**
109
+ * Re-fetch the PR's metadata from GitHub and its git objects, picking up any
110
+ * new commits / head movement and an updated title/base.
111
+ */
112
+ refresh(): Promise<void>;
113
+ /** PR commits mapped from metadata (no clone required). */
114
+ commits(): CommitInfo[];
115
+ /** Commit topology (baseline + commits) for range selection (clones lazily). */
116
+ commitsWithBaseline(): Promise<CommitsWithBaseline>;
117
+ /** The default range to show on open (last-reviewed → head, else whole PR). */
118
+ defaultRange(): Promise<Range>;
119
+ /**
120
+ * Assemble the file/hunk view for a commit range.
121
+ *
122
+ * @param ignoreWhitespace - When true, collapse whitespace-only changes so a
123
+ * line/hunk that differs only in spacing no longer appears as changed.
124
+ */
125
+ rangeView(startSha: string, endSha: string, ignoreWhitespace?: boolean): Promise<FileView[]>;
126
+ /** Set or clear a hunk's viewed state (by hunk hash). */
127
+ setHunkViewed(hunkHash: string, viewed: boolean): void;
128
+ /** Mark a commit range reviewed. */
129
+ addReviewedRange(startSha: string, endSha: string): void;
130
+ /** Un-mark a reviewed range by its exact (startSha, endSha) pair. */
131
+ removeReviewedRange(startSha: string, endSha: string): void;
132
+ /** List reviewed ranges (oldest → newest). */
133
+ listReviewedRanges(): ReviewedRangeRow[];
134
+ /** Create a comment; returns its id. */
135
+ addComment(input: NewComment): number;
136
+ /** Edit a comment's body; propagates to GitHub if the comment was posted. */
137
+ editComment(id: number, body: string): Promise<void>;
138
+ /** Delete a comment; also deletes it on GitHub if it was posted. */
139
+ deleteComment(id: number): Promise<void>;
140
+ /** All comments for this PR. */
141
+ listComments(): CommentRow[];
142
+ /**
143
+ * Unified list for the "All comments" view: local comments merged with
144
+ * fetched GitHub threads (deduped by GitHub id), classified by origin and
145
+ * authorship. Async because it resolves the viewer's GitHub login.
146
+ */
147
+ listAllComments(): Promise<AllCommentEntry[]>;
148
+ /** Resolve where a comment would post (line + warnings) without posting. */
149
+ postCommentPreview(id: number, target: PostTarget): Promise<PostPreview>;
150
+ /** Post a comment to GitHub; records the GitHub id/url on success. */
151
+ postComment(id: number, target: PostTarget): Promise<{ githubId: string; githubUrl: string }>;
152
+ /** Pull GitHub inline comments into the local cache; returns the count synced. */
153
+ syncGithub(): Promise<number>;
154
+ /** Reply to a GitHub thread (by root comment id) and refresh the cache. */
155
+ replyToThread(rootGithubId: string, body: string): Promise<void>;
156
+ /**
157
+ * Edit a GitHub comment (by GitHub id) that the viewer authored, updating it
158
+ * on GitHub and in the local cache. Refuses comments authored by others.
159
+ */
160
+ editGithubComment(githubId: string, body: string): Promise<void>;
161
+ /**
162
+ * Delete a GitHub comment (by GitHub id) that the viewer authored, on GitHub
163
+ * and from the local cache. Refuses comments authored by others.
164
+ */
165
+ deleteGithubComment(githubId: string): Promise<void>;
166
+ /**
167
+ * Definition of a symbol (via `sem`) at the given commit's checkout.
168
+ * `file` (repo-relative) scopes the lookup; if the scoped call is empty it
169
+ * retries unscoped.
170
+ */
171
+ symbolDefinition(symbol: string, sha: string, file?: string): Promise<CodeResult[]>;
172
+ /**
173
+ * Usages of a symbol (via `sem`) at the given commit's checkout, resolved to
174
+ * the real reference lines within each dependent. `file` scopes as above.
175
+ */
176
+ symbolUsages(symbol: string, sha: string, file?: string): Promise<CodeResult[]>;
177
+ /**
178
+ * Literal (default) or regex word search (via `rg`) at a commit's checkout.
179
+ * @param opts - `caseSensitive` (default false), `regex` (default false).
180
+ */
181
+ symbolSearch(word: string, sha: string, opts?: { caseSensitive?: boolean; regex?: boolean }): Promise<CodeResult[]>;
182
+ /** Full text of a file at a commit (null if absent/binary). */
183
+ fileAt(sha: string, path: string): Promise<string | null>;
184
+ /** Start an AI chat session scoped to a hunk or file; returns its id. */
185
+ createChatSession(scopeKind: ChatScopeKind, scopeRef: string, model: string, title?: string): number;
186
+ /** List chat sessions, optionally filtered to a scope. */
187
+ listChatSessions(scopeKind?: ChatScopeKind, scopeRef?: string): ChatSessionRow[];
188
+ /** List messages for a chat session (oldest first). */
189
+ listChatMessages(sessionId: number): ChatMessageRow[];
190
+ /**
191
+ * Run one agentic chat turn: persist the user prompt, stream live events via
192
+ * `onEvent` (`delta` = incremental reply text; `activity` = a note that the
193
+ * agent is using a tool), persist the full reply, and return it. When a
194
+ * `range` is given, any files the agent writes to the artifacts folder are
195
+ * captured and linked to that commit range.
196
+ */
197
+ streamChat(sessionId: number, prompt: string, onEvent: (ev: LiveChatEvent) => void, range?: ChatRange): Promise<string>;
198
+ /** List generated artifacts, optionally scoped to a commit range. */
199
+ listArtifacts(range?: ChatRange): ArtifactRow[];
200
+ /** Absolute path of an artifact file if it exists within the artifacts dir. */
201
+ resolveArtifact(relPath: string): string | null;
202
+ /** Run an AI review of a commit range; persists and returns the result. */
203
+ runAiReview(range: ChatRange, opts: AiReviewOptions): Promise<AiReviewRow>;
204
+ /** List AI reviews, optionally scoped to a commit range. */
205
+ listAiReviews(range?: ChatRange): AiReviewRow[];
206
+ /**
207
+ * Snapshot of in-flight and recently-completed AI reviews for this PR, keyed
208
+ * by range. Powers the app-level progress indicator; completed entries linger
209
+ * until dismissed.
210
+ */
211
+ aiReviewStatuses(): AiReviewStatus[];
212
+ /** Dismiss a completed (done/failed) AI-review status for a range. */
213
+ dismissAiReviewStatus(range: ChatRange): void;
214
+ /**
215
+ * Full-file side-by-side split rows for a path over a range.
216
+ *
217
+ * @param ignoreWhitespace - When true, collapse whitespace-only changes.
218
+ */
219
+ fileSplit(path: string, startSha: string, endSha: string, ignoreWhitespace?: boolean): Promise<SplitRow[]>;
220
+ }
221
+
222
+ /** A pull request loaded into the daemon. */
223
+ export interface LoadedPr {
224
+ /** Stable id for routing/UI (e.g. `owner_repo_number`). */
225
+ id: string;
226
+ /** The original PR URL. */
227
+ url: string;
228
+ /** Repository owner. */
229
+ owner: string;
230
+ /** Repository name. */
231
+ repo: string;
232
+ /** PR number. */
233
+ number: number;
234
+ /** PR title (from GitHub). */
235
+ title: string;
236
+ /** PR description / body (from GitHub), as GitHub-flavored markdown.
237
+ * Empty string when the PR has no description. Kept current by Refresh PR. */
238
+ body: string;
239
+ /** Target branch the PR merges into. */
240
+ baseRef: string;
241
+ /** Source branch the PR merges from. */
242
+ headRef: string;
243
+ }
244
+
245
+ /** Manages the set of PRs the daemon is serving. */
246
+ export interface PrRegistry {
247
+ /** Load (or attach to an already-loaded) PR by URL. */
248
+ loadPr(url: string): Promise<LoadedPr>;
249
+ /** All currently-loaded PRs. */
250
+ listPrs(): LoadedPr[];
251
+ /** A loaded PR by id, or undefined. */
252
+ getPr(id: string): LoadedPr | undefined;
253
+ /** Commits belonging to a loaded PR, oldest → newest. */
254
+ commits(id: string): Promise<CommitInfo[]>;
255
+ /** The review workspace for a loaded PR, or undefined if not loaded. */
256
+ getWorkspace(id: string): Workspace | undefined;
257
+ /**
258
+ * Wait for in-flight AI operations (chat turns, reviews) to finish before
259
+ * shutdown, up to `timeoutMs`. Resolves `true` if they drained, `false` on
260
+ * timeout.
261
+ */
262
+ drainAi(timeoutMs: number): Promise<boolean>;
263
+ }
@@ -0,0 +1,22 @@
1
+ import type { ChatRange } from "./registry.ts";
2
+
3
+ /**
4
+ * Assemble the prompt for an AI review: the chosen template's prompt (if any),
5
+ * the user's optional focus prompt (if any), and an instruction to inspect the
6
+ * range's diff in the working directory.
7
+ */
8
+ export function buildReviewPrompt(
9
+ templatePrompt: string | null,
10
+ userPrompt: string | null,
11
+ range: ChatRange,
12
+ ): string {
13
+ const parts: string[] = [];
14
+ if (templatePrompt) parts.push(templatePrompt.trim());
15
+ if (userPrompt) parts.push(userPrompt.trim());
16
+ parts.push(
17
+ `Review the changes in this pull request from commit ${range.start} to ${range.end}. ` +
18
+ `Run \`git diff ${range.start} ${range.end}\` in the working directory to see them, ` +
19
+ `explore the code as needed, and respond in markdown.`,
20
+ );
21
+ return parts.join("\n\n");
22
+ }