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.
Files changed (43) hide show
  1. package/package.json +11 -1
  2. package/src/analyzer/pipeline.ts +37 -15
  3. package/src/analyzer/progress.ts +2 -0
  4. package/src/cli/index.ts +7 -2
  5. package/src/cli/preflight.ts +126 -0
  6. package/src/github/fetch-pr.ts +53 -1
  7. package/src/history/store.ts +107 -1
  8. package/src/history/types.ts +1 -0
  9. package/src/llm/client.ts +197 -0
  10. package/src/llm/prompts.ts +80 -19
  11. package/src/llm/response-parser.ts +13 -1
  12. package/src/tui/Shell.tsx +7 -2
  13. package/src/types/github.ts +14 -0
  14. package/src/types/output.ts +50 -0
  15. package/src/web/client/App.tsx +33 -5
  16. package/src/web/client/components/AppShell.tsx +107 -47
  17. package/src/web/client/components/ChatSection.tsx +427 -0
  18. package/src/web/client/components/DetailPane.tsx +217 -77
  19. package/src/web/client/components/DiffViewer.tsx +713 -0
  20. package/src/web/client/components/InputScreen.tsx +178 -27
  21. package/src/web/client/components/LoadingTimeline.tsx +19 -6
  22. package/src/web/client/components/Markdown.tsx +220 -41
  23. package/src/web/client/components/ResultsScreen.tsx +109 -73
  24. package/src/web/client/components/ReviewModal.tsx +187 -0
  25. package/src/web/client/components/SettingsPanel.tsx +62 -86
  26. package/src/web/client/components/TipTapEditor.tsx +405 -0
  27. package/src/web/client/hooks/useAnalysis.ts +8 -1
  28. package/src/web/client/lib/shiki.ts +63 -0
  29. package/src/web/client/panels/CartoonPanel.tsx +94 -37
  30. package/src/web/client/panels/DiscussionPanel.tsx +158 -0
  31. package/src/web/client/panels/FilesPanel.tsx +435 -54
  32. package/src/web/client/panels/GroupsPanel.tsx +62 -40
  33. package/src/web/client/panels/StoryPanel.tsx +43 -23
  34. package/src/web/components/ui/tabs.tsx +3 -3
  35. package/src/web/server/routes.ts +856 -14
  36. package/src/web/server/session-manager.ts +11 -2
  37. package/src/web/server.ts +66 -4
  38. package/src/web/styles/built.css +1 -1
  39. package/src/web/styles/globals.css +117 -1
  40. package/src/workspace/agent.ts +22 -6
  41. package/src/workspace/explore.ts +41 -16
  42. package/src/web/client/panels/NarrativePanel.tsx +0 -9
  43. package/src/web/client/panels/SummaryPanel.tsx +0 -20
@@ -1,16 +1,15 @@
1
1
  import { useState, useCallback, useEffect, useRef } from "react";
2
- import { ArrowLeft, FileText, Layers, FolderTree, BookOpen, LayoutList, GitBranch, User, Files, Bot, Sparkles } from "lucide-react";
3
- import { Button } from "../../components/ui/button.tsx";
2
+ import { ArrowLeft, Layers, FolderTree, BookOpen, MessageSquare, GitBranch, Sparkles, Check, ChevronDown } 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";
11
9
  import { CartoonPanel } from "../panels/CartoonPanel.tsx";
10
+ import { ReviewModal } from "./ReviewModal.tsx";
12
11
 
13
- const VALID_TABS = ["story", "summary", "groups", "files", "narrative", "cartoon"] as const;
12
+ const VALID_TABS = ["story", "discussion", "groups", "files", "cartoon"] as const;
14
13
  type TabValue = typeof VALID_TABS[number];
15
14
 
