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.
Files changed (37) hide show
  1. package/package.json +11 -1
  2. package/src/analyzer/pipeline.ts +22 -5
  3. package/src/cli/args.ts +6 -1
  4. package/src/cli/index.ts +2 -2
  5. package/src/github/fetch-pr.ts +43 -1
  6. package/src/history/store.ts +106 -1
  7. package/src/llm/cartoon.ts +128 -0
  8. package/src/llm/client.ts +197 -0
  9. package/src/llm/prompts.ts +33 -8
  10. package/src/tui/Shell.tsx +7 -2
  11. package/src/types/github.ts +11 -0
  12. package/src/types/output.ts +51 -0
  13. package/src/web/client/App.tsx +32 -2
  14. package/src/web/client/components/AppShell.tsx +94 -47
  15. package/src/web/client/components/ChatSection.tsx +427 -0
  16. package/src/web/client/components/DetailPane.tsx +163 -75
  17. package/src/web/client/components/DiffViewer.tsx +679 -0
  18. package/src/web/client/components/InputScreen.tsx +110 -26
  19. package/src/web/client/components/Markdown.tsx +169 -43
  20. package/src/web/client/components/ResultsScreen.tsx +135 -110
  21. package/src/web/client/components/TipTapEditor.tsx +405 -0
  22. package/src/web/client/hooks/useAnalysis.ts +8 -1
  23. package/src/web/client/hooks/useFeatures.ts +18 -0
  24. package/src/web/client/lib/shiki.ts +63 -0
  25. package/src/web/client/panels/CartoonPanel.tsx +153 -0
  26. package/src/web/client/panels/DiscussionPanel.tsx +158 -0
  27. package/src/web/client/panels/FilesPanel.tsx +435 -54
  28. package/src/web/client/panels/GroupsPanel.tsx +49 -40
  29. package/src/web/client/panels/StoryPanel.tsx +42 -22
  30. package/src/web/components/ui/tabs.tsx +3 -3
  31. package/src/web/server/routes.ts +752 -2
  32. package/src/web/server/session-manager.ts +11 -2
  33. package/src/web/server.ts +42 -2
  34. package/src/web/styles/built.css +1 -1
  35. package/src/web/styles/globals.css +117 -1
  36. package/src/web/client/panels/NarrativePanel.tsx +0 -9
  37. package/src/web/client/panels/SummaryPanel.tsx +0 -20
@@ -1,15 +1,14 @@
1
- import { useState, useRef, useEffect } from "react";
2
- import { ArrowLeft, FileText, Layers, FolderTree, BookOpen, LayoutList, GitBranch, User, Files, Bot } from "lucide-react";
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", "summary", "groups", "files", "narrative"] as const;
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 RISK_COLORS: Record<string, string> = {
28
- low: "bg-green-500/10 text-green-600 dark:text-green-400",
29
- medium: "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
30
- high: "bg-red-500/10 text-red-600 dark:text-red-400",
31
- critical: "bg-red-500/20 text-red-700 dark:text-red-300",
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
- const [scrolled, setScrolled] = useState(false);
48
- const sentinelRef = useRef<HTMLDivElement>(null);
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 sentinel = sentinelRef.current;
52
- if (!sentinel) return;
53
- const observer = new IntersectionObserver(
54
- ([entry]) => setScrolled(!entry!.isIntersecting),
55
- { threshold: 0 },
56
- );
57
- observer.observe(sentinel);
58
- return () => observer.disconnect();
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
- function handleTabChange(value: string) {
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
- <div className="flex flex-col">
70
- <div ref={sentinelRef} />
71
-
72
- <div className={`sticky top-0 z-10 bg-background transition-all ${scrolled ? "pb-3 pt-1 border-b" : "pb-6 pt-0"}`}>
73
- {!scrolled && (
74
- <>
75
- <div className="flex items-center gap-3 mb-4">
76
- <Button variant="ghost" size="icon" className="shrink-0 -ml-2" onClick={onBack}>
77
- <ArrowLeft className="h-4 w-4" />
78
- </Button>
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 text-sm hover:text-foreground transition-colors"
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={`text-xs font-medium px-2 py-0.5 rounded-full ${RISK_COLORS[summary.risk_level] ?? RISK_COLORS.medium}`}>
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-2xl font-bold tracking-tight mb-5" style={{ textWrap: "balance" }}>{meta.pr_title}</h1>
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-5 gap-y-2 mb-6">
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 text-sm text-muted-foreground hover:text-foreground transition-colors"
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
- <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
109
- <GitBranch className="h-3.5 w-3.5" />
110
- <span className="font-mono text-xs">{meta.base_branch}</span>
111
- <span className="text-muted-foreground/50">←</span>
112
- <span className="font-mono text-xs">{meta.head_branch}</span>
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
- <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
115
- <Files className="h-3.5 w-3.5" />
116
- <span className="text-green-500">+{meta.total_additions}</span>
117
- <span className="text-red-500">−{meta.total_deletions}</span>
118
- <span className="text-muted-foreground/50">·</span>
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
- {scrolled && (
130
- <div className="flex items-center gap-3 min-w-0">
131
- <Button variant="ghost" size="icon" className="shrink-0 -ml-2 h-7 w-7" onClick={onBack}>
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
- </Button>
134
- <span className="text-sm font-semibold truncate flex-1">{meta.pr_title}</span>
135
- <span className="text-xs text-muted-foreground font-mono shrink-0">{repoSlug}</span>
136
- <span className={`text-[10px] font-medium px-1.5 py-0.5 rounded-full shrink-0 ${RISK_COLORS[summary.risk_level] ?? RISK_COLORS.medium}`}>
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
- <Tabs value={tab} onValueChange={handleTabChange} className="w-full">
143
- <TabsList className="w-full justify-start overflow-x-auto">
144
- <TabsTrigger value="story" className="gap-1.5">
145
- <BookOpen className="h-3.5 w-3.5 shrink-0" />
146
- Story
147
- </TabsTrigger>
148
- <TabsTrigger value="summary" className="gap-1.5">
149
- <LayoutList className="h-3.5 w-3.5 shrink-0" />
150
- Summary
151
- </TabsTrigger>
152
- <TabsTrigger value="groups" className="gap-1.5">
153
- <Layers className="h-3.5 w-3.5 shrink-0" />
154
- Groups
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
- <TabsTrigger value="files" className="gap-1.5">
157
- <FolderTree className="h-3.5 w-3.5 shrink-0" />
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
- </div>
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
  }