newpr 0.1.1 → 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/cli/args.ts +6 -1
- package/src/cli/index.ts +2 -2
- package/src/github/fetch-pr.ts +43 -1
- package/src/history/store.ts +106 -1
- package/src/llm/cartoon.ts +128 -0
- 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 +51 -0
- package/src/web/client/App.tsx +32 -2
- 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 +135 -110
- package/src/web/client/components/TipTapEditor.tsx +405 -0
- package/src/web/client/hooks/useAnalysis.ts +8 -1
- package/src/web/client/hooks/useFeatures.ts +18 -0
- package/src/web/client/lib/shiki.ts +63 -0
- package/src/web/client/panels/CartoonPanel.tsx +153 -0
- 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 +752 -2
- package/src/web/server/session-manager.ts +11 -2
- package/src/web/server.ts +42 -2
- 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,15 +1,14 @@
|
|
|
1
|
-
import { useState,
|
|
2
|
-
import { ArrowLeft,
|
|
3
|
-
import { Button } from "../../components/ui/button.tsx";
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { ArrowLeft, Layers, FolderTree, BookOpen, MessageSquare, GitBranch, Sparkles } from "lucide-react";
|
|
4
3
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../../components/ui/tabs.tsx";
|
|
5
4
|
import type { NewprOutput } from "../../../types/output.ts";
|
|
6
|
-
import { SummaryPanel } from "../panels/SummaryPanel.tsx";
|
|
7
5
|
import { GroupsPanel } from "../panels/GroupsPanel.tsx";
|
|
8
6
|
import { FilesPanel } from "../panels/FilesPanel.tsx";
|
|
9
|
-
import { NarrativePanel } from "../panels/NarrativePanel.tsx";
|
|
10
7
|
import { StoryPanel } from "../panels/StoryPanel.tsx";
|
|
8
|
+
import { DiscussionPanel } from "../panels/DiscussionPanel.tsx";
|
|
9
|
+
import { CartoonPanel } from "../panels/CartoonPanel.tsx";
|
|
11
10
|
|
|
12
|
-
const VALID_TABS = ["story", "
|
|
11
|
+
const VALID_TABS = ["story", "discussion", "groups", "files", "cartoon"] as const;
|
|
13
12
|
type TabValue = typeof VALID_TABS[number];
|
|
14
13
|
|
|
15
14
|
function getInitialTab(): TabValue {
|
|
@@ -24,11 +23,11 @@ function setTabParam(tab: string) {
|
|
|
24
23
|
window.history.replaceState(null, "", url.toString());
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
const
|
|
28
|
-
low: "bg-green-500
|
|
29
|
-
medium: "bg-yellow-500
|
|
30
|
-
high: "bg-red-500
|
|
31
|
-
critical: "bg-red-
|
|
26
|
+
const RISK_DOT: Record<string, string> = {
|
|
27
|
+
low: "bg-green-500",
|
|
28
|
+
medium: "bg-yellow-500",
|
|
29
|
+
high: "bg-red-500",
|
|
30
|
+
critical: "bg-red-600",
|
|
32
31
|
};
|
|
33
32
|
|
|
34
33
|
export function ResultsScreen({
|
|
@@ -36,150 +35,176 @@ export function ResultsScreen({
|
|
|
36
35
|
onBack,
|
|
37
36
|
activeId,
|
|
38
37
|
onAnchorClick,
|
|
38
|
+
cartoonEnabled,
|
|
39
|
+
sessionId,
|
|
39
40
|
}: {
|
|
40
41
|
data: NewprOutput;
|
|
41
42
|
onBack: () => void;
|
|
42
43
|
activeId: string | null;
|
|
43
44
|
onAnchorClick: (kind: "group" | "file", id: string) => void;
|
|
45
|
+
cartoonEnabled?: boolean;
|
|
46
|
+
sessionId?: string | null;
|
|
44
47
|
}) {
|
|
45
48
|
const { meta, summary } = data;
|
|
46
49
|
const [tab, setTab] = useState<TabValue>(getInitialTab);
|
|
47
|
-
|
|
48
|
-
const
|
|
50
|
+
|
|
51
|
+
const stickyRef = useRef<HTMLDivElement>(null);
|
|
52
|
+
const collapsibleRef = useRef<HTMLDivElement>(null);
|
|
53
|
+
const compactRef = useRef<HTMLDivElement>(null);
|
|
49
54
|
|
|
50
55
|
useEffect(() => {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
const sticky = stickyRef.current;
|
|
57
|
+
const collapsible = collapsibleRef.current;
|
|
58
|
+
const compact = compactRef.current;
|
|
59
|
+
if (!sticky || !collapsible || !compact) return;
|
|
60
|
+
|
|
61
|
+
const scrollParent = sticky.closest("main") ?? sticky.closest("[class*=overflow-y-auto]");
|
|
62
|
+
if (!scrollParent) return;
|
|
63
|
+
|
|
64
|
+
let wasScrolled = false;
|
|
65
|
+
|
|
66
|
+
const onScroll = () => {
|
|
67
|
+
const scrolled = scrollParent.scrollTop > 0;
|
|
68
|
+
if (scrolled === wasScrolled) return;
|
|
69
|
+
wasScrolled = scrolled;
|
|
70
|
+
|
|
71
|
+
collapsible.style.maxHeight = scrolled ? "0px" : "none";
|
|
72
|
+
collapsible.style.opacity = scrolled ? "0" : "1";
|
|
73
|
+
compact.style.maxHeight = scrolled ? "40px" : "0px";
|
|
74
|
+
compact.style.opacity = scrolled ? "1" : "0";
|
|
75
|
+
sticky.classList.toggle("border-b", scrolled);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
scrollParent.addEventListener("scroll", onScroll, { passive: true });
|
|
79
|
+
return () => scrollParent.removeEventListener("scroll", onScroll);
|
|
59
80
|
}, []);
|
|
60
81
|
|
|
61
|
-
|
|
82
|
+
const handleTabChange = useCallback((value: string) => {
|
|
62
83
|
setTab(value as TabValue);
|
|
63
84
|
setTabParam(value);
|
|
64
|
-
}
|
|
85
|
+
}, []);
|
|
65
86
|
|
|
66
87
|
const repoSlug = meta.pr_url.replace(/^https?:\/\/github\.com\//, "").replace(/\/pull\//, "#");
|
|
67
88
|
|
|
68
89
|
return (
|
|
69
|
-
<
|
|
70
|
-
<div ref={
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
<Tabs value={tab} onValueChange={handleTabChange} className="flex flex-col">
|
|
91
|
+
<div ref={stickyRef} className="sticky top-0 z-10 bg-background -mx-10 px-10">
|
|
92
|
+
<div ref={collapsibleRef} className="overflow-hidden transition-[max-height,opacity] duration-200">
|
|
93
|
+
<div className="pb-4 pt-1">
|
|
94
|
+
<div className="flex items-center gap-2 mb-3">
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={onBack}
|
|
98
|
+
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 shrink-0 -ml-1"
|
|
99
|
+
>
|
|
100
|
+
<ArrowLeft className="h-3.5 w-3.5" />
|
|
101
|
+
</button>
|
|
79
102
|
<a
|
|
80
103
|
href={meta.pr_url}
|
|
81
104
|
target="_blank"
|
|
82
105
|
rel="noopener noreferrer"
|
|
83
|
-
className="text-muted-foreground font-mono
|
|
106
|
+
className="text-[11px] text-muted-foreground/50 font-mono hover:text-foreground transition-colors"
|
|
84
107
|
>
|
|
85
108
|
{repoSlug}
|
|
86
109
|
</a>
|
|
87
|
-
<span className={`
|
|
88
|
-
{summary.risk_level}
|
|
89
|
-
</span>
|
|
110
|
+
<span className={`h-1.5 w-1.5 rounded-full shrink-0 ${RISK_DOT[summary.risk_level] ?? RISK_DOT.medium}`} />
|
|
90
111
|
</div>
|
|
91
112
|
|
|
92
|
-
<h1 className="text-
|
|
113
|
+
<h1 className="text-sm font-semibold tracking-tight mb-3 line-clamp-2">{meta.pr_title}</h1>
|
|
93
114
|
|
|
94
|
-
<div className="flex flex-wrap gap-x-
|
|
115
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 text-[11px] text-muted-foreground/50">
|
|
95
116
|
<a
|
|
96
117
|
href={meta.author_url ?? `https://github.com/${meta.author}`}
|
|
97
118
|
target="_blank"
|
|
98
119
|
rel="noopener noreferrer"
|
|
99
|
-
className="flex items-center gap-1.5
|
|
120
|
+
className="flex items-center gap-1.5 hover:text-foreground transition-colors"
|
|
100
121
|
>
|
|
101
|
-
{meta.author_avatar
|
|
122
|
+
{meta.author_avatar && (
|
|
102
123
|
<img src={meta.author_avatar} alt={meta.author} className="h-4 w-4 rounded-full" />
|
|
103
|
-
) : (
|
|
104
|
-
<User className="h-3.5 w-3.5" />
|
|
105
124
|
)}
|
|
106
125
|
<span>{meta.author}</span>
|
|
107
126
|
</a>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
<
|
|
111
|
-
<span className="
|
|
112
|
-
<span className="
|
|
127
|
+
<span className="text-muted-foreground/15">|</span>
|
|
128
|
+
<div className="flex items-center gap-1">
|
|
129
|
+
<GitBranch className="h-3 w-3 text-muted-foreground/30" />
|
|
130
|
+
<span className="font-mono">{meta.head_branch}</span>
|
|
131
|
+
<span className="text-muted-foreground/25">→</span>
|
|
132
|
+
<span className="font-mono">{meta.base_branch}</span>
|
|
113
133
|
</div>
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
<span className="text-green-
|
|
117
|
-
<span className="text-red-
|
|
118
|
-
<span className="text-muted-foreground/
|
|
119
|
-
<span>{meta.total_files_changed} files</span>
|
|
120
|
-
</div>
|
|
121
|
-
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
122
|
-
<Bot className="h-3.5 w-3.5" />
|
|
123
|
-
<span>{meta.model_used.split("/").pop()}</span>
|
|
134
|
+
<span className="text-muted-foreground/15">|</span>
|
|
135
|
+
<div className="flex items-center gap-1.5">
|
|
136
|
+
<span className="text-green-600 dark:text-green-400 tabular-nums">+{meta.total_additions}</span>
|
|
137
|
+
<span className="text-red-600 dark:text-red-400 tabular-nums">-{meta.total_deletions}</span>
|
|
138
|
+
<span className="text-muted-foreground/25">·</span>
|
|
139
|
+
<span className="tabular-nums">{meta.total_files_changed} files</span>
|
|
124
140
|
</div>
|
|
125
141
|
</div>
|
|
126
|
-
|
|
127
|
-
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
128
144
|
|
|
129
|
-
{
|
|
130
|
-
<div className="flex items-center gap-
|
|
131
|
-
<
|
|
145
|
+
<div ref={compactRef} className="overflow-hidden transition-[max-height,opacity] duration-200" style={{ maxHeight: 0, opacity: 0 }}>
|
|
146
|
+
<div className="flex items-center gap-2.5 min-w-0 pb-2.5">
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={onBack}
|
|
150
|
+
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 shrink-0 -ml-1"
|
|
151
|
+
>
|
|
132
152
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
133
|
-
</
|
|
134
|
-
<span className=
|
|
135
|
-
<span className="text-xs
|
|
136
|
-
<span className=
|
|
137
|
-
{summary.risk_level}
|
|
138
|
-
</span>
|
|
153
|
+
</button>
|
|
154
|
+
<span className={`h-1.5 w-1.5 rounded-full shrink-0 ${RISK_DOT[summary.risk_level] ?? RISK_DOT.medium}`} />
|
|
155
|
+
<span className="text-xs font-medium truncate flex-1">{meta.pr_title}</span>
|
|
156
|
+
<span className="text-[10px] text-muted-foreground/30 font-mono shrink-0">{repoSlug}</span>
|
|
139
157
|
</div>
|
|
140
|
-
|
|
158
|
+
</div>
|
|
141
159
|
|
|
142
|
-
<
|
|
143
|
-
<
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
<TabsList className="w-full justify-start">
|
|
161
|
+
<TabsTrigger value="story">
|
|
162
|
+
<BookOpen className="h-3 w-3 shrink-0" />
|
|
163
|
+
Story
|
|
164
|
+
</TabsTrigger>
|
|
165
|
+
<TabsTrigger value="discussion">
|
|
166
|
+
<MessageSquare className="h-3 w-3 shrink-0" />
|
|
167
|
+
Discussion
|
|
168
|
+
</TabsTrigger>
|
|
169
|
+
<TabsTrigger value="groups">
|
|
170
|
+
<Layers className="h-3 w-3 shrink-0" />
|
|
171
|
+
Groups
|
|
172
|
+
</TabsTrigger>
|
|
173
|
+
<TabsTrigger value="files">
|
|
174
|
+
<FolderTree className="h-3 w-3 shrink-0" />
|
|
175
|
+
Files
|
|
176
|
+
</TabsTrigger>
|
|
177
|
+
{cartoonEnabled && (
|
|
178
|
+
<TabsTrigger value="cartoon">
|
|
179
|
+
<Sparkles className="h-3 w-3 shrink-0" />
|
|
180
|
+
Comic
|
|
155
181
|
</TabsTrigger>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
Files
|
|
159
|
-
</TabsTrigger>
|
|
160
|
-
<TabsTrigger value="narrative" className="gap-1.5">
|
|
161
|
-
<FileText className="h-3.5 w-3.5 shrink-0" />
|
|
162
|
-
Narrative
|
|
163
|
-
</TabsTrigger>
|
|
164
|
-
</TabsList>
|
|
165
|
-
|
|
166
|
-
<TabsContent value="story">
|
|
167
|
-
<StoryPanel data={data} activeId={activeId} onAnchorClick={onAnchorClick} />
|
|
168
|
-
</TabsContent>
|
|
169
|
-
<TabsContent value="summary">
|
|
170
|
-
<SummaryPanel summary={data.summary} />
|
|
171
|
-
</TabsContent>
|
|
172
|
-
<TabsContent value="groups">
|
|
173
|
-
<GroupsPanel groups={data.groups} />
|
|
174
|
-
</TabsContent>
|
|
175
|
-
<TabsContent value="files">
|
|
176
|
-
<FilesPanel files={data.files} />
|
|
177
|
-
</TabsContent>
|
|
178
|
-
<TabsContent value="narrative">
|
|
179
|
-
<NarrativePanel narrative={data.narrative} />
|
|
180
|
-
</TabsContent>
|
|
181
|
-
</Tabs>
|
|
182
|
+
)}
|
|
183
|
+
</TabsList>
|
|
182
184
|
</div>
|
|
183
|
-
|
|
185
|
+
|
|
186
|
+
<TabsContent value="story">
|
|
187
|
+
<StoryPanel data={data} activeId={activeId} onAnchorClick={onAnchorClick} />
|
|
188
|
+
</TabsContent>
|
|
189
|
+
<TabsContent value="discussion">
|
|
190
|
+
<DiscussionPanel sessionId={sessionId} />
|
|
191
|
+
</TabsContent>
|
|
192
|
+
<TabsContent value="groups">
|
|
193
|
+
<GroupsPanel groups={data.groups} />
|
|
194
|
+
</TabsContent>
|
|
195
|
+
<TabsContent value="files">
|
|
196
|
+
<FilesPanel
|
|
197
|
+
files={data.files}
|
|
198
|
+
groups={data.groups}
|
|
199
|
+
selectedPath={activeId?.startsWith("file:") ? activeId.slice(5) : null}
|
|
200
|
+
onFileSelect={(path: string) => onAnchorClick("file", path)}
|
|
201
|
+
/>
|
|
202
|
+
</TabsContent>
|
|
203
|
+
{cartoonEnabled && (
|
|
204
|
+
<TabsContent value="cartoon">
|
|
205
|
+
<CartoonPanel data={data} sessionId={sessionId} />
|
|
206
|
+
</TabsContent>
|
|
207
|
+
)}
|
|
208
|
+
</Tabs>
|
|
184
209
|
);
|
|
185
210
|
}
|