@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.
- package/README.md +51 -0
- package/bin/hbench.js +59 -0
- package/bun-env.d.ts +17 -0
- package/lib/.gitkeep +0 -0
- package/package.json +74 -0
- package/server/build.ts +172 -0
- package/server/index.ts +539 -0
- package/server/review.ts +162 -0
- package/server/tsconfig.json +9 -0
- package/tsconfig.base.json +25 -0
- package/tsconfig.json +4 -0
- package/ui/app.tsx +15 -0
- package/ui/components/button.tsx +57 -0
- package/ui/components/cmd-bar.tsx +131 -0
- package/ui/components/diff-view.tsx +51 -0
- package/ui/components/input.tsx +18 -0
- package/ui/components/review-sheet.tsx +261 -0
- package/ui/components/review-view.tsx +40 -0
- package/ui/components/select.tsx +199 -0
- package/ui/components/sheet.tsx +131 -0
- package/ui/components/sonner.tsx +41 -0
- package/ui/components/tui.tsx +313 -0
- package/ui/ghostty-web.tsx +138 -0
- package/ui/index.html +13 -0
- package/ui/index.tsx +20 -0
- package/ui/lib/agent-patterns.ts +127 -0
- package/ui/lib/diff-client.ts +38 -0
- package/ui/lib/models.json +8 -0
- package/ui/lib/reviewer.ts +82 -0
- package/ui/lib/store.ts +90 -0
- package/ui/lib/utils.ts +7 -0
- package/ui/lib/websocket.tsx +144 -0
- package/ui/styles.css +89 -0
- package/ui/tsconfig.json +8 -0
|
@@ -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";
|