@frixaco/hbench 0.1.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.
@@ -0,0 +1,199 @@
1
+ const Select = SelectPrimitive.Root;
2
+
3
+ function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
4
+ return (
5
+ <SelectPrimitive.Group
6
+ data-slot="select-group"
7
+ className={cn("scroll-my-1 p-1", className)}
8
+ {...props}
9
+ />
10
+ );
11
+ }
12
+
13
+ function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
14
+ return (
15
+ <SelectPrimitive.Value
16
+ data-slot="select-value"
17
+ className={cn("flex flex-1 text-left", className)}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ function SelectTrigger({
24
+ className,
25
+ size = "default",
26
+ children,
27
+ ...props
28
+ }: SelectPrimitive.Trigger.Props & {
29
+ size?: "sm" | "default";
30
+ }) {
31
+ return (
32
+ <SelectPrimitive.Trigger
33
+ data-slot="select-trigger"
34
+ data-size={size}
35
+ className={cn(
36
+ "border-input data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-lg border bg-transparent py-2 pr-2 pl-2.5 text-sm transition-colors select-none focus-visible:ring-3 aria-invalid:ring-3 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
37
+ className,
38
+ )}
39
+ {...props}
40
+ >
41
+ {children}
42
+ <SelectPrimitive.Icon
43
+ render={
44
+ <ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" />
45
+ }
46
+ />
47
+ </SelectPrimitive.Trigger>
48
+ );
49
+ }
50
+
51
+ function SelectContent({
52
+ className,
53
+ children,
54
+ side = "bottom",
55
+ sideOffset = 4,
56
+ align = "center",
57
+ alignOffset = 0,
58
+ alignItemWithTrigger = true,
59
+ ...props
60
+ }: SelectPrimitive.Popup.Props &
61
+ Pick<
62
+ SelectPrimitive.Positioner.Props,
63
+ "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
64
+ >) {
65
+ return (
66
+ <SelectPrimitive.Portal>
67
+ <SelectPrimitive.Positioner
68
+ side={side}
69
+ sideOffset={sideOffset}
70
+ align={align}
71
+ alignOffset={alignOffset}
72
+ alignItemWithTrigger={alignItemWithTrigger}
73
+ className="isolate z-50"
74
+ >
75
+ <SelectPrimitive.Popup
76
+ data-slot="select-content"
77
+ data-align-trigger={alignItemWithTrigger}
78
+ className={cn(
79
+ "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-lg shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none",
80
+ className,
81
+ )}
82
+ {...props}
83
+ >
84
+ <SelectScrollUpButton />
85
+ <SelectPrimitive.List>{children}</SelectPrimitive.List>
86
+ <SelectScrollDownButton />
87
+ </SelectPrimitive.Popup>
88
+ </SelectPrimitive.Positioner>
89
+ </SelectPrimitive.Portal>
90
+ );
91
+ }
92
+
93
+ function SelectLabel({
94
+ className,
95
+ ...props
96
+ }: SelectPrimitive.GroupLabel.Props) {
97
+ return (
98
+ <SelectPrimitive.GroupLabel
99
+ data-slot="select-label"
100
+ className={cn("text-muted-foreground px-1.5 py-1 text-xs", className)}
101
+ {...props}
102
+ />
103
+ );
104
+ }
105
+
106
+ function SelectItem({
107
+ className,
108
+ children,
109
+ ...props
110
+ }: SelectPrimitive.Item.Props) {
111
+ return (
112
+ <SelectPrimitive.Item
113
+ data-slot="select-item"
114
+ className={cn(
115
+ "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
116
+ className,
117
+ )}
118
+ {...props}
119
+ >
120
+ <SelectPrimitive.ItemText className="flex flex-1 gap-2 shrink-0 whitespace-nowrap">
121
+ {children}
122
+ </SelectPrimitive.ItemText>
123
+ <SelectPrimitive.ItemIndicator
124
+ render={
125
+ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
126
+ <CheckIcon className="pointer-events-none" />
127
+ </span>
128
+ }
129
+ />
130
+ </SelectPrimitive.Item>
131
+ );
132
+ }
133
+
134
+ function SelectSeparator({
135
+ className,
136
+ ...props
137
+ }: SelectPrimitive.Separator.Props) {
138
+ return (
139
+ <SelectPrimitive.Separator
140
+ data-slot="select-separator"
141
+ className={cn("bg-border -mx-1 my-1 h-px pointer-events-none", className)}
142
+ {...props}
143
+ />
144
+ );
145
+ }
146
+
147
+ function SelectScrollUpButton({
148
+ className,
149
+ ...props
150
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
151
+ return (
152
+ <SelectPrimitive.ScrollUpArrow
153
+ data-slot="select-scroll-up-button"
154
+ className={cn(
155
+ "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full",
156
+ className,
157
+ )}
158
+ {...props}
159
+ >
160
+ <ChevronUpIcon />
161
+ </SelectPrimitive.ScrollUpArrow>
162
+ );
163
+ }
164
+
165
+ function SelectScrollDownButton({
166
+ className,
167
+ ...props
168
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
169
+ return (
170
+ <SelectPrimitive.ScrollDownArrow
171
+ data-slot="select-scroll-down-button"
172
+ className={cn(
173
+ "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full",
174
+ className,
175
+ )}
176
+ {...props}
177
+ >
178
+ <ChevronDownIcon />
179
+ </SelectPrimitive.ScrollDownArrow>
180
+ );
181
+ }
182
+
183
+ export {
184
+ Select,
185
+ SelectContent,
186
+ SelectGroup,
187
+ SelectItem,
188
+ SelectLabel,
189
+ SelectScrollDownButton,
190
+ SelectScrollUpButton,
191
+ SelectSeparator,
192
+ SelectTrigger,
193
+ SelectValue,
194
+ };
195
+
196
+ import { Select as SelectPrimitive } from "@base-ui/react/select";
197
+
198
+ import { cn } from "@/lib/utils";
199
+ import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
@@ -0,0 +1,131 @@
1
+ function Sheet({ ...props }: SheetBase.Root.Props) {
2
+ return <SheetBase.Root data-slot="sheet" {...props} />;
3
+ }
4
+
5
+ function SheetTrigger({ ...props }: SheetBase.Trigger.Props) {
6
+ return <SheetBase.Trigger data-slot="sheet-trigger" {...props} />;
7
+ }
8
+
9
+ function SheetClose({ ...props }: SheetBase.Close.Props) {
10
+ return <SheetBase.Close data-slot="sheet-close" {...props} />;
11
+ }
12
+
13
+ function SheetPortal({ ...props }: SheetBase.Portal.Props) {
14
+ return <SheetBase.Portal data-slot="sheet-portal" {...props} />;
15
+ }
16
+
17
+ function SheetOverlay({ className, ...props }: SheetBase.Backdrop.Props) {
18
+ return (
19
+ <SheetBase.Backdrop
20
+ data-slot="sheet-overlay"
21
+ className={cn(
22
+ "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50",
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ );
28
+ }
29
+
30
+ function SheetContent({
31
+ className,
32
+ children,
33
+ side = "right",
34
+ showCloseButton = true,
35
+ ...props
36
+ }: SheetBase.Popup.Props & {
37
+ side?: "top" | "right" | "bottom" | "left";
38
+ showCloseButton?: boolean;
39
+ }) {
40
+ return (
41
+ <SheetPortal>
42
+ <SheetOverlay />
43
+ <SheetBase.Popup
44
+ data-slot="sheet-content"
45
+ data-side={side}
46
+ className={cn(
47
+ "bg-background data-open:animate-in data-closed:animate-out data-[side=right]:data-closed:slide-out-to-right-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
48
+ className,
49
+ )}
50
+ {...props}
51
+ >
52
+ {children}
53
+ {showCloseButton && (
54
+ <SheetBase.Close
55
+ data-slot="sheet-close"
56
+ render={
57
+ <Button
58
+ variant="ghost"
59
+ className="absolute top-3 right-3"
60
+ size="icon-sm"
61
+ />
62
+ }
63
+ >
64
+ <XIcon />
65
+ <span className="sr-only">Close</span>
66
+ </SheetBase.Close>
67
+ )}
68
+ </SheetBase.Popup>
69
+ </SheetPortal>
70
+ );
71
+ }
72
+
73
+ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
74
+ return (
75
+ <div
76
+ data-slot="sheet-header"
77
+ className={cn("gap-0.5 p-4 flex flex-col", className)}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
84
+ return (
85
+ <div
86
+ data-slot="sheet-footer"
87
+ className={cn("gap-2 p-4 mt-auto flex flex-col", className)}
88
+ {...props}
89
+ />
90
+ );
91
+ }
92
+
93
+ function SheetTitle({ className, ...props }: SheetBase.Title.Props) {
94
+ return (
95
+ <SheetBase.Title
96
+ data-slot="sheet-title"
97
+ className={cn("text-foreground text-base font-medium", className)}
98
+ {...props}
99
+ />
100
+ );
101
+ }
102
+
103
+ function SheetDescription({
104
+ className,
105
+ ...props
106
+ }: SheetBase.Description.Props) {
107
+ return (
108
+ <SheetBase.Description
109
+ data-slot="sheet-description"
110
+ className={cn("text-muted-foreground text-sm", className)}
111
+ {...props}
112
+ />
113
+ );
114
+ }
115
+
116
+ export {
117
+ Sheet,
118
+ SheetTrigger,
119
+ SheetClose,
120
+ SheetContent,
121
+ SheetHeader,
122
+ SheetFooter,
123
+ SheetTitle,
124
+ SheetDescription,
125
+ };
126
+
127
+ import { Dialog as SheetBase } from "@base-ui/react/dialog";
128
+
129
+ import { XIcon } from "lucide-react";
130
+ import { cn } from "@/lib/utils";
131
+ import { Button } from "./button";
@@ -0,0 +1,41 @@
1
+ const Toaster = ({ ...props }: ToasterProps) => {
2
+ return (
3
+ <Sonner
4
+ theme="system"
5
+ className="toaster group"
6
+ icons={{
7
+ success: <CircleCheckIcon className="size-4" />,
8
+ info: <InfoIcon className="size-4" />,
9
+ warning: <TriangleAlertIcon className="size-4" />,
10
+ error: <OctagonXIcon className="size-4" />,
11
+ loading: <Loader2Icon className="size-4 animate-spin" />,
12
+ }}
13
+ style={
14
+ {
15
+ "--normal-bg": "var(--popover)",
16
+ "--normal-text": "var(--popover-foreground)",
17
+ "--normal-border": "var(--border)",
18
+ "--border-radius": "var(--radius)",
19
+ } as React.CSSProperties
20
+ }
21
+ toastOptions={{
22
+ classNames: {
23
+ toast: "cn-toast",
24
+ },
25
+ }}
26
+ {...props}
27
+ />
28
+ );
29
+ };
30
+
31
+ export { Toaster };
32
+
33
+ import { Toaster as Sonner } from "sonner";
34
+ import {
35
+ CircleCheckIcon,
36
+ InfoIcon,
37
+ Loader2Icon,
38
+ OctagonXIcon,
39
+ TriangleAlertIcon,
40
+ } from "lucide-react";
41
+ import type { ToasterProps } from "sonner";
@@ -0,0 +1,313 @@
1
+ type TUIProps = {
2
+ name: string;
3
+ runRequested: boolean;
4
+ isRepoReady: boolean;
5
+ repoUrlInput: string;
6
+ onLaunch: () => void;
7
+ };
8
+
9
+ export function TUI({
10
+ name,
11
+ runRequested,
12
+ isRepoReady,
13
+ repoUrlInput,
14
+ onLaunch,
15
+ }: TUIProps) {
16
+ const ws = useWS();
17
+ const termDivContainer = useRef<HTMLDivElement | null>(null);
18
+ const termInstance = useRef<Terminal | null>(null);
19
+ const [diffOpen, setDiffOpen] = useState(false);
20
+ const [diffPatch, setDiffPatch] = useState<string | null>(null);
21
+ const [diffError, setDiffError] = useState<string | null>(null);
22
+ const [diffLoading, setDiffLoading] = useState(false);
23
+ const [diffRepoUrlInput, setDiffRepoUrlInput] = useState<string | null>(null);
24
+ const headerPattern = getAgentPattern(name.toLowerCase());
25
+
26
+ useEffect(() => {
27
+ let active = true;
28
+ let dataDisposable: { dispose: () => void } | null = null;
29
+ let resizeDisposable: { dispose: () => void } | null = null;
30
+ let resizeObserver: ResizeObserver | null = null;
31
+ let socket: WebSocket | null = null;
32
+
33
+ const teardownSocket = () => {
34
+ dataDisposable?.dispose();
35
+ dataDisposable = null;
36
+ resizeDisposable?.dispose();
37
+ resizeDisposable = null;
38
+ resizeObserver?.disconnect();
39
+ resizeObserver = null;
40
+ if (socket) {
41
+ socket.removeEventListener("message", handleMessage);
42
+ socket.removeEventListener("close", handleClose);
43
+ socket = null;
44
+ }
45
+ };
46
+
47
+ const attachSocket = (conn: WebSocket, term: Terminal) => {
48
+ teardownSocket();
49
+ socket = conn;
50
+ dataDisposable = term.onData((data) => {
51
+ socket?.send(
52
+ JSON.stringify({
53
+ type: "input",
54
+ agent: name,
55
+ data,
56
+ }),
57
+ );
58
+ });
59
+ socket.addEventListener("message", handleMessage);
60
+ socket.addEventListener("close", handleClose);
61
+ };
62
+
63
+ const handleMessage = (event: MessageEvent) => {
64
+ const term = termInstance.current;
65
+ if (!term) return;
66
+ const payload = event.data;
67
+ if (typeof payload === "string") {
68
+ try {
69
+ const message = JSON.parse(payload);
70
+ if (message.type !== "output") return;
71
+ if (message.agent !== name) return;
72
+ if (typeof message.data !== "string") return;
73
+ const decoded = atob(message.data);
74
+ const bytes = new Uint8Array(decoded.length);
75
+ for (let i = 0; i < decoded.length; i += 1) {
76
+ bytes[i] = decoded.charCodeAt(i);
77
+ }
78
+ term.write(bytes);
79
+ } catch (error) {
80
+ console.warn("Invalid output payload", error);
81
+ }
82
+ } else if (payload instanceof ArrayBuffer) {
83
+ term.write(new Uint8Array(payload));
84
+ }
85
+ };
86
+
87
+ const handleClose = () => {
88
+ termInstance.current?.dispose();
89
+ termInstance.current = null;
90
+ };
91
+
92
+ const sendResize = (cols: number, rows: number) => {
93
+ if (!ws.socket || !ws.isReady) return;
94
+ ws.send(
95
+ JSON.stringify({
96
+ type: "resize",
97
+ agent: name,
98
+ cols,
99
+ rows,
100
+ }),
101
+ );
102
+ };
103
+
104
+ const fitTerminal = (term: Terminal) => {
105
+ const host = termDivContainer.current;
106
+ if (!host || !term.renderer) return;
107
+ const metrics = term.renderer.getMetrics();
108
+ if (!metrics.width || !metrics.height) return;
109
+ const cols = Math.max(2, Math.floor(host.clientWidth / metrics.width));
110
+ const rows = Math.max(1, Math.floor(host.clientHeight / metrics.height));
111
+ term.resize(cols, rows);
112
+ };
113
+
114
+ async function ensureTerminalSetup() {
115
+ const host = termDivContainer.current;
116
+ if (!host || termInstance.current) return;
117
+ await init();
118
+ if (!active) return;
119
+
120
+ const term = new Terminal({
121
+ fontSize: 14,
122
+ theme: {
123
+ background: "#16181a",
124
+ foreground: "#ffffff",
125
+ black: "#16181a",
126
+ red: "#ff6e5e",
127
+ green: "#5eff6c",
128
+ yellow: "#f1ff5e",
129
+ blue: "#5ea1ff",
130
+ magenta: "#ff5ef1",
131
+ cyan: "#5ef1ff",
132
+ white: "#ffffff",
133
+ brightBlack: "#3c4048",
134
+ brightRed: "#ffbd5e",
135
+ brightGreen: "#5eff6c",
136
+ brightYellow: "#f1ff5e",
137
+ brightBlue: "#5ea1ff",
138
+ brightMagenta: "#ff5ea0",
139
+ brightCyan: "#5ef1ff",
140
+ brightWhite: "#ffffff",
141
+ },
142
+ });
143
+ term.open(host);
144
+ termInstance.current = term;
145
+ fitTerminal(term);
146
+
147
+ resizeObserver = new ResizeObserver(() => {
148
+ if (termInstance.current) {
149
+ fitTerminal(termInstance.current);
150
+ }
151
+ });
152
+ resizeObserver.observe(host);
153
+ }
154
+
155
+ async function ensureSocketAttached() {
156
+ await ensureTerminalSetup();
157
+ if (!active || !ws.socket || !termInstance.current) return;
158
+ attachSocket(ws.socket, termInstance.current);
159
+
160
+ resizeDisposable = termInstance.current.onResize((size) => {
161
+ sendResize(size.cols, size.rows);
162
+ });
163
+
164
+ fitTerminal(termInstance.current);
165
+ sendResize(termInstance.current.cols, termInstance.current.rows);
166
+ }
167
+
168
+ if (runRequested) {
169
+ ensureSocketAttached();
170
+ }
171
+
172
+ return () => {
173
+ active = false;
174
+ teardownSocket();
175
+ termInstance.current?.dispose();
176
+ termInstance.current = null;
177
+ };
178
+ }, [runRequested, ws.socket]);
179
+
180
+ const fetchDiff = useCallback(
181
+ async (repoUrlInputOverride?: string) => {
182
+ setDiffLoading(true);
183
+ setDiffError(null);
184
+ try {
185
+ const nextRepoUrlInput =
186
+ repoUrlInputOverride ?? diffRepoUrlInput ?? repoUrlInput;
187
+ const body = await fetchAgentDiff({
188
+ agent: name,
189
+ repoUrlInput: nextRepoUrlInput,
190
+ });
191
+ const trimmed = body.trim();
192
+ setDiffPatch(trimmed.length > 0 ? body : null);
193
+ } catch (error) {
194
+ const message =
195
+ error instanceof Error ? error.message : "Unknown error";
196
+ setDiffError(message);
197
+ setDiffPatch(null);
198
+ } finally {
199
+ setDiffLoading(false);
200
+ }
201
+ },
202
+ [diffRepoUrlInput, name, repoUrlInput],
203
+ );
204
+
205
+ return (
206
+ <div className="flex h-[38rem] min-h-[28rem] flex-col overflow-hidden rounded-lg border bg-[#16181a]">
207
+ {/* Card header: agent name + model + actions */}
208
+ <div
209
+ className="flex items-center gap-2 border-b border-white/[0.06] bg-[#1c1e21] px-2.5 py-1.5"
210
+ style={headerPattern}
211
+ >
212
+ <span
213
+ className={cn(
214
+ "size-1.5 shrink-0 rounded-full",
215
+ runRequested ? "bg-emerald-500 animate-pulse" : "bg-white/20",
216
+ )}
217
+ />
218
+ <span className="text-xs font-semibold capitalize text-white/90">
219
+ {name}
220
+ </span>
221
+
222
+ <div className="flex-1" />
223
+
224
+ <Button
225
+ size="icon-xs"
226
+ variant={runRequested ? "secondary" : "ghost"}
227
+ disabled={!isRepoReady}
228
+ onClick={onLaunch}
229
+ className={cn(
230
+ "text-white/60 hover:text-white",
231
+ runRequested && "text-white",
232
+ )}
233
+ aria-label={`Launch ${name}`}
234
+ >
235
+ {runRequested ? <RefreshCcw className="animate-spin" /> : <Play />}
236
+ </Button>
237
+
238
+ <Sheet
239
+ open={diffOpen}
240
+ onOpenChange={(open) => {
241
+ setDiffOpen(open);
242
+ if (open) {
243
+ setDiffRepoUrlInput(repoUrlInput);
244
+ fetchDiff(repoUrlInput);
245
+ }
246
+ }}
247
+ >
248
+ <SheetTrigger
249
+ render={
250
+ <Button
251
+ size="icon-xs"
252
+ variant="ghost"
253
+ className="text-white/60 hover:text-white"
254
+ disabled={!isRepoReady}
255
+ onClick={() => {
256
+ if (!diffOpen) {
257
+ setDiffOpen(true);
258
+ }
259
+ setDiffRepoUrlInput(repoUrlInput);
260
+ fetchDiff(repoUrlInput);
261
+ }}
262
+ />
263
+ }
264
+ >
265
+ <Columns2 />
266
+ </SheetTrigger>
267
+ <SheetContent
268
+ side="right"
269
+ className="data-[side=right]:w-[96vw] data-[side=right]:sm:max-w-[92vw]"
270
+ >
271
+ <SheetHeader>
272
+ <SheetTitle className="capitalize">{name} — Diff</SheetTitle>
273
+ </SheetHeader>
274
+ <div className="flex-1 overflow-auto p-4">
275
+ <DiffView
276
+ loading={diffLoading}
277
+ error={diffError}
278
+ patch={diffPatch}
279
+ />
280
+ </div>
281
+ </SheetContent>
282
+ </Sheet>
283
+ </div>
284
+
285
+ {/* Terminal area */}
286
+ <div className="relative flex-1">
287
+ {!runRequested && (
288
+ <div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center">
289
+ <span className="text-xs text-white/25">press ▶ to launch</span>
290
+ </div>
291
+ )}
292
+ <div ref={termDivContainer} className="size-full caret-background" />
293
+ </div>
294
+ </div>
295
+ );
296
+ }
297
+
298
+ import { useCallback, useEffect, useRef, useState } from "react";
299
+ import { Terminal, init } from "ghostty-web";
300
+ import { Columns2, Play, RefreshCcw } from "lucide-react";
301
+ import { Button } from "./button";
302
+ import { getAgentPattern } from "@/lib/agent-patterns";
303
+ import { fetchAgentDiff } from "@/lib/diff-client";
304
+ import { useWS } from "@/lib/websocket";
305
+ import {
306
+ Sheet,
307
+ SheetContent,
308
+ SheetHeader,
309
+ SheetTitle,
310
+ SheetTrigger,
311
+ } from "./sheet";
312
+ import { cn } from "@/lib/utils";
313
+ import { DiffView } from "./diff-view";