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
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { Plus, Pencil, Trash2, ArrowRight, X, Loader2, AlertCircle } from "lucide-react";
|
|
2
3
|
import type { FileGroup, FileChange, FileStatus } from "../../../types/output.ts";
|
|
4
|
+
import { DiffViewer } from "./DiffViewer.tsx";
|
|
3
5
|
|
|
4
6
|
export interface DetailTarget {
|
|
5
|
-
kind: "group" | "file";
|
|
7
|
+
kind: "group" | "file" | "line";
|
|
6
8
|
group?: FileGroup;
|
|
7
9
|
file?: FileChange;
|
|
8
10
|
files: FileChange[];
|
|
11
|
+
scrollToLine?: number;
|
|
12
|
+
scrollToLineEnd?: number;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
const STATUS_ICON: Record<FileStatus, typeof Plus> = {
|
|
@@ -22,18 +26,18 @@ const STATUS_COLOR: Record<FileStatus, string> = {
|
|
|
22
26
|
renamed: "text-blue-500",
|
|
23
27
|
};
|
|
24
28
|
|
|
25
|
-
const
|
|
26
|
-
feature: "bg-blue-500
|
|
27
|
-
refactor: "bg-purple-500
|
|
28
|
-
bugfix: "bg-red-500
|
|
29
|
-
chore: "bg-
|
|
30
|
-
docs: "bg-teal-500
|
|
31
|
-
test: "bg-yellow-500
|
|
32
|
-
config: "bg-orange-500
|
|
29
|
+
const TYPE_DOT: Record<string, string> = {
|
|
30
|
+
feature: "bg-blue-500",
|
|
31
|
+
refactor: "bg-purple-500",
|
|
32
|
+
bugfix: "bg-red-500",
|
|
33
|
+
chore: "bg-neutral-400",
|
|
34
|
+
docs: "bg-teal-500",
|
|
35
|
+
test: "bg-yellow-500",
|
|
36
|
+
config: "bg-orange-500",
|
|
33
37
|
};
|
|
34
38
|
|
|
35
39
|
export function resolveDetail(
|
|
36
|
-
kind: "group" | "file",
|
|
40
|
+
kind: "group" | "file" | "line",
|
|
37
41
|
id: string,
|
|
38
42
|
groups: FileGroup[],
|
|
39
43
|
files: FileChange[],
|
|
@@ -44,98 +48,234 @@ export function resolveDetail(
|
|
|
44
48
|
const groupFiles = files.filter((f) => group.files.includes(f.path));
|
|
45
49
|
return { kind: "group", group, files: groupFiles };
|
|
46
50
|
}
|
|
51
|
+
if (kind === "line") {
|
|
52
|
+
const hashIdx = id.indexOf("#");
|
|
53
|
+
if (hashIdx < 0) return null;
|
|
54
|
+
const filePath = id.slice(0, hashIdx);
|
|
55
|
+
const lineRef = id.slice(hashIdx + 1);
|
|
56
|
+
const rangeMatch = lineRef.match(/^L(\d+)(?:-L?(\d+))?/);
|
|
57
|
+
const lineNum = rangeMatch ? Number.parseInt(rangeMatch[1]!, 10) : undefined;
|
|
58
|
+
const lineEnd = rangeMatch?.[2] ? Number.parseInt(rangeMatch[2]!, 10) : undefined;
|
|
59
|
+
const file = files.find((f) => f.path === filePath);
|
|
60
|
+
if (!file) return null;
|
|
61
|
+
return { kind: "line", file, files: [file], scrollToLine: lineNum, scrollToLineEnd: lineEnd };
|
|
62
|
+
}
|
|
47
63
|
const file = files.find((f) => f.path === id);
|
|
48
64
|
if (!file) return null;
|
|
49
65
|
return { kind: "file", file, files: [file] };
|
|
50
66
|
}
|
|
51
67
|
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
function usePatchFetcher(sessionId: string | null | undefined, filePath: string | undefined) {
|
|
69
|
+
const [patch, setPatch] = useState<string | null>(null);
|
|
70
|
+
const [loading, setLoading] = useState(false);
|
|
71
|
+
const [error, setError] = useState<string | null>(null);
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setPatch(null);
|
|
75
|
+
setError(null);
|
|
76
|
+
setLoading(false);
|
|
77
|
+
}, [sessionId, filePath]);
|
|
78
|
+
|
|
79
|
+
const fetchPatch = useCallback(async () => {
|
|
80
|
+
if (!sessionId || !filePath) return;
|
|
81
|
+
setLoading(true);
|
|
82
|
+
setError(null);
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch(`/api/sessions/${sessionId}/diff?path=${encodeURIComponent(filePath)}`);
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const body = await res.json().catch(() => ({ error: "Failed to load diff" }));
|
|
87
|
+
throw new Error((body as { error?: string }).error ?? `HTTP ${res.status}`);
|
|
88
|
+
}
|
|
89
|
+
const data = await res.json() as { patch: string };
|
|
90
|
+
setPatch(data.patch);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
93
|
+
} finally {
|
|
94
|
+
setLoading(false);
|
|
95
|
+
}
|
|
96
|
+
}, [sessionId, filePath]);
|
|
97
|
+
|
|
98
|
+
return { patch, loading, error, fetchPatch };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function FileDetail({
|
|
102
|
+
file,
|
|
103
|
+
sessionId,
|
|
104
|
+
prUrl,
|
|
105
|
+
onClose,
|
|
106
|
+
scrollToLine,
|
|
107
|
+
scrollToLineEnd,
|
|
108
|
+
}: {
|
|
109
|
+
file: FileChange;
|
|
110
|
+
sessionId?: string | null;
|
|
111
|
+
prUrl?: string;
|
|
112
|
+
onClose?: () => void;
|
|
113
|
+
scrollToLine?: number;
|
|
114
|
+
scrollToLineEnd?: number;
|
|
115
|
+
}) {
|
|
116
|
+
const Icon = STATUS_ICON[file.status];
|
|
117
|
+
const { patch, loading, error, fetchPatch } = usePatchFetcher(sessionId, file.path);
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (sessionId && !patch && !loading && !error) {
|
|
121
|
+
fetchPatch();
|
|
122
|
+
}
|
|
123
|
+
}, [sessionId, patch, loading, error, fetchPatch]);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="flex flex-col h-full">
|
|
127
|
+
<div className="shrink-0 flex items-center justify-between gap-2 px-4 h-12 border-b">
|
|
128
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
129
|
+
<Icon className={`h-3 w-3 shrink-0 ${STATUS_COLOR[file.status]}`} />
|
|
130
|
+
<span className="text-[11px] font-mono truncate" title={file.path}>{file.path}</span>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
133
|
+
<span className="text-[10px] tabular-nums text-green-600 dark:text-green-400">+{file.additions}</span>
|
|
134
|
+
<span className="text-[10px] tabular-nums text-red-600 dark:text-red-400">-{file.deletions}</span>
|
|
69
135
|
{onClose && (
|
|
70
|
-
<button type="button" onClick={onClose} className="
|
|
136
|
+
<button type="button" onClick={onClose} className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground/40 hover:text-foreground hover:bg-accent/40 transition-colors">
|
|
71
137
|
<X className="h-3.5 w-3.5" />
|
|
72
138
|
</button>
|
|
73
139
|
)}
|
|
74
140
|
</div>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<div key={f.path} className="space-y-1">
|
|
83
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
84
|
-
<Icon className={`h-3 w-3 shrink-0 ${STATUS_COLOR[f.status]}`} />
|
|
85
|
-
<span className="text-xs font-mono truncate" title={f.path}>{f.path}</span>
|
|
86
|
-
<span className="text-xs text-green-500 shrink-0">+{f.additions}</span>
|
|
87
|
-
<span className="text-xs text-red-500 shrink-0">−{f.deletions}</span>
|
|
88
|
-
</div>
|
|
89
|
-
<p className="text-xs text-muted-foreground pl-5 break-words">{f.summary}</p>
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
})}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className="flex-1 overflow-y-auto">
|
|
144
|
+
{loading && (
|
|
145
|
+
<div className="flex items-center justify-center py-16 gap-2">
|
|
146
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground/40" />
|
|
147
|
+
<span className="text-xs text-muted-foreground/50">Loading diff</span>
|
|
93
148
|
</div>
|
|
94
|
-
|
|
149
|
+
)}
|
|
150
|
+
{error && (
|
|
151
|
+
<div className="flex flex-col items-center justify-center py-16 gap-3">
|
|
152
|
+
<div className="flex items-center gap-2 text-destructive">
|
|
153
|
+
<AlertCircle className="h-3.5 w-3.5" />
|
|
154
|
+
<p className="text-xs">{error}</p>
|
|
155
|
+
</div>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
onClick={fetchPatch}
|
|
159
|
+
className="text-[11px] text-muted-foreground/50 hover:text-foreground transition-colors"
|
|
160
|
+
>
|
|
161
|
+
Retry
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
{patch && (
|
|
166
|
+
<DiffViewer
|
|
167
|
+
patch={patch}
|
|
168
|
+
filePath={file.path}
|
|
169
|
+
sessionId={sessionId}
|
|
170
|
+
githubUrl={prUrl ? `${prUrl}/files` : undefined}
|
|
171
|
+
scrollToLine={scrollToLine}
|
|
172
|
+
scrollToLineEnd={scrollToLineEnd}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
95
175
|
</div>
|
|
96
|
-
|
|
97
|
-
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function DetailPane({
|
|
181
|
+
target,
|
|
182
|
+
sessionId,
|
|
183
|
+
prUrl,
|
|
184
|
+
onClose,
|
|
185
|
+
}: {
|
|
186
|
+
target: DetailTarget | null;
|
|
187
|
+
sessionId?: string | null;
|
|
188
|
+
prUrl?: string;
|
|
189
|
+
onClose?: () => void;
|
|
190
|
+
}) {
|
|
191
|
+
if (!target) return null;
|
|
192
|
+
|
|
193
|
+
if (target.kind === "group" && target.group) {
|
|
194
|
+
const g = target.group;
|
|
195
|
+
const totalAdd = target.files.reduce((s, f) => s + f.additions, 0);
|
|
196
|
+
const totalDel = target.files.reduce((s, f) => s + f.deletions, 0);
|
|
98
197
|
|
|
99
|
-
if (target.kind === "file" && target.file) {
|
|
100
|
-
const f = target.file;
|
|
101
|
-
const Icon = STATUS_ICON[f.status];
|
|
102
198
|
return (
|
|
103
|
-
<div className="
|
|
104
|
-
<div className="flex items-
|
|
105
|
-
<div className="
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
</div>
|
|
110
|
-
<div className="flex items-center gap-3">
|
|
111
|
-
<div className="flex items-center gap-1.5">
|
|
112
|
-
<Icon className={`h-3 w-3 ${STATUS_COLOR[f.status]}`} />
|
|
113
|
-
<span className="text-xs text-muted-foreground">{f.status}</span>
|
|
114
|
-
</div>
|
|
115
|
-
<span className="text-xs text-green-500">+{f.additions}</span>
|
|
116
|
-
<span className="text-xs text-red-500">−{f.deletions}</span>
|
|
117
|
-
</div>
|
|
199
|
+
<div className="flex flex-col h-full">
|
|
200
|
+
<div className="shrink-0 flex items-center justify-between gap-2 px-4 h-12 border-b">
|
|
201
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
202
|
+
<span className={`h-2 w-2 rounded-full shrink-0 ${TYPE_DOT[g.type] ?? TYPE_DOT.chore}`} />
|
|
203
|
+
<span className="text-xs font-medium truncate">{g.name}</span>
|
|
204
|
+
<span className="text-[10px] text-muted-foreground/30">{g.type}</span>
|
|
118
205
|
</div>
|
|
119
206
|
{onClose && (
|
|
120
|
-
<button type="button" onClick={onClose} className="
|
|
207
|
+
<button type="button" onClick={onClose} className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground/40 hover:text-foreground hover:bg-accent/40 transition-colors">
|
|
121
208
|
<X className="h-3.5 w-3.5" />
|
|
122
209
|
</button>
|
|
123
210
|
)}
|
|
124
211
|
</div>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
212
|
+
|
|
213
|
+
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-5">
|
|
214
|
+
<p className="text-[11px] text-muted-foreground/60 leading-relaxed">{g.description}</p>
|
|
215
|
+
|
|
216
|
+
{g.key_changes && g.key_changes.length > 0 && (
|
|
217
|
+
<div>
|
|
218
|
+
<div className="text-[10px] font-medium text-muted-foreground/40 uppercase tracking-wider mb-2">Key Changes</div>
|
|
219
|
+
<ul className="space-y-1.5">
|
|
220
|
+
{g.key_changes.map((change, i) => (
|
|
221
|
+
<li key={i} className="flex gap-2 text-[11px] text-muted-foreground/70 leading-relaxed">
|
|
222
|
+
<span className="text-muted-foreground/25 shrink-0 mt-px">·</span>
|
|
223
|
+
<span>{change}</span>
|
|
224
|
+
</li>
|
|
225
|
+
))}
|
|
226
|
+
</ul>
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{g.risk && (
|
|
231
|
+
<div>
|
|
232
|
+
<div className="text-[10px] font-medium text-muted-foreground/40 uppercase tracking-wider mb-1.5">Risk</div>
|
|
233
|
+
<p className="text-[11px] text-muted-foreground/60 leading-relaxed">{g.risk}</p>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
{g.dependencies && g.dependencies.length > 0 && (
|
|
238
|
+
<div>
|
|
239
|
+
<div className="text-[10px] font-medium text-muted-foreground/40 uppercase tracking-wider mb-1.5">Dependencies</div>
|
|
240
|
+
<div className="flex flex-wrap gap-1.5">
|
|
241
|
+
{g.dependencies.map((dep) => (
|
|
242
|
+
<span key={dep} className="text-[10px] px-1.5 py-0.5 rounded-md bg-accent/60 text-muted-foreground/60">{dep}</span>
|
|
243
|
+
))}
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
<div>
|
|
249
|
+
<div className="flex items-center gap-2 mb-2.5">
|
|
250
|
+
<span className="text-[10px] font-medium text-muted-foreground/40 uppercase tracking-wider">{target.files.length} files</span>
|
|
251
|
+
<span className="text-[10px] tabular-nums text-green-600 dark:text-green-400">+{totalAdd}</span>
|
|
252
|
+
<span className="text-[10px] tabular-nums text-red-600 dark:text-red-400">-{totalDel}</span>
|
|
253
|
+
</div>
|
|
254
|
+
<div className="space-y-px">
|
|
255
|
+
{target.files.map((f) => {
|
|
256
|
+
const Icon = STATUS_ICON[f.status];
|
|
257
|
+
return (
|
|
258
|
+
<div key={f.path} className="py-2">
|
|
259
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
260
|
+
<Icon className={`h-2.5 w-2.5 shrink-0 ${STATUS_COLOR[f.status]}`} />
|
|
261
|
+
<span className="text-[11px] font-mono truncate flex-1" title={f.path}>{f.path}</span>
|
|
262
|
+
<span className="text-[10px] tabular-nums text-green-600 dark:text-green-400 shrink-0">+{f.additions}</span>
|
|
263
|
+
<span className="text-[10px] tabular-nums text-red-600 dark:text-red-400 shrink-0">-{f.deletions}</span>
|
|
264
|
+
</div>
|
|
265
|
+
<p className="text-[11px] text-muted-foreground/40 mt-1 pl-[18px] leading-relaxed">{f.summary}</p>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
})}
|
|
133
269
|
</div>
|
|
134
270
|
</div>
|
|
135
|
-
|
|
271
|
+
</div>
|
|
136
272
|
</div>
|
|
137
273
|
);
|
|
138
274
|
}
|
|
139
275
|
|
|
276
|
+
if ((target.kind === "file" || target.kind === "line") && target.file) {
|
|
277
|
+
return <FileDetail file={target.file} sessionId={sessionId} prUrl={prUrl} onClose={onClose} scrollToLine={target.scrollToLine} scrollToLineEnd={target.scrollToLineEnd} />;
|
|
278
|
+
}
|
|
279
|
+
|
|
140
280
|
return null;
|
|
141
281
|
}
|