newpr 0.2.0 → 0.4.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/README.md +135 -103
- package/package.json +1 -1
- package/src/analyzer/pipeline.ts +12 -11
- package/src/analyzer/progress.ts +2 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/index.ts +8 -2
- package/src/cli/preflight.ts +126 -0
- package/src/github/fetch-pr.ts +11 -1
- package/src/history/store.ts +1 -0
- package/src/history/types.ts +1 -0
- package/src/llm/prompts.ts +110 -19
- package/src/llm/response-parser.ts +13 -1
- package/src/types/config.ts +1 -1
- package/src/types/github.ts +3 -0
- package/src/types/output.ts +6 -0
- package/src/version.ts +23 -0
- package/src/web/client/App.tsx +51 -3
- package/src/web/client/components/AppShell.tsx +180 -39
- package/src/web/client/components/ChatSection.tsx +4 -172
- package/src/web/client/components/DetailPane.tsx +58 -5
- package/src/web/client/components/DiffViewer.tsx +47 -1
- package/src/web/client/components/InputScreen.tsx +72 -2
- package/src/web/client/components/LoadingTimeline.tsx +19 -6
- package/src/web/client/components/Markdown.tsx +107 -4
- package/src/web/client/components/ResultsScreen.tsx +44 -3
- package/src/web/client/components/ReviewModal.tsx +187 -0
- package/src/web/client/components/SettingsPanel.tsx +63 -87
- package/src/web/client/hooks/useBackgroundAnalyses.ts +147 -0
- package/src/web/client/hooks/useChatStore.ts +244 -0
- package/src/web/client/hooks/useFeatures.ts +2 -1
- package/src/web/client/panels/GroupsPanel.tsx +15 -2
- package/src/web/client/panels/StoryPanel.tsx +1 -1
- package/src/web/index.html +1 -0
- package/src/web/server/routes.ts +195 -16
- package/src/web/server/session-manager.ts +34 -0
- package/src/web/server.ts +37 -4
- package/src/web/styles/built.css +1 -1
- package/src/workspace/agent.ts +22 -6
- package/src/workspace/explore.ts +74 -16
- package/src/workspace/types.ts +1 -0
|
@@ -2,10 +2,11 @@ import { useState, useEffect } from "react";
|
|
|
2
2
|
|
|
3
3
|
interface Features {
|
|
4
4
|
cartoon: boolean;
|
|
5
|
+
version: string;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export function useFeatures(): Features {
|
|
8
|
-
const [features, setFeatures] = useState<Features>({ cartoon: false });
|
|
9
|
+
const [features, setFeatures] = useState<Features>({ cartoon: false, version: "" });
|
|
9
10
|
|
|
10
11
|
useEffect(() => {
|
|
11
12
|
fetch("/api/features")
|
|
@@ -47,8 +47,21 @@ export function GroupsPanel({ groups }: { groups: FileGroup[] }) {
|
|
|
47
47
|
<span className="text-[10px] text-muted-foreground/30 shrink-0 tabular-nums">{group.files.length}</span>
|
|
48
48
|
</button>
|
|
49
49
|
{isOpen && (
|
|
50
|
-
<div className="pl-[34px] pr-2 pb-3 pt-1">
|
|
51
|
-
<p className="text-[11px] text-muted-foreground/60 leading-relaxed
|
|
50
|
+
<div className="pl-[34px] pr-2 pb-3 pt-1 space-y-2.5">
|
|
51
|
+
<p className="text-[11px] text-muted-foreground/60 leading-relaxed">{group.description}</p>
|
|
52
|
+
{group.key_changes && group.key_changes.length > 0 && (
|
|
53
|
+
<ul className="space-y-1">
|
|
54
|
+
{group.key_changes.map((change, ci) => (
|
|
55
|
+
<li key={ci} className="flex gap-1.5 text-[11px] text-muted-foreground/50 leading-relaxed">
|
|
56
|
+
<span className="text-muted-foreground/25 shrink-0">·</span>
|
|
57
|
+
<span>{change}</span>
|
|
58
|
+
</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
)}
|
|
62
|
+
{group.risk && (
|
|
63
|
+
<p className="text-[10px] text-muted-foreground/40 leading-relaxed">{group.risk}</p>
|
|
64
|
+
)}
|
|
52
65
|
<div className="space-y-0.5">
|
|
53
66
|
{group.files.map((f) => (
|
|
54
67
|
<div
|
|
@@ -19,7 +19,7 @@ export function StoryPanel({
|
|
|
19
19
|
}: {
|
|
20
20
|
data: NewprOutput;
|
|
21
21
|
activeId: string | null;
|
|
22
|
-
onAnchorClick: (kind: "group" | "file", id: string) => void;
|
|
22
|
+
onAnchorClick: (kind: "group" | "file" | "line", id: string) => void;
|
|
23
23
|
}) {
|
|
24
24
|
const { summary, groups, narrative } = data;
|
|
25
25
|
|
package/src/web/index.html
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>newpr</title>
|
|
7
7
|
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css" />
|
|
8
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.28/dist/katex.min.css" crossorigin />
|
|
8
9
|
<script>document.head.appendChild(Object.assign(document.createElement("link"),{rel:"stylesheet",href:"/styles.css"}))</script>
|
|
9
10
|
</head>
|
|
10
11
|
<body>
|
package/src/web/server/routes.ts
CHANGED
|
@@ -8,9 +8,10 @@ import { fetchPrBody, fetchPrComments } from "../../github/fetch-pr.ts";
|
|
|
8
8
|
import { parseDiff } from "../../diff/parser.ts";
|
|
9
9
|
import { parsePrInput } from "../../github/parse-pr.ts";
|
|
10
10
|
import { writeStoredConfig, type StoredConfig } from "../../config/store.ts";
|
|
11
|
-
import { startAnalysis, getSession, cancelAnalysis, subscribe } from "./session-manager.ts";
|
|
11
|
+
import { startAnalysis, getSession, cancelAnalysis, subscribe, listActiveSessions } from "./session-manager.ts";
|
|
12
12
|
import { generateCartoon } from "../../llm/cartoon.ts";
|
|
13
13
|
import { chatWithTools, type ChatTool, type ChatStreamEvent } from "../../llm/client.ts";
|
|
14
|
+
import { detectAgents, runAgent } from "../../workspace/agent.ts";
|
|
14
15
|
import { randomBytes } from "node:crypto";
|
|
15
16
|
|
|
16
17
|
function json(data: unknown, status = 200): Response {
|
|
@@ -20,8 +21,11 @@ function json(data: unknown, status = 200): Response {
|
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
import type { PreflightResult } from "../../cli/preflight.ts";
|
|
25
|
+
|
|
23
26
|
interface RouteOptions {
|
|
24
27
|
cartoon?: boolean;
|
|
28
|
+
preflight?: PreflightResult;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export function createRoutes(token: string, config: NewprConfig, options: RouteOptions = {}) {
|
|
@@ -100,13 +104,19 @@ ${data.narrative}
|
|
|
100
104
|
${data.meta.pr_body ? `## PR Description\n${data.meta.pr_body}` : ""}
|
|
101
105
|
|
|
102
106
|
## Anchor Syntax (CRITICAL)
|
|
103
|
-
You MUST use these anchors
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
You MUST use these anchors. They become clickable links in the UI.
|
|
108
|
+
|
|
109
|
+
1. Group: [[group:Exact Group Name]] — renders as a clickable chip.
|
|
110
|
+
2. File: [[file:exact/path/here.ts]] — renders as a clickable chip.
|
|
111
|
+
3. Line reference: [[line:exact/path/here.ts#L42-L50]](descriptive text) — the "descriptive text" becomes an underlined link that opens the diff scrolled to that line. The line info is NOT shown — only the text is visible.
|
|
112
|
+
|
|
113
|
+
RULES:
|
|
114
|
+
- Use EXACT paths and names from the lists above.
|
|
115
|
+
- For line references, ALWAYS use [[line:path#L-L]](text). NEVER bare [[line:...]] without (text).
|
|
116
|
+
- The (text) should describe what the code does, NOT show file names or line numbers.
|
|
117
|
+
- Do NOT place [[file:...]] and [[line:...]] adjacent for the same file.
|
|
118
|
+
- Use the get_file_diff tool to find exact line numbers before referencing them.
|
|
119
|
+
- Aim for most statements about code to include at least one line reference.
|
|
110
120
|
|
|
111
121
|
## Math / LaTeX
|
|
112
122
|
When expressing mathematical formulas, algorithms, or complexity analysis, use LaTeX syntax:
|
|
@@ -172,6 +182,42 @@ $$
|
|
|
172
182
|
parameters: { type: "object", properties: {} },
|
|
173
183
|
},
|
|
174
184
|
},
|
|
185
|
+
{
|
|
186
|
+
type: "function",
|
|
187
|
+
function: {
|
|
188
|
+
name: "run_react_doctor",
|
|
189
|
+
description: "Run react-doctor on the PR's codebase to get a React code quality score (0-100) and diagnostics for security, performance, correctness, and architecture issues. Only useful for React/JSX/TSX projects.",
|
|
190
|
+
parameters: { type: "object", properties: {} },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
type: "function",
|
|
195
|
+
function: {
|
|
196
|
+
name: "web_search",
|
|
197
|
+
description: "Search the web for documentation, library references, best practices, or any technical question. Returns top search results with snippets.",
|
|
198
|
+
parameters: {
|
|
199
|
+
type: "object",
|
|
200
|
+
properties: {
|
|
201
|
+
query: { type: "string", description: "Search query (e.g. 'React useEffect cleanup pattern', 'zod discriminated union')" },
|
|
202
|
+
},
|
|
203
|
+
required: ["query"],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
type: "function",
|
|
209
|
+
function: {
|
|
210
|
+
name: "web_fetch",
|
|
211
|
+
description: "Fetch the text content of a web page. Use this to read documentation pages, blog posts, or API references found via web_search.",
|
|
212
|
+
parameters: {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
url: { type: "string", description: "URL to fetch (must start with https://)" },
|
|
216
|
+
},
|
|
217
|
+
required: ["url"],
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
175
221
|
];
|
|
176
222
|
}
|
|
177
223
|
|
|
@@ -384,13 +430,11 @@ $$
|
|
|
384
430
|
if (!allowed) return json({ error: "URL not allowed" }, 403);
|
|
385
431
|
|
|
386
432
|
try {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
redirect: "follow",
|
|
393
|
-
});
|
|
433
|
+
const headers: Record<string, string> = { "User-Agent": "newpr-cli" };
|
|
434
|
+
if (token && target.startsWith("https://github.com/")) {
|
|
435
|
+
headers.Authorization = `token ${token}`;
|
|
436
|
+
}
|
|
437
|
+
const res = await fetch(target, { headers, redirect: "follow" });
|
|
394
438
|
if (!res.ok) return new Response(null, { status: res.status });
|
|
395
439
|
|
|
396
440
|
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
@@ -490,7 +534,47 @@ $$
|
|
|
490
534
|
},
|
|
491
535
|
|
|
492
536
|
"GET /api/features": () => {
|
|
493
|
-
|
|
537
|
+
const { getVersion } = require("../../version.ts");
|
|
538
|
+
return json({ cartoon: !!options.cartoon, version: getVersion() });
|
|
539
|
+
},
|
|
540
|
+
|
|
541
|
+
"POST /api/review": async (req: Request) => {
|
|
542
|
+
const body = await req.json() as { pr_url: string; event: string; body?: string };
|
|
543
|
+
if (!body.pr_url || !body.event) return json({ error: "Missing pr_url or event" }, 400);
|
|
544
|
+
|
|
545
|
+
const validEvents = ["APPROVE", "REQUEST_CHANGES", "COMMENT"];
|
|
546
|
+
if (!validEvents.includes(body.event)) return json({ error: `Invalid event: ${body.event}` }, 400);
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const pr = parsePrInput(body.pr_url);
|
|
550
|
+
const res = await fetch(
|
|
551
|
+
`https://api.github.com/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}/reviews`,
|
|
552
|
+
{
|
|
553
|
+
method: "POST",
|
|
554
|
+
headers: ghHeaders,
|
|
555
|
+
body: JSON.stringify({
|
|
556
|
+
body: body.body ?? "",
|
|
557
|
+
event: body.event,
|
|
558
|
+
}),
|
|
559
|
+
},
|
|
560
|
+
);
|
|
561
|
+
if (!res.ok) {
|
|
562
|
+
const errBody = await res.json().catch(() => ({})) as { message?: string };
|
|
563
|
+
return json({ error: errBody.message ?? `GitHub API error: ${res.status}` }, res.status);
|
|
564
|
+
}
|
|
565
|
+
const data = await res.json() as { id: number; state: string; html_url: string };
|
|
566
|
+
return json({ ok: true, id: data.id, state: data.state, html_url: data.html_url });
|
|
567
|
+
} catch (err) {
|
|
568
|
+
return json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
"GET /api/preflight": () => {
|
|
573
|
+
return json(options.preflight ?? null);
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
"GET /api/active-analyses": () => {
|
|
577
|
+
return json(listActiveSessions());
|
|
494
578
|
},
|
|
495
579
|
|
|
496
580
|
"GET /api/sessions/:id/comments": async (req: Request) => {
|
|
@@ -804,6 +888,101 @@ $$
|
|
|
804
888
|
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
805
889
|
}
|
|
806
890
|
}
|
|
891
|
+
case "run_react_doctor": {
|
|
892
|
+
const agents = await detectAgents();
|
|
893
|
+
if (agents.length > 0) {
|
|
894
|
+
try {
|
|
895
|
+
const result = await runAgent(
|
|
896
|
+
agents[0]!,
|
|
897
|
+
process.cwd(),
|
|
898
|
+
"Run react-doctor on this project:\n\nnpx -y react-doctor@latest . --verbose\n\nReturn the FULL output including the score and all diagnostics.",
|
|
899
|
+
{ timeout: 60_000 },
|
|
900
|
+
);
|
|
901
|
+
if (result.answer.trim()) return result.answer;
|
|
902
|
+
} catch {}
|
|
903
|
+
}
|
|
904
|
+
try {
|
|
905
|
+
const proc = Bun.spawn(["npx", "-y", "react-doctor@latest", ".", "--verbose"], {
|
|
906
|
+
cwd: process.cwd(),
|
|
907
|
+
stdout: "pipe",
|
|
908
|
+
stderr: "pipe",
|
|
909
|
+
});
|
|
910
|
+
const output = await new Response(proc.stdout).text();
|
|
911
|
+
const stderr = await new Response(proc.stderr).text();
|
|
912
|
+
return output.trim() || stderr.trim() || "react-doctor produced no output";
|
|
913
|
+
} catch (err) {
|
|
914
|
+
return `Error running react-doctor: ${err instanceof Error ? err.message : String(err)}`;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
case "web_search": {
|
|
918
|
+
const query = args.query as string;
|
|
919
|
+
if (!query) return "Error: query argument required";
|
|
920
|
+
const agents = await detectAgents();
|
|
921
|
+
if (agents.length > 0) {
|
|
922
|
+
try {
|
|
923
|
+
const result = await runAgent(agents[0]!, process.cwd(), `Search the web for: "${query}"\n\nReturn the top results with titles, URLs, and brief descriptions. Be concise.`, { timeout: 30_000 });
|
|
924
|
+
if (result.answer.trim()) return result.answer;
|
|
925
|
+
} catch {}
|
|
926
|
+
}
|
|
927
|
+
try {
|
|
928
|
+
const encoded = encodeURIComponent(query);
|
|
929
|
+
const res = await fetch(`https://html.duckduckgo.com/html/?q=${encoded}`, {
|
|
930
|
+
headers: { "User-Agent": "newpr-cli/0.2.0" },
|
|
931
|
+
});
|
|
932
|
+
if (!res.ok) return `Search failed: HTTP ${res.status}`;
|
|
933
|
+
const html = await res.text();
|
|
934
|
+
const results: string[] = [];
|
|
935
|
+
const resultRe = /<a[^>]+class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
|
|
936
|
+
let m;
|
|
937
|
+
while ((m = resultRe.exec(html)) !== null && results.length < 8) {
|
|
938
|
+
const href = m[1]?.replace(/&/g, "&") ?? "";
|
|
939
|
+
const title = (m[2] ?? "").replace(/<[^>]+>/g, "").trim();
|
|
940
|
+
const snippet = (m[3] ?? "").replace(/<[^>]+>/g, "").trim();
|
|
941
|
+
if (title && href) results.push(`${title}\n${href}\n${snippet}`);
|
|
942
|
+
}
|
|
943
|
+
if (results.length === 0) return `No results found for "${query}"`;
|
|
944
|
+
return results.join("\n\n---\n\n");
|
|
945
|
+
} catch (err) {
|
|
946
|
+
return `Search error: ${err instanceof Error ? err.message : String(err)}`;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
case "web_fetch": {
|
|
950
|
+
const url = args.url as string;
|
|
951
|
+
if (!url?.startsWith("https://")) return "Error: url must start with https://";
|
|
952
|
+
const agents = await detectAgents();
|
|
953
|
+
if (agents.length > 0) {
|
|
954
|
+
try {
|
|
955
|
+
const result = await runAgent(agents[0]!, process.cwd(), `Fetch and summarize the content of this URL: ${url}\n\nReturn the key information from the page. Be thorough but concise.`, { timeout: 30_000 });
|
|
956
|
+
if (result.answer.trim()) return result.answer;
|
|
957
|
+
} catch {}
|
|
958
|
+
}
|
|
959
|
+
try {
|
|
960
|
+
const controller = new AbortController();
|
|
961
|
+
const t = setTimeout(() => controller.abort(), 15000);
|
|
962
|
+
const res = await fetch(url, {
|
|
963
|
+
signal: controller.signal,
|
|
964
|
+
headers: { "User-Agent": "newpr-cli/0.2.0", Accept: "text/html,text/plain,application/json" },
|
|
965
|
+
redirect: "follow",
|
|
966
|
+
});
|
|
967
|
+
clearTimeout(t);
|
|
968
|
+
if (!res.ok) return `Fetch failed: HTTP ${res.status}`;
|
|
969
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
970
|
+
const text = await res.text();
|
|
971
|
+
if (contentType.includes("json")) return text.slice(0, 15000);
|
|
972
|
+
const stripped = text
|
|
973
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
974
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
975
|
+
.replace(/<nav[\s\S]*?<\/nav>/gi, "")
|
|
976
|
+
.replace(/<footer[\s\S]*?<\/footer>/gi, "")
|
|
977
|
+
.replace(/<header[\s\S]*?<\/header>/gi, "")
|
|
978
|
+
.replace(/<[^>]+>/g, " ")
|
|
979
|
+
.replace(/\s+/g, " ")
|
|
980
|
+
.trim();
|
|
981
|
+
return stripped.slice(0, 15000) + (stripped.length > 15000 ? "\n\n... (truncated)" : "");
|
|
982
|
+
} catch (err) {
|
|
983
|
+
return `Fetch error: ${err instanceof Error ? err.message : String(err)}`;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
807
986
|
default:
|
|
808
987
|
return `Unknown tool: ${name}`;
|
|
809
988
|
}
|
|
@@ -9,6 +9,7 @@ type SessionStatus = "running" | "done" | "error" | "canceled";
|
|
|
9
9
|
|
|
10
10
|
interface AnalysisSession {
|
|
11
11
|
id: string;
|
|
12
|
+
prInput: string;
|
|
12
13
|
status: SessionStatus;
|
|
13
14
|
events: ProgressEvent[];
|
|
14
15
|
result?: NewprOutput;
|
|
@@ -16,6 +17,8 @@ interface AnalysisSession {
|
|
|
16
17
|
error?: string;
|
|
17
18
|
startedAt: number;
|
|
18
19
|
finishedAt?: number;
|
|
20
|
+
prTitle?: string;
|
|
21
|
+
prNumber?: number;
|
|
19
22
|
abortController: AbortController;
|
|
20
23
|
subscribers: Set<(event: ProgressEvent | { type: "done" | "error"; data?: string }) => void>;
|
|
21
24
|
}
|
|
@@ -53,6 +56,7 @@ export function startAnalysis(
|
|
|
53
56
|
|
|
54
57
|
const session: AnalysisSession = {
|
|
55
58
|
id,
|
|
59
|
+
prInput,
|
|
56
60
|
status: "running",
|
|
57
61
|
events: [],
|
|
58
62
|
startedAt: Date.now(),
|
|
@@ -84,6 +88,8 @@ async function runPipeline(
|
|
|
84
88
|
onProgress: (event: ProgressEvent) => {
|
|
85
89
|
const stamped = { ...event, timestamp: event.timestamp ?? Date.now() };
|
|
86
90
|
session.events.push(stamped);
|
|
91
|
+
if (event.pr_title) session.prTitle = event.pr_title;
|
|
92
|
+
if (event.pr_number) session.prNumber = event.pr_number;
|
|
87
93
|
for (const sub of session.subscribers) {
|
|
88
94
|
sub(stamped);
|
|
89
95
|
}
|
|
@@ -129,6 +135,34 @@ export function cancelAnalysis(id: string): boolean {
|
|
|
129
135
|
return true;
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
export function listActiveSessions(): Array<{
|
|
139
|
+
id: string;
|
|
140
|
+
prInput: string;
|
|
141
|
+
status: SessionStatus;
|
|
142
|
+
startedAt: number;
|
|
143
|
+
prTitle?: string;
|
|
144
|
+
prNumber?: number;
|
|
145
|
+
lastStage?: string;
|
|
146
|
+
lastMessage?: string;
|
|
147
|
+
}> {
|
|
148
|
+
const result: ReturnType<typeof listActiveSessions> = [];
|
|
149
|
+
for (const s of sessions.values()) {
|
|
150
|
+
if (s.status !== "running") continue;
|
|
151
|
+
const lastEvent = s.events[s.events.length - 1];
|
|
152
|
+
result.push({
|
|
153
|
+
id: s.id,
|
|
154
|
+
prInput: s.prInput,
|
|
155
|
+
status: s.status,
|
|
156
|
+
startedAt: s.startedAt,
|
|
157
|
+
prTitle: s.prTitle,
|
|
158
|
+
prNumber: s.prNumber,
|
|
159
|
+
lastStage: lastEvent?.stage,
|
|
160
|
+
lastMessage: lastEvent?.message,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
132
166
|
export function subscribe(
|
|
133
167
|
id: string,
|
|
134
168
|
callback: (event: ProgressEvent | { type: "done" | "error"; data?: string }) => void,
|
package/src/web/server.ts
CHANGED
|
@@ -3,11 +3,15 @@ import type { NewprConfig } from "../types/config.ts";
|
|
|
3
3
|
import { createRoutes } from "./server/routes.ts";
|
|
4
4
|
import index from "./index.html";
|
|
5
5
|
|
|
6
|
+
import type { PreflightResult } from "../cli/preflight.ts";
|
|
7
|
+
import { getVersion } from "../version.ts";
|
|
8
|
+
|
|
6
9
|
interface WebServerOptions {
|
|
7
10
|
port: number;
|
|
8
11
|
token: string;
|
|
9
12
|
config: NewprConfig;
|
|
10
13
|
cartoon?: boolean;
|
|
14
|
+
preflight?: PreflightResult;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
function getCssPaths() {
|
|
@@ -32,8 +36,8 @@ async function buildCss(bin: string, input: string, output: string): Promise<voi
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
export async function startWebServer(options: WebServerOptions): Promise<void> {
|
|
35
|
-
const { port, token, config, cartoon } = options;
|
|
36
|
-
const routes = createRoutes(token, config, { cartoon });
|
|
39
|
+
const { port, token, config, cartoon, preflight } = options;
|
|
40
|
+
const routes = createRoutes(token, config, { cartoon, preflight });
|
|
37
41
|
const css = getCssPaths();
|
|
38
42
|
|
|
39
43
|
await buildCss(css.bin, css.input, css.output);
|
|
@@ -119,9 +123,18 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
|
|
|
119
123
|
if (path === "/api/features" && req.method === "GET") {
|
|
120
124
|
return routes["GET /api/features"]();
|
|
121
125
|
}
|
|
126
|
+
if (path === "/api/preflight" && req.method === "GET") {
|
|
127
|
+
return routes["GET /api/preflight"]();
|
|
128
|
+
}
|
|
129
|
+
if (path === "/api/active-analyses" && req.method === "GET") {
|
|
130
|
+
return routes["GET /api/active-analyses"]();
|
|
131
|
+
}
|
|
122
132
|
if (path === "/api/cartoon" && req.method === "POST") {
|
|
123
133
|
return routes["POST /api/cartoon"](req);
|
|
124
134
|
}
|
|
135
|
+
if (path === "/api/review" && req.method === "POST") {
|
|
136
|
+
return routes["POST /api/review"](req);
|
|
137
|
+
}
|
|
125
138
|
|
|
126
139
|
return new Response("Not Found", { status: 404 });
|
|
127
140
|
},
|
|
@@ -131,6 +144,26 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
|
|
|
131
144
|
},
|
|
132
145
|
});
|
|
133
146
|
|
|
134
|
-
|
|
135
|
-
|
|
147
|
+
const url = `http://localhost:${server.port}`;
|
|
148
|
+
|
|
149
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
150
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
|
151
|
+
const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
|
|
152
|
+
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
|
|
153
|
+
|
|
154
|
+
console.log("");
|
|
155
|
+
console.log(` ${bold("newpr")} ${dim(`v${getVersion()}`)}`);
|
|
156
|
+
console.log("");
|
|
157
|
+
console.log(` ${dim("→")} Local ${cyan(url)}`);
|
|
158
|
+
console.log(` ${dim("→")} Model ${dim(config.model)}`);
|
|
159
|
+
if (cartoon) console.log(` ${dim("→")} Comic ${green("enabled")}`);
|
|
160
|
+
console.log("");
|
|
161
|
+
console.log(` ${dim("press")} ${bold("ctrl+c")} ${dim("to stop")}`);
|
|
162
|
+
console.log("");
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const { platform } = process;
|
|
166
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
167
|
+
Bun.spawn([cmd, url], { stdout: "ignore", stderr: "ignore" });
|
|
168
|
+
} catch {}
|
|
136
169
|
}
|