@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,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "Preserve",
5
+ "moduleDetection": "force",
6
+ "jsx": "react-jsx",
7
+ "allowJs": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "verbatimModuleSyntax": true,
11
+ "noEmit": true,
12
+ "strict": true,
13
+ "skipLibCheck": true,
14
+ "noFallthroughCasesInSwitch": true,
15
+ "noUncheckedIndexedAccess": true,
16
+ "noImplicitOverride": true,
17
+ "paths": {
18
+ "@/*": ["./ui/*", "./lib/*"]
19
+ },
20
+ "noUnusedLocals": false,
21
+ "noUnusedParameters": false,
22
+ "noPropertyAccessFromIndexSignature": false
23
+ },
24
+ "exclude": ["dist", "node_modules"]
25
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "files": [],
3
+ "references": [{ "path": "./ui" }, { "path": "./server" }]
4
+ }
package/ui/app.tsx ADDED
@@ -0,0 +1,15 @@
1
+ export function App() {
2
+ return (
3
+ <WebSocketProvider>
4
+ <Dashboard />
5
+ <Toaster />
6
+ </WebSocketProvider>
7
+ );
8
+ }
9
+
10
+ export default App;
11
+
12
+ import "./styles.css";
13
+ import { Dashboard } from "./ghostty-web";
14
+ import { WebSocketProvider } from "./lib/websocket";
15
+ import { Toaster } from "./components/sonner";
@@ -0,0 +1,57 @@
1
+ const buttonVariants = cva(
2
+ "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 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
3
+ {
4
+ variants: {
5
+ variant: {
6
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
7
+ outline:
8
+ "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
9
+ secondary:
10
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
11
+ ghost:
12
+ "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
13
+ destructive:
14
+ "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
15
+ link: "text-primary underline-offset-4 hover:underline",
16
+ },
17
+ size: {
18
+ default:
19
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
20
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
21
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
22
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
23
+ icon: "size-8",
24
+ "icon-xs":
25
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
26
+ "icon-sm":
27
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
28
+ "icon-lg": "size-9",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: "default",
33
+ size: "default",
34
+ },
35
+ },
36
+ );
37
+
38
+ function Button({
39
+ className,
40
+ variant = "default",
41
+ size = "default",
42
+ ...props
43
+ }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
44
+ return (
45
+ <ButtonPrimitive
46
+ data-slot="button"
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
53
+ export { Button, buttonVariants };
54
+
55
+ import { Button as ButtonPrimitive } from "@base-ui/react/button";
56
+ import { cva, type VariantProps } from "class-variance-authority";
57
+ import { cn } from "@/lib/utils";
@@ -0,0 +1,131 @@
1
+ export function CommandBar() {
2
+ const ws = useWS();
3
+ const trimmedRepoUrlInput = useDashboardStore(selectTrimmedRepoUrlInput);
4
+ const isRepoReady = useDashboardStore(selectIsRepoReady);
5
+ const launchedAgentCount = useDashboardStore(selectLaunchedAgentCount);
6
+ const prompt = useDashboardStore(selectPrompt);
7
+ const isStoppingAgents = useDashboardStore(selectIsStoppingAgents);
8
+ const trimmedPrompt = useDashboardStore(selectTrimmedPrompt);
9
+
10
+ const handleLaunchAllAgents = useCallback(() => {
11
+ agents.forEach((agent) => ws.send(agent));
12
+ launchAllAgents();
13
+ }, [ws]);
14
+
15
+ const runPromptOnAllAgents = useCallback(() => {
16
+ if (!trimmedPrompt) return;
17
+
18
+ agents.forEach((agent) => {
19
+ ws.send(
20
+ JSON.stringify({
21
+ type: "input",
22
+ agent,
23
+ data: trimmedPrompt,
24
+ }),
25
+ );
26
+ window.setTimeout(() => {
27
+ ws.send(
28
+ JSON.stringify({
29
+ type: "input",
30
+ agent,
31
+ data: "\r",
32
+ }),
33
+ );
34
+ }, 250);
35
+ });
36
+ }, [trimmedPrompt, ws]);
37
+
38
+ const stopAllAgents = useCallback(async () => {
39
+ if (isStoppingAgents) return;
40
+ setIsStoppingAgents(true);
41
+ try {
42
+ const response = await fetch("/api/stop", {
43
+ method: "POST",
44
+ });
45
+ if (!response.ok) {
46
+ const message = (await response.text()).trim();
47
+ throw new Error(message || "Failed to stop agents");
48
+ }
49
+ toast.success("Stopped all agents");
50
+ } catch (error) {
51
+ const message = error instanceof Error ? error.message : "Unknown error";
52
+ toast.error("Stop failed", { description: message });
53
+ } finally {
54
+ resetRunRequested();
55
+ setIsStoppingAgents(false);
56
+ }
57
+ }, [isStoppingAgents]);
58
+
59
+ return (
60
+ <div className="sticky top-11 z-20 border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/80">
61
+ <div className="mx-auto flex max-w-480 items-center gap-2 px-4 py-1.5">
62
+ <input
63
+ value={prompt}
64
+ onChange={(e) => setPrompt(e.currentTarget.value)}
65
+ onKeyDown={(e) => {
66
+ if (e.key === "Enter" && !e.shiftKey) {
67
+ e.preventDefault();
68
+ runPromptOnAllAgents();
69
+ }
70
+ }}
71
+ placeholder="Broadcast prompt to all agents…"
72
+ className="h-7 flex-1 rounded-md border border-input bg-transparent px-2.5 font-mono text-xs outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 dark:bg-input/30"
73
+ />
74
+ <Button
75
+ size="xs"
76
+ disabled={!isRepoReady || trimmedPrompt.length === 0}
77
+ onClick={runPromptOnAllAgents}
78
+ >
79
+ <Send /> Send
80
+ </Button>
81
+
82
+ <div className="mx-1 h-4 w-px bg-border" />
83
+
84
+ <Button
85
+ size="xs"
86
+ variant="outline"
87
+ disabled={!isRepoReady}
88
+ onClick={handleLaunchAllAgents}
89
+ >
90
+ <Play /> All
91
+ </Button>
92
+ <Button
93
+ size="xs"
94
+ variant="destructive"
95
+ disabled={isStoppingAgents || launchedAgentCount === 0}
96
+ onClick={() => void stopAllAgents()}
97
+ >
98
+ <Square /> Stop
99
+ </Button>
100
+
101
+ <div className="mx-1 h-4 w-px bg-border" />
102
+
103
+ <ReviewSheet
104
+ isRepoReady={isRepoReady}
105
+ repoUrlInput={trimmedRepoUrlInput}
106
+ />
107
+ </div>
108
+ </div>
109
+ );
110
+ }
111
+
112
+ import { useCallback } from "react";
113
+ import { Play, Send, Square } from "lucide-react";
114
+ import { toast } from "sonner";
115
+ import { Button } from "./button";
116
+ import { ReviewSheet } from "./review-sheet";
117
+ import {
118
+ agents,
119
+ launchAllAgents,
120
+ resetRunRequested,
121
+ selectIsRepoReady,
122
+ selectIsStoppingAgents,
123
+ selectPrompt,
124
+ selectLaunchedAgentCount,
125
+ selectTrimmedPrompt,
126
+ selectTrimmedRepoUrlInput,
127
+ setIsStoppingAgents,
128
+ setPrompt,
129
+ useDashboardStore,
130
+ } from "@/lib/store";
131
+ import { useWS } from "@/lib/websocket";
@@ -0,0 +1,51 @@
1
+ export function DiffView({
2
+ loading,
3
+ error,
4
+ patch,
5
+ }: {
6
+ loading: boolean;
7
+ error: string | null;
8
+ patch: string | null;
9
+ }) {
10
+ const files = useMemo(
11
+ () =>
12
+ patch
13
+ ? parsePatchFiles(patch).flatMap((parsedPatch) => parsedPatch.files)
14
+ : [],
15
+ [patch],
16
+ );
17
+
18
+ if (loading) {
19
+ return <p className="text-muted-foreground">Loading diff...</p>;
20
+ }
21
+ if (error) {
22
+ return <p className="text-destructive">{error}</p>;
23
+ }
24
+ if (!patch) {
25
+ return <p className="text-muted-foreground">No changes yet.</p>;
26
+ }
27
+
28
+ if (files.length === 0) {
29
+ return (
30
+ <pre className="max-h-full overflow-auto rounded bg-background/60 p-3 text-xs">
31
+ {patch}
32
+ </pre>
33
+ );
34
+ }
35
+
36
+ return (
37
+ <div className="space-y-4">
38
+ {files.map((file, index) => (
39
+ <FileDiff
40
+ key={file.cacheKey ?? `${file.name}-${index}`}
41
+ fileDiff={file}
42
+ options={{ theme: "pierre-dark" }}
43
+ />
44
+ ))}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ import { useMemo } from "react";
50
+ import { parsePatchFiles } from "@pierre/diffs";
51
+ import { FileDiff } from "@pierre/diffs/react";
@@ -0,0 +1,18 @@
1
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
2
+ return (
3
+ <BaseInput
4
+ type={type}
5
+ data-slot="input"
6
+ className={cn(
7
+ "dark:bg-input/30 border-input 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 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
8
+ className,
9
+ )}
10
+ {...props}
11
+ />
12
+ );
13
+ }
14
+
15
+ export { Input };
16
+
17
+ import { cn } from "@/lib/utils";
18
+ import { Input as BaseInput } from "@base-ui/react/input";
@@ -0,0 +1,261 @@
1
+ export function ReviewSheet({
2
+ isRepoReady,
3
+ repoUrlInput,
4
+ }: {
5
+ isRepoReady: boolean;
6
+ repoUrlInput: string;
7
+ }) {
8
+ const [open, setOpen] = useState(false);
9
+ const [collectingDiffs, setCollectingDiffs] = useState(false);
10
+ const [error, setError] = useState<string | null>(null);
11
+ const [reviewModel, setReviewModel] = useState(
12
+ reviewModelOptions[0]?.id ?? "",
13
+ );
14
+ const [reviewApiKey, setReviewApiKey] = useState("");
15
+
16
+ const reviewAbortRef = useRef<AbortController | null>(null);
17
+ const requestInFlightRef = useRef(false);
18
+
19
+ const {
20
+ completion,
21
+ complete,
22
+ error: completionError,
23
+ isLoading,
24
+ setCompletion,
25
+ stop,
26
+ } = useCompletion({
27
+ api: "/api/review",
28
+ streamProtocol: "text",
29
+ experimental_throttle: 60,
30
+ });
31
+
32
+ const loading = collectingDiffs || isLoading;
33
+ const markdown = completion.length > 0 ? completion : null;
34
+
35
+ const abortDiffCollection = useCallback(() => {
36
+ reviewAbortRef.current?.abort();
37
+ reviewAbortRef.current = null;
38
+ }, []);
39
+
40
+ const stopReview = useCallback(() => {
41
+ stop();
42
+ }, [stop]);
43
+
44
+ useEffect(
45
+ () => () => {
46
+ abortDiffCollection();
47
+ stopReview();
48
+ },
49
+ [abortDiffCollection, stopReview],
50
+ );
51
+
52
+ useEffect(() => {
53
+ if (!completionError) return;
54
+ setError(`Review stream failed: ${completionError.message}`);
55
+ }, [completionError]);
56
+
57
+ const requestReview = useCallback(async () => {
58
+ if (requestInFlightRef.current) {
59
+ return;
60
+ }
61
+ requestInFlightRef.current = true;
62
+
63
+ abortDiffCollection();
64
+ stopReview();
65
+
66
+ const controller = new AbortController();
67
+ reviewAbortRef.current = controller;
68
+
69
+ setOpen(true);
70
+ setCollectingDiffs(true);
71
+ setError(null);
72
+ setCompletion("");
73
+
74
+ try {
75
+ const requestIdSeed = Date.now();
76
+ const diffResults = await Promise.all(
77
+ agents.map(async (agent, index) => {
78
+ let body: string;
79
+ try {
80
+ body = await fetchAgentDiff({
81
+ agent,
82
+ repoUrlInput,
83
+ signal: controller.signal,
84
+ requestId: `${requestIdSeed}-${index}`,
85
+ });
86
+ } catch (error) {
87
+ const message =
88
+ error instanceof Error ? error.message : "Failed to load diff";
89
+ throw new Error(`${agent}: ${message}`, { cause: error });
90
+ }
91
+
92
+ return {
93
+ agent,
94
+ diff: body.trim(),
95
+ };
96
+ }),
97
+ );
98
+
99
+ const diffs = diffResults.filter((entry) => entry.diff.length > 0);
100
+ if (diffs.length === 0) {
101
+ throw new Error(
102
+ "No agent diffs found. Run agents and make changes first.",
103
+ );
104
+ }
105
+
106
+ const selectedModel =
107
+ reviewModelOptions.find((option) => option.id === reviewModel) ??
108
+ reviewModelOptions[0];
109
+
110
+ if (!selectedModel) {
111
+ throw new Error("No review model configured");
112
+ }
113
+
114
+ setCollectingDiffs(false);
115
+ const output = await complete(
116
+ buildReviewPrompt({ repoUrl: repoUrlInput, diffs }),
117
+ {
118
+ body: {
119
+ model: selectedModel.id,
120
+ apiKey: reviewApiKey.trim() || undefined,
121
+ },
122
+ },
123
+ );
124
+
125
+ if (
126
+ !controller.signal.aborted &&
127
+ typeof output === "string" &&
128
+ output.trim().length === 0
129
+ ) {
130
+ setCompletion("No review content returned.");
131
+ }
132
+ } catch (nextError) {
133
+ if (
134
+ controller.signal.aborted ||
135
+ (nextError instanceof Error && nextError.name === "AbortError")
136
+ ) {
137
+ return;
138
+ }
139
+
140
+ const message =
141
+ nextError instanceof Error ? nextError.message : "Unknown error";
142
+ setError(`Review stream failed: ${message}`);
143
+ } finally {
144
+ if (reviewAbortRef.current === controller) {
145
+ reviewAbortRef.current = null;
146
+ }
147
+ setCollectingDiffs(false);
148
+ requestInFlightRef.current = false;
149
+ }
150
+ }, [
151
+ abortDiffCollection,
152
+ complete,
153
+ repoUrlInput,
154
+ reviewApiKey,
155
+ reviewModel,
156
+ setCompletion,
157
+ stopReview,
158
+ ]);
159
+
160
+ return (
161
+ <Sheet
162
+ open={open}
163
+ onOpenChange={(nextOpen) => {
164
+ setOpen(nextOpen);
165
+ if (!nextOpen) {
166
+ abortDiffCollection();
167
+ stopReview();
168
+ setCollectingDiffs(false);
169
+ requestInFlightRef.current = false;
170
+ }
171
+ }}
172
+ >
173
+ <SheetTrigger
174
+ render={
175
+ <Button
176
+ size="xs"
177
+ variant="outline"
178
+ disabled={!isRepoReady || loading}
179
+ />
180
+ }
181
+ >
182
+ {loading ? <Loader2 className="animate-spin" /> : <MessageSquareCode />}{" "}
183
+ Review
184
+ </SheetTrigger>
185
+ <SheetContent
186
+ side="right"
187
+ className="data-[side=right]:w-[96vw] data-[side=right]:sm:max-w-3xl"
188
+ >
189
+ <SheetHeader>
190
+ <div className="space-y-2">
191
+ <div className="flex items-center gap-2">
192
+ <Select
193
+ value={reviewModel}
194
+ onValueChange={(nextModel) =>
195
+ nextModel && setReviewModel(nextModel)
196
+ }
197
+ >
198
+ <SelectTrigger
199
+ size="sm"
200
+ className="h-6 gap-1 px-2 text-xs font-mono"
201
+ >
202
+ <SelectValue />
203
+ </SelectTrigger>
204
+ <SelectContent>
205
+ {reviewModelOptions.map((option) => (
206
+ <SelectItem key={option.id} value={option.id}>
207
+ {option.label}
208
+ </SelectItem>
209
+ ))}
210
+ </SelectContent>
211
+ </Select>
212
+ <Button
213
+ size="xs"
214
+ disabled={loading}
215
+ onClick={() => void requestReview()}
216
+ >
217
+ {loading ? (
218
+ <Loader2 className="animate-spin" />
219
+ ) : (
220
+ <RefreshCcw />
221
+ )}{" "}
222
+ Run
223
+ </Button>
224
+ </div>
225
+ <Input
226
+ value={reviewApiKey}
227
+ onChange={(event) => setReviewApiKey(event.currentTarget.value)}
228
+ type="password"
229
+ placeholder="OpenRouter API key (optional)"
230
+ className="h-7 font-mono text-xs"
231
+ />
232
+ <p className="text-xs text-muted-foreground">
233
+ Empty = use <code>OPENROUTER_API_KEY</code> from shell env.
234
+ </p>
235
+ </div>
236
+ </SheetHeader>
237
+ <div className="flex-1 overflow-auto p-4">
238
+ <ReviewView loading={loading} error={error} markdown={markdown} />
239
+ </div>
240
+ </SheetContent>
241
+ </Sheet>
242
+ );
243
+ }
244
+
245
+ import { useCallback, useEffect, useRef, useState } from "react";
246
+ import { useCompletion } from "@ai-sdk/react";
247
+ import { Loader2, MessageSquareCode, RefreshCcw } from "lucide-react";
248
+ import { Button } from "./button";
249
+ import { Input } from "./input";
250
+ import { ReviewView } from "./review-view";
251
+ import { Sheet, SheetContent, SheetHeader, SheetTrigger } from "./sheet";
252
+ import {
253
+ Select,
254
+ SelectContent,
255
+ SelectItem,
256
+ SelectTrigger,
257
+ SelectValue,
258
+ } from "./select";
259
+ import { fetchAgentDiff } from "@/lib/diff-client";
260
+ import { agents } from "@/lib/store";
261
+ import { buildReviewPrompt, reviewModelOptions } from "@/lib/reviewer";
@@ -0,0 +1,40 @@
1
+ export function ReviewView({
2
+ loading,
3
+ error,
4
+ markdown,
5
+ }: {
6
+ loading: boolean;
7
+ error: string | null;
8
+ markdown: string | null;
9
+ }) {
10
+ if (!markdown && !loading && !error) {
11
+ return (
12
+ <p className="text-sm text-muted-foreground">
13
+ Click Review to compare all agent diffs.
14
+ </p>
15
+ );
16
+ }
17
+ if (!markdown && loading) {
18
+ return (
19
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
20
+ <Loader2 className="size-4 animate-spin" />
21
+ Collecting diffs and sending for review…
22
+ </div>
23
+ );
24
+ }
25
+ if (error && !markdown) {
26
+ return <p className="text-sm text-destructive">{error}</p>;
27
+ }
28
+
29
+ return (
30
+ <div className="space-y-3">
31
+ {error ? <p className="text-sm text-destructive">{error}</p> : null}
32
+ <Streamdown isAnimating={loading} caret={loading ? "block" : undefined}>
33
+ {markdown ?? ""}
34
+ </Streamdown>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ import { Streamdown } from "streamdown";
40
+ import { Loader2 } from "lucide-react";