16
15
  function getInitialTab(): TabValue {
@@ -25,11 +24,18 @@ function setTabParam(tab: string) {
25
24
  window.history.replaceState(null, "", url.toString());
26
25
  }
27
26
 
28
- const RISK_COLORS: Record<string, string> = {
29
- low: "bg-green-500/10 text-green-600 dark:text-green-400",
30
- medium: "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
31
- high: "bg-red-500/10 text-red-600 dark:text-red-400",
32
- critical: "bg-red-500/20 text-red-700 dark:text-red-300",
27
+ const RISK_DOT: Record<string, string> = {
28
+ low: "bg-green-500",
29
+ medium: "bg-yellow-500",
30
+ high: "bg-red-500",
31
+ critical: "bg-red-600",
32
+ };
33
+
34
+ const STATE_STYLES: Record<string, { bg: string; text: string; label: string }> = {
35
+ open: { bg: "bg-green-500/10", text: "text-green-600 dark:text-green-400", label: "Open" },
36
+ merged: { bg: "bg-purple-500/10", text: "text-purple-600 dark:text-purple-400", label: "Merged" },
37
+ closed: { bg: "bg-red-500/10", text: "text-red-600 dark:text-red-400", label: "Closed" },
38
+ draft: { bg: "bg-neutral-500/10", text: "text-neutral-500", label: "Draft" },
33
39
  };
34
40
 
35
41
  export function ResultsScreen({
@@ -39,16 +45,19 @@ export function ResultsScreen({
39
45
  onAnchorClick,
40
46
  cartoonEnabled,
41
47
  sessionId,
48
+ onTabChange,
42
49
  }: {
43
50
  data: NewprOutput;
44
51
  onBack: () => void;
45
52
  activeId: string | null;
46
- onAnchorClick: (kind: "group" | "file", id: string) => void;
53
+ onAnchorClick: (kind: "group" | "file" | "line", id: string) => void;
47
54
  cartoonEnabled?: boolean;
48
55
  sessionId?: string | null;
56
+ onTabChange?: (tab: string) => void;
49
57
  }) {
50
58
  const { meta, summary } = data;
51
59
  const [tab, setTab] = useState<TabValue>(getInitialTab);
60
+ const [reviewOpen, setReviewOpen] = useState(false);
52
61
 
53
62
  const stickyRef = useRef<HTMLDivElement>(null);
54
63
  const collapsibleRef = useRef<HTMLDivElement>(null);
@@ -84,106 +93,127 @@ export function ResultsScreen({
84
93
  const handleTabChange = useCallback((value: string) => {
85
94
  setTab(value as TabValue);
86
95
  setTabParam(value);
87
- }, []);
96
+ onTabChange?.(value);
97
+ }, [onTabChange]);
88
98
 
89
99
  const repoSlug = meta.pr_url.replace(/^https?:\/\/github\.com\//, "").replace(/\/pull\//, "#");
90
100
 
91
101
  return (
102
+ <>
92
103
  <Tabs value={tab} onValueChange={handleTabChange} className="flex flex-col">
93
- <div ref={stickyRef} className="sticky top-0 z-10 bg-background pb-2 -mx-10 px-10">
104
+ <div ref={stickyRef} className="sticky top-0 z-10 bg-background -mx-10 px-10">
94
105
  <div ref={collapsibleRef} className="overflow-hidden transition-[max-height,opacity] duration-200">
95
- <div className="pb-3 pt-1">
96
- <div className="flex items-center gap-3 mb-3">
97
- <Button variant="ghost" size="icon" className="shrink-0 -ml-2" onClick={onBack}>
98
- <ArrowLeft className="h-4 w-4" />
99
- </Button>
106
+ <div className="pb-4 pt-1">
107
+ <div className="flex items-center gap-2 mb-3">
108
+ <button
109
+ type="button"
110
+ onClick={onBack}
111
+ 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"
112
+ >
113
+ <ArrowLeft className="h-3.5 w-3.5" />
114
+ </button>
100
115
  <a
101
116
  href={meta.pr_url}
102
117
  target="_blank"
103
118
  rel="noopener noreferrer"
104
- className="text-muted-foreground font-mono text-sm hover:text-foreground transition-colors"
119
+ className="text-[11px] text-muted-foreground/50 font-mono hover:text-foreground transition-colors"
105
120
  >
106
121
  {repoSlug}
107
122
  </a>
108
- <span className={`text-xs font-medium px-2 py-0.5 rounded-full ${RISK_COLORS[summary.risk_level] ?? RISK_COLORS.medium}`}>
109
- {summary.risk_level}
110
- </span>
123
+ {meta.pr_state && (() => {
124
+ const s = STATE_STYLES[meta.pr_state] ?? STATE_STYLES.open!;
125
+ return (
126
+ <span className={`text-[10px] font-medium px-1.5 py-0.5 rounded-md ${s!.bg} ${s!.text}`}>
127
+ {s!.label}
128
+ </span>
129
+ );
130
+ })()}
131
+ <span className={`h-1.5 w-1.5 rounded-full shrink-0 ${RISK_DOT[summary.risk_level] ?? RISK_DOT.medium}`} />
132
+ <div className="flex-1" />
133
+ {meta.pr_state !== "merged" && meta.pr_state !== "closed" && (
134
+ <button
135
+ type="button"
136
+ onClick={() => setReviewOpen(true)}
137
+ className="flex items-center gap-1.5 h-7 px-3 rounded-md border text-[11px] font-medium text-foreground hover:bg-accent/40 transition-colors shrink-0"
138
+ >
139
+ <Check className="h-3 w-3" />
140
+ Review
141
+ <ChevronDown className="h-3 w-3 text-muted-foreground/40" />
142
+ </button>
143
+ )}
111
144
  </div>
112
145
 
113
- <h1 className="text-lg font-bold tracking-tight mb-2 line-clamp-2">{meta.pr_title}</h1>
146
+ <h1 className="text-sm font-semibold tracking-tight mb-3 line-clamp-2">{meta.pr_title}</h1>
114
147
 
115
- <div className="flex flex-wrap gap-x-4 gap-y-1">
148
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 text-[11px] text-muted-foreground/50">
116
149
  <a
117
150
  href={meta.author_url ?? `https://github.com/${meta.author}`}
118
151
  target="_blank"
119
152
  rel="noopener noreferrer"
120
- className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
153
+ className="flex items-center gap-1.5 hover:text-foreground transition-colors"
121
154
  >
122
- {meta.author_avatar ? (
123
- <img src={meta.author_avatar} alt={meta.author} className="h-3.5 w-3.5 rounded-full" />
124
- ) : (
125
- <User className="h-3 w-3" />
155
+ {meta.author_avatar && (
156
+ <img src={meta.author_avatar} alt={meta.author} className="h-4 w-4 rounded-full" />
126
157
  )}
127
158
  <span>{meta.author}</span>
128
159
  </a>
129
- <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
130
- <GitBranch className="h-3 w-3" />
131
- <span className="font-mono">{meta.base_branch}</span>
132
- <span className="text-muted-foreground/50">←</span>
160
+ <span className="text-muted-foreground/15">|</span>
161
+ <div className="flex items-center gap-1">
162
+ <GitBranch className="h-3 w-3 text-muted-foreground/30" />
133
163
  <span className="font-mono">{meta.head_branch}</span>
164
+ <span className="text-muted-foreground/25">→</span>
165
+ <span className="font-mono">{meta.base_branch}</span>
134
166
  </div>
135
- <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
136
- <Files className="h-3 w-3" />
137
- <span className="text-green-500">+{meta.total_additions}</span>
138
- <span className="text-red-500">−{meta.total_deletions}</span>
139
- <span className="text-muted-foreground/50">·</span>
140
- <span>{meta.total_files_changed} files</span>
141
- </div>
142
- <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
143
- <Bot className="h-3 w-3" />
144
- <span>{meta.model_used.split("/").pop()}</span>
167
+ <span className="text-muted-foreground/15">|</span>
168
+ <div className="flex items-center gap-1.5">
169
+ <span className="text-green-600 dark:text-green-400 tabular-nums">+{meta.total_additions}</span>
170
+ <span className="text-red-600 dark:text-red-400 tabular-nums">-{meta.total_deletions}</span>
171
+ <span className="text-muted-foreground/25">·</span>
172
+ <span className="tabular-nums">{meta.total_files_changed} files</span>
145
173
  </div>
146
174
  </div>
147
175
  </div>
148
176
  </div>
149
177
 
150
178
  <div ref={compactRef} className="overflow-hidden transition-[max-height,opacity] duration-200" style={{ maxHeight: 0, opacity: 0 }}>
151
- <div className="flex items-center gap-3 min-w-0 pb-2">
152
- <Button variant="ghost" size="icon" className="shrink-0 -ml-2 h-7 w-7" onClick={onBack}>
179
+ <div className="flex items-center gap-2.5 min-w-0 pb-2.5">
180
+ <button
181
+ type="button"
182
+ onClick={onBack}
183
+ 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"
184
+ >
153
185
  <ArrowLeft className="h-3.5 w-3.5" />
154
- </Button>
155
- <span className="text-sm font-semibold truncate flex-1">{meta.pr_title}</span>
156
- <span className="text-xs text-muted-foreground font-mono shrink-0">{repoSlug}</span>
157
- <span className={`text-[10px] font-medium px-1.5 py-0.5 rounded-full shrink-0 ${RISK_COLORS[summary.risk_level] ?? RISK_COLORS.medium}`}>
158
- {summary.risk_level}
159
- </span>
186
+ </button>
187
+ <span className={`h-1.5 w-1.5 rounded-full shrink-0 ${RISK_DOT[summary.risk_level] ?? RISK_DOT.medium}`} />
188
+ {meta.pr_state && (() => {
189
+ const s = STATE_STYLES[meta.pr_state]!;
190
+ return <span className={`text-[9px] font-medium px-1 py-px rounded ${s.bg} ${s.text} shrink-0`}>{s.label}</span>;
191
+ })()}
192
+ <span className="text-xs font-medium truncate flex-1">{meta.pr_title}</span>
193
+ <span className="text-[10px] text-muted-foreground/30 font-mono shrink-0">{repoSlug}</span>
160
194
  </div>
161
195
  </div>
162
196
 
163
- <TabsList className="w-full justify-start overflow-x-auto">
164
- <TabsTrigger value="story" className="gap-1.5">
165
- <BookOpen className="h-3.5 w-3.5 shrink-0" />
197
+ <TabsList className="w-full justify-start">
198
+ <TabsTrigger value="story">
199
+ <BookOpen className="h-3 w-3 shrink-0" />
166
200
  Story
167
201
  </TabsTrigger>
168
- <TabsTrigger value="summary" className="gap-1.5">
169
- <LayoutList className="h-3.5 w-3.5 shrink-0" />
170
- Summary
202
+ <TabsTrigger value="discussion">
203
+ <MessageSquare className="h-3 w-3 shrink-0" />
204
+ Discussion
171
205
  </TabsTrigger>
172
- <TabsTrigger value="groups" className="gap-1.5">
173
- <Layers className="h-3.5 w-3.5 shrink-0" />
206
+ <TabsTrigger value="groups">
207
+ <Layers className="h-3 w-3 shrink-0" />
174
208
  Groups
175
209
  </TabsTrigger>
176
- <TabsTrigger value="files" className="gap-1.5">
177
- <FolderTree className="h-3.5 w-3.5 shrink-0" />
210
+ <TabsTrigger value="files">
211
+ <FolderTree className="h-3 w-3 shrink-0" />
178
212
  Files
179
213
  </TabsTrigger>
180
- <TabsTrigger value="narrative" className="gap-1.5">
181
- <FileText className="h-3.5 w-3.5 shrink-0" />
182
- Narrative
183
- </TabsTrigger>
184
214
  {cartoonEnabled && (
185
- <TabsTrigger value="cartoon" className="gap-1.5">
186
- <Sparkles className="h-3.5 w-3.5 shrink-0" />
215
+ <TabsTrigger value="cartoon">
216
+ <Sparkles className="h-3 w-3 shrink-0" />
187
217
  Comic
188
218
  </TabsTrigger>
189
219
  )}
@@ -193,17 +223,19 @@ export function ResultsScreen({
193
223
  <TabsContent value="story">
194
224
  <StoryPanel data={data} activeId={activeId} onAnchorClick={onAnchorClick} />
195
225
  </TabsContent>
196
- <TabsContent value="summary">
197
- <SummaryPanel summary={data.summary} />
226
+ <TabsContent value="discussion">
227
+ <DiscussionPanel sessionId={sessionId} />
198
228
  </TabsContent>
199
229
  <TabsContent value="groups">
200
230
  <GroupsPanel groups={data.groups} />
201
231
  </TabsContent>
202
232
  <TabsContent value="files">
203
- <FilesPanel files={data.files} />
204
- </TabsContent>
205
- <TabsContent value="narrative">
206
- <NarrativePanel narrative={data.narrative} />
233
+ <FilesPanel
234
+ files={data.files}
235
+ groups={data.groups}
236
+ selectedPath={activeId?.startsWith("file:") ? activeId.slice(5) : null}
237
+ onFileSelect={(path: string) => onAnchorClick("file", path)}
238
+ />
207
239
  </TabsContent>
208
240
  {cartoonEnabled && (
209
241
  <TabsContent value="cartoon">
@@ -211,5 +243,9 @@ export function ResultsScreen({
211
243
  </TabsContent>
212
244
  )}
213
245
  </Tabs>
246
+ {reviewOpen && (
247
+ <ReviewModal prUrl={meta.pr_url} onClose={() => setReviewOpen(false)} />
248
+ )}
249
+ </>
214
250
  );
215
251
  }
@@ -0,0 +1,187 @@
1
+ import { useState, useRef, useCallback } from "react";
2
+ import { X, Check, MessageSquare, Loader2, AlertCircle, ExternalLink } from "lucide-react";
3
+ import { TipTapEditor, getTextWithAnchors } from "./TipTapEditor.tsx";
4
+ import type { useEditor } from "@tiptap/react";
5
+
6
+ type ReviewEvent = "APPROVE" | "REQUEST_CHANGES" | "COMMENT";
7
+
8
+ const EVENTS: { value: ReviewEvent; label: string; description: string; class: string; activeClass: string }[] = [
9
+ {
10
+ value: "APPROVE",
11
+ label: "Approve",
12
+ description: "Submit approval for this PR",
13
+ class: "text-green-600 dark:text-green-400 hover:bg-green-500/10",
14
+ activeClass: "bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/30",
15
+ },
16
+ {
17
+ value: "REQUEST_CHANGES",
18
+ label: "Request changes",
19
+ description: "Submit feedback that must be addressed",
20
+ class: "text-red-600 dark:text-red-400 hover:bg-red-500/10",
21
+ activeClass: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/30",
22
+ },
23
+ {
24
+ value: "COMMENT",
25
+ label: "Comment",
26
+ description: "Submit general feedback",
27
+ class: "text-muted-foreground hover:bg-accent/50",
28
+ activeClass: "bg-accent text-foreground border-border",
29
+ },
30
+ ];
31
+
32
+ interface ReviewModalProps {
33
+ prUrl: string;
34
+ onClose: () => void;
35
+ }
36
+
37
+ export function ReviewModal({ prUrl, onClose }: ReviewModalProps) {
38
+ const [event, setEvent] = useState<ReviewEvent>("APPROVE");
39
+ const [submitting, setSubmitting] = useState(false);
40
+ const [result, setResult] = useState<{ ok: boolean; html_url?: string; error?: string } | null>(null);
41
+ const editorRef = useRef<ReturnType<typeof useEditor>>(null);
42
+
43
+ const handleSubmit = useCallback(async () => {
44
+ if (submitting) return;
45
+ setSubmitting(true);
46
+ setResult(null);
47
+ try {
48
+ const body = editorRef.current ? getTextWithAnchors(editorRef.current) : "";
49
+ const res = await fetch("/api/review", {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify({ pr_url: prUrl, event, body }),
53
+ });
54
+ const data = await res.json() as { ok?: boolean; html_url?: string; error?: string };
55
+ if (data.ok) {
56
+ setResult({ ok: true, html_url: data.html_url });
57
+ } else {
58
+ setResult({ ok: false, error: data.error ?? "Failed to submit review" });
59
+ }
60
+ } catch (err) {
61
+ setResult({ ok: false, error: err instanceof Error ? err.message : String(err) });
62
+ } finally {
63
+ setSubmitting(false);
64
+ }
65
+ }, [prUrl, event, submitting]);
66
+
67
+ return (
68
+ <div className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh]" onClick={onClose}>
69
+ <div className="fixed inset-0 bg-background/60 backdrop-blur-sm" />
70
+ <div
71
+ className="relative z-10 w-full max-w-md rounded-xl border bg-background shadow-lg"
72
+ onClick={(e) => e.stopPropagation()}
73
+ >
74
+ <div className="flex items-center justify-between px-4 h-11 border-b">
75
+ <span className="text-xs font-medium">Submit Review</span>
76
+ <button
77
+ type="button"
78
+ onClick={onClose}
79
+ 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"
80
+ >
81
+ <X className="h-3.5 w-3.5" />
82
+ </button>
83
+ </div>
84
+
85
+ <div className="px-4 py-4 space-y-4">
86
+ {result?.ok ? (
87
+ <div className="space-y-4 py-2">
88
+ <div className="flex items-center gap-2 text-green-600 dark:text-green-400">
89
+ <Check className="h-4 w-4" />
90
+ <span className="text-xs font-medium">Review submitted</span>
91
+ </div>
92
+ {result.html_url && (
93
+ <a
94
+ href={result.html_url}
95
+ target="_blank"
96
+ rel="noopener noreferrer"
97
+ className="inline-flex items-center gap-1.5 text-[11px] text-muted-foreground/60 hover:text-foreground transition-colors"
98
+ >
99
+ <ExternalLink className="h-3 w-3" />
100
+ View on GitHub
101
+ </a>
102
+ )}
103
+ <div className="flex justify-end">
104
+ <button
105
+ type="button"
106
+ onClick={onClose}
107
+ className="text-[11px] text-muted-foreground/50 hover:text-foreground px-3 py-1.5 rounded-md hover:bg-accent/40 transition-colors"
108
+ >
109
+ Close
110
+ </button>
111
+ </div>
112
+ </div>
113
+ ) : (
114
+ <>
115
+ <div className="flex gap-1.5 p-0.5 rounded-lg border">
116
+ {EVENTS.map((e) => (
117
+ <button
118
+ key={e.value}
119
+ type="button"
120
+ onClick={() => setEvent(e.value)}
121
+ className={`flex-1 text-[11px] font-medium px-2 py-1.5 rounded-md transition-colors border border-transparent ${
122
+ event === e.value ? e.activeClass : e.class
123
+ }`}
124
+ >
125
+ {e.label}
126
+ </button>
127
+ ))}
128
+ </div>
129
+
130
+ <div>
131
+ <div className="text-[10px] font-medium text-muted-foreground/40 uppercase tracking-wider mb-2">
132
+ Message {event !== "APPROVE" && <span className="text-red-500/60 normal-case">*</span>}
133
+ </div>
134
+ <div className="rounded-lg border px-3 py-2.5 min-h-[80px] focus-within:border-foreground/15 transition-colors">
135
+ <TipTapEditor
136
+ editorRef={editorRef}
137
+ placeholder={event === "APPROVE" ? "Optional message..." : "Describe the changes needed..."}
138
+ autoFocus
139
+ submitOnModEnter
140
+ onSubmit={handleSubmit}
141
+ />
142
+ </div>
143
+ </div>
144
+
145
+ {result?.error && (
146
+ <div className="flex items-start gap-2 px-3 py-2 rounded-md bg-red-500/5 border border-red-500/20">
147
+ <AlertCircle className="h-3.5 w-3.5 text-red-500 shrink-0 mt-0.5" />
148
+ <p className="text-[11px] text-red-600 dark:text-red-400">{result.error}</p>
149
+ </div>
150
+ )}
151
+
152
+ <div className="flex items-center justify-between pt-1">
153
+ <span className="text-[10px] text-muted-foreground/25">
154
+ {navigator.platform.includes("Mac") ? "⌘" : "Ctrl"}+Enter to submit
155
+ </span>
156
+ <div className="flex gap-2">
157
+ <button
158
+ type="button"
159
+ onClick={onClose}
160
+ className="text-[11px] text-muted-foreground/50 hover:text-foreground px-3 py-1.5 rounded-md hover:bg-accent/40 transition-colors"
161
+ >
162
+ Cancel
163
+ </button>
164
+ <button
165
+ type="button"
166
+ onClick={handleSubmit}
167
+ disabled={submitting}
168
+ className="flex items-center gap-1.5 text-[11px] font-medium px-3 py-1.5 rounded-md bg-foreground text-background hover:opacity-80 disabled:opacity-30 transition-opacity"
169
+ >
170
+ {submitting ? (
171
+ <Loader2 className="h-3 w-3 animate-spin" />
172
+ ) : event === "APPROVE" ? (
173
+ <Check className="h-3 w-3" />
174
+ ) : (
175
+ <MessageSquare className="h-3 w-3" />
176
+ )}
177
+ Submit
178
+ </button>
179
+ </div>
180
+ </div>
181
+ </>
182
+ )}
183
+ </div>
184
+ </div>
185
+ </div>
186
+ );
187
+ }