newpr 0.1.3 → 0.2.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 +22 -5
- package/src/github/fetch-pr.ts +43 -1
- package/src/history/store.ts +106 -1
- package/src/llm/client.ts +197 -0
- package/src/llm/prompts.ts +33 -8
- package/src/tui/Shell.tsx +7 -2
- package/src/types/github.ts +11 -0
- package/src/types/output.ts +44 -0
- package/src/web/client/App.tsx +29 -3
- package/src/web/client/components/AppShell.tsx +94 -47
- package/src/web/client/components/ChatSection.tsx +427 -0
- package/src/web/client/components/DetailPane.tsx +163 -75
- package/src/web/client/components/DiffViewer.tsx +679 -0
- package/src/web/client/components/InputScreen.tsx +110 -26
- package/src/web/client/components/Markdown.tsx +169 -43
- package/src/web/client/components/ResultsScreen.tsx +66 -71
- 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 +49 -40
- package/src/web/client/panels/StoryPanel.tsx +42 -22
- package/src/web/components/ui/tabs.tsx +3 -3
- package/src/web/server/routes.ts +716 -14
- package/src/web/server/session-manager.ts +11 -2
- package/src/web/server.ts +33 -0
- package/src/web/styles/built.css +1 -1
- package/src/web/styles/globals.css +117 -1
- package/src/web/client/panels/NarrativePanel.tsx +0 -9
- package/src/web/client/panels/SummaryPanel.tsx +0 -20
|
@@ -1,5 +1,7 @@
|
|
|
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
7
|
kind: "group" | "file";
|
|
@@ -22,14 +24,14 @@ const STATUS_COLOR: Record<FileStatus, string> = {
|
|
|
22
24
|
renamed: "text-blue-500",
|
|
23
25
|
};
|
|
24
26
|
|
|
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
|
|
27
|
+
const TYPE_DOT: Record<string, string> = {
|
|
28
|
+
feature: "bg-blue-500",
|
|
29
|
+
refactor: "bg-purple-500",
|
|
30
|
+
bugfix: "bg-red-500",
|
|
31
|
+
chore: "bg-neutral-400",
|
|
32
|
+
docs: "bg-teal-500",
|
|
33
|
+
test: "bg-yellow-500",
|
|
34
|
+
config: "bg-orange-500",
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
export function resolveDetail(
|
|
@@ -49,93 +51,179 @@ export function resolveDetail(
|
|
|
49
51
|
return { kind: "file", file, files: [file] };
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
function usePatchFetcher(sessionId: string | null | undefined, filePath: string | undefined) {
|
|
55
|
+
const [patch, setPatch] = useState<string | null>(null);
|
|
56
|
+
const [loading, setLoading] = useState(false);
|
|
57
|
+
const [error, setError] = useState<string | null>(null);
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
setPatch(null);
|
|
61
|
+
setError(null);
|
|
62
|
+
setLoading(false);
|
|
63
|
+
}, [sessionId, filePath]);
|
|
64
|
+
|
|
65
|
+
const fetchPatch = useCallback(async () => {
|
|
66
|
+
if (!sessionId || !filePath) return;
|
|
67
|
+
setLoading(true);
|
|
68
|
+
setError(null);
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(`/api/sessions/${sessionId}/diff?path=${encodeURIComponent(filePath)}`);
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
const body = await res.json().catch(() => ({ error: "Failed to load diff" }));
|
|
73
|
+
throw new Error((body as { error?: string }).error ?? `HTTP ${res.status}`);
|
|
74
|
+
}
|
|
75
|
+
const data = await res.json() as { patch: string };
|
|
76
|
+
setPatch(data.patch);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
79
|
+
} finally {
|
|
80
|
+
setLoading(false);
|
|
81
|
+
}
|
|
82
|
+
}, [sessionId, filePath]);
|
|
83
|
+
|
|
84
|
+
return { patch, loading, error, fetchPatch };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function FileDetail({
|
|
88
|
+
file,
|
|
89
|
+
sessionId,
|
|
90
|
+
prUrl,
|
|
91
|
+
onClose,
|
|
92
|
+
}: {
|
|
93
|
+
file: FileChange;
|
|
94
|
+
sessionId?: string | null;
|
|
95
|
+
prUrl?: string;
|
|
96
|
+
onClose?: () => void;
|
|
97
|
+
}) {
|
|
98
|
+
const Icon = STATUS_ICON[file.status];
|
|
99
|
+
const { patch, loading, error, fetchPatch } = usePatchFetcher(sessionId, file.path);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (sessionId && !patch && !loading && !error) {
|
|
103
|
+
fetchPatch();
|
|
104
|
+
}
|
|
105
|
+
}, [sessionId, patch, loading, error, fetchPatch]);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="flex flex-col h-full">
|
|
109
|
+
<div className="shrink-0 flex items-center justify-between gap-2 px-4 h-12 border-b">
|
|
110
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
111
|
+
<Icon className={`h-3 w-3 shrink-0 ${STATUS_COLOR[file.status]}`} />
|
|
112
|
+
<span className="text-[11px] font-mono truncate" title={file.path}>{file.path}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
115
|
+
<span className="text-[10px] tabular-nums text-green-600 dark:text-green-400">+{file.additions}</span>
|
|
116
|
+
<span className="text-[10px] tabular-nums text-red-600 dark:text-red-400">-{file.deletions}</span>
|
|
69
117
|
{onClose && (
|
|
70
|
-
<button type="button" onClick={onClose} className="
|
|
118
|
+
<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
119
|
<X className="h-3.5 w-3.5" />
|
|
72
120
|
</button>
|
|
73
121
|
)}
|
|
74
122
|
</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
|
-
})}
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div className="flex-1 overflow-y-auto">
|
|
126
|
+
{loading && (
|
|
127
|
+
<div className="flex items-center justify-center py-16 gap-2">
|
|
128
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground/40" />
|
|
129
|
+
<span className="text-xs text-muted-foreground/50">Loading diff</span>
|
|
93
130
|
</div>
|
|
94
|
-
|
|
131
|
+
)}
|
|
132
|
+
{error && (
|
|
133
|
+
<div className="flex flex-col items-center justify-center py-16 gap-3">
|
|
134
|
+
<div className="flex items-center gap-2 text-destructive">
|
|
135
|
+
<AlertCircle className="h-3.5 w-3.5" />
|
|
136
|
+
<p className="text-xs">{error}</p>
|
|
137
|
+
</div>
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
onClick={fetchPatch}
|
|
141
|
+
className="text-[11px] text-muted-foreground/50 hover:text-foreground transition-colors"
|
|
142
|
+
>
|
|
143
|
+
Retry
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
{patch && (
|
|
148
|
+
<DiffViewer
|
|
149
|
+
patch={patch}
|
|
150
|
+
filePath={file.path}
|
|
151
|
+
sessionId={sessionId}
|
|
152
|
+
githubUrl={prUrl ? `${prUrl}/files` : undefined}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
95
155
|
</div>
|
|
96
|
-
|
|
97
|
-
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function DetailPane({
|
|
161
|
+
target,
|
|
162
|
+
sessionId,
|
|
163
|
+
prUrl,
|
|
164
|
+
onClose,
|
|
165
|
+
}: {
|
|
166
|
+
target: DetailTarget | null;
|
|
167
|
+
sessionId?: string | null;
|
|
168
|
+
prUrl?: string;
|
|
169
|
+
onClose?: () => void;
|
|
170
|
+
}) {
|
|
171
|
+
if (!target) return null;
|
|
172
|
+
|
|
173
|
+
if (target.kind === "group" && target.group) {
|
|
174
|
+
const g = target.group;
|
|
175
|
+
const totalAdd = target.files.reduce((s, f) => s + f.additions, 0);
|
|
176
|
+
const totalDel = target.files.reduce((s, f) => s + f.deletions, 0);
|
|
98
177
|
|
|
99
|
-
if (target.kind === "file" && target.file) {
|
|
100
|
-
const f = target.file;
|
|
101
|
-
const Icon = STATUS_ICON[f.status];
|
|
102
178
|
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>
|
|
179
|
+
<div className="flex flex-col h-full">
|
|
180
|
+
<div className="shrink-0 flex items-center justify-between gap-2 px-4 h-12 border-b">
|
|
181
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
182
|
+
<span className={`h-2 w-2 rounded-full shrink-0 ${TYPE_DOT[g.type] ?? TYPE_DOT.chore}`} />
|
|
183
|
+
<span className="text-xs font-medium truncate">{g.name}</span>
|
|
184
|
+
<span className="text-[10px] text-muted-foreground/30">{g.type}</span>
|
|
118
185
|
</div>
|
|
119
186
|
{onClose && (
|
|
120
|
-
<button type="button" onClick={onClose} className="
|
|
187
|
+
<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
188
|
<X className="h-3.5 w-3.5" />
|
|
122
189
|
</button>
|
|
123
190
|
)}
|
|
124
191
|
</div>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
192
|
+
|
|
193
|
+
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-4">
|
|
194
|
+
<p className="text-[11px] text-muted-foreground/60 leading-relaxed">{g.description}</p>
|
|
195
|
+
|
|
196
|
+
<div>
|
|
197
|
+
<div className="flex items-center gap-2 mb-2.5">
|
|
198
|
+
<span className="text-[10px] font-medium text-muted-foreground/40 uppercase tracking-wider">{target.files.length} files</span>
|
|
199
|
+
<span className="text-[10px] tabular-nums text-green-600 dark:text-green-400">+{totalAdd}</span>
|
|
200
|
+
<span className="text-[10px] tabular-nums text-red-600 dark:text-red-400">-{totalDel}</span>
|
|
201
|
+
</div>
|
|
202
|
+
<div className="space-y-px">
|
|
203
|
+
{target.files.map((f) => {
|
|
204
|
+
const Icon = STATUS_ICON[f.status];
|
|
205
|
+
return (
|
|
206
|
+
<div key={f.path} className="py-2">
|
|
207
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
208
|
+
<Icon className={`h-2.5 w-2.5 shrink-0 ${STATUS_COLOR[f.status]}`} />
|
|
209
|
+
<span className="text-[11px] font-mono truncate flex-1" title={f.path}>{f.path}</span>
|
|
210
|
+
<span className="text-[10px] tabular-nums text-green-600 dark:text-green-400 shrink-0">+{f.additions}</span>
|
|
211
|
+
<span className="text-[10px] tabular-nums text-red-600 dark:text-red-400 shrink-0">-{f.deletions}</span>
|
|
212
|
+
</div>
|
|
213
|
+
<p className="text-[11px] text-muted-foreground/40 mt-1 pl-[18px] leading-relaxed">{f.summary}</p>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
})}
|
|
133
217
|
</div>
|
|
134
218
|
</div>
|
|
135
|
-
|
|
219
|
+
</div>
|
|
136
220
|
</div>
|
|
137
221
|
);
|
|
138
222
|
}
|
|
139
223
|
|
|
224
|
+
if (target.kind === "file" && target.file) {
|
|
225
|
+
return <FileDetail file={target.file} sessionId={sessionId} prUrl={prUrl} onClose={onClose} />;
|
|
226
|
+
}
|
|
227
|
+
|
|
140
228
|
return null;
|
|
141
229
|
}
|