newpr 0.1.3 → 0.3.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.
- package/package.json +11 -1
- package/src/analyzer/pipeline.ts +37 -15
- package/src/analyzer/progress.ts +2 -0
- package/src/cli/index.ts +7 -2
- package/src/cli/preflight.ts +126 -0
- package/src/github/fetch-pr.ts +53 -1
- package/src/history/store.ts +107 -1
- package/src/history/types.ts +1 -0
- package/src/llm/client.ts +197 -0
- package/src/llm/prompts.ts +80 -19
- package/src/llm/response-parser.ts +13 -1
- package/src/tui/Shell.tsx +7 -2
- package/src/types/github.ts +14 -0
- package/src/types/output.ts +50 -0
- package/src/web/client/App.tsx +33 -5
- package/src/web/client/components/AppShell.tsx +107 -47
- package/src/web/client/components/ChatSection.tsx +427 -0
- package/src/web/client/components/DetailPane.tsx +217 -77
- package/src/web/client/components/DiffViewer.tsx +713 -0
- package/src/web/client/components/InputScreen.tsx +178 -27
- package/src/web/client/components/LoadingTimeline.tsx +19 -6
- package/src/web/client/components/Markdown.tsx +220 -41
- package/src/web/client/components/ResultsScreen.tsx +109 -73
- package/src/web/client/components/ReviewModal.tsx +187 -0
- package/src/web/client/components/SettingsPanel.tsx +62 -86
- package/src/web/client/components/TipTapEditor.tsx +405 -0
- package/src/web/client/hooks/useAnalysis.ts +8 -1
- package/src/web/client/lib/shiki.ts +63 -0
- package/src/web/client/panels/CartoonPanel.tsx +94 -37
- package/src/web/client/panels/DiscussionPanel.tsx +158 -0
- package/src/web/client/panels/FilesPanel.tsx +435 -54
- package/src/web/client/panels/GroupsPanel.tsx +62 -40
- package/src/web/client/panels/StoryPanel.tsx +43 -23
- package/src/web/components/ui/tabs.tsx +3 -3
- package/src/web/server/routes.ts +856 -14
- package/src/web/server/session-manager.ts +11 -2
- package/src/web/server.ts +66 -4
- package/src/web/styles/built.css +1 -1
- package/src/web/styles/globals.css +117 -1
- package/src/workspace/agent.ts +22 -6
- package/src/workspace/explore.ts +41 -16
- package/src/web/client/panels/NarrativePanel.tsx +0 -9
- package/src/web/client/panels/SummaryPanel.tsx +0 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newpr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AI-powered large PR review tool - understand PRs with 1000+ lines of changes",
|
|
5
5
|
"module": "src/cli/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -61,16 +61,26 @@
|
|
|
61
61
|
"@radix-ui/react-slot": "^1.2.4",
|
|
62
62
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
63
63
|
"@tailwindcss/cli": "^4.1.18",
|
|
64
|
+
"@tiptap/extension-mention": "^3.19.0",
|
|
65
|
+
"@tiptap/extension-placeholder": "^3.19.0",
|
|
66
|
+
"@tiptap/pm": "^3.19.0",
|
|
67
|
+
"@tiptap/react": "^3.19.0",
|
|
68
|
+
"@tiptap/starter-kit": "^3.19.0",
|
|
64
69
|
"class-variance-authority": "^0.7.1",
|
|
65
70
|
"clsx": "^2.1.1",
|
|
66
71
|
"ink": "6.6.0",
|
|
67
72
|
"ink-spinner": "5.0.0",
|
|
68
73
|
"ink-text-input": "6.0.0",
|
|
74
|
+
"katex": "^0.16.28",
|
|
69
75
|
"lucide-react": "^0.567.0",
|
|
70
76
|
"react": "19.1.0",
|
|
71
77
|
"react-dom": "19.1.0",
|
|
72
78
|
"react-markdown": "^10.1.0",
|
|
79
|
+
"rehype-katex": "^7.0.1",
|
|
80
|
+
"rehype-raw": "^7.0.0",
|
|
73
81
|
"remark-gfm": "^4.0.1",
|
|
82
|
+
"remark-math": "^6.0.0",
|
|
83
|
+
"shiki": "^3.22.0",
|
|
74
84
|
"tailwind-merge": "^3.4.1",
|
|
75
85
|
"tailwindcss": "^4.1.18",
|
|
76
86
|
"tailwindcss-animate": "^1.0.7"
|
package/src/analyzer/pipeline.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { ExplorationResult } from "../workspace/types.ts";
|
|
|
6
6
|
import type { AgentToolName } from "../workspace/types.ts";
|
|
7
7
|
import { parseDiff } from "../diff/parser.ts";
|
|
8
8
|
import { chunkDiff } from "../diff/chunker.ts";
|
|
9
|
-
import { fetchPrData } from "../github/fetch-pr.ts";
|
|
9
|
+
import { fetchPrData, fetchPrComments } from "../github/fetch-pr.ts";
|
|
10
10
|
import { fetchPrDiff } from "../github/fetch-diff.ts";
|
|
11
11
|
import { createLlmClient, type LlmClient, type LlmResponse } from "../llm/client.ts";
|
|
12
12
|
import {
|
|
@@ -50,6 +50,7 @@ interface PipelineOptions {
|
|
|
50
50
|
token: string;
|
|
51
51
|
config: NewprConfig;
|
|
52
52
|
onProgress?: ProgressCallback;
|
|
53
|
+
onFilePatches?: (patches: Record<string, string>) => void;
|
|
53
54
|
noClone?: boolean;
|
|
54
55
|
preferredAgent?: AgentToolName;
|
|
55
56
|
}
|
|
@@ -84,25 +85,25 @@ async function runExploration(
|
|
|
84
85
|
): Promise<ExplorationResult> {
|
|
85
86
|
const agent = await requireAgent(preferredAgent);
|
|
86
87
|
|
|
87
|
-
onProgress?.({ stage: "cloning", message:
|
|
88
|
+
onProgress?.({ stage: "cloning", message: `Cloning ${pr.owner}/${pr.repo}...` });
|
|
88
89
|
const bareRepoPath = await ensureRepo(pr.owner, pr.repo, token, (msg) => {
|
|
89
|
-
onProgress?.({ stage: "cloning", message: msg });
|
|
90
|
+
onProgress?.({ stage: "cloning", message: `📦 ${msg}` });
|
|
90
91
|
});
|
|
91
|
-
onProgress?.({ stage: "cloning", message:
|
|
92
|
+
onProgress?.({ stage: "cloning", message: `📦 ${pr.owner}/${pr.repo} cached` });
|
|
92
93
|
|
|
93
|
-
onProgress?.({ stage: "checkout", message:
|
|
94
|
+
onProgress?.({ stage: "checkout", message: `🌿 Preparing worktrees: ${baseBranch} ← PR #${pr.number}` });
|
|
94
95
|
const worktrees = await createWorktrees(
|
|
95
96
|
bareRepoPath, baseBranch, pr.number, pr.owner, pr.repo,
|
|
96
|
-
(msg) => onProgress?.({ stage: "checkout", message: msg }),
|
|
97
|
+
(msg) => onProgress?.({ stage: "checkout", message: `🌿 ${msg}` }),
|
|
97
98
|
);
|
|
98
|
-
onProgress?.({ stage: "checkout", message:
|
|
99
|
+
onProgress?.({ stage: "checkout", message: `🌿 Worktrees ready: ${baseBranch} ← PR #${pr.number}` });
|
|
99
100
|
|
|
100
|
-
onProgress?.({ stage: "exploring", message:
|
|
101
|
+
onProgress?.({ stage: "exploring", message: `🤖 ${agent.name}: exploring ${changedFiles.length} changed files...` });
|
|
101
102
|
const exploration = await exploreCodebase(
|
|
102
103
|
agent, worktrees.headPath, changedFiles, prTitle, rawDiff,
|
|
103
104
|
(msg, current, total) => onProgress?.({ stage: "exploring", message: msg, current, total }),
|
|
104
105
|
);
|
|
105
|
-
onProgress?.({ stage: "exploring", message:
|
|
106
|
+
onProgress?.({ stage: "exploring", message: `🤖 ${agent.name}: exploration complete` });
|
|
106
107
|
|
|
107
108
|
await cleanupWorktrees(bareRepoPath, pr.number, pr.owner, pr.repo).catch(() => {});
|
|
108
109
|
|
|
@@ -158,12 +159,13 @@ export async function analyzePr(options: PipelineOptions): Promise<NewprOutput>
|
|
|
158
159
|
timeout: config.timeout,
|
|
159
160
|
});
|
|
160
161
|
|
|
161
|
-
progress({ stage: "fetching", message: "Fetching PR data and
|
|
162
|
-
const [prData, rawDiff] = await Promise.all([
|
|
162
|
+
progress({ stage: "fetching", message: "Fetching PR data, diff, and discussion..." });
|
|
163
|
+
const [prData, rawDiff, prComments] = await Promise.all([
|
|
163
164
|
fetchPrData(pr, token),
|
|
164
165
|
fetchPrDiff(pr, token),
|
|
166
|
+
fetchPrComments(pr, token).catch(() => []),
|
|
165
167
|
]);
|
|
166
|
-
progress({ stage: "fetching", message: `#${prData.number} "${prData.title}" by ${prData.author} · +${prData.additions} −${prData.deletions}
|
|
168
|
+
progress({ stage: "fetching", message: `#${prData.number} "${prData.title}" by ${prData.author} · +${prData.additions} −${prData.deletions} · ${prComments.length} comments`, pr_title: prData.title, pr_number: prData.number });
|
|
167
169
|
|
|
168
170
|
progress({ stage: "parsing", message: "Parsing diff..." });
|
|
169
171
|
const parsed = parseDiff(rawDiff);
|
|
@@ -175,6 +177,15 @@ export async function analyzePr(options: PipelineOptions): Promise<NewprOutput>
|
|
|
175
177
|
const totalDel = chunks.reduce((s, c) => s + c.deletions, 0);
|
|
176
178
|
progress({ stage: "parsing", message: `${chunks.length} files · +${totalAdd} −${totalDel}${wasTruncated ? ` (${allChunks.length - config.max_files} skipped)` : ""}` });
|
|
177
179
|
|
|
180
|
+
const changedFilesSet = new Set(changedFiles);
|
|
181
|
+
const filePatches: Record<string, string> = {};
|
|
182
|
+
for (const fileDiff of parsed.files) {
|
|
183
|
+
if (changedFilesSet.has(fileDiff.path)) {
|
|
184
|
+
filePatches[fileDiff.path] = fileDiff.raw;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
options.onFilePatches?.(filePatches);
|
|
188
|
+
|
|
178
189
|
let exploration: ExplorationResult | null = null;
|
|
179
190
|
if (!noClone) {
|
|
180
191
|
exploration = await tryExploreCodebase(
|
|
@@ -183,7 +194,12 @@ export async function analyzePr(options: PipelineOptions): Promise<NewprOutput>
|
|
|
183
194
|
);
|
|
184
195
|
}
|
|
185
196
|
|
|
186
|
-
const promptCtx: PromptContext = {
|
|
197
|
+
const promptCtx: PromptContext = {
|
|
198
|
+
commits: prData.commits,
|
|
199
|
+
language: config.language,
|
|
200
|
+
prBody: prData.body,
|
|
201
|
+
discussion: prComments.map((c) => ({ author: c.author, body: c.body })),
|
|
202
|
+
};
|
|
187
203
|
const enrichedTag = exploration ? " + codebase context" : "";
|
|
188
204
|
|
|
189
205
|
progress({
|
|
@@ -243,9 +259,13 @@ export async function analyzePr(options: PipelineOptions): Promise<NewprOutput>
|
|
|
243
259
|
progress({ stage: "summarizing", message: `${summary.risk_level} risk · ${summary.purpose.slice(0, 60)}` });
|
|
244
260
|
|
|
245
261
|
progress({ stage: "narrating", message: `Writing narrative${enrichedTag}...` });
|
|
262
|
+
const fileDiffs = chunks.slice(0, 30).map((c) => ({
|
|
263
|
+
path: c.file_path,
|
|
264
|
+
diff: c.diff_content.length > 3000 ? `${c.diff_content.slice(0, 3000)}\n... (truncated)` : c.diff_content,
|
|
265
|
+
}));
|
|
246
266
|
const narrativePrompt = exploration
|
|
247
|
-
? buildEnrichedNarrativePrompt(prData.title, summary, groups, exploration, promptCtx)
|
|
248
|
-
: buildNarrativePrompt(prData.title, summary, groups, promptCtx);
|
|
267
|
+
? buildEnrichedNarrativePrompt(prData.title, summary, groups, exploration, promptCtx, fileDiffs)
|
|
268
|
+
: buildNarrativePrompt(prData.title, summary, groups, promptCtx, fileDiffs);
|
|
249
269
|
const narrativeResponse = await streamLlmCall(
|
|
250
270
|
client, narrativePrompt.system, narrativePrompt.user, "narrating", "Writing narrative...", progress,
|
|
251
271
|
);
|
|
@@ -279,7 +299,9 @@ export async function analyzePr(options: PipelineOptions): Promise<NewprOutput>
|
|
|
279
299
|
meta: {
|
|
280
300
|
pr_number: prData.number,
|
|
281
301
|
pr_title: prData.title,
|
|
302
|
+
pr_body: prData.body || undefined,
|
|
282
303
|
pr_url: prData.url,
|
|
304
|
+
pr_state: prData.state,
|
|
283
305
|
base_branch: prData.base_branch,
|
|
284
306
|
head_branch: prData.head_branch,
|
|
285
307
|
author: prData.author,
|
package/src/analyzer/progress.ts
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -10,8 +10,9 @@ import { analyzePr } from "../analyzer/pipeline.ts";
|
|
|
10
10
|
import { createStderrProgress, createSilentProgress, createStreamJsonProgress } from "../analyzer/progress.ts";
|
|
11
11
|
import { renderLoading, renderShell } from "../tui/render.tsx";
|
|
12
12
|
import { checkForUpdate, printUpdateNotice } from "./update-check.ts";
|
|
13
|
+
import { runPreflight, printPreflight } from "./preflight.ts";
|
|
13
14
|
|
|
14
|
-
const VERSION = "0.
|
|
15
|
+
const VERSION = "0.3.0";
|
|
15
16
|
|
|
16
17
|
async function main(): Promise<void> {
|
|
17
18
|
const args = parseArgs(process.argv);
|
|
@@ -51,12 +52,14 @@ async function main(): Promise<void> {
|
|
|
51
52
|
|
|
52
53
|
if (args.command === "web") {
|
|
53
54
|
try {
|
|
55
|
+
const preflight = await runPreflight();
|
|
56
|
+
printPreflight(preflight);
|
|
54
57
|
const config = await loadConfig({ model: args.model });
|
|
55
58
|
const token = await getGithubToken();
|
|
56
59
|
const updateInfo = await updatePromise;
|
|
57
60
|
if (updateInfo) printUpdateNotice(updateInfo);
|
|
58
61
|
const { startWebServer } = await import("../web/server.ts");
|
|
59
|
-
await startWebServer({ port: args.port ?? 3000, token, config, cartoon: args.cartoon });
|
|
62
|
+
await startWebServer({ port: args.port ?? 3000, token, config, cartoon: args.cartoon, preflight });
|
|
60
63
|
} catch (error) {
|
|
61
64
|
const message = error instanceof Error ? error.message : String(error);
|
|
62
65
|
process.stderr.write(`Error: ${message}\n`);
|
|
@@ -67,6 +70,8 @@ async function main(): Promise<void> {
|
|
|
67
70
|
|
|
68
71
|
if (args.command === "shell") {
|
|
69
72
|
try {
|
|
73
|
+
const preflight = await runPreflight();
|
|
74
|
+
printPreflight(preflight);
|
|
70
75
|
const config = await loadConfig({ model: args.model });
|
|
71
76
|
const token = await getGithubToken();
|
|
72
77
|
const updateInfo = await updatePromise;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { AgentToolName } from "../workspace/types.ts";
|
|
2
|
+
|
|
3
|
+
export interface ToolStatus {
|
|
4
|
+
name: string;
|
|
5
|
+
installed: boolean;
|
|
6
|
+
version?: string;
|
|
7
|
+
detail?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PreflightResult {
|
|
11
|
+
github: ToolStatus & { authenticated: boolean; user?: string };
|
|
12
|
+
agents: ToolStatus[];
|
|
13
|
+
openrouterKey: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function which(cmd: string): Promise<string | null> {
|
|
17
|
+
try {
|
|
18
|
+
const result = await Bun.$`which ${cmd}`.text();
|
|
19
|
+
return result.trim() || null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getVersion(cmd: string, flag = "--version"): Promise<string | null> {
|
|
26
|
+
try {
|
|
27
|
+
const result = await Bun.$`${cmd} ${flag} 2>&1`.text();
|
|
28
|
+
const match = result.match(/[\d]+\.[\d]+[\d.]*/);
|
|
29
|
+
return match?.[0] ?? result.trim().slice(0, 30);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function checkGithubCli(): Promise<PreflightResult["github"]> {
|
|
36
|
+
const path = await which("gh");
|
|
37
|
+
if (!path) {
|
|
38
|
+
return { name: "gh", installed: false, authenticated: false, detail: "brew install gh" };
|
|
39
|
+
}
|
|
40
|
+
const version = await getVersion("gh");
|
|
41
|
+
try {
|
|
42
|
+
const status = await Bun.$`gh auth status 2>&1`.text();
|
|
43
|
+
const userMatch = status.match(/Logged in to github\.com account (\S+)/i)
|
|
44
|
+
?? status.match(/account (\S+)/i);
|
|
45
|
+
return {
|
|
46
|
+
name: "gh",
|
|
47
|
+
installed: true,
|
|
48
|
+
version: version ?? undefined,
|
|
49
|
+
authenticated: true,
|
|
50
|
+
user: userMatch?.[1]?.replace(/\s*\(.*/, ""),
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
return { name: "gh", installed: true, version: version ?? undefined, authenticated: false, detail: "gh auth login" };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function checkAgent(name: AgentToolName): Promise<ToolStatus> {
|
|
58
|
+
const path = await which(name);
|
|
59
|
+
if (!path) return { name, installed: false };
|
|
60
|
+
const version = await getVersion(name);
|
|
61
|
+
return { name, installed: true, version: version ?? undefined };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function runPreflight(): Promise<PreflightResult> {
|
|
65
|
+
const [github, claude, opencode, codex] = await Promise.all([
|
|
66
|
+
checkGithubCli(),
|
|
67
|
+
checkAgent("claude"),
|
|
68
|
+
checkAgent("opencode"),
|
|
69
|
+
checkAgent("codex"),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
github,
|
|
74
|
+
agents: [claude, opencode, codex],
|
|
75
|
+
openrouterKey: !!(process.env.OPENROUTER_API_KEY || await hasStoredApiKey()),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function hasStoredApiKey(): Promise<boolean> {
|
|
80
|
+
try {
|
|
81
|
+
const { readStoredConfig } = await import("../config/store.ts");
|
|
82
|
+
const stored = await readStoredConfig();
|
|
83
|
+
return !!stored.openrouter_api_key;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function printPreflight(result: PreflightResult): void {
|
|
90
|
+
const check = "\x1b[32m✓\x1b[0m";
|
|
91
|
+
const cross = "\x1b[31m✗\x1b[0m";
|
|
92
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
93
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
|
94
|
+
|
|
95
|
+
console.log("");
|
|
96
|
+
console.log(` ${bold("Preflight")}`);
|
|
97
|
+
console.log("");
|
|
98
|
+
|
|
99
|
+
const gh = result.github;
|
|
100
|
+
if (gh.installed && gh.authenticated) {
|
|
101
|
+
console.log(` ${check} gh ${dim(gh.version ?? "")} ${dim(`· ${gh.user ?? ""}`)}`);
|
|
102
|
+
} else if (gh.installed) {
|
|
103
|
+
console.log(` ${cross} gh ${dim(gh.version ?? "")} ${dim("· not authenticated")}`);
|
|
104
|
+
console.log(` ${dim(`run: ${gh.detail}`)}`);
|
|
105
|
+
} else {
|
|
106
|
+
console.log(` ${cross} gh ${dim("· not installed")}`);
|
|
107
|
+
console.log(` ${dim(`run: ${gh.detail}`)}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const agent of result.agents) {
|
|
111
|
+
if (agent.installed) {
|
|
112
|
+
console.log(` ${check} ${agent.name} ${dim(agent.version ?? "")}`);
|
|
113
|
+
} else {
|
|
114
|
+
console.log(` ${dim("·")} ${dim(agent.name)} ${dim("not found")}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (result.openrouterKey) {
|
|
119
|
+
console.log(` ${check} OpenRouter API key`);
|
|
120
|
+
} else {
|
|
121
|
+
console.log(` ${cross} OpenRouter API key ${dim("· not configured")}`);
|
|
122
|
+
console.log(` ${dim("run: newpr auth")}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log("");
|
|
126
|
+
}
|
package/src/github/fetch-pr.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import type { GithubPrData, PrCommit, PrIdentifier } from "../types/github.ts";
|
|
1
|
+
import type { GithubPrData, PrComment, PrCommit, PrIdentifier, PrState } from "../types/github.ts";
|
|
2
2
|
|
|
3
3
|
export function mapPrResponse(json: Record<string, unknown>): Omit<GithubPrData, "commits"> {
|
|
4
4
|
const user = json.user as Record<string, unknown> | undefined;
|
|
5
5
|
const base = json.base as Record<string, unknown> | undefined;
|
|
6
6
|
const head = json.head as Record<string, unknown> | undefined;
|
|
7
7
|
|
|
8
|
+
let state: PrState = "open";
|
|
9
|
+
if (json.draft) {
|
|
10
|
+
state = "draft";
|
|
11
|
+
} else if (json.merged) {
|
|
12
|
+
state = "merged";
|
|
13
|
+
} else if (json.state === "closed") {
|
|
14
|
+
state = "closed";
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
return {
|
|
9
18
|
number: json.number as number,
|
|
10
19
|
title: json.title as string,
|
|
20
|
+
body: (json.body as string) ?? "",
|
|
11
21
|
url: json.html_url as string,
|
|
22
|
+
state,
|
|
12
23
|
base_branch: (base?.ref as string) ?? "unknown",
|
|
13
24
|
head_branch: (head?.ref as string) ?? "unknown",
|
|
14
25
|
author: (user?.login as string) ?? "unknown",
|
|
@@ -88,3 +99,44 @@ export async function fetchPrData(pr: PrIdentifier, token: string): Promise<Gith
|
|
|
88
99
|
|
|
89
100
|
return { ...base, commits };
|
|
90
101
|
}
|
|
102
|
+
|
|
103
|
+
interface GithubCommentResponse {
|
|
104
|
+
id: number;
|
|
105
|
+
user: { login: string; avatar_url?: string } | null;
|
|
106
|
+
body: string;
|
|
107
|
+
created_at: string;
|
|
108
|
+
updated_at: string;
|
|
109
|
+
html_url: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function fetchPrComments(pr: PrIdentifier, token: string): Promise<PrComment[]> {
|
|
113
|
+
const allComments: GithubCommentResponse[] = [];
|
|
114
|
+
let page = 1;
|
|
115
|
+
|
|
116
|
+
while (true) {
|
|
117
|
+
const url = `https://api.github.com/repos/${pr.owner}/${pr.repo}/issues/${pr.number}/comments?per_page=100&page=${page}`;
|
|
118
|
+
const response = await githubGet(url, token);
|
|
119
|
+
const items = (await response.json()) as GithubCommentResponse[];
|
|
120
|
+
if (items.length === 0) break;
|
|
121
|
+
allComments.push(...items);
|
|
122
|
+
if (items.length < 100) break;
|
|
123
|
+
page++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return allComments.map((c) => ({
|
|
127
|
+
id: c.id,
|
|
128
|
+
author: c.user?.login ?? "unknown",
|
|
129
|
+
author_avatar: c.user?.avatar_url ?? undefined,
|
|
130
|
+
body: c.body,
|
|
131
|
+
created_at: c.created_at,
|
|
132
|
+
updated_at: c.updated_at,
|
|
133
|
+
html_url: c.html_url,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function fetchPrBody(pr: PrIdentifier, token: string): Promise<string> {
|
|
138
|
+
const url = `https://api.github.com/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}`;
|
|
139
|
+
const response = await githubGet(url, token);
|
|
140
|
+
const json = (await response.json()) as Record<string, unknown>;
|
|
141
|
+
return (json.body as string) ?? "";
|
|
142
|
+
}
|
package/src/history/store.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { mkdirSync, rmSync, existsSync } from "node:fs";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
|
-
import type { NewprOutput } from "../types/output.ts";
|
|
5
|
+
import type { NewprOutput, DiffComment, ChatMessage, CartoonImage } from "../types/output.ts";
|
|
6
6
|
import type { SessionRecord } from "./types.ts";
|
|
7
7
|
|
|
8
8
|
const HISTORY_DIR = join(homedir(), ".newpr", "history");
|
|
@@ -25,6 +25,7 @@ export function buildSessionRecord(id: string, data: NewprOutput): SessionRecord
|
|
|
25
25
|
pr_url: meta.pr_url,
|
|
26
26
|
pr_number: meta.pr_number,
|
|
27
27
|
pr_title: meta.pr_title,
|
|
28
|
+
pr_state: meta.pr_state,
|
|
28
29
|
repo: repoParts?.[1] ?? "unknown",
|
|
29
30
|
author: meta.author,
|
|
30
31
|
analyzed_at: meta.analyzed_at,
|
|
@@ -91,6 +92,111 @@ export async function clearHistory(): Promise<void> {
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
export async function savePatchesSidecar(
|
|
96
|
+
id: string,
|
|
97
|
+
patches: Record<string, string>,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
ensureDirs();
|
|
100
|
+
await Bun.write(
|
|
101
|
+
join(SESSIONS_DIR, `${id}.patches.json`),
|
|
102
|
+
JSON.stringify(patches),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function loadPatchesSidecar(
|
|
107
|
+
id: string,
|
|
108
|
+
): Promise<Record<string, string> | null> {
|
|
109
|
+
try {
|
|
110
|
+
const filePath = join(SESSIONS_DIR, `${id}.patches.json`);
|
|
111
|
+
const file = Bun.file(filePath);
|
|
112
|
+
if (!(await file.exists())) return null;
|
|
113
|
+
return JSON.parse(await file.text()) as Record<string, string>;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function loadSinglePatch(
|
|
120
|
+
id: string,
|
|
121
|
+
filePath: string,
|
|
122
|
+
): Promise<string | null> {
|
|
123
|
+
const patches = await loadPatchesSidecar(id);
|
|
124
|
+
if (!patches) return null;
|
|
125
|
+
return patches[filePath] ?? null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function saveCommentsSidecar(
|
|
129
|
+
id: string,
|
|
130
|
+
comments: DiffComment[],
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
ensureDirs();
|
|
133
|
+
await Bun.write(
|
|
134
|
+
join(SESSIONS_DIR, `${id}.comments.json`),
|
|
135
|
+
JSON.stringify(comments, null, 2),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function loadCommentsSidecar(
|
|
140
|
+
id: string,
|
|
141
|
+
): Promise<DiffComment[] | null> {
|
|
142
|
+
try {
|
|
143
|
+
const filePath = join(SESSIONS_DIR, `${id}.comments.json`);
|
|
144
|
+
const file = Bun.file(filePath);
|
|
145
|
+
if (!(await file.exists())) return null;
|
|
146
|
+
return JSON.parse(await file.text()) as DiffComment[];
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function saveChatSidecar(
|
|
153
|
+
id: string,
|
|
154
|
+
messages: ChatMessage[],
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
ensureDirs();
|
|
157
|
+
await Bun.write(
|
|
158
|
+
join(SESSIONS_DIR, `${id}.chat.json`),
|
|
159
|
+
JSON.stringify(messages, null, 2),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function loadChatSidecar(
|
|
164
|
+
id: string,
|
|
165
|
+
): Promise<ChatMessage[] | null> {
|
|
166
|
+
try {
|
|
167
|
+
const filePath = join(SESSIONS_DIR, `${id}.chat.json`);
|
|
168
|
+
const file = Bun.file(filePath);
|
|
169
|
+
if (!(await file.exists())) return null;
|
|
170
|
+
return JSON.parse(await file.text()) as ChatMessage[];
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function saveCartoonSidecar(
|
|
177
|
+
id: string,
|
|
178
|
+
cartoon: CartoonImage,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
ensureDirs();
|
|
181
|
+
await Bun.write(
|
|
182
|
+
join(SESSIONS_DIR, `${id}.cartoon.json`),
|
|
183
|
+
JSON.stringify(cartoon),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function loadCartoonSidecar(
|
|
188
|
+
id: string,
|
|
189
|
+
): Promise<CartoonImage | null> {
|
|
190
|
+
try {
|
|
191
|
+
const filePath = join(SESSIONS_DIR, `${id}.cartoon.json`);
|
|
192
|
+
const file = Bun.file(filePath);
|
|
193
|
+
if (!(await file.exists())) return null;
|
|
194
|
+
return JSON.parse(await file.text()) as CartoonImage;
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
94
200
|
export function getHistoryPath(): string {
|
|
95
201
|
return HISTORY_DIR;
|
|
96
202
|
}
|