@aku11i/phantom 6.2.0-0 → 6.3.0-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,1450 @@
1
+ import { r as __toESM } from "../_runtime.mjs";
2
+ import { d as require_react, u as require_jsx_runtime } from "../_libs/@tanstack/react-router+[...].mjs";
3
+ import { _ as ChevronRight, a as Send, b as Bot, c as PanelLeft, d as Inbox, f as GitBranch, g as ChevronsUpDown, h as Clock3, i as Sparkles, l as MessageSquare, m as FileText, n as TriangleAlert, o as Search, p as FolderGit2, r as Square, s as Plus, t as X, u as MessageSquarePlus, v as Check, y as Brain } from "../_libs/lucide-react.mjs";
4
+ //#region node_modules/.nitro/vite/services/ssr/assets/routes-DpnTz1lw.js
5
+ var import_react = /* @__PURE__ */ __toESM(require_react());
6
+ var import_jsx_runtime = require_jsx_runtime();
7
+ function cn(...classes) {
8
+ return classes.filter(Boolean).join(" ");
9
+ }
10
+ var variants$1 = {
11
+ default: "border-transparent bg-primary text-primary-foreground",
12
+ danger: "border-[var(--semantic-danger-border)] bg-[var(--semantic-danger-bg)] text-[var(--semantic-danger-fg)]",
13
+ info: "border-[var(--semantic-info-border)] bg-[var(--semantic-info-bg)] text-[var(--semantic-info-fg)]",
14
+ outline: "border-border bg-transparent text-foreground",
15
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
16
+ success: "border-[var(--semantic-success-border)] bg-[var(--semantic-success-bg)] text-[var(--semantic-success-fg)]",
17
+ warning: "border-[var(--semantic-warning-border)] bg-[var(--semantic-warning-bg)] text-[var(--semantic-warning-fg)]"
18
+ };
19
+ function Badge({ className, variant = "default", ...props }) {
20
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
21
+ className: cn("inline-flex items-center gap-1 rounded-[var(--radius-xs)] border px-2 py-0.5 text-[length:var(--font-size-xs)] font-medium whitespace-nowrap transition-colors", variants$1[variant], className),
22
+ ...props
23
+ });
24
+ }
25
+ var variants = {
26
+ default: "bg-primary text-primary-foreground shadow-[var(--shadow-xs)] hover:bg-[var(--color-gray-800)]",
27
+ destructive: "bg-destructive text-destructive-foreground shadow-[var(--shadow-xs)] hover:bg-[var(--color-rose-500)] focus-visible:ring-[var(--semantic-danger-border)]/40",
28
+ ghost: "hover:bg-accent hover:text-accent-foreground",
29
+ outline: "border border-input bg-[var(--surface-card)] shadow-[var(--shadow-xs)] hover:bg-accent hover:text-accent-foreground",
30
+ secondary: "bg-secondary text-secondary-foreground shadow-[var(--shadow-xs)] hover:bg-[var(--color-gray-150)]"
31
+ };
32
+ var sizes = {
33
+ default: "h-9 px-4 py-2",
34
+ icon: "size-8",
35
+ sm: "h-8 gap-1.5 px-3"
36
+ };
37
+ function Button({ className, size = "default", variant = "default", ...props }) {
38
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
39
+ className: cn("inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius-sm)] text-[length:var(--font-size-sm)] font-medium outline-none transition-colors duration-[var(--motion-duration-fast)] ease-[var(--motion-ease-standard)] focus-visible:border-ring focus-visible:shadow-[var(--state-focus-ring)] disabled:pointer-events-none disabled:opacity-[var(--opacity-disabled)] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", variants[variant], sizes[size], className),
40
+ ...props
41
+ });
42
+ }
43
+ function Input({ className, ...props }) {
44
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
45
+ className: cn("flex h-9 w-full min-w-0 rounded-[var(--radius-sm)] border border-input bg-[var(--surface-input)] px-3 py-1 text-[length:var(--font-size-md)] shadow-[var(--shadow-xs)] outline-none transition-[border-color,box-shadow] duration-[var(--motion-duration-fast)] placeholder:text-[var(--text-tertiary)] focus-visible:border-ring focus-visible:shadow-[var(--state-focus-ring)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-[var(--opacity-disabled)]", className),
46
+ ...props
47
+ });
48
+ }
49
+ function Combobox({ "aria-label": ariaLabel, align = "start", className, disabled = false, emptyMessage = "No results", icon, onQueryChange, onValueChange, options, placeholder, query, searchPlaceholder = "Search", shouldFilter = true, side = "bottom", triggerClassName, value }) {
50
+ const [isOpen, setIsOpen] = (0, import_react.useState)(false);
51
+ const [activeOptionIndex, setActiveOptionIndex] = (0, import_react.useState)(-1);
52
+ const [internalQuery, setInternalQuery] = (0, import_react.useState)("");
53
+ const rootRef = (0, import_react.useRef)(null);
54
+ const listboxId = (0, import_react.useId)();
55
+ const searchQuery = query ?? internalQuery;
56
+ const selectedOption = options.find((option) => option.value === value);
57
+ const filteredOptions = (0, import_react.useMemo)(() => {
58
+ if (!shouldFilter || !searchQuery.trim()) return options;
59
+ const normalizedQuery = searchQuery.trim().toLowerCase();
60
+ return options.filter((option) => {
61
+ return [
62
+ option.label,
63
+ option.description,
64
+ ...option.keywords ?? []
65
+ ].filter(Boolean).join(" ").toLowerCase().includes(normalizedQuery);
66
+ });
67
+ }, [
68
+ options,
69
+ searchQuery,
70
+ shouldFilter
71
+ ]);
72
+ (0, import_react.useEffect)(() => {
73
+ if (!isOpen) return;
74
+ const handlePointerDown = (event) => {
75
+ if (!rootRef.current?.contains(event.target)) setIsOpen(false);
76
+ };
77
+ const handleKeyDown = (event) => {
78
+ if (event.key === "Escape") setIsOpen(false);
79
+ };
80
+ document.addEventListener("pointerdown", handlePointerDown);
81
+ document.addEventListener("keydown", handleKeyDown);
82
+ return () => {
83
+ document.removeEventListener("pointerdown", handlePointerDown);
84
+ document.removeEventListener("keydown", handleKeyDown);
85
+ };
86
+ }, [isOpen]);
87
+ (0, import_react.useEffect)(() => {
88
+ if (!isOpen && query === void 0) setInternalQuery("");
89
+ }, [isOpen, query]);
90
+ (0, import_react.useEffect)(() => {
91
+ if (!isOpen) {
92
+ setActiveOptionIndex(-1);
93
+ return;
94
+ }
95
+ setActiveOptionIndex(getNextEnabledIndex(filteredOptions, -1, 1));
96
+ }, [filteredOptions, isOpen]);
97
+ function updateQuery(nextQuery) {
98
+ if (onQueryChange) onQueryChange(nextQuery);
99
+ else setInternalQuery(nextQuery);
100
+ }
101
+ function selectOption(option) {
102
+ if (option.disabled) return;
103
+ onValueChange(option.value);
104
+ setIsOpen(false);
105
+ updateQuery("");
106
+ }
107
+ function handleSearchKeyDown(event) {
108
+ if (event.key === "ArrowDown") {
109
+ event.preventDefault();
110
+ setActiveOptionIndex((current) => getNextEnabledIndex(filteredOptions, current, 1));
111
+ return;
112
+ }
113
+ if (event.key === "ArrowUp") {
114
+ event.preventDefault();
115
+ setActiveOptionIndex((current) => getNextEnabledIndex(filteredOptions, current, -1));
116
+ return;
117
+ }
118
+ if (event.key === "Home") {
119
+ event.preventDefault();
120
+ setActiveOptionIndex(getNextEnabledIndex(filteredOptions, -1, 1));
121
+ return;
122
+ }
123
+ if (event.key === "End") {
124
+ event.preventDefault();
125
+ setActiveOptionIndex(getNextEnabledIndex(filteredOptions, filteredOptions.length, -1));
126
+ return;
127
+ }
128
+ if (event.key === "Enter") {
129
+ event.preventDefault();
130
+ if (activeOptionIndex >= 0) {
131
+ const option = filteredOptions[activeOptionIndex];
132
+ if (!option) return;
133
+ selectOption(option);
134
+ }
135
+ }
136
+ }
137
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
138
+ className: cn("relative min-w-0", className),
139
+ ref: rootRef,
140
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
141
+ "aria-controls": listboxId,
142
+ "aria-expanded": isOpen,
143
+ "aria-label": ariaLabel,
144
+ className: cn("inline-flex h-8 max-w-full items-center gap-1.5 rounded-[var(--radius-sm)] border border-input bg-[var(--surface-card)] px-2.5 text-[length:var(--font-size-sm)] font-medium text-[var(--text-secondary)] shadow-[var(--shadow-xs)] outline-none transition-colors hover:bg-accent focus-visible:border-ring focus-visible:shadow-[var(--state-focus-ring)] disabled:pointer-events-none disabled:opacity-[var(--opacity-disabled)]", triggerClassName),
145
+ disabled,
146
+ onClick: () => setIsOpen((current) => !current),
147
+ role: "combobox",
148
+ type: "button",
149
+ children: [
150
+ icon,
151
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
152
+ className: "min-w-0 truncate",
153
+ children: selectedOption?.label ?? placeholder
154
+ }),
155
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronsUpDown, { className: "size-3.5 shrink-0 text-[var(--icon-color-muted)]" })
156
+ ]
157
+ }), isOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
158
+ className: cn("absolute z-50 w-80 max-w-[calc(100vw-2rem)] overflow-hidden rounded-[var(--radius-md)] border border-border bg-popover text-popover-foreground shadow-[var(--shadow-md)]", side === "top" ? "bottom-full mb-1" : "top-full mt-1", align === "end" ? "right-0" : "left-0"),
159
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
160
+ className: "flex items-center gap-2 border-b border-[var(--border-divider)] px-2 py-2",
161
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Search, { className: "size-3.5 shrink-0 text-[var(--icon-color-muted)]" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
162
+ "aria-activedescendant": activeOptionIndex >= 0 ? `${listboxId}-option-${activeOptionIndex}` : void 0,
163
+ "aria-autocomplete": "list",
164
+ "aria-controls": listboxId,
165
+ autoFocus: true,
166
+ className: "h-7 border-0 bg-transparent px-0 py-0 shadow-none focus-visible:shadow-none",
167
+ placeholder: searchPlaceholder,
168
+ value: searchQuery,
169
+ onChange: (event) => updateQuery(event.target.value),
170
+ onKeyDown: handleSearchKeyDown
171
+ })]
172
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
173
+ className: "max-h-64 overflow-y-auto p-1",
174
+ id: listboxId,
175
+ role: "listbox",
176
+ children: filteredOptions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
177
+ className: "px-2 py-3 text-[length:var(--font-size-sm)] text-[var(--text-tertiary)]",
178
+ children: emptyMessage
179
+ }) : filteredOptions.map((option, index) => {
180
+ const isSelected = option.value === value;
181
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
182
+ "aria-selected": isSelected,
183
+ className: cn("flex w-full min-w-0 items-start gap-2 rounded-[var(--radius-sm)] px-2 py-2 text-left outline-none transition-colors hover:bg-accent focus-visible:shadow-[var(--state-focus-ring)] disabled:pointer-events-none disabled:opacity-[var(--opacity-disabled)]", (index === activeOptionIndex || isSelected) && "bg-[var(--state-selected-bg)]"),
184
+ disabled: option.disabled,
185
+ id: `${listboxId}-option-${index}`,
186
+ onClick: () => selectOption(option),
187
+ onMouseEnter: () => setActiveOptionIndex(index),
188
+ role: "option",
189
+ type: "button",
190
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: cn("mt-0.5 size-3.5 shrink-0 text-[var(--icon-color-active)]", !isSelected && "opacity-0") }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
191
+ className: "min-w-0 flex-1",
192
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
193
+ className: "block truncate text-[length:var(--font-size-sm)] font-medium text-[var(--text-primary)]",
194
+ children: option.label
195
+ }), option.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
196
+ className: "mt-0.5 block truncate text-[length:var(--font-size-xs)] text-[var(--text-tertiary)]",
197
+ children: option.description
198
+ })]
199
+ })]
200
+ }, option.value);
201
+ })
202
+ })]
203
+ })]
204
+ });
205
+ }
206
+ function getNextEnabledIndex(options, currentIndex, direction) {
207
+ if (options.length === 0) return -1;
208
+ let nextIndex = currentIndex;
209
+ for (let offset = 0; offset < options.length; offset += 1) {
210
+ nextIndex = (nextIndex + direction + options.length) % options.length;
211
+ if (!options[nextIndex]?.disabled) return nextIndex;
212
+ }
213
+ return -1;
214
+ }
215
+ var focusableSelector = [
216
+ "a[href]",
217
+ "button:not([disabled])",
218
+ "input:not([disabled])",
219
+ "select:not([disabled])",
220
+ "textarea:not([disabled])",
221
+ "[tabindex]:not([tabindex='-1'])"
222
+ ].join(",");
223
+ function Dialog({ children, onOpenChange, open }) {
224
+ const contentRef = (0, import_react.useRef)(null);
225
+ (0, import_react.useEffect)(() => {
226
+ if (!open) return;
227
+ const previousActiveElement = document.activeElement;
228
+ getFocusableElements(contentRef.current)[0]?.focus();
229
+ return () => {
230
+ if (previousActiveElement instanceof HTMLElement) previousActiveElement.focus();
231
+ };
232
+ }, [open]);
233
+ function handleKeyDown(event) {
234
+ if (event.key === "Escape") {
235
+ event.preventDefault();
236
+ onOpenChange(false);
237
+ return;
238
+ }
239
+ if (event.key !== "Tab") return;
240
+ const focusableElements = getFocusableElements(contentRef.current);
241
+ if (focusableElements.length === 0) {
242
+ event.preventDefault();
243
+ return;
244
+ }
245
+ const firstElement = focusableElements[0];
246
+ const lastElement = focusableElements[focusableElements.length - 1];
247
+ if (!firstElement || !lastElement) return;
248
+ if (event.shiftKey && document.activeElement === firstElement) {
249
+ event.preventDefault();
250
+ lastElement.focus();
251
+ }
252
+ if (!event.shiftKey && document.activeElement === lastElement) {
253
+ event.preventDefault();
254
+ firstElement.focus();
255
+ }
256
+ }
257
+ if (!open) return null;
258
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
259
+ className: "fixed inset-0 z-50 flex items-center justify-center p-4",
260
+ onKeyDown: handleKeyDown,
261
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
262
+ "aria-label": "Close dialog",
263
+ className: "absolute inset-0 bg-[var(--surface-overlay)]",
264
+ onClick: () => onOpenChange(false),
265
+ tabIndex: -1,
266
+ type: "button"
267
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
268
+ ref: contentRef,
269
+ className: "contents",
270
+ children
271
+ })]
272
+ });
273
+ }
274
+ function DialogContent({ className, ...props }) {
275
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
276
+ className: cn("relative z-10 grid w-full max-w-md gap-4 rounded-[var(--radius-lg)] border border-border bg-card p-5 text-card-foreground shadow-[var(--shadow-lg)]", className),
277
+ "aria-modal": "true",
278
+ role: "dialog",
279
+ ...props
280
+ });
281
+ }
282
+ function DialogHeader({ className, ...props }) {
283
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
284
+ className: cn("grid gap-1.5", className),
285
+ ...props
286
+ });
287
+ }
288
+ function DialogTitle({ className, ...props }) {
289
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
290
+ className: cn("text-[length:var(--font-size-xl)] font-semibold leading-none", className),
291
+ ...props
292
+ });
293
+ }
294
+ function DialogDescription({ className, ...props }) {
295
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
296
+ className: cn("text-[length:var(--font-size-md)] text-muted-foreground", className),
297
+ ...props
298
+ });
299
+ }
300
+ function DialogFooter({ className, ...props }) {
301
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
302
+ className: cn("flex justify-end gap-2", className),
303
+ ...props
304
+ });
305
+ }
306
+ function getFocusableElements(root) {
307
+ if (!root) return [];
308
+ return Array.from(root.querySelectorAll(focusableSelector)).filter((element) => !element.hasAttribute("disabled") && element.getAttribute("aria-hidden") !== "true");
309
+ }
310
+ function Label({ className, ...props }) {
311
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", {
312
+ className: cn("text-[length:var(--font-size-sm)] font-medium leading-none text-foreground", className),
313
+ ...props
314
+ });
315
+ }
316
+ var SIDEBAR_KEYBOARD_SHORTCUT = "b";
317
+ var SidebarContext = (0, import_react.createContext)(null);
318
+ function useSidebar() {
319
+ const context = (0, import_react.useContext)(SidebarContext);
320
+ if (!context) throw new Error("useSidebar must be used within a SidebarProvider.");
321
+ return context;
322
+ }
323
+ function SidebarProvider({ children, className, defaultOpen = true, open: openProp, onOpenChange, style, ...props }) {
324
+ const [_open, _setOpen] = (0, import_react.useState)(defaultOpen);
325
+ const open = openProp ?? _open;
326
+ const setOpen = (0, import_react.useCallback)((value) => {
327
+ const nextOpen = typeof value === "function" ? value(open) : value;
328
+ onOpenChange?.(nextOpen);
329
+ if (openProp === void 0) _setOpen(nextOpen);
330
+ }, [
331
+ onOpenChange,
332
+ open,
333
+ openProp
334
+ ]);
335
+ const toggleSidebar = (0, import_react.useCallback)(() => {
336
+ setOpen((current) => !current);
337
+ }, [setOpen]);
338
+ (0, import_react.useEffect)(() => {
339
+ const handleKeyDown = (event) => {
340
+ if (event.key.toLowerCase() === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
341
+ event.preventDefault();
342
+ toggleSidebar();
343
+ }
344
+ };
345
+ window.addEventListener("keydown", handleKeyDown);
346
+ return () => window.removeEventListener("keydown", handleKeyDown);
347
+ }, [toggleSidebar]);
348
+ const contextValue = (0, import_react.useMemo)(() => ({
349
+ open,
350
+ setOpen,
351
+ state: open ? "expanded" : "collapsed",
352
+ toggleSidebar
353
+ }), [
354
+ open,
355
+ setOpen,
356
+ toggleSidebar
357
+ ]);
358
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarContext.Provider, {
359
+ value: contextValue,
360
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
361
+ className: cn("group/sidebar-wrapper flex min-h-svh w-full bg-background text-foreground has-[[data-variant=inset]]:bg-background", className),
362
+ style: {
363
+ "--sidebar-width": "var(--layout-sidebar-width)",
364
+ ...style
365
+ },
366
+ ...props,
367
+ children
368
+ })
369
+ });
370
+ }
371
+ function Sidebar({ children, className, collapsible = "offcanvas", variant = "sidebar", ...props }) {
372
+ const { setOpen, state } = useSidebar();
373
+ const isOffcanvasCollapsed = state === "collapsed" && collapsible === "offcanvas";
374
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [!isOffcanvasCollapsed && collapsible === "offcanvas" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
375
+ "aria-label": "Close sidebar",
376
+ className: "fixed inset-0 z-30 bg-[var(--surface-overlay)] md:hidden",
377
+ onClick: () => setOpen(false),
378
+ type: "button"
379
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("aside", {
380
+ "data-collapsible": state === "collapsed" ? collapsible : "",
381
+ "data-state": state,
382
+ "data-variant": variant,
383
+ className: cn("group/sidebar fixed inset-y-0 left-0 z-40 flex h-svh w-[var(--sidebar-width)] shrink-0 flex-col overflow-hidden text-sidebar-foreground transition-transform duration-[var(--motion-duration-normal)] ease-[var(--motion-ease-standard)] md:relative md:z-auto", isOffcanvasCollapsed && "hidden", !isOffcanvasCollapsed && "translate-x-0", !isOffcanvasCollapsed && variant === "sidebar" && "border-r border-sidebar-border bg-sidebar", !isOffcanvasCollapsed && variant === "inset" && "border-r border-sidebar-border bg-sidebar", className),
384
+ ...props,
385
+ children: !isOffcanvasCollapsed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
386
+ className: "flex h-full min-h-0 flex-col bg-sidebar",
387
+ children
388
+ })
389
+ })] });
390
+ }
391
+ function SidebarHeader({ className, ...props }) {
392
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
393
+ className: cn("flex min-h-[var(--layout-topbar-height)] items-center gap-2 border-b border-sidebar-border px-3 group-data-[state=collapsed]/sidebar:justify-center group-data-[state=collapsed]/sidebar:px-2", className),
394
+ ...props
395
+ });
396
+ }
397
+ function SidebarInset({ className, ...props }) {
398
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("main", {
399
+ className: cn("relative flex min-w-0 flex-1 flex-col bg-[var(--surface-panel)]", className),
400
+ ...props
401
+ });
402
+ }
403
+ function SidebarTrigger({ className, ...props }) {
404
+ const { toggleSidebar } = useSidebar();
405
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
406
+ className: cn("inline-flex size-8 shrink-0 items-center justify-center rounded-[var(--radius-sm)] text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:shadow-[var(--state-focus-ring)] disabled:pointer-events-none disabled:opacity-[var(--opacity-disabled)]", className),
407
+ onClick: toggleSidebar,
408
+ title: "Toggle sidebar",
409
+ type: "button",
410
+ ...props,
411
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PanelLeft, { className: "size-4" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
412
+ className: "sr-only",
413
+ children: "Toggle sidebar"
414
+ })]
415
+ });
416
+ }
417
+ function SidebarRail({ className, ...props }) {
418
+ const { toggleSidebar } = useSidebar();
419
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
420
+ "aria-label": "Toggle sidebar",
421
+ className: cn("absolute inset-y-0 -right-3 z-20 hidden w-6 -translate-x-px transition-colors after:absolute after:inset-y-0 after:left-1/2 after:w-px hover:after:bg-sidebar-border sm:flex", className),
422
+ onClick: toggleSidebar,
423
+ tabIndex: -1,
424
+ type: "button",
425
+ ...props
426
+ });
427
+ }
428
+ function SidebarContent({ className, ...props }) {
429
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
430
+ className: cn("min-h-0 flex-1 overflow-y-auto p-2 group-data-[state=collapsed]/sidebar:px-2", className),
431
+ ...props
432
+ });
433
+ }
434
+ function SidebarGroup({ className, ...props }) {
435
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("section", {
436
+ className: cn("relative space-y-1 group-data-[state=collapsed]/sidebar:space-y-2", className),
437
+ ...props
438
+ });
439
+ }
440
+ function SidebarGroupHeader({ className, ...props }) {
441
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
442
+ className: cn("flex items-center gap-2 group-data-[state=collapsed]/sidebar:hidden", className),
443
+ ...props
444
+ });
445
+ }
446
+ function SidebarGroupLabel({ className, ...props }) {
447
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
448
+ className: cn("min-w-0 flex-1 px-2 py-1 text-[length:var(--font-size-xs)] font-medium text-[var(--text-secondary)]", className),
449
+ ...props
450
+ });
451
+ }
452
+ function SidebarGroupAction({ className, ...props }) {
453
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
454
+ className: cn("inline-flex size-7 shrink-0 items-center justify-center rounded-[var(--radius-sm)] text-[var(--icon-color-default)] outline-none transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:shadow-[var(--state-focus-ring)]", className),
455
+ type: "button",
456
+ ...props
457
+ });
458
+ }
459
+ function SidebarGroupContent({ className, ...props }) {
460
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
461
+ className: cn("w-full", className),
462
+ ...props
463
+ });
464
+ }
465
+ function SidebarMenu({ className, ...props }) {
466
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
467
+ className: cn("space-y-1", className),
468
+ ...props
469
+ });
470
+ }
471
+ function SidebarMenuItem({ className, ...props }) {
472
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", {
473
+ className: cn("min-w-0", className),
474
+ ...props
475
+ });
476
+ }
477
+ function SidebarMenuButton({ className, isActive, ...props }) {
478
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
479
+ className: cn("flex min-h-8 w-full min-w-0 items-center gap-2 rounded-[var(--radius-sm)] px-2 py-1.5 text-left text-[length:var(--font-size-sm)] outline-none transition-colors duration-[var(--motion-duration-fast)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:shadow-[var(--state-focus-ring)] group-data-[state=collapsed]/sidebar:justify-center group-data-[state=collapsed]/sidebar:px-0", isActive && "bg-sidebar-accent text-sidebar-accent-foreground", className),
480
+ ...props
481
+ });
482
+ }
483
+ function SidebarMenuSub({ className, ...props }) {
484
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
485
+ className: cn("ml-4 mt-1 space-y-1 border-l border-sidebar-border pl-2 group-data-[state=collapsed]/sidebar:hidden", className),
486
+ ...props
487
+ });
488
+ }
489
+ function SidebarMenuSubItem({ className, ...props }) {
490
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", {
491
+ className: cn("min-w-0", className),
492
+ ...props
493
+ });
494
+ }
495
+ function SidebarMenuSubButton({ className, isActive, ...props }) {
496
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
497
+ className: cn("flex min-h-7 w-full min-w-0 items-center gap-2 rounded-[var(--radius-sm)] px-2 py-1 text-left text-[length:var(--font-size-sm)] outline-none transition-colors duration-[var(--motion-duration-fast)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:shadow-[var(--state-focus-ring)]", isActive && "bg-sidebar-accent text-sidebar-accent-foreground", className),
498
+ ...props
499
+ });
500
+ }
501
+ function Textarea({ className, ...props }) {
502
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", {
503
+ className: cn("flex min-h-16 w-full min-w-0 resize-none rounded-[var(--radius-sm)] border border-input bg-[var(--surface-input)] px-3 py-2 text-[length:var(--font-size-md)] shadow-[var(--shadow-xs)] outline-none transition-[border-color,box-shadow] duration-[var(--motion-duration-fast)] placeholder:text-[var(--text-tertiary)] focus-visible:border-ring focus-visible:shadow-[var(--state-focus-ring)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-[var(--opacity-disabled)]", className),
504
+ ...props
505
+ });
506
+ }
507
+ var chatEventNames = [
508
+ "chat.created",
509
+ "chat.updated",
510
+ "chat.message.created",
511
+ "agent.thread.started",
512
+ "agent.turn.started",
513
+ "agent.item.updated",
514
+ "agent.item.delta",
515
+ "agent.approval.requested",
516
+ "agent.approval.resolved",
517
+ "agent.turn.completed",
518
+ "agent.error",
519
+ "agent.event",
520
+ "auth.updated"
521
+ ];
522
+ var statusMeta = {
523
+ archived: {
524
+ badge: "secondary",
525
+ dot: "bg-[var(--color-gray-400)]",
526
+ label: "Archived"
527
+ },
528
+ failed: {
529
+ badge: "danger",
530
+ dot: "bg-[var(--semantic-danger-fg)]",
531
+ label: "Failed"
532
+ },
533
+ idle: {
534
+ badge: "secondary",
535
+ dot: "bg-[var(--color-gray-500)]",
536
+ label: "Idle"
537
+ },
538
+ running: {
539
+ badge: "info",
540
+ dot: "bg-[var(--semantic-info-fg)]",
541
+ label: "Running"
542
+ },
543
+ waitingForApproval: {
544
+ badge: "warning",
545
+ dot: "bg-[var(--semantic-warning-fg)]",
546
+ label: "Approval"
547
+ }
548
+ };
549
+ function firstProjectWorktree(projectId, worktreesByProject) {
550
+ if (!projectId) return null;
551
+ return worktreesByProject[projectId]?.[0] ?? null;
552
+ }
553
+ function formatLeadingEllipsisPath(path, maxLength = 44) {
554
+ if (path.length <= maxLength) return path;
555
+ const suffixLength = maxLength - 3;
556
+ const suffix = path.slice(-suffixLength);
557
+ const slashIndex = suffix.indexOf("/");
558
+ return `...${slashIndex > 0 ? suffix.slice(slashIndex) : suffix}`;
559
+ }
560
+ function dedupeChatThreads(chats) {
561
+ const chatsWithThreads = chats.filter((chat) => chat.codexThreadId);
562
+ const source = chatsWithThreads.length > 0 ? chatsWithThreads : chats;
563
+ const chatsByThread = /* @__PURE__ */ new Map();
564
+ for (const chat of source) {
565
+ const key = chat.codexThreadId ?? chat.id;
566
+ const current = chatsByThread.get(key);
567
+ if (!current || chat.updatedAt.localeCompare(current.updatedAt) > 0) chatsByThread.set(key, chat);
568
+ }
569
+ return [...chatsByThread.values()].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
570
+ }
571
+ function Home() {
572
+ const [projects, setProjects] = (0, import_react.useState)([]);
573
+ const [selectedProjectId, setSelectedProjectId] = (0, import_react.useState)(null);
574
+ const [chatsByProject, setChatsByProject] = (0, import_react.useState)({});
575
+ const [worktreesByProject, setWorktreesByProject] = (0, import_react.useState)({});
576
+ const [expandedProjectIds, setExpandedProjectIds] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
577
+ const [selectedWorktreePath, setSelectedWorktreePath] = (0, import_react.useState)(null);
578
+ const [selectedChatId, setSelectedChatId] = (0, import_react.useState)(null);
579
+ const [messages, setMessages] = (0, import_react.useState)([]);
580
+ const [isAddProjectOpen, setIsAddProjectOpen] = (0, import_react.useState)(false);
581
+ const [projectPath, setProjectPath] = (0, import_react.useState)("");
582
+ const [composerText, setComposerText] = (0, import_react.useState)("");
583
+ const [models, setModels] = (0, import_react.useState)([]);
584
+ const [selectedModelId, setSelectedModelId] = (0, import_react.useState)(null);
585
+ const [selectedEffort, setSelectedEffort] = (0, import_react.useState)(null);
586
+ const [skills, setSkills] = (0, import_react.useState)([]);
587
+ const [selectedSkillPaths, setSelectedSkillPaths] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
588
+ const [fileSearchQuery, setFileSearchQuery] = (0, import_react.useState)("");
589
+ const fileSearchRequestIdRef = (0, import_react.useRef)(0);
590
+ const [fileSearchResults, setFileSearchResults] = (0, import_react.useState)([]);
591
+ const [selectedFiles, setSelectedFiles] = (0, import_react.useState)([]);
592
+ const [status, setStatus] = (0, import_react.useState)("Starting");
593
+ const [error, setError] = (0, import_react.useState)(null);
594
+ const [isBusy, setIsBusy] = (0, import_react.useState)(false);
595
+ const createChatInFlightRef = (0, import_react.useRef)(false);
596
+ const selectedChatIdRef = (0, import_react.useRef)(null);
597
+ const selectedChatVersionRef = (0, import_react.useRef)(0);
598
+ const sendMessageRequestIdRef = (0, import_react.useRef)(0);
599
+ const [pendingApproval, setPendingApproval] = (0, import_react.useState)(null);
600
+ const selectedProject = (0, import_react.useMemo)(() => projects.find((project) => project.id === selectedProjectId) ?? null, [projects, selectedProjectId]);
601
+ const selectedChat = (0, import_react.useMemo)(() => Object.values(chatsByProject).flat().find((chat) => chat.id === selectedChatId) ?? null, [chatsByProject, selectedChatId]);
602
+ const isChatRunning = Boolean(selectedChat?.activeTurnId);
603
+ const selectedModel = (0, import_react.useMemo)(() => models.find((model) => model.id === selectedModelId) ?? models.find((model) => model.isDefault) ?? models[0] ?? null, [models, selectedModelId]);
604
+ const selectedSkills = (0, import_react.useMemo)(() => skills.filter((skill) => selectedSkillPaths.has(skill.path)), [selectedSkillPaths, skills]);
605
+ const modelOptions = (0, import_react.useMemo)(() => models.map((model) => ({
606
+ value: model.id,
607
+ label: model.displayName,
608
+ description: model.description || model.model,
609
+ keywords: [model.model]
610
+ })), [models]);
611
+ const effortOptions = (0, import_react.useMemo)(() => {
612
+ const supportedEfforts = selectedModel?.supportedReasoningEfforts.length ? selectedModel.supportedReasoningEfforts : [
613
+ "low",
614
+ "medium",
615
+ "high",
616
+ "xhigh"
617
+ ];
618
+ return [{
619
+ value: "auto",
620
+ label: "Auto",
621
+ description: selectedModel?.defaultReasoningEffort ? `Default: ${selectedModel.defaultReasoningEffort}` : "Use model default"
622
+ }, ...supportedEfforts.map((effort) => ({
623
+ value: effort,
624
+ label: formatReasoningEffort(effort)
625
+ }))];
626
+ }, [selectedModel]);
627
+ const skillOptions = (0, import_react.useMemo)(() => skills.filter((skill) => skill.enabled && !selectedSkillPaths.has(skill.path)).map((skill) => ({
628
+ value: skill.path,
629
+ label: skill.displayName,
630
+ description: skill.shortDescription ?? skill.description,
631
+ keywords: [skill.name]
632
+ })), [selectedSkillPaths, skills]);
633
+ const fileOptions = (0, import_react.useMemo)(() => fileSearchResults.filter((file) => !selectedFiles.some((selectedFile) => selectedFile.path === file.path)).map((file) => ({
634
+ value: file.path,
635
+ label: file.relativePath,
636
+ description: file.root,
637
+ keywords: [file.name]
638
+ })), [fileSearchResults, selectedFiles]);
639
+ const selectedWorktree = (0, import_react.useMemo)(() => {
640
+ if (!selectedProjectId || !selectedWorktreePath) return null;
641
+ return (worktreesByProject[selectedProjectId] ?? []).find((worktree) => worktree.path === selectedWorktreePath) ?? null;
642
+ }, [
643
+ selectedProjectId,
644
+ selectedWorktreePath,
645
+ worktreesByProject
646
+ ]);
647
+ const selectedWorktreeChats = (0, import_react.useMemo)(() => {
648
+ if (!selectedProjectId || !selectedWorktree) return [];
649
+ return dedupeChatThreads((chatsByProject[selectedProjectId] ?? []).filter((chat) => chat.worktreePath === selectedWorktree.path));
650
+ }, [
651
+ chatsByProject,
652
+ selectedProjectId,
653
+ selectedWorktree
654
+ ]);
655
+ const visibleMessages = (0, import_react.useMemo)(() => messages.filter((message) => message.role !== "event"), [messages]);
656
+ (0, import_react.useEffect)(() => {
657
+ refreshProjects();
658
+ refreshModels();
659
+ fetchJson("/api/auth").then(() => setStatus("Ready")).catch((err) => setStatus(err.message));
660
+ }, []);
661
+ (0, import_react.useEffect)(() => {
662
+ if (!selectedProjectId) {
663
+ setSelectedChatId(null);
664
+ return;
665
+ }
666
+ setExpandedProjectIds((current) => {
667
+ const next = new Set(current);
668
+ next.add(selectedProjectId);
669
+ return next;
670
+ });
671
+ refreshChats(selectedProjectId, { sync: true });
672
+ }, [selectedProjectId]);
673
+ (0, import_react.useEffect)(() => {
674
+ selectedChatIdRef.current = selectedChatId;
675
+ selectedChatVersionRef.current += 1;
676
+ if (!selectedChatId) {
677
+ setMessages([]);
678
+ setPendingApproval(null);
679
+ setSelectedFiles([]);
680
+ setSelectedSkillPaths(/* @__PURE__ */ new Set());
681
+ setFileSearchQuery("");
682
+ setFileSearchResults([]);
683
+ setSkills([]);
684
+ return;
685
+ }
686
+ setSelectedFiles([]);
687
+ setSelectedSkillPaths(/* @__PURE__ */ new Set());
688
+ setFileSearchQuery("");
689
+ setFileSearchResults([]);
690
+ setSkills([]);
691
+ const chatContextController = new AbortController();
692
+ refreshMessages(selectedChatId);
693
+ refreshSelectedChat(selectedChatId);
694
+ refreshChatContext(selectedChatId, chatContextController.signal);
695
+ const source = new EventSource(`/api/chats/${selectedChatId}/events`);
696
+ const handleEvent = (event) => {
697
+ const phantomEvent = JSON.parse(event.data);
698
+ if (phantomEvent.type === "agent.approval.requested") {
699
+ const data = phantomEvent.data;
700
+ setPendingApproval(data);
701
+ }
702
+ if (phantomEvent.type === "agent.approval.resolved") setPendingApproval(null);
703
+ refreshMessages(selectedChatId);
704
+ refreshSelectedChat(selectedChatId);
705
+ if (selectedProjectId) refreshChats(selectedProjectId);
706
+ };
707
+ for (const eventName of chatEventNames) source.addEventListener(eventName, handleEvent);
708
+ source.onerror = () => setStatus("Event stream disconnected");
709
+ return () => {
710
+ chatContextController.abort();
711
+ for (const eventName of chatEventNames) source.removeEventListener(eventName, handleEvent);
712
+ source.close();
713
+ };
714
+ }, [selectedChatId, selectedProjectId]);
715
+ (0, import_react.useEffect)(() => {
716
+ if (!selectedEffort || selectedEffort === "auto") return;
717
+ const supportedEfforts = selectedModel?.supportedReasoningEfforts ?? [];
718
+ if (supportedEfforts.length > 0 && !supportedEfforts.includes(selectedEffort)) setSelectedEffort(null);
719
+ }, [selectedEffort, selectedModel]);
720
+ (0, import_react.useEffect)(() => {
721
+ const requestId = fileSearchRequestIdRef.current + 1;
722
+ fileSearchRequestIdRef.current = requestId;
723
+ if (!selectedChatId || !fileSearchQuery.trim()) {
724
+ setFileSearchResults([]);
725
+ return;
726
+ }
727
+ const controller = new AbortController();
728
+ const query = fileSearchQuery.trim();
729
+ const timeout = setTimeout(() => {
730
+ fetchJson(`/api/chats/${selectedChatId}?fileQuery=${encodeURIComponent(query)}`, { signal: controller.signal }).then((data) => {
731
+ if (controller.signal.aborted || fileSearchRequestIdRef.current !== requestId) return;
732
+ setFileSearchResults(data.files);
733
+ }).catch((err) => {
734
+ if (err.name !== "AbortError") setError(err.message);
735
+ });
736
+ }, 160);
737
+ return () => {
738
+ clearTimeout(timeout);
739
+ controller.abort();
740
+ };
741
+ }, [fileSearchQuery, selectedChatId]);
742
+ (0, import_react.useEffect)(() => {
743
+ if (selectedWorktreeChats.length === 0 || selectedWorktreeChats.some((chat) => chat.id === selectedChatId)) return;
744
+ setSelectedChatId(selectedWorktreeChats[0]?.id ?? null);
745
+ }, [selectedChatId, selectedWorktreeChats]);
746
+ async function refreshProjects() {
747
+ const data = await fetchJson("/api/projects");
748
+ setProjects(data.projects);
749
+ const projectDataEntries = await Promise.all(data.projects.map(async (project) => [project.id, await loadProjectData(project.id, { sync: true })]));
750
+ const nextChatsByProject = Object.fromEntries(projectDataEntries.map(([projectId, projectData]) => [projectId, projectData.chats]));
751
+ const nextWorktreesByProject = Object.fromEntries(projectDataEntries.map(([projectId, projectData]) => [projectId, projectData.worktrees]));
752
+ setChatsByProject(nextChatsByProject);
753
+ setWorktreesByProject(nextWorktreesByProject);
754
+ const fallbackProjectId = selectedProjectId ?? data.projects[0]?.id ?? null;
755
+ const fallbackWorktree = firstProjectWorktree(fallbackProjectId, nextWorktreesByProject);
756
+ setSelectedProjectId((current) => {
757
+ const nextProjectId = current ?? data.projects[0]?.id ?? null;
758
+ if (nextProjectId) setExpandedProjectIds((expanded) => {
759
+ const next = new Set(expanded);
760
+ next.add(nextProjectId);
761
+ return next;
762
+ });
763
+ return nextProjectId;
764
+ });
765
+ setSelectedWorktreePath((current) => {
766
+ if (fallbackProjectId && current && (nextWorktreesByProject[fallbackProjectId] ?? []).some((worktree) => worktree.path === current)) return current;
767
+ return fallbackWorktree?.path ?? null;
768
+ });
769
+ setSelectedChatId((current) => Object.values(nextChatsByProject).flat().some((chat) => chat.id === current) ? current : fallbackWorktree?.chatId ?? null);
770
+ }
771
+ async function refreshModels() {
772
+ try {
773
+ const data = await fetchJson("/api/models");
774
+ setModels(data.models);
775
+ setSelectedModelId((current) => {
776
+ if (current && data.models.some((model) => model.id === current)) return current;
777
+ return data.models.find((model) => model.isDefault)?.id ?? data.models[0]?.id ?? null;
778
+ });
779
+ } catch (err) {
780
+ setError(err instanceof Error ? err.message : String(err));
781
+ }
782
+ }
783
+ async function refreshChatContext(chatId, signal) {
784
+ try {
785
+ const data = await fetchJson(`/api/chats/${chatId}?context=skills`, { signal });
786
+ if (signal?.aborted) return;
787
+ setSkills(data.skills);
788
+ } catch (err) {
789
+ if (err instanceof Error && err.name === "AbortError") return;
790
+ setError(err instanceof Error ? err.message : String(err));
791
+ }
792
+ }
793
+ async function loadProjectData(projectId, options = {}) {
794
+ return await fetchJson(`/api/projects/${projectId}/chats${options.sync ? "?sync=1" : ""}`);
795
+ }
796
+ async function refreshChats(projectId, options = {}) {
797
+ const projectData = await loadProjectData(projectId, options);
798
+ setChatsByProject((current) => ({
799
+ ...current,
800
+ [projectId]: projectData.chats
801
+ }));
802
+ setWorktreesByProject((current) => ({
803
+ ...current,
804
+ [projectId]: projectData.worktrees
805
+ }));
806
+ const fallbackWorktree = firstProjectWorktree(projectId, {
807
+ ...worktreesByProject,
808
+ [projectId]: projectData.worktrees
809
+ });
810
+ setSelectedWorktreePath((current) => current && projectData.worktrees.some((worktree) => worktree.path === current) ? current : fallbackWorktree?.path ?? null);
811
+ setSelectedChatId((current) => projectData.chats.some((chat) => chat.id === current) ? current : fallbackWorktree?.chatId ?? null);
812
+ }
813
+ async function refreshSelectedChat(chatId) {
814
+ const data = await fetchJson(`/api/chats/${chatId}`);
815
+ setChatsByProject((current) => {
816
+ const projectChats = current[data.chat.projectId] ?? [];
817
+ return {
818
+ ...current,
819
+ [data.chat.projectId]: projectChats.map((chat) => chat.id === chatId ? data.chat : chat)
820
+ };
821
+ });
822
+ setWorktreesByProject((current) => ({
823
+ ...current,
824
+ [data.chat.projectId]: (current[data.chat.projectId] ?? []).map((worktree) => worktree.chatId === chatId ? {
825
+ ...worktree,
826
+ chatStatus: data.chat.status,
827
+ chatTitle: data.chat.title
828
+ } : worktree)
829
+ }));
830
+ }
831
+ async function refreshMessages(chatId) {
832
+ setMessages((await fetchJson(`/api/chats/${chatId}/messages`)).messages);
833
+ }
834
+ async function addProject(event) {
835
+ event.preventDefault();
836
+ const trimmedProjectPath = projectPath.trim();
837
+ if (!trimmedProjectPath) return;
838
+ setError(null);
839
+ setIsBusy(true);
840
+ try {
841
+ const data = await fetchJson("/api/projects", {
842
+ method: "POST",
843
+ body: JSON.stringify({ path: trimmedProjectPath })
844
+ });
845
+ setProjectPath("");
846
+ setIsAddProjectOpen(false);
847
+ await refreshProjects();
848
+ setSelectedProjectId(data.project.id);
849
+ } catch (err) {
850
+ setError(err instanceof Error ? err.message : String(err));
851
+ } finally {
852
+ setIsBusy(false);
853
+ }
854
+ }
855
+ async function createChat(projectId) {
856
+ if (isBusy || createChatInFlightRef.current) return;
857
+ setError(null);
858
+ createChatInFlightRef.current = true;
859
+ setIsBusy(true);
860
+ try {
861
+ const data = await fetchJson(`/api/projects/${projectId}/chats`, {
862
+ method: "POST",
863
+ body: JSON.stringify({})
864
+ });
865
+ setSelectedProjectId(projectId);
866
+ setExpandedProjectIds((current) => new Set(current).add(projectId));
867
+ await refreshChats(projectId, { sync: true });
868
+ setSelectedWorktreePath(data.chat.worktreePath);
869
+ setSelectedChatId(data.chat.id);
870
+ } catch (err) {
871
+ setError(err instanceof Error ? err.message : String(err));
872
+ } finally {
873
+ createChatInFlightRef.current = false;
874
+ setIsBusy(false);
875
+ }
876
+ }
877
+ async function sendMessage(event) {
878
+ event.preventDefault();
879
+ if (!selectedChatId || !composerText.trim()) return;
880
+ setError(null);
881
+ const requestChatId = selectedChatId;
882
+ const requestChatVersion = selectedChatVersionRef.current;
883
+ const requestId = sendMessageRequestIdRef.current + 1;
884
+ sendMessageRequestIdRef.current = requestId;
885
+ const isCurrentSendRequest = () => selectedChatIdRef.current === requestChatId && selectedChatVersionRef.current === requestChatVersion && sendMessageRequestIdRef.current === requestId;
886
+ const text = composerText;
887
+ setComposerText("");
888
+ const turnModel = selectedModel?.id ?? selectedModel?.model;
889
+ const turnEffort = selectedEffort === "auto" ? null : selectedEffort;
890
+ const files = selectedFiles.map((file) => ({
891
+ name: file.relativePath,
892
+ path: file.path
893
+ }));
894
+ const selectedSkillItems = selectedSkills.map((skill) => ({
895
+ name: skill.name,
896
+ path: skill.path
897
+ }));
898
+ try {
899
+ await fetchJson(`/api/chats/${requestChatId}/messages`, {
900
+ method: "POST",
901
+ body: JSON.stringify({
902
+ effort: turnEffort,
903
+ files,
904
+ model: turnModel,
905
+ skills: selectedSkillItems,
906
+ text
907
+ })
908
+ });
909
+ if (!isCurrentSendRequest()) return;
910
+ setSelectedFiles([]);
911
+ setSelectedSkillPaths(/* @__PURE__ */ new Set());
912
+ setFileSearchQuery("");
913
+ await refreshMessages(requestChatId);
914
+ } catch (err) {
915
+ if (!isCurrentSendRequest()) return;
916
+ setComposerText(text);
917
+ setError(err instanceof Error ? err.message : String(err));
918
+ }
919
+ }
920
+ function handleComposerKeyDown(event) {
921
+ const isImeComposing = event.nativeEvent.isComposing || event.keyCode === 229;
922
+ if (event.key !== "Enter" || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey || isImeComposing) return;
923
+ if (!selectedChatId || !composerText.trim() || isChatRunning) return;
924
+ event.preventDefault();
925
+ event.currentTarget.form?.requestSubmit();
926
+ }
927
+ async function interruptChat() {
928
+ if (!selectedChatId) return;
929
+ setError(null);
930
+ try {
931
+ await fetchJson(`/api/chats/${selectedChatId}/interrupt`, { method: "POST" });
932
+ } catch (err) {
933
+ setError(err instanceof Error ? err.message : String(err));
934
+ }
935
+ }
936
+ async function answerApproval(decision) {
937
+ if (!selectedChatId || !pendingApproval) return;
938
+ setError(null);
939
+ try {
940
+ await fetchJson(`/api/chats/${encodeURIComponent(selectedChatId)}/approvals/${encodeURIComponent(pendingApproval.requestId)}`, {
941
+ method: "POST",
942
+ body: JSON.stringify({ decision })
943
+ });
944
+ setPendingApproval(null);
945
+ } catch (err) {
946
+ setError(err instanceof Error ? err.message : String(err));
947
+ }
948
+ }
949
+ function toggleProject(projectId) {
950
+ setExpandedProjectIds((current) => {
951
+ const next = new Set(current);
952
+ if (next.has(projectId)) next.delete(projectId);
953
+ else next.add(projectId);
954
+ return next;
955
+ });
956
+ }
957
+ function selectWorktree(projectId, worktree) {
958
+ setSelectedProjectId(projectId);
959
+ setSelectedWorktreePath(worktree.path);
960
+ setSelectedChatId(worktree.chatId);
961
+ setExpandedProjectIds((current) => new Set(current).add(projectId));
962
+ }
963
+ function selectFile(path) {
964
+ const file = fileSearchResults.find((candidate) => candidate.path === path);
965
+ if (!file) return;
966
+ setSelectedFiles((current) => current.some((selectedFile) => selectedFile.path === file.path) ? current : [...current, file]);
967
+ setFileSearchQuery("");
968
+ }
969
+ function selectSkill(path) {
970
+ setSelectedSkillPaths((current) => new Set(current).add(path));
971
+ }
972
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SidebarProvider, {
973
+ className: "h-screen min-h-0",
974
+ children: [
975
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Sidebar, {
976
+ collapsible: "offcanvas",
977
+ variant: "inset",
978
+ children: [
979
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SidebarHeader, { children: [
980
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
981
+ className: "flex size-8 shrink-0 items-center justify-center rounded-[var(--radius-sm)] bg-[var(--color-gray-900)] text-primary-foreground",
982
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FolderGit2, { className: "size-4" })
983
+ }),
984
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
985
+ className: "min-w-0 flex-1 group-data-[state=collapsed]/sidebar:hidden",
986
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
987
+ className: "truncate text-[length:var(--font-size-lg)] font-semibold leading-tight",
988
+ children: "Phantom"
989
+ })
990
+ }),
991
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
992
+ className: "max-w-24 truncate group-data-[state=collapsed]/sidebar:hidden",
993
+ variant: status === "Ready" ? "success" : "warning",
994
+ children: status
995
+ })
996
+ ] }),
997
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SidebarGroup, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SidebarGroupHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarGroupLabel, { children: "Projects" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarGroupAction, {
998
+ "aria-label": "Add project",
999
+ onClick: () => setIsAddProjectOpen(true),
1000
+ title: "Add project",
1001
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, { className: "size-4" })
1002
+ })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarGroupContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarMenu, { children: projects.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", {
1003
+ className: "px-2 py-4 group-data-[state=collapsed]/sidebar:hidden",
1004
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1005
+ className: "rounded-[var(--radius-md)] border border-dashed border-sidebar-border bg-[var(--surface-card)] px-3 py-3 text-[length:var(--font-size-sm)] text-muted-foreground",
1006
+ children: "Add a Git project to begin."
1007
+ })
1008
+ }) : projects.map((project) => {
1009
+ const isProjectExpanded = expandedProjectIds.has(project.id);
1010
+ const projectWorktrees = worktreesByProject[project.id] ?? [];
1011
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SidebarMenuItem, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1012
+ className: "group/project flex items-center rounded-[var(--radius-sm)]",
1013
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SidebarMenuButton, {
1014
+ "aria-expanded": isProjectExpanded,
1015
+ className: "min-h-8 flex-1 group-data-[state=collapsed]/sidebar:flex-none",
1016
+ onClick: () => toggleProject(project.id),
1017
+ title: project.name,
1018
+ type: "button",
1019
+ children: [
1020
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronRight, { className: cn("size-4 shrink-0 text-[var(--icon-color-default)] transition-transform duration-[var(--motion-duration-fast)] group-data-[state=collapsed]/sidebar:hidden", isProjectExpanded && "rotate-90") }),
1021
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FolderGit2, { className: "size-4 text-[var(--icon-color-default)]" }),
1022
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1023
+ className: "min-w-0 flex-1 group-data-[state=collapsed]/sidebar:hidden",
1024
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1025
+ className: "block truncate font-medium",
1026
+ children: project.name
1027
+ })
1028
+ })
1029
+ ]
1030
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
1031
+ "aria-label": `Create worktree in ${project.name}`,
1032
+ className: "mr-1 size-7 text-[var(--icon-color-default)] group-data-[state=collapsed]/sidebar:hidden",
1033
+ disabled: isBusy,
1034
+ onClick: () => void createChat(project.id),
1035
+ size: "icon",
1036
+ title: "Create worktree",
1037
+ type: "button",
1038
+ variant: "ghost",
1039
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageSquarePlus, { className: "size-4" })
1040
+ })]
1041
+ }), isProjectExpanded && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarMenuSub, { children: projectWorktrees.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", {
1042
+ className: "px-2 py-1.5 text-[length:var(--font-size-xs)] text-[var(--text-tertiary)]",
1043
+ children: "No worktrees"
1044
+ }) : projectWorktrees.map((worktree) => {
1045
+ const title = `${worktree.name} (${worktree.path})${worktree.isClean ? "" : " [dirty]"}`;
1046
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarMenuSubItem, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SidebarMenuSubButton, {
1047
+ disabled: !worktree.chatId,
1048
+ isActive: worktree.path === selectedWorktreePath,
1049
+ onClick: () => selectWorktree(project.id, worktree),
1050
+ title,
1051
+ type: "button",
1052
+ children: [
1053
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GitBranch, { className: "size-3.5 text-[var(--icon-color-default)]" }),
1054
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1055
+ className: "min-w-0 flex-1",
1056
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1057
+ className: "block truncate font-medium",
1058
+ children: worktree.name
1059
+ })
1060
+ }),
1061
+ !worktree.isClean && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "size-1.5 shrink-0 rounded-full bg-[var(--semantic-warning-fg)]" })
1062
+ ]
1063
+ }) }, worktree.path);
1064
+ }) })] }, project.id);
1065
+ }) }) })] }) }),
1066
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarRail, {})
1067
+ ]
1068
+ }),
1069
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog, {
1070
+ open: isAddProjectOpen,
1071
+ onOpenChange: setIsAddProjectOpen,
1072
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogContent, {
1073
+ "aria-labelledby": "add-project-title",
1074
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogTitle, {
1075
+ id: "add-project-title",
1076
+ children: "Add project"
1077
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DialogDescription, { children: "Add a local Git project to the Phantom sidebar." })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", {
1078
+ className: "grid gap-4",
1079
+ onSubmit: addProject,
1080
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1081
+ className: "grid gap-2",
1082
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, {
1083
+ htmlFor: "project-path",
1084
+ children: "Project path"
1085
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
1086
+ id: "project-path",
1087
+ placeholder: "/Users/me/project",
1088
+ value: projectPath,
1089
+ onChange: (event) => setProjectPath(event.target.value)
1090
+ })]
1091
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
1092
+ onClick: () => setIsAddProjectOpen(false),
1093
+ type: "button",
1094
+ variant: "outline",
1095
+ children: "Cancel"
1096
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
1097
+ disabled: isBusy || !projectPath.trim(),
1098
+ type: "submit",
1099
+ children: "Add project"
1100
+ })] })]
1101
+ })]
1102
+ })
1103
+ }),
1104
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SidebarInset, { children: [
1105
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", {
1106
+ className: "flex min-h-[var(--layout-topbar-height)] items-center gap-3 border-b border-border bg-[var(--surface-panel)] px-4",
1107
+ children: [
1108
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SidebarTrigger, { className: "-ml-1" }),
1109
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-5 w-px bg-[var(--border-divider)]" }),
1110
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1111
+ className: "min-w-0 flex-1",
1112
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1113
+ className: "flex min-w-0 items-center gap-2",
1114
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1115
+ className: "truncate text-[length:var(--font-size-xl)] font-semibold leading-tight",
1116
+ children: selectedWorktree?.name ?? selectedProject?.name ?? "Workspace"
1117
+ }), selectedChat && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusBadge, { status: selectedChat.status })]
1118
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
1119
+ className: "flex min-w-0 text-[length:var(--font-size-xs)] text-muted-foreground",
1120
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
1121
+ className: "shrink-0",
1122
+ children: [selectedProject?.name ?? "No project selected", selectedWorktree ? " / " : ""]
1123
+ }), selectedWorktree && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LeadingEllipsisText, { text: selectedWorktree.path })]
1124
+ })]
1125
+ })
1126
+ ]
1127
+ }),
1128
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SystemBanner, {
1129
+ tone: "danger",
1130
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, { className: "size-4" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: error })]
1131
+ }),
1132
+ pendingApproval && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1133
+ className: "border-b border-[var(--semantic-warning-border)] bg-[var(--semantic-warning-bg)] px-4 py-3 text-[var(--semantic-warning-fg)]",
1134
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1135
+ className: "mx-auto flex max-w-[var(--layout-max-content-width)] flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
1136
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1137
+ className: "min-w-0",
1138
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
1139
+ className: "flex items-center gap-2 text-[length:var(--font-size-md)] font-semibold",
1140
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Clock3, { className: "size-4" }), "Approval requested"]
1141
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1142
+ className: "mt-1 truncate font-mono text-[length:var(--font-size-xs)]",
1143
+ children: pendingApproval.method
1144
+ })]
1145
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1146
+ className: "flex shrink-0 flex-wrap gap-2",
1147
+ children: [
1148
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
1149
+ onClick: () => void answerApproval("accept"),
1150
+ size: "sm",
1151
+ type: "button",
1152
+ children: "Accept"
1153
+ }),
1154
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
1155
+ onClick: () => void answerApproval("acceptForSession"),
1156
+ size: "sm",
1157
+ type: "button",
1158
+ variant: "outline",
1159
+ children: "Accept for session"
1160
+ }),
1161
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
1162
+ onClick: () => void answerApproval("decline"),
1163
+ size: "sm",
1164
+ type: "button",
1165
+ variant: "outline",
1166
+ children: "Decline"
1167
+ })
1168
+ ]
1169
+ })]
1170
+ })
1171
+ }),
1172
+ selectedWorktree && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChatHistoryBar, {
1173
+ chats: selectedWorktreeChats,
1174
+ selectedChatId,
1175
+ onSelectChat: setSelectedChatId
1176
+ }),
1177
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("section", {
1178
+ className: "min-h-0 flex-1 overflow-y-auto px-4 py-4",
1179
+ children: visibleMessages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyTimeline, {
1180
+ hasChat: Boolean(selectedChat),
1181
+ hasWorktree: Boolean(selectedWorktree),
1182
+ selectedProject,
1183
+ onOpenProjectDialog: () => setIsAddProjectOpen(true)
1184
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1185
+ className: "mx-auto flex max-w-[var(--layout-max-content-width)] flex-col gap-2",
1186
+ children: visibleMessages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageCard, { message }, message.id))
1187
+ })
1188
+ }),
1189
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", {
1190
+ className: "border-t border-border bg-[var(--surface-floating)] p-3 backdrop-blur",
1191
+ onSubmit: sendMessage,
1192
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1193
+ className: "mx-auto flex max-w-[var(--layout-max-content-width)] flex-col gap-2",
1194
+ children: [
1195
+ (selectedFiles.length > 0 || selectedSkills.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1196
+ className: "flex min-h-8 flex-wrap items-center gap-2 px-1",
1197
+ children: [selectedFiles.map((file) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ContextChip, {
1198
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FileText, { className: "size-3.5" }),
1199
+ label: file.relativePath,
1200
+ onRemove: () => setSelectedFiles((current) => current.filter((selectedFile) => selectedFile.path !== file.path))
1201
+ }, file.path)), selectedSkills.map((skill) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ContextChip, {
1202
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Sparkles, { className: "size-3.5" }),
1203
+ label: skill.displayName,
1204
+ onRemove: () => setSelectedSkillPaths((current) => {
1205
+ const next = new Set(current);
1206
+ next.delete(skill.path);
1207
+ return next;
1208
+ })
1209
+ }, skill.path))]
1210
+ }),
1211
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1212
+ className: "flex items-end gap-2",
1213
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1214
+ className: "min-w-0 flex-1",
1215
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, {
1216
+ className: "sr-only",
1217
+ htmlFor: "composer",
1218
+ children: "Message"
1219
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
1220
+ className: "min-h-12 border-0 bg-transparent px-2 py-2 shadow-none focus-visible:shadow-none",
1221
+ disabled: !selectedChatId,
1222
+ id: "composer",
1223
+ placeholder: selectedChatId ? "Ask Codex to work in this worktree" : "Create or select a worktree to start",
1224
+ rows: 2,
1225
+ value: composerText,
1226
+ onChange: (event) => setComposerText(event.target.value),
1227
+ onKeyDown: handleComposerKeyDown
1228
+ })]
1229
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
1230
+ "aria-label": isChatRunning ? "Stop turn" : "Send message",
1231
+ className: "size-10",
1232
+ disabled: isChatRunning ? !selectedChat?.activeTurnId : !selectedChatId || !composerText.trim(),
1233
+ onClick: isChatRunning ? interruptChat : void 0,
1234
+ size: "icon",
1235
+ title: isChatRunning ? "Stop turn" : "Send",
1236
+ type: isChatRunning ? "button" : "submit",
1237
+ variant: isChatRunning ? "destructive" : "default",
1238
+ children: isChatRunning ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Square, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Send, {})
1239
+ })]
1240
+ }),
1241
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1242
+ className: "flex min-h-8 flex-wrap items-center gap-2 border-t border-[var(--border-divider)] px-1 pt-2",
1243
+ children: [
1244
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Combobox, {
1245
+ "aria-label": "Select model",
1246
+ className: "w-36 max-w-full sm:w-40",
1247
+ disabled: models.length === 0 || isChatRunning,
1248
+ emptyMessage: "No models",
1249
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Bot, { className: "size-3.5" }),
1250
+ options: modelOptions,
1251
+ placeholder: "Model",
1252
+ searchPlaceholder: "Search models",
1253
+ side: "top",
1254
+ triggerClassName: "w-full justify-between",
1255
+ value: selectedModel?.id ?? null,
1256
+ onValueChange: setSelectedModelId
1257
+ }),
1258
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Combobox, {
1259
+ "aria-label": "Select reasoning effort",
1260
+ className: "w-28 max-w-full",
1261
+ disabled: !selectedModel || isChatRunning,
1262
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Brain, { className: "size-3.5" }),
1263
+ options: effortOptions,
1264
+ placeholder: "Effort",
1265
+ searchPlaceholder: "Search effort",
1266
+ side: "top",
1267
+ triggerClassName: "w-full justify-between",
1268
+ value: selectedEffort ?? "auto",
1269
+ onValueChange: (value) => setSelectedEffort(value === "auto" ? null : value)
1270
+ }),
1271
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Combobox, {
1272
+ "aria-label": "Attach file",
1273
+ className: "w-32 max-w-full",
1274
+ disabled: !selectedChatId || isChatRunning,
1275
+ emptyMessage: fileSearchQuery.trim() ? "No files" : "Type to search",
1276
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FileText, { className: "size-3.5" }),
1277
+ options: fileOptions,
1278
+ placeholder: "Files",
1279
+ query: fileSearchQuery,
1280
+ searchPlaceholder: "Search files",
1281
+ shouldFilter: false,
1282
+ side: "top",
1283
+ triggerClassName: "w-full justify-between",
1284
+ value: null,
1285
+ onQueryChange: setFileSearchQuery,
1286
+ onValueChange: selectFile
1287
+ }),
1288
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Combobox, {
1289
+ "aria-label": "Select skill",
1290
+ align: "end",
1291
+ className: "w-32 max-w-full",
1292
+ disabled: !selectedChatId || isChatRunning,
1293
+ emptyMessage: "No skills",
1294
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Sparkles, { className: "size-3.5" }),
1295
+ options: skillOptions,
1296
+ placeholder: "Skills",
1297
+ searchPlaceholder: "Search skills",
1298
+ side: "top",
1299
+ triggerClassName: "w-full justify-between",
1300
+ value: null,
1301
+ onValueChange: selectSkill
1302
+ })
1303
+ ]
1304
+ })
1305
+ ]
1306
+ })
1307
+ })
1308
+ ] })
1309
+ ]
1310
+ });
1311
+ }
1312
+ function ChatHistoryBar({ chats, onSelectChat, selectedChatId }) {
1313
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1314
+ className: "border-b border-border bg-[var(--surface-panel)] px-4 py-2",
1315
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1316
+ className: "mx-auto flex max-w-[var(--layout-max-content-width)] items-center gap-2",
1317
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1318
+ className: "shrink-0 text-[length:var(--font-size-xs)] font-medium text-[var(--text-secondary)]",
1319
+ children: "Chat history"
1320
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1321
+ "aria-label": "Chat history",
1322
+ className: "flex min-w-0 flex-1 gap-1 overflow-x-auto",
1323
+ role: "tablist",
1324
+ children: chats.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1325
+ className: "px-2 py-1 text-[length:var(--font-size-xs)] text-[var(--text-tertiary)]",
1326
+ children: "No chat history"
1327
+ }) : chats.map((chat) => {
1328
+ const isSelected = chat.id === selectedChatId;
1329
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
1330
+ "aria-selected": isSelected,
1331
+ className: cn("inline-flex max-w-44 shrink-0 items-center gap-1.5 rounded-[var(--radius-sm)] px-2 py-1 text-[length:var(--font-size-xs)] outline-none transition-colors hover:bg-sidebar-accent focus-visible:shadow-[var(--state-focus-ring)]", isSelected ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-[var(--text-secondary)]"),
1332
+ onClick: () => onSelectChat(chat.id),
1333
+ role: "tab",
1334
+ title: chat.title,
1335
+ type: "button",
1336
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageSquare, { className: "size-3.5 shrink-0" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1337
+ className: "truncate",
1338
+ children: chat.title
1339
+ })]
1340
+ }, chat.id);
1341
+ })
1342
+ })]
1343
+ })
1344
+ });
1345
+ }
1346
+ function StatusBadge({ status }) {
1347
+ const meta = statusMeta[status];
1348
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge, {
1349
+ variant: meta.badge,
1350
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn("size-1.5 rounded-full", meta.dot) }), meta.label]
1351
+ });
1352
+ }
1353
+ function LeadingEllipsisText({ text }) {
1354
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1355
+ className: "block min-w-0 truncate",
1356
+ title: text,
1357
+ children: formatLeadingEllipsisPath(text)
1358
+ });
1359
+ }
1360
+ function ContextChip({ icon, label, onRemove }) {
1361
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
1362
+ className: "inline-flex h-8 max-w-52 items-center gap-1.5 rounded-[var(--radius-sm)] border border-[var(--border-divider)] bg-[var(--surface-code)] px-2 text-[length:var(--font-size-sm)] text-[var(--text-secondary)]",
1363
+ children: [
1364
+ icon,
1365
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1366
+ className: "min-w-0 truncate",
1367
+ children: label
1368
+ }),
1369
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
1370
+ "aria-label": `Remove ${label}`,
1371
+ className: "rounded-[var(--radius-xs)] text-[var(--icon-color-muted)] outline-none transition-colors hover:bg-[var(--state-hover-bg)] hover:text-[var(--icon-color-active)] focus-visible:shadow-[var(--state-focus-ring)]",
1372
+ onClick: onRemove,
1373
+ title: `Remove ${label}`,
1374
+ type: "button",
1375
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(X, { className: "size-3.5" })
1376
+ })
1377
+ ]
1378
+ });
1379
+ }
1380
+ function formatReasoningEffort(effort) {
1381
+ return effort.split(/[-_\s]+/).filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join(" ");
1382
+ }
1383
+ function SystemBanner({ children, tone }) {
1384
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1385
+ className: cn("border-b px-4 py-2 text-[length:var(--font-size-sm)]", tone === "danger" ? "border-[var(--semantic-danger-border)] bg-[var(--semantic-danger-bg)] text-[var(--semantic-danger-fg)]" : "border-[var(--semantic-info-border)] bg-[var(--semantic-info-bg)] text-[var(--semantic-info-fg)]"),
1386
+ role: "status",
1387
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1388
+ className: "mx-auto flex max-w-[var(--layout-max-content-width)] items-center gap-2",
1389
+ children
1390
+ })
1391
+ });
1392
+ }
1393
+ function EmptyTimeline({ hasChat, hasWorktree, onOpenProjectDialog, selectedProject }) {
1394
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1395
+ className: "mx-auto flex h-full max-w-[var(--layout-max-content-width)] items-center justify-center py-8",
1396
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
1397
+ className: "grid w-full max-w-xl gap-4 px-5 py-6 text-center",
1398
+ children: [
1399
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1400
+ className: "mx-auto flex size-10 items-center justify-center rounded-[var(--radius-md)] bg-[var(--surface-code)] text-[var(--icon-color-default)]",
1401
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Inbox, { className: "size-5" })
1402
+ }),
1403
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
1404
+ className: "text-[length:var(--font-size-xl)] font-semibold",
1405
+ children: hasChat ? "No messages yet" : hasWorktree ? "Select chat history" : "Select a worktree"
1406
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1407
+ className: "mt-1 text-[length:var(--font-size-md)] text-muted-foreground",
1408
+ children: hasChat ? "Send a message to start a focused Codex session." : hasWorktree ? "Choose a chat history for this worktree." : "Create a worktree under a project to begin a Codex session."
1409
+ })] }),
1410
+ !selectedProject && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1411
+ className: "flex justify-center gap-2",
1412
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
1413
+ onClick: onOpenProjectDialog,
1414
+ type: "button",
1415
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, { className: "size-4" }), "Add project"]
1416
+ })
1417
+ })
1418
+ ]
1419
+ })
1420
+ });
1421
+ }
1422
+ function MessageCard({ message }) {
1423
+ const isUser = message.role === "user";
1424
+ const isError = message.role === "error";
1425
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("article", {
1426
+ className: cn("rounded-[var(--radius-lg)] border px-4 py-3 shadow-[var(--shadow-xs)]", isUser && "ml-auto max-w-[78%] border-transparent bg-[var(--color-gray-900)] text-primary-foreground", message.role === "assistant" && "mr-auto max-w-[82%] border-border bg-card text-card-foreground", isError && "border-[var(--semantic-danger-border)] bg-[var(--semantic-danger-bg)] text-[var(--semantic-danger-fg)]"),
1427
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", {
1428
+ className: "whitespace-pre-wrap break-words font-sans text-[length:var(--font-size-md)] leading-[var(--line-height-relaxed)]",
1429
+ children: message.text
1430
+ })
1431
+ });
1432
+ }
1433
+ async function fetchJson(input, init = {}) {
1434
+ const response = await fetch(input, {
1435
+ headers: {
1436
+ "Content-Type": "application/json",
1437
+ ...init.headers
1438
+ },
1439
+ ...init
1440
+ });
1441
+ const data = await response.json().catch(() => ({}));
1442
+ if (!response.ok) {
1443
+ const errorBody = data;
1444
+ const message = errorBody.error?.message ? errorBody.error.message : `Request failed with status ${response.status}`;
1445
+ throw new Error(message);
1446
+ }
1447
+ return data;
1448
+ }
1449
+ //#endregion
1450
+ export { Home as component };