@invergent/agent-chat-react 2.15.3 → 2.15.6

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/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { cn, __publicField } from './chunk-47MF6JUW.js';
2
2
  import { createContext, memo, useRef, useEffect, useCallback, useMemo, useState, useLayoutEffect, forwardRef, useId, lazy, useContext, useSyncExternalStore, Fragment as Fragment$1, Suspense, Children } from 'react';
3
- import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, CreditCard, File as File$1, FileImage, FileAudio, FileVideo, FileText, FileCode, FileArchive, FileSpreadsheet, PlugZapIcon, ChevronRightIcon, ArrowLeftIcon, Loader2Icon, InboxIcon, Loader2, AlertCircle, Pause, Play, Ban, TargetIcon, CalendarClockIcon, UsersIcon, Pen, Eraser, Type, MousePointer2, Hand, Maximize2, Undo2, Redo2, CheckIcon, CopyIcon, DownloadIcon, Maximize2Icon, MessageSquareIcon, ZapIcon, FolderOpenIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, ExternalLinkIcon, ClipboardCheckIcon, ShieldAlertIcon, TimerIcon, CircleDotIcon, ChevronLeftIcon, XIcon, Trash2Icon, SquareIcon, ArrowDownIcon, RotateCcwIcon, MousePointer2Icon, TrashIcon, ImageIcon, FileTextIcon, Clock, ArrowRight, ListTreeIcon, PenLineIcon, TerminalIcon, SparklesIcon, ClockIcon, PaperclipIcon, CloudIcon, HardDriveIcon, GlobeIcon, FolderIcon, IdCardIcon, PlusIcon, ShrinkIcon, FileIcon, MinusIcon, SearchIcon, CircleDot, XCircle, CheckCircle2, ArrowUpIcon, CircleCheckIcon, ThumbsUpIcon, ThumbsDownIcon, BookOpenIcon, GitBranchIcon, ActivityIcon, EyeIcon, ListTodoIcon, BotIcon, ListIcon, FileEditIcon, WrenchIcon, FileCodeIcon, CodeIcon, FileArchiveIcon, FileVideoIcon, FileAudioIcon, FileImageIcon, FileChartLineIcon, FileSpreadsheetIcon, XCircleIcon, CheckCircle2Icon, Code, CircleXIcon, SkullIcon, CheckCircleIcon, CircleIcon } from 'lucide-react';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight as ChevronRight$1, RefreshCw, CreditCard, File as File$1, FileImage, FileAudio, FileVideo, FileText, FileCode, FileArchive, FileSpreadsheet, PlugZapIcon, ChevronRightIcon, ArrowLeftIcon, Loader2Icon, InboxIcon, Loader2, AlertCircle, Pause, Play, Ban, TargetIcon, CalendarClockIcon, UsersIcon, Pen, Eraser, Type, MousePointer2, Hand, Maximize2, Undo2, Redo2, CheckIcon, CopyIcon, DownloadIcon, Maximize2Icon, GlobeIcon, FolderIcon, MessageSquareIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, ExternalLinkIcon, ClipboardCheckIcon, ShieldAlertIcon, TimerIcon, CircleDotIcon, ChevronLeftIcon, XIcon, Trash2Icon, SquareIcon, ArrowDownIcon, TrashIcon, ImageIcon, FileTextIcon, Clock, ArrowRight, ListTreeIcon, PenLineIcon, TerminalIcon, SparklesIcon, ClockIcon, PaperclipIcon, CloudIcon, HardDriveIcon, IdCardIcon, PlusIcon, ShrinkIcon, FolderOpenIcon, FileIcon, MinusIcon, SearchIcon, CircleDot, XCircle, CheckCircle2, ZapIcon, ArrowUpIcon, CircleCheckIcon, ThumbsUpIcon, ThumbsDownIcon, BookOpenIcon, GitBranchIcon, ActivityIcon, EyeIcon, LibraryIcon, ListTodoIcon, BotIcon, ListIcon, FileEditIcon, WrenchIcon, FileCodeIcon, CodeIcon, FileArchiveIcon, FileVideoIcon, FileAudioIcon, FileImageIcon, FileChartLineIcon, FileSpreadsheetIcon, XCircleIcon, CheckCircle2Icon, Code, CircleXIcon, SkullIcon, CheckCircleIcon, CircleIcon } from 'lucide-react';
4
5
  import { cva } from 'class-variance-authority';
5
6
  import { Collapsible as Collapsible$1, Slot, Tooltip as Tooltip$1, Checkbox as Checkbox$1, Label as Label$1, Progress as Progress$1, Tabs as Tabs$1, Dialog as Dialog$1, ScrollArea as ScrollArea$1, Popover as Popover$1 } from 'radix-ui';
6
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
  import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';
8
8
  import { cjk } from '@streamdown/cjk';
9
9
  import { code } from '@streamdown/code';
@@ -32,6 +32,481 @@ function useAgentChatAdapterContext() {
32
32
  }
33
33
  return value;
34
34
  }
35
+ var NAMED_KEYS = /* @__PURE__ */ new Set([
36
+ "Enter",
37
+ "Tab",
38
+ "Escape",
39
+ "Backspace",
40
+ "Delete",
41
+ "ArrowUp",
42
+ "ArrowDown",
43
+ "ArrowLeft",
44
+ "ArrowRight",
45
+ "Home",
46
+ "End",
47
+ "PageUp",
48
+ "PageDown"
49
+ ]);
50
+ function toWsUrl(src) {
51
+ const url = new URL(src, window.location.href);
52
+ if (url.protocol === "http:") url.protocol = "ws:";
53
+ else if (url.protocol === "https:") url.protocol = "wss:";
54
+ return url.toString();
55
+ }
56
+ function normalizePoint(clientX, clientY, rect, naturalWidth, naturalHeight) {
57
+ let { width, height } = rect;
58
+ let offsetX = 0;
59
+ let offsetY = 0;
60
+ if (naturalWidth > 0 && naturalHeight > 0) {
61
+ const scale = Math.min(rect.width / naturalWidth, rect.height / naturalHeight);
62
+ width = naturalWidth * scale;
63
+ height = naturalHeight * scale;
64
+ offsetX = (rect.width - width) / 2;
65
+ offsetY = (rect.height - height) / 2;
66
+ }
67
+ if (width <= 0 || height <= 0) return null;
68
+ const x = (clientX - rect.left - offsetX) / width;
69
+ const y = (clientY - rect.top - offsetY) / height;
70
+ if (x < 0 || x > 1 || y < 0 || y > 1) return null;
71
+ return { x, y };
72
+ }
73
+ function hostOf(url) {
74
+ try {
75
+ const parsed = new URL(url);
76
+ return { origin: parsed.host, rest: `${parsed.pathname}${parsed.search}` };
77
+ } catch {
78
+ return { origin: url, rest: "" };
79
+ }
80
+ }
81
+ var ICON = "flex size-[26px] shrink-0 items-center justify-center rounded-[5px] text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground disabled:pointer-events-none disabled:opacity-40";
82
+ function BrowserShell({
83
+ src,
84
+ hasControl,
85
+ onToggleControl,
86
+ onClose,
87
+ onMaximize,
88
+ onDisconnect,
89
+ testId = "browser-shell"
90
+ }) {
91
+ const socketRef = useRef(null);
92
+ const frameRef = useRef(null);
93
+ const onDisconnectRef = useRef(onDisconnect);
94
+ onDisconnectRef.current = onDisconnect;
95
+ const objectUrlRef = useRef(null);
96
+ const [state, setState] = useState("connecting");
97
+ const [frame2, setFrame] = useState(null);
98
+ const [tabs, setTabs] = useState([]);
99
+ const [nav, setNav] = useState({ url: "", title: "" });
100
+ const [menuOpen, setMenuOpen] = useState(false);
101
+ useEffect(() => {
102
+ setState("connecting");
103
+ let disposed = false;
104
+ const socket = new WebSocket(toWsUrl(src));
105
+ socketRef.current = socket;
106
+ socket.onopen = () => {
107
+ if (!disposed) setState("connected");
108
+ };
109
+ socket.onerror = () => {
110
+ if (!disposed) setState("disconnected");
111
+ };
112
+ socket.onclose = (event) => {
113
+ if (disposed) return;
114
+ setState("disconnected");
115
+ onDisconnectRef.current?.(event?.wasClean ?? false);
116
+ };
117
+ socket.onmessage = (event) => {
118
+ if (disposed) return;
119
+ const data = event.data;
120
+ if (typeof data !== "string") {
121
+ const next = URL.createObjectURL(
122
+ data instanceof Blob ? data : new Blob([data])
123
+ );
124
+ if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);
125
+ objectUrlRef.current = next;
126
+ setFrame(next);
127
+ return;
128
+ }
129
+ let message;
130
+ try {
131
+ message = JSON.parse(data);
132
+ } catch {
133
+ return;
134
+ }
135
+ if (message.t === "tabs") {
136
+ const next = message.tabs ?? [];
137
+ setTabs(next);
138
+ const active = next.find((tab) => tab.active);
139
+ if (active) setNav({ url: active.url, title: active.title });
140
+ } else if (message.t === "nav") {
141
+ setNav({
142
+ url: String(message.url ?? ""),
143
+ title: String(message.title ?? "")
144
+ });
145
+ }
146
+ };
147
+ return () => {
148
+ disposed = true;
149
+ socket.onopen = null;
150
+ socket.onmessage = null;
151
+ socket.onclose = null;
152
+ socket.onerror = null;
153
+ socket.close();
154
+ if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);
155
+ objectUrlRef.current = null;
156
+ socketRef.current = null;
157
+ };
158
+ }, [src]);
159
+ const send = useCallback((message) => {
160
+ const socket = socketRef.current;
161
+ if (!socket || socket.readyState !== WebSocket.OPEN) return;
162
+ socket.send(JSON.stringify(message));
163
+ }, []);
164
+ const command = useCallback(
165
+ (message) => {
166
+ if (!hasControl) return;
167
+ send(message);
168
+ },
169
+ [hasControl, send]
170
+ );
171
+ const pointerToPage = (event) => {
172
+ const image = frameRef.current;
173
+ if (!image) return null;
174
+ return normalizePoint(
175
+ event.clientX,
176
+ event.clientY,
177
+ image.getBoundingClientRect(),
178
+ image.naturalWidth,
179
+ image.naturalHeight
180
+ );
181
+ };
182
+ const handleKeyDown = (event) => {
183
+ if (!hasControl) return;
184
+ if (NAMED_KEYS.has(event.key)) {
185
+ event.preventDefault();
186
+ command({
187
+ t: "key",
188
+ key: event.key,
189
+ mods: {
190
+ shift: event.shiftKey,
191
+ ctrl: event.ctrlKey,
192
+ alt: event.altKey,
193
+ meta: event.metaKey
194
+ }
195
+ });
196
+ return;
197
+ }
198
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
199
+ event.preventDefault();
200
+ command({ t: "type", text: event.key });
201
+ }
202
+ };
203
+ const { origin, rest } = hostOf(nav.url);
204
+ const showTabs = tabs.length > 1;
205
+ return /* @__PURE__ */ jsxs(
206
+ "div",
207
+ {
208
+ "data-testid": testId,
209
+ className: "flex h-full min-h-0 flex-col bg-background",
210
+ children: [
211
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-11 items-center gap-1.5 border-b border-line bg-card px-2.5", children: [
212
+ /* @__PURE__ */ jsx(
213
+ "button",
214
+ {
215
+ type: "button",
216
+ "aria-label": "Back",
217
+ className: ICON,
218
+ disabled: !hasControl,
219
+ onClick: () => command({ t: "back" }),
220
+ children: /* @__PURE__ */ jsx(ChevronLeft, {})
221
+ }
222
+ ),
223
+ /* @__PURE__ */ jsx(
224
+ "button",
225
+ {
226
+ type: "button",
227
+ "aria-label": "Forward",
228
+ className: ICON,
229
+ disabled: !hasControl,
230
+ onClick: () => command({ t: "forward" }),
231
+ children: /* @__PURE__ */ jsx(ChevronRight, {})
232
+ }
233
+ ),
234
+ /* @__PURE__ */ jsx(
235
+ "button",
236
+ {
237
+ type: "button",
238
+ "aria-label": "Reload",
239
+ className: ICON,
240
+ disabled: !hasControl,
241
+ onClick: () => command({ t: "reload" }),
242
+ children: /* @__PURE__ */ jsx(Reload, {})
243
+ }
244
+ ),
245
+ /* @__PURE__ */ jsx(
246
+ "button",
247
+ {
248
+ type: "button",
249
+ "data-testid": "browser-shell-control",
250
+ "aria-label": hasControl ? "Return control" : "Take control",
251
+ "aria-pressed": hasControl,
252
+ onClick: onToggleControl,
253
+ className: hasControl ? "flex size-[26px] shrink-0 items-center justify-center rounded-[5px] bg-[#F5A524] text-black" : ICON,
254
+ children: /* @__PURE__ */ jsx(Pointer, {})
255
+ }
256
+ ),
257
+ /* @__PURE__ */ jsxs(
258
+ "div",
259
+ {
260
+ "data-testid": "browser-shell-url",
261
+ className: "flex h-7 min-w-0 flex-grow items-center gap-1.5 rounded-md border border-line bg-background px-2.5 text-[11px] text-foreground",
262
+ children: [
263
+ /* @__PURE__ */ jsx(
264
+ "span",
265
+ {
266
+ "aria-hidden": "true",
267
+ className: `size-1.5 shrink-0 rounded-full ${state === "connected" ? "bg-emerald-500" : "bg-muted-foreground"}`
268
+ }
269
+ ),
270
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
271
+ origin,
272
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: rest })
273
+ ] })
274
+ ]
275
+ }
276
+ ),
277
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
278
+ /* @__PURE__ */ jsx(
279
+ "button",
280
+ {
281
+ type: "button",
282
+ "aria-label": "More",
283
+ className: ICON,
284
+ onClick: () => setMenuOpen((open) => !open),
285
+ children: /* @__PURE__ */ jsx(Ellipsis, {})
286
+ }
287
+ ),
288
+ menuOpen && /* @__PURE__ */ jsxs(
289
+ "div",
290
+ {
291
+ "data-testid": "browser-shell-menu",
292
+ className: "absolute right-0 top-8 z-10 min-w-36 rounded-md border border-line bg-card py-1 text-xs shadow-md",
293
+ children: [
294
+ onMaximize && /* @__PURE__ */ jsx(
295
+ "button",
296
+ {
297
+ type: "button",
298
+ className: "block w-full px-3 py-1.5 text-left hover:bg-secondary",
299
+ onClick: () => {
300
+ setMenuOpen(false);
301
+ onMaximize();
302
+ },
303
+ children: "Maximize"
304
+ }
305
+ ),
306
+ onClose && /* @__PURE__ */ jsx(
307
+ "button",
308
+ {
309
+ type: "button",
310
+ className: "block w-full px-3 py-1.5 text-left hover:bg-secondary",
311
+ onClick: () => {
312
+ setMenuOpen(false);
313
+ onClose();
314
+ },
315
+ children: "Close browser"
316
+ }
317
+ )
318
+ ]
319
+ }
320
+ )
321
+ ] })
322
+ ] }),
323
+ showTabs && /* @__PURE__ */ jsx(
324
+ "div",
325
+ {
326
+ "data-testid": "browser-shell-tabs",
327
+ className: "flex min-h-[34px] items-center gap-0.5 border-b border-line bg-card px-1.5",
328
+ children: tabs.map((tab) => (
329
+ // A div, not a button: the close control nests inside, and a
330
+ // button inside a button is invalid markup.
331
+ /* @__PURE__ */ jsxs(
332
+ "div",
333
+ {
334
+ role: "tab",
335
+ tabIndex: 0,
336
+ "aria-selected": tab.active,
337
+ "data-testid": "browser-shell-tab",
338
+ onClick: () => send({ t: "switch_tab", id: tab.id }),
339
+ onKeyDown: (event) => {
340
+ if (event.key === "Enter") send({ t: "switch_tab", id: tab.id });
341
+ },
342
+ className: `flex h-6 max-w-33 cursor-pointer items-center gap-1 truncate rounded px-2 text-[11px] ${tab.active ? "bg-secondary text-foreground" : "text-muted-foreground hover:text-foreground"}`,
343
+ children: [
344
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: tab.title || tab.url }),
345
+ hasControl && /* @__PURE__ */ jsx(
346
+ "button",
347
+ {
348
+ type: "button",
349
+ "data-testid": "browser-shell-tab-close",
350
+ "aria-label": `Close ${tab.title || tab.url}`,
351
+ onClick: (event) => {
352
+ event.stopPropagation();
353
+ command({ t: "close_tab", id: tab.id });
354
+ },
355
+ className: "flex size-3.5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-secondary hover:text-foreground",
356
+ children: /* @__PURE__ */ jsx(XSmall, {})
357
+ }
358
+ )
359
+ ]
360
+ },
361
+ tab.id
362
+ )
363
+ ))
364
+ }
365
+ ),
366
+ /* @__PURE__ */ jsxs(
367
+ "div",
368
+ {
369
+ className: `relative min-h-0 flex-1 bg-black ${hasControl ? "ring-2 ring-inset ring-[#F5A524]" : ""}`,
370
+ tabIndex: hasControl ? 0 : -1,
371
+ onKeyDown: handleKeyDown,
372
+ children: [
373
+ /* @__PURE__ */ jsx(
374
+ "img",
375
+ {
376
+ ref: frameRef,
377
+ "data-testid": "browser-shell-frame",
378
+ alt: "",
379
+ src: frame2 ?? void 0,
380
+ draggable: false,
381
+ className: "h-full w-full object-contain",
382
+ onMouseDown: (event) => {
383
+ const point = pointerToPage(event);
384
+ if (point) command({ t: "click", ...point, count: event.detail || 1 });
385
+ },
386
+ onWheel: (event) => {
387
+ const point = pointerToPage(event);
388
+ if (point) {
389
+ command({
390
+ t: "scroll",
391
+ ...point,
392
+ dx: event.deltaX,
393
+ dy: event.deltaY
394
+ });
395
+ }
396
+ }
397
+ }
398
+ ),
399
+ state !== "connected" && /* @__PURE__ */ jsx(
400
+ "div",
401
+ {
402
+ "data-testid": "browser-shell-overlay",
403
+ className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/70 text-sm text-muted-foreground",
404
+ children: state === "connecting" ? "Connecting to browser\u2026" : "Browser disconnected."
405
+ }
406
+ ),
407
+ hasControl && /* @__PURE__ */ jsxs("div", { className: "pointer-events-none absolute bottom-3 left-1/2 flex -translate-x-1/2 items-center gap-1.5 rounded-full bg-black/85 px-3 py-1 text-[11px] font-medium text-amber-200", children: [
408
+ /* @__PURE__ */ jsx(Pointer, {}),
409
+ "You have control \xB7 click the pointer to return"
410
+ ] })
411
+ ]
412
+ }
413
+ )
414
+ ]
415
+ }
416
+ );
417
+ }
418
+ var svg = {
419
+ width: 14,
420
+ height: 14,
421
+ viewBox: "0 0 24 24",
422
+ fill: "none",
423
+ stroke: "currentColor",
424
+ strokeWidth: 2,
425
+ strokeLinecap: "round",
426
+ strokeLinejoin: "round",
427
+ "aria-hidden": true
428
+ };
429
+ var ChevronLeft = () => /* @__PURE__ */ jsx("svg", { ...svg, children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" }) });
430
+ var ChevronRight = () => /* @__PURE__ */ jsx("svg", { ...svg, children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" }) });
431
+ var Reload = () => /* @__PURE__ */ jsxs("svg", { ...svg, children: [
432
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
433
+ /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
434
+ ] });
435
+ var Pointer = () => /* @__PURE__ */ jsxs("svg", { ...svg, children: [
436
+ /* @__PURE__ */ jsx("path", { d: "M12.586 12.586 19 19" }),
437
+ /* @__PURE__ */ jsx("path", { d: "M3.688 3.037a.497.497 0 0 0-.651.651l6.5 15.999a.501.501 0 0 0 .947-.062l1.569-6.083a2 2 0 0 1 1.448-1.479l6.124-1.579a.5.5 0 0 0 .063-.947z" })
438
+ ] });
439
+ var XSmall = () => /* @__PURE__ */ jsxs("svg", { ...svg, width: 10, height: 10, children: [
440
+ /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
441
+ /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
442
+ ] });
443
+ var Ellipsis = () => /* @__PURE__ */ jsxs("svg", { ...svg, children: [
444
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1" }),
445
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "12", r: "1" }),
446
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "12", r: "1" })
447
+ ] });
448
+ function useBrowserControl({
449
+ sessionId,
450
+ hasControl,
451
+ adapter,
452
+ onControlAcquired,
453
+ onControlReleased,
454
+ onClose
455
+ }) {
456
+ const [pending, setPending] = useState(false);
457
+ const [confirmOpen, setConfirmOpen] = useState(false);
458
+ const [error, setError] = useState(null);
459
+ async function toggleControl() {
460
+ if (!sessionId || pending) return;
461
+ setPending(true);
462
+ setError(null);
463
+ try {
464
+ if (hasControl) {
465
+ await adapter.releaseBrowserControl(sessionId);
466
+ onControlReleased?.();
467
+ } else {
468
+ await adapter.acquireBrowserControl(sessionId);
469
+ onControlAcquired?.();
470
+ }
471
+ } catch (nextError) {
472
+ setError(nextError.message);
473
+ } finally {
474
+ setPending(false);
475
+ }
476
+ }
477
+ async function handleConfirmClose() {
478
+ setError(null);
479
+ try {
480
+ if (hasControl) {
481
+ try {
482
+ await adapter.releaseBrowserControl(sessionId);
483
+ onControlReleased?.();
484
+ } catch (releaseError) {
485
+ console.error(
486
+ "Failed to release browser control before close",
487
+ releaseError
488
+ );
489
+ }
490
+ }
491
+ if (adapter.closeBrowserSession) {
492
+ await adapter.closeBrowserSession(sessionId);
493
+ }
494
+ setConfirmOpen(false);
495
+ onClose?.();
496
+ } catch (nextError) {
497
+ setError(nextError.message);
498
+ throw nextError;
499
+ }
500
+ }
501
+ return {
502
+ pending,
503
+ error,
504
+ confirmOpen,
505
+ setConfirmOpen,
506
+ toggleControl,
507
+ handleConfirmClose
508
+ };
509
+ }
35
510
  var buttonVariants = cva(
36
511
  "group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-semibold tracking-widest whitespace-nowrap uppercase transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
37
512
  {
@@ -260,369 +735,34 @@ function ConfirmDialog({
260
735
  }
261
736
  );
262
737
  }
263
- function BrowserControlBar({
738
+ function BrowserPane({
264
739
  sessionId,
265
- hasControl,
266
- adapter,
267
- onControlAcquired,
268
- onControlReleased,
269
- onClose
270
- }) {
271
- const [pending, setPending] = useState(false);
272
- const [confirmOpen, setConfirmOpen] = useState(false);
273
- const [error, setError] = useState(null);
274
- async function toggleControl() {
275
- if (!sessionId || pending) return;
276
- setPending(true);
277
- setError(null);
278
- try {
279
- if (hasControl) {
280
- await adapter.releaseBrowserControl(sessionId);
281
- onControlReleased?.();
282
- } else {
283
- await adapter.acquireBrowserControl(sessionId);
284
- onControlAcquired?.();
285
- }
286
- } catch (nextError) {
287
- setError(nextError.message);
288
- } finally {
289
- setPending(false);
290
- }
291
- }
292
- async function handleConfirmClose() {
293
- setError(null);
294
- try {
295
- if (hasControl) {
296
- try {
297
- await adapter.releaseBrowserControl(sessionId);
298
- onControlReleased?.();
299
- } catch (releaseError) {
300
- console.error(
301
- "Failed to release browser control before close",
302
- releaseError
303
- );
304
- }
305
- }
306
- if (adapter.closeBrowserSession) {
307
- await adapter.closeBrowserSession(sessionId);
308
- }
309
- setConfirmOpen(false);
310
- onClose?.();
311
- } catch (nextError) {
312
- setError(nextError.message);
313
- throw nextError;
314
- }
315
- }
316
- return /* @__PURE__ */ jsxs("div", { className: "flex min-h-11 items-center gap-2 border-t border-line bg-card px-3 py-2", children: [
317
- /* @__PURE__ */ jsxs(
318
- Button,
319
- {
320
- type: "button",
321
- size: "xs",
322
- variant: hasControl ? "secondary" : "default",
323
- disabled: !sessionId || pending,
324
- onClick: () => void toggleControl(),
325
- children: [
326
- hasControl ? /* @__PURE__ */ jsx(RotateCcwIcon, { className: "size-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(MousePointer2Icon, { className: "size-3", "aria-hidden": "true" }),
327
- hasControl ? "Return control" : "Take control"
328
- ]
329
- }
330
- ),
331
- onClose && /* @__PURE__ */ jsxs(
332
- Button,
333
- {
334
- type: "button",
335
- size: "xs",
336
- variant: "secondary",
337
- disabled: !sessionId,
338
- onClick: () => {
339
- setError(null);
340
- setConfirmOpen(true);
341
- },
342
- children: [
343
- /* @__PURE__ */ jsx(XIcon, { className: "size-3", "aria-hidden": "true" }),
344
- "Close"
345
- ]
346
- }
347
- ),
348
- /* @__PURE__ */ jsxs("span", { className: "ml-auto flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [
349
- /* @__PURE__ */ jsx("span", { className: "size-1.5 rounded-full bg-red-500", "aria-hidden": "true" }),
350
- "Live"
351
- ] }),
352
- error && /* @__PURE__ */ jsx("span", { className: "max-w-40 truncate text-[11px] text-destructive", children: error }),
353
- /* @__PURE__ */ jsx(
354
- ConfirmDialog,
355
- {
356
- open: confirmOpen,
357
- title: "Close browser session?",
358
- description: adapter.closeBrowserSession ? "This permanently shuts down the browser sandbox for this session. The agent will lose access to the page until it re-opens a browser. This cannot be undone." : "This hides the browser panel. The sandbox stays running so the agent can keep using it.",
359
- confirmLabel: "Close browser",
360
- variant: "destructive",
361
- confirmIcon: /* @__PURE__ */ jsx(XIcon, { className: "size-3.5", "aria-hidden": "true" }),
362
- onConfirm: handleConfirmClose,
363
- onCancel: () => setConfirmOpen(false)
364
- }
365
- )
366
- ] });
367
- }
368
- var MIN_ZOOM = 1;
369
- var MAX_ZOOM = 3;
370
- var ZOOM_STEP = 0.5;
371
- function clampZoom(zoom) {
372
- const bounded = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
373
- return Math.round(bounded * 100) / 100;
374
- }
375
- function toWsUrl(src) {
376
- const url = new URL(src, window.location.href);
377
- if (url.protocol === "http:") {
378
- url.protocol = "ws:";
379
- } else if (url.protocol === "https:") {
380
- url.protocol = "wss:";
381
- }
382
- return url.toString();
383
- }
384
- function BrowserLiveView({
385
- src,
386
- testId = "browser-rfb",
387
- onDisconnect
388
- }) {
389
- const containerRef = useRef(null);
390
- const onDisconnectRef = useRef(onDisconnect);
391
- onDisconnectRef.current = onDisconnect;
392
- const [state, setState] = useState("connecting");
393
- const [zoom, setZoom] = useState(MIN_ZOOM);
394
- useEffect(() => {
395
- const container = containerRef.current;
396
- if (!container) return;
397
- setState("connecting");
398
- setZoom(MIN_ZOOM);
399
- let rfb;
400
- let disposed = false;
401
- const handleConnect = () => setState("connected");
402
- const handleDisconnect = (event) => {
403
- const detail = event.detail;
404
- setState("disconnected");
405
- onDisconnectRef.current?.(detail?.clean ?? false);
406
- };
407
- void import('@novnc/novnc').then((mod) => {
408
- if (disposed || !containerRef.current) return;
409
- const instance = new mod.default(containerRef.current, toWsUrl(src), {
410
- wsProtocols: ["binary"]
411
- });
412
- instance.viewOnly = false;
413
- instance.scaleViewport = true;
414
- instance.addEventListener("connect", handleConnect);
415
- instance.addEventListener("disconnect", handleDisconnect);
416
- rfb = instance;
417
- }).catch(() => {
418
- if (!disposed) setState("disconnected");
419
- });
420
- return () => {
421
- disposed = true;
422
- if (rfb) {
423
- rfb.removeEventListener("connect", handleConnect);
424
- rfb.removeEventListener("disconnect", handleDisconnect);
425
- rfb.disconnect();
426
- }
427
- };
428
- }, [src]);
429
- const keepCanvasFocus = (event) => event.preventDefault();
430
- const zoomTo = (next) => setZoom((current) => clampZoom(next || current));
431
- return /* @__PURE__ */ jsxs("div", { className: "relative h-full w-full bg-black", children: [
432
- /* @__PURE__ */ jsx("div", { className: "absolute inset-0 overflow-auto", children: /* @__PURE__ */ jsx(
433
- "div",
434
- {
435
- ref: containerRef,
436
- "data-testid": testId,
437
- style: { width: `${zoom * 100}%`, height: `${zoom * 100}%` }
438
- }
439
- ) }),
440
- state === "connected" && /* @__PURE__ */ jsxs(
441
- "div",
442
- {
443
- "data-testid": "browser-rfb-zoom",
444
- className: "absolute bottom-3 right-3 flex items-center gap-0.5 rounded-md border border-line bg-card/90 p-0.5 text-muted-foreground shadow-sm backdrop-blur",
445
- children: [
446
- /* @__PURE__ */ jsx(
447
- "button",
448
- {
449
- type: "button",
450
- tabIndex: -1,
451
- "aria-label": "Zoom out",
452
- disabled: zoom <= MIN_ZOOM,
453
- onMouseDown: keepCanvasFocus,
454
- onClick: () => zoomTo(zoom - ZOOM_STEP),
455
- className: "flex size-6 items-center justify-center rounded text-sm leading-none hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40",
456
- children: "\u2212"
457
- }
458
- ),
459
- /* @__PURE__ */ jsxs(
460
- "button",
461
- {
462
- type: "button",
463
- tabIndex: -1,
464
- "aria-label": "Reset zoom",
465
- disabled: zoom === MIN_ZOOM,
466
- onMouseDown: keepCanvasFocus,
467
- onClick: () => setZoom(MIN_ZOOM),
468
- className: "min-w-10 rounded px-1 text-center text-xs tabular-nums hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none",
469
- children: [
470
- Math.round(zoom * 100),
471
- "%"
472
- ]
473
- }
474
- ),
475
- /* @__PURE__ */ jsx(
476
- "button",
477
- {
478
- type: "button",
479
- tabIndex: -1,
480
- "aria-label": "Zoom in",
481
- disabled: zoom >= MAX_ZOOM,
482
- onMouseDown: keepCanvasFocus,
483
- onClick: () => zoomTo(zoom + ZOOM_STEP),
484
- className: "flex size-6 items-center justify-center rounded text-sm leading-none hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-40",
485
- children: "+"
486
- }
487
- )
488
- ]
489
- }
490
- ),
491
- state !== "connected" && /* @__PURE__ */ jsx(
492
- "div",
493
- {
494
- "data-testid": "browser-rfb-overlay",
495
- className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/70 text-sm text-muted-foreground",
496
- children: state === "connecting" ? "Connecting to browser\u2026" : "Live view disconnected \u2014 take control again to reconnect."
497
- }
498
- )
499
- ] });
500
- }
501
- function BrowserStatusDot({
502
- status
503
- }) {
504
- return /* @__PURE__ */ jsx(
505
- "span",
506
- {
507
- "aria-hidden": "true",
508
- className: cn(
509
- "inline-block size-2 shrink-0 rounded-full",
510
- status === "live" && "bg-emerald-500",
511
- status === "user-control" && "bg-amber-500",
512
- status === "provisioning" && "animate-pulse bg-sky-500",
513
- status === "closed" && "bg-muted-foreground"
514
- )
515
- }
516
- );
517
- }
518
- function TooltipProvider({
519
- delayDuration = 0,
520
- ...props
521
- }) {
522
- return /* @__PURE__ */ jsx(
523
- Tooltip$1.Provider,
524
- {
525
- "data-slot": "tooltip-provider",
526
- delayDuration,
527
- ...props
528
- }
529
- );
530
- }
531
- function Tooltip({
532
- ...props
533
- }) {
534
- return /* @__PURE__ */ jsx(Tooltip$1.Root, { "data-slot": "tooltip", ...props });
535
- }
536
- function TooltipTrigger({
537
- ...props
538
- }) {
539
- return /* @__PURE__ */ jsx(Tooltip$1.Trigger, { "data-slot": "tooltip-trigger", ...props });
540
- }
541
- function TooltipContent({
542
- className,
543
- sideOffset = 0,
544
- children,
545
- ...props
546
- }) {
547
- return /* @__PURE__ */ jsx(Tooltip$1.Portal, { children: /* @__PURE__ */ jsxs(
548
- Tooltip$1.Content,
549
- {
550
- "data-slot": "tooltip-content",
551
- sideOffset,
552
- className: cn(
553
- "z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-none bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 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 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-none data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
554
- className
555
- ),
556
- ...props,
557
- children: [
558
- children,
559
- /* @__PURE__ */ jsx(Tooltip$1.Arrow, { className: "z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-none bg-foreground fill-foreground" })
560
- ]
561
- }
562
- ) });
563
- }
564
- function BrowserPane({
565
- sessionId,
566
- state,
740
+ state,
567
741
  adapter,
568
742
  onClose
569
743
  }) {
570
744
  const [fullscreenOpen, setFullscreenOpen] = useState(false);
571
745
  const [openFullscreenOnControl, setOpenFullscreenOnControl] = useState(false);
572
746
  const [localControlActive, setLocalControlActive] = useState(false);
573
- const [previewSnapshot, setPreviewSnapshot] = useState(null);
574
- const [previewLoading, setPreviewLoading] = useState(false);
575
- const [previewError, setPreviewError] = useState(null);
576
- const hasLiveViewAdapter = typeof adapter.browserLiveViewUrl === "function";
577
- const hasPreviewAdapter = typeof adapter.getBrowserPreviewSnapshot === "function";
747
+ const hasShellAdapter = typeof adapter.browserShellUrl === "function";
578
748
  const hasControlAdapter = typeof adapter.acquireBrowserControl === "function" && typeof adapter.releaseBrowserControl === "function";
579
- const liveViewUrl = useMemo(() => {
580
- if (!hasLiveViewAdapter) return "";
581
- return adapter.browserLiveViewUrl(sessionId);
582
- }, [adapter, hasLiveViewAdapter, sessionId]);
749
+ const shellUrl = useMemo(() => {
750
+ if (!hasShellAdapter) return "";
751
+ return adapter.browserShellUrl(sessionId);
752
+ }, [adapter, hasShellAdapter, sessionId]);
583
753
  const hasLiveView = state.status !== "provisioning" && state.status !== "closed";
584
- const hasUserControl = localControlActive;
585
- const canUseLiveView = hasLiveView && hasUserControl && Boolean(liveViewUrl);
586
- const canOpenPreview = hasLiveView && (hasPreviewAdapter || canUseLiveView);
587
- const canOpenFullscreen = canOpenPreview;
588
- const refreshPreview = useCallback(
589
- async (signal) => {
590
- if (!hasPreviewAdapter || canUseLiveView) return;
591
- setPreviewLoading(true);
592
- setPreviewError(null);
593
- try {
594
- const snapshot = await adapter.getBrowserPreviewSnapshot?.(sessionId);
595
- if (signal?.aborted) return;
596
- setPreviewSnapshot(snapshot ?? null);
597
- if (!snapshot) {
598
- setPreviewError("Browser preview is unavailable.");
599
- }
600
- } catch (error) {
601
- if (signal?.aborted) return;
602
- setPreviewSnapshot(null);
603
- setPreviewError(
604
- error instanceof Error ? error.message : "Browser preview failed."
605
- );
606
- } finally {
607
- if (!signal?.aborted) setPreviewLoading(false);
608
- }
609
- },
610
- [adapter, canUseLiveView, hasPreviewAdapter, sessionId]
611
- );
754
+ const canUseShell = hasLiveView && Boolean(shellUrl);
612
755
  useEffect(() => {
613
756
  setFullscreenOpen(false);
614
757
  setOpenFullscreenOnControl(false);
615
758
  setLocalControlActive(false);
616
- setPreviewSnapshot(null);
617
- setPreviewLoading(false);
618
- setPreviewError(null);
619
759
  }, [sessionId]);
620
760
  useEffect(() => {
621
761
  if (!openFullscreenOnControl) return;
622
- if (!canUseLiveView) return;
762
+ if (!canUseShell) return;
623
763
  setFullscreenOpen(true);
624
764
  setOpenFullscreenOnControl(false);
625
- }, [canUseLiveView, openFullscreenOnControl]);
765
+ }, [canUseShell, openFullscreenOnControl]);
626
766
  const handleFullscreenOpenChange = useCallback(
627
767
  (open) => {
628
768
  setFullscreenOpen(open);
@@ -630,7 +770,7 @@ function BrowserPane({
630
770
  []
631
771
  );
632
772
  useEffect(() => {
633
- if (!canUseLiveView || !hasControlAdapter) return;
773
+ if (!localControlActive || !hasControlAdapter) return;
634
774
  const handle = window.setInterval(() => {
635
775
  void adapter.acquireBrowserControl(sessionId).catch((error) => {
636
776
  console.error("Failed to refresh browser control", error);
@@ -638,30 +778,31 @@ function BrowserPane({
638
778
  });
639
779
  }, 25e3);
640
780
  return () => window.clearInterval(handle);
641
- }, [adapter, canUseLiveView, hasControlAdapter, sessionId]);
642
- useEffect(() => {
643
- if (!hasLiveView || canUseLiveView) {
644
- setPreviewLoading(false);
645
- setPreviewError(null);
646
- return;
781
+ }, [adapter, localControlActive, hasControlAdapter, sessionId]);
782
+ const control = useBrowserControl({
783
+ sessionId,
784
+ hasControl: localControlActive,
785
+ adapter,
786
+ onControlAcquired: () => {
787
+ setLocalControlActive(true);
788
+ setOpenFullscreenOnControl(true);
789
+ },
790
+ onControlReleased: () => setLocalControlActive(false),
791
+ onClose
792
+ });
793
+ const shell = (testId) => /* @__PURE__ */ jsx(
794
+ BrowserShell,
795
+ {
796
+ src: shellUrl,
797
+ testId,
798
+ hasControl: localControlActive,
799
+ onToggleControl: hasControlAdapter ? () => void control.toggleControl() : void 0,
800
+ onClose: onClose ? () => control.setConfirmOpen(true) : void 0,
801
+ onMaximize: fullscreenOpen ? void 0 : () => setFullscreenOpen(true),
802
+ onDisconnect: () => setLocalControlActive(false)
647
803
  }
648
- if (!hasPreviewAdapter) return;
649
- const abort = new AbortController();
650
- void refreshPreview(abort.signal);
651
- const timer = window.setInterval(() => {
652
- void refreshPreview(abort.signal);
653
- }, 5e3);
654
- return () => {
655
- abort.abort();
656
- window.clearInterval(timer);
657
- };
658
- }, [
659
- canUseLiveView,
660
- fullscreenOpen,
661
- hasLiveView,
662
- hasPreviewAdapter,
663
- refreshPreview
664
- ]);
804
+ );
805
+ const placeholder = (message) => /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: message });
665
806
  return /* @__PURE__ */ jsxs(Fragment, { children: [
666
807
  /* @__PURE__ */ jsxs(
667
808
  "div",
@@ -669,56 +810,8 @@ function BrowserPane({
669
810
  "data-testid": "browser-pane",
670
811
  className: "flex h-full min-h-0 flex-col bg-background",
671
812
  children: [
672
- /* @__PURE__ */ jsxs("header", { className: "flex min-h-10 items-center gap-2 border-b border-line bg-card px-3 text-xs text-foreground", children: [
673
- /* @__PURE__ */ jsx(ZapIcon, { className: "size-3.5 text-muted-foreground", "aria-hidden": "true" }),
674
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Browser" }),
675
- /* @__PURE__ */ jsx(BrowserStatusDot, { status: state.status }),
676
- state.controlOwner && /* @__PURE__ */ jsxs("span", { className: "min-w-0 truncate text-amber-500", children: [
677
- state.controlOwner,
678
- " has control"
679
- ] }),
680
- canOpenFullscreen && /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center", children: /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
681
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
682
- "button",
683
- {
684
- type: "button",
685
- "aria-label": "Maximize browser",
686
- className: "inline-flex size-7 items-center justify-center text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
687
- onClick: () => setFullscreenOpen(true),
688
- children: /* @__PURE__ */ jsx(Maximize2Icon, { className: "size-3.5", "aria-hidden": "true" })
689
- }
690
- ) }),
691
- /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: "Full screen" })
692
- ] }) }) })
693
- ] }),
694
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-black", children: state.status === "provisioning" ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Starting browser..." }) : state.status === "closed" ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser closed." }) : canUseLiveView ? /* @__PURE__ */ jsx(
695
- BrowserLiveView,
696
- {
697
- src: liveViewUrl,
698
- onDisconnect: () => setLocalControlActive(false)
699
- }
700
- ) : previewSnapshot ? /* @__PURE__ */ jsx(
701
- BrowserPreviewImage,
702
- {
703
- src: previewSnapshot.src,
704
- fit: "contain",
705
- testId: "browser-preview-image"
706
- }
707
- ) : previewLoading ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Loading browser preview..." }) : previewError ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: previewError }) : /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser preview is unavailable." }) }),
708
- hasLiveView && hasControlAdapter && /* @__PURE__ */ jsx(
709
- BrowserControlBar,
710
- {
711
- sessionId,
712
- hasControl: localControlActive,
713
- adapter,
714
- onControlAcquired: () => {
715
- setLocalControlActive(true);
716
- setOpenFullscreenOnControl(true);
717
- },
718
- onControlReleased: () => setLocalControlActive(false),
719
- onClose
720
- }
721
- )
813
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: state.status === "provisioning" ? placeholder("Starting browser...") : state.status === "closed" ? placeholder("Browser closed.") : canUseShell ? shell() : placeholder("Browser view is unavailable.") }),
814
+ control.error && /* @__PURE__ */ jsx("div", { className: "border-t border-line bg-card px-3 py-1.5 text-[11px] text-destructive", children: control.error })
722
815
  ]
723
816
  }
724
817
  ),
@@ -728,50 +821,72 @@ function BrowserPane({
728
821
  "aria-describedby": void 0,
729
822
  className: "flex h-dvh w-screen max-w-none flex-col gap-0 overflow-hidden rounded-none border-0 bg-background p-0 shadow-none ring-0 sm:max-w-none",
730
823
  children: [
731
- /* @__PURE__ */ jsxs(DialogHeader, { className: "h-10 shrink-0 flex-row items-center gap-2 border-b border-line bg-card px-4 py-0", children: [
732
- /* @__PURE__ */ jsx(ZapIcon, { className: "size-3.5 text-muted-foreground", "aria-hidden": "true" }),
733
- /* @__PURE__ */ jsx(DialogTitle, { className: "text-xs normal-case tracking-normal", children: "Browser" }),
734
- /* @__PURE__ */ jsx(BrowserStatusDot, { status: state.status }),
735
- state.controlOwner && /* @__PURE__ */ jsxs("span", { className: "min-w-0 truncate text-xs text-amber-500", children: [
736
- state.controlOwner,
737
- " has control"
738
- ] })
739
- ] }),
740
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-black", children: canUseLiveView ? /* @__PURE__ */ jsx(
741
- BrowserLiveView,
742
- {
743
- src: liveViewUrl,
744
- testId: "browser-fullscreen-rfb",
745
- onDisconnect: () => setLocalControlActive(false)
746
- }
747
- ) : previewSnapshot ? /* @__PURE__ */ jsx(
748
- BrowserPreviewImage,
749
- {
750
- src: previewSnapshot.src,
751
- fit: "contain",
752
- testId: "browser-fullscreen-preview-image"
753
- }
754
- ) : previewLoading ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Loading browser preview..." }) : previewError ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: previewError }) : /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser preview is unavailable." }) })
824
+ /* @__PURE__ */ jsx(DialogHeader, { className: "sr-only", children: /* @__PURE__ */ jsx(DialogTitle, { children: "Browser" }) }),
825
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: canUseShell ? shell("browser-fullscreen-shell") : placeholder("Browser view is unavailable.") })
755
826
  ]
756
827
  }
757
- ) })
828
+ ) }),
829
+ /* @__PURE__ */ jsx(
830
+ ConfirmDialog,
831
+ {
832
+ open: control.confirmOpen,
833
+ title: "Close browser session?",
834
+ description: adapter.closeBrowserSession ? "This permanently shuts down the browser sandbox for this session. The agent will lose access to the page until it re-opens a browser. This cannot be undone." : "This hides the browser panel. The sandbox stays running so the agent can keep using it.",
835
+ confirmLabel: "Close browser",
836
+ variant: "destructive",
837
+ onConfirm: control.handleConfirmClose,
838
+ onCancel: () => control.setConfirmOpen(false)
839
+ }
840
+ )
758
841
  ] });
759
842
  }
760
- function BrowserPreviewImage({
761
- src,
762
- fit,
763
- testId
843
+ var POLL_MS = 15e3;
844
+ var MAX_FAILURES = 3;
845
+ function useBrowserPreview({
846
+ adapter,
847
+ sessionId,
848
+ enabled
764
849
  }) {
765
- return /* @__PURE__ */ jsx(
766
- "img",
767
- {
768
- "data-testid": testId,
769
- src,
770
- alt: "Browser preview",
771
- className: `h-full w-full bg-black ${fit === "cover" ? "object-cover object-top" : "object-contain"}`,
772
- draggable: false
850
+ const [src, setSrc] = useState(null);
851
+ const adapterRef = useRef(adapter);
852
+ adapterRef.current = adapter;
853
+ useEffect(() => {
854
+ if (!enabled || !sessionId) {
855
+ setSrc(null);
856
+ return;
773
857
  }
774
- );
858
+ let cancelled = false;
859
+ let failures = 0;
860
+ let timer = 0;
861
+ const refresh = async () => {
862
+ if (document.hidden) return;
863
+ const fetchSnapshot = adapterRef.current.getBrowserPreviewSnapshot;
864
+ if (typeof fetchSnapshot !== "function") return;
865
+ try {
866
+ const snapshot = await fetchSnapshot(sessionId);
867
+ if (cancelled) return;
868
+ if (!snapshot) {
869
+ window.clearInterval(timer);
870
+ return;
871
+ }
872
+ failures = 0;
873
+ if (snapshot.src) setSrc(snapshot.src);
874
+ } catch {
875
+ if (cancelled) return;
876
+ failures += 1;
877
+ if (failures >= MAX_FAILURES) {
878
+ window.clearInterval(timer);
879
+ }
880
+ }
881
+ };
882
+ void refresh();
883
+ timer = window.setInterval(() => void refresh(), POLL_MS);
884
+ return () => {
885
+ cancelled = true;
886
+ window.clearInterval(timer);
887
+ };
888
+ }, [enabled, sessionId]);
889
+ return src;
775
890
  }
776
891
  var Conversation = ({ className, ...props }) => /* @__PURE__ */ jsx(
777
892
  StickToBottom,
@@ -871,18 +986,64 @@ function ButtonGroup({
871
986
  }
872
987
  );
873
988
  }
874
-
875
- // src/lib/oauth-popup.ts
876
- function openOAuthPopup(url) {
877
- const w = 600;
878
- const h = 720;
879
- const dualLeft = window.screenLeft ?? window.screenX ?? 0;
880
- const dualTop = window.screenTop ?? window.screenY ?? 0;
881
- const width = window.innerWidth || document.documentElement.clientWidth || w;
882
- const height = window.innerHeight || document.documentElement.clientHeight || h;
883
- const left = dualLeft + Math.max(0, (width - w) / 2);
884
- const top = dualTop + Math.max(0, (height - h) / 2);
885
- const features = `popup=yes,width=${w},height=${h},left=${left},top=${top}`;
989
+ function TooltipProvider({
990
+ delayDuration = 0,
991
+ ...props
992
+ }) {
993
+ return /* @__PURE__ */ jsx(
994
+ Tooltip$1.Provider,
995
+ {
996
+ "data-slot": "tooltip-provider",
997
+ delayDuration,
998
+ ...props
999
+ }
1000
+ );
1001
+ }
1002
+ function Tooltip({
1003
+ ...props
1004
+ }) {
1005
+ return /* @__PURE__ */ jsx(Tooltip$1.Root, { "data-slot": "tooltip", ...props });
1006
+ }
1007
+ function TooltipTrigger({
1008
+ ...props
1009
+ }) {
1010
+ return /* @__PURE__ */ jsx(Tooltip$1.Trigger, { "data-slot": "tooltip-trigger", ...props });
1011
+ }
1012
+ function TooltipContent({
1013
+ className,
1014
+ sideOffset = 0,
1015
+ children,
1016
+ ...props
1017
+ }) {
1018
+ return /* @__PURE__ */ jsx(Tooltip$1.Portal, { children: /* @__PURE__ */ jsxs(
1019
+ Tooltip$1.Content,
1020
+ {
1021
+ "data-slot": "tooltip-content",
1022
+ sideOffset,
1023
+ className: cn(
1024
+ "z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-none bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 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 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-none data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
1025
+ className
1026
+ ),
1027
+ ...props,
1028
+ children: [
1029
+ children,
1030
+ /* @__PURE__ */ jsx(Tooltip$1.Arrow, { className: "z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-none bg-foreground fill-foreground" })
1031
+ ]
1032
+ }
1033
+ ) });
1034
+ }
1035
+
1036
+ // src/lib/oauth-popup.ts
1037
+ function openOAuthPopup(url) {
1038
+ const w = 600;
1039
+ const h = 720;
1040
+ const dualLeft = window.screenLeft ?? window.screenX ?? 0;
1041
+ const dualTop = window.screenTop ?? window.screenY ?? 0;
1042
+ const width = window.innerWidth || document.documentElement.clientWidth || w;
1043
+ const height = window.innerHeight || document.documentElement.clientHeight || h;
1044
+ const left = dualLeft + Math.max(0, (width - w) / 2);
1045
+ const top = dualTop + Math.max(0, (height - h) / 2);
1046
+ const features = `popup=yes,width=${w},height=${h},left=${left},top=${top}`;
886
1047
  const popup = window.open(url, "composio-oauth", features);
887
1048
  if (!popup) {
888
1049
  window.open(url, "_blank", "noopener,noreferrer");
@@ -2648,6 +2809,25 @@ function hostname(url) {
2648
2809
  function truncate2(s, n) {
2649
2810
  return s.length > n ? `${s.slice(0, n)}\u2026` : s;
2650
2811
  }
2812
+ function KnowledgeBaseToolBlock({ tc }) {
2813
+ const args = parseArgs(tc.args) ?? {};
2814
+ const label = {
2815
+ kb_search_pages: "KB Search",
2816
+ kb_list_pages: "KB Browse",
2817
+ kb_read_page: "KB Read"
2818
+ }[tc.toolName] ?? tc.toolName;
2819
+ let detail = "";
2820
+ if (tc.toolName === "kb_search_pages" && args.query) {
2821
+ detail = `\u201C${truncate2(args.query, 60)}\u201D`;
2822
+ } else if (tc.toolName === "kb_read_page" && args.path) {
2823
+ detail = args.pages ? `${args.path} \xB7 pages ${args.pages}` : args.path;
2824
+ }
2825
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm py-0.5", children: [
2826
+ /* @__PURE__ */ jsx(LibraryIcon, { className: "size-3.5 text-muted-foreground/60 shrink-0" }),
2827
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: label }),
2828
+ detail && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/70 truncate text-xs", children: detail })
2829
+ ] });
2830
+ }
2651
2831
  function VisionAnalyzeBlock({ tc }) {
2652
2832
  let image = "";
2653
2833
  try {
@@ -4933,6 +5113,10 @@ function ToolCallBlock({
4933
5113
  case "web_search":
4934
5114
  case "web_crawl":
4935
5115
  return /* @__PURE__ */ jsx(WebToolBlock, { tc });
5116
+ case "kb_search_pages":
5117
+ case "kb_list_pages":
5118
+ case "kb_read_page":
5119
+ return /* @__PURE__ */ jsx(KnowledgeBaseToolBlock, { tc });
4936
5120
  case "vision_analyze":
4937
5121
  return /* @__PURE__ */ jsx(VisionAnalyzeBlock, { tc });
4938
5122
  case "read_file":
@@ -9952,6 +10136,72 @@ function ChatComposerInner({
9952
10136
  }
9953
10137
  );
9954
10138
  }
10139
+ function SessionPaneCard({
10140
+ title,
10141
+ subtitle,
10142
+ thumbnail,
10143
+ count,
10144
+ icon: Icon = GlobeIcon,
10145
+ onOpen,
10146
+ testId = "session-pane-card",
10147
+ className,
10148
+ expanded
10149
+ }) {
10150
+ return /* @__PURE__ */ jsxs(
10151
+ "button",
10152
+ {
10153
+ type: "button",
10154
+ "data-testid": testId,
10155
+ "aria-label": `Open ${title}`,
10156
+ "aria-expanded": expanded === void 0 ? void 0 : expanded,
10157
+ onClick: onOpen,
10158
+ className: cn(
10159
+ // No radius here: the caller sets it. Declaring rounded-xl in the base
10160
+ // and cancelling the bottom from a className leaves both rules live,
10161
+ // and which corner wins comes down to stylesheet order rather than
10162
+ // intent -- which is how the stacked cards kept round bottoms.
10163
+ "flex w-full items-center gap-3 border border-line px-3 py-3.5 text-left transition-colors hover:bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
10164
+ className
10165
+ ),
10166
+ children: [
10167
+ thumbnail ? /* @__PURE__ */ jsx(
10168
+ "img",
10169
+ {
10170
+ "data-testid": "session-pane-card-thumb",
10171
+ src: thumbnail,
10172
+ alt: "",
10173
+ className: "h-11 w-16 shrink-0 rounded-md border border-line object-cover object-top"
10174
+ }
10175
+ ) : /* @__PURE__ */ jsx(
10176
+ Icon,
10177
+ {
10178
+ "data-testid": "session-pane-card-icon",
10179
+ className: "size-5 shrink-0 text-muted-foreground",
10180
+ "aria-hidden": "true"
10181
+ }
10182
+ ),
10183
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col", children: [
10184
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-sm font-medium text-foreground", children: [
10185
+ title,
10186
+ count !== void 0 && count > 0 && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-secondary px-1.5 text-[11px] font-normal text-muted-foreground", children: count })
10187
+ ] }),
10188
+ subtitle && /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: subtitle })
10189
+ ] }),
10190
+ expanded !== void 0 && /* @__PURE__ */ jsx(
10191
+ ChevronDownIcon,
10192
+ {
10193
+ "data-testid": "session-pane-card-chevron",
10194
+ className: cn(
10195
+ "ml-auto size-4 shrink-0 text-muted-foreground transition-transform",
10196
+ expanded && "rotate-180"
10197
+ ),
10198
+ "aria-hidden": "true"
10199
+ }
10200
+ )
10201
+ ]
10202
+ }
10203
+ );
10204
+ }
9955
10205
  var MAX_LOGOS = 10;
9956
10206
  function IntegrationsBand({
9957
10207
  agentId,
@@ -10523,9 +10773,9 @@ function ArtifactTable({ spec }) {
10523
10773
  }
10524
10774
  var DEFAULT_HEIGHT_PX = 480;
10525
10775
  var EXPANDED_HEIGHT_PX = 800;
10526
- var MIN_ZOOM2 = 0.5;
10527
- var MAX_ZOOM2 = 2;
10528
- var ZOOM_STEP2 = 0.1;
10776
+ var MIN_ZOOM = 0.5;
10777
+ var MAX_ZOOM = 2;
10778
+ var ZOOM_STEP = 0.1;
10529
10779
  var SAFETY_STYLE_PREAMBLE = "<style>html,body{max-height:100vh}canvas{max-height:80vh!important}</style>";
10530
10780
  function injectSafetyStyle(html) {
10531
10781
  const headCloseIdx = html.search(/<\/head\s*>/i);
@@ -10542,7 +10792,7 @@ function ArtifactHtml({
10542
10792
  const [zoom, setZoom] = useState(1);
10543
10793
  const safeSrcdoc = useMemo(() => injectSafetyStyle(spec.html), [spec.html]);
10544
10794
  const viewportHeight = expanded ? EXPANDED_HEIGHT_PX : DEFAULT_HEIGHT_PX;
10545
- const clamp4 = (v) => Math.round(Math.min(MAX_ZOOM2, Math.max(MIN_ZOOM2, v)) * 100) / 100;
10795
+ const clamp4 = (v) => Math.round(Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, v)) * 100) / 100;
10546
10796
  return /* @__PURE__ */ jsxs(
10547
10797
  "div",
10548
10798
  {
@@ -10588,8 +10838,8 @@ function ArtifactHtml({
10588
10838
  "button",
10589
10839
  {
10590
10840
  type: "button",
10591
- onClick: () => setZoom((z) => clamp4(z - ZOOM_STEP2)),
10592
- disabled: zoom <= MIN_ZOOM2,
10841
+ onClick: () => setZoom((z) => clamp4(z - ZOOM_STEP)),
10842
+ disabled: zoom <= MIN_ZOOM,
10593
10843
  className: "hover:text-foreground disabled:opacity-40 cursor-pointer",
10594
10844
  "aria-label": "zoom out",
10595
10845
  children: "\u2212"
@@ -10612,8 +10862,8 @@ function ArtifactHtml({
10612
10862
  "button",
10613
10863
  {
10614
10864
  type: "button",
10615
- onClick: () => setZoom((z) => clamp4(z + ZOOM_STEP2)),
10616
- disabled: zoom >= MAX_ZOOM2,
10865
+ onClick: () => setZoom((z) => clamp4(z + ZOOM_STEP)),
10866
+ disabled: zoom >= MAX_ZOOM,
10617
10867
  className: "hover:text-foreground disabled:opacity-40",
10618
10868
  "aria-label": "zoom in",
10619
10869
  children: "+"
@@ -11035,7 +11285,7 @@ var ErrorMessage = memo(function ErrorMessage2({
11035
11285
  className: "flex items-center gap-1 self-start text-xs text-muted-foreground hover:text-foreground",
11036
11286
  "aria-expanded": detailOpen,
11037
11287
  children: [
11038
- detailOpen ? /* @__PURE__ */ jsx(ChevronDown, { className: "size-3" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "size-3" }),
11288
+ detailOpen ? /* @__PURE__ */ jsx(ChevronDown, { className: "size-3" }) : /* @__PURE__ */ jsx(ChevronRight$1, { className: "size-3" }),
11039
11289
  detailOpen ? "Hide details" : "Show details"
11040
11290
  ]
11041
11291
  }
@@ -11111,745 +11361,294 @@ var InsufficientCreditsCard = memo(function InsufficientCreditsCard2({
11111
11361
  }
11112
11362
  );
11113
11363
  });
11114
- var SKELETON_WIDTHS = [70, 85, 55, 90, 60, 78, 45, 82, 65, 72];
11115
- var PDF_WORKER_SRC = new URL(
11116
- "pdfjs-dist/legacy/build/pdf.worker.mjs",
11117
- import.meta.url
11118
- ).toString();
11119
- function FileViewer({
11120
- file,
11121
- loading,
11122
- error,
11123
- downloadUrl,
11124
- onDelete,
11125
- onClose,
11126
- hideHeader = false
11364
+ var _EXTENSION_KIND = {
11365
+ // Documents
11366
+ pdf: { icon: FileTextIcon, typeLabel: "PDF" },
11367
+ doc: { icon: FileTextIcon, typeLabel: "Word document" },
11368
+ docx: { icon: FileTextIcon, typeLabel: "Word document" },
11369
+ odt: { icon: FileTextIcon, typeLabel: "OpenDocument text" },
11370
+ rtf: { icon: FileTextIcon, typeLabel: "Rich text" },
11371
+ txt: { icon: FileTextIcon, typeLabel: "Text" },
11372
+ md: { icon: FileTextIcon, typeLabel: "Markdown" },
11373
+ // Spreadsheets
11374
+ xls: { icon: FileSpreadsheetIcon, typeLabel: "Spreadsheet" },
11375
+ xlsx: { icon: FileSpreadsheetIcon, typeLabel: "Spreadsheet" },
11376
+ ods: { icon: FileSpreadsheetIcon, typeLabel: "OpenDocument spreadsheet" },
11377
+ csv: { icon: FileSpreadsheetIcon, typeLabel: "CSV" },
11378
+ tsv: { icon: FileSpreadsheetIcon, typeLabel: "TSV" },
11379
+ // Presentations / charts
11380
+ ppt: { icon: FileChartLineIcon, typeLabel: "Presentation" },
11381
+ pptx: { icon: FileChartLineIcon, typeLabel: "Presentation" },
11382
+ // Images
11383
+ png: { icon: FileImageIcon, typeLabel: "Image" },
11384
+ jpg: { icon: FileImageIcon, typeLabel: "Image" },
11385
+ jpeg: { icon: FileImageIcon, typeLabel: "Image" },
11386
+ gif: { icon: FileImageIcon, typeLabel: "Image" },
11387
+ svg: { icon: FileImageIcon, typeLabel: "SVG image" },
11388
+ webp: { icon: FileImageIcon, typeLabel: "Image" },
11389
+ bmp: { icon: FileImageIcon, typeLabel: "Image" },
11390
+ // Audio / video
11391
+ mp3: { icon: FileAudioIcon, typeLabel: "Audio" },
11392
+ wav: { icon: FileAudioIcon, typeLabel: "Audio" },
11393
+ ogg: { icon: FileAudioIcon, typeLabel: "Audio" },
11394
+ mp4: { icon: FileVideoIcon, typeLabel: "Video" },
11395
+ mov: { icon: FileVideoIcon, typeLabel: "Video" },
11396
+ webm: { icon: FileVideoIcon, typeLabel: "Video" },
11397
+ // Archives
11398
+ zip: { icon: FileArchiveIcon, typeLabel: "Archive" },
11399
+ tar: { icon: FileArchiveIcon, typeLabel: "Archive" },
11400
+ gz: { icon: FileArchiveIcon, typeLabel: "Archive" },
11401
+ bz2: { icon: FileArchiveIcon, typeLabel: "Archive" },
11402
+ "7z": { icon: FileArchiveIcon, typeLabel: "Archive" },
11403
+ // Code
11404
+ py: { icon: FileCodeIcon, typeLabel: "Python" },
11405
+ js: { icon: FileCodeIcon, typeLabel: "JavaScript" },
11406
+ ts: { icon: FileCodeIcon, typeLabel: "TypeScript" },
11407
+ tsx: { icon: FileCodeIcon, typeLabel: "TypeScript" },
11408
+ jsx: { icon: FileCodeIcon, typeLabel: "JavaScript" },
11409
+ json: { icon: FileCodeIcon, typeLabel: "JSON" },
11410
+ yaml: { icon: FileCodeIcon, typeLabel: "YAML" },
11411
+ yml: { icon: FileCodeIcon, typeLabel: "YAML" },
11412
+ toml: { icon: FileCodeIcon, typeLabel: "TOML" },
11413
+ sh: { icon: CodeIcon, typeLabel: "Shell" },
11414
+ rs: { icon: FileCodeIcon, typeLabel: "Rust" },
11415
+ go: { icon: FileCodeIcon, typeLabel: "Go" },
11416
+ rb: { icon: FileCodeIcon, typeLabel: "Ruby" },
11417
+ java: { icon: FileCodeIcon, typeLabel: "Java" },
11418
+ cpp: { icon: FileCodeIcon, typeLabel: "C++" },
11419
+ c: { icon: FileCodeIcon, typeLabel: "C" },
11420
+ html: { icon: FileCodeIcon, typeLabel: "HTML" },
11421
+ css: { icon: FileCodeIcon, typeLabel: "CSS" },
11422
+ scss: { icon: FileCodeIcon, typeLabel: "Sass" },
11423
+ sql: { icon: FileCodeIcon, typeLabel: "SQL" }
11424
+ };
11425
+ function _extension(path) {
11426
+ const idx = path.lastIndexOf(".");
11427
+ if (idx < 0 || idx === path.length - 1) return "";
11428
+ return path.slice(idx + 1).toLowerCase();
11429
+ }
11430
+ function _fileKindFor(path) {
11431
+ const ext = _extension(path);
11432
+ return _EXTENSION_KIND[ext] ?? { icon: FileIcon, typeLabel: "File" };
11433
+ }
11434
+ function _basename(path) {
11435
+ const idx = path.lastIndexOf("/");
11436
+ return idx >= 0 ? path.slice(idx + 1) : path;
11437
+ }
11438
+ function WorkspaceFileCard({
11439
+ sessionId,
11440
+ path,
11441
+ label
11127
11442
  }) {
11128
- const visible = loading || file !== null || error !== null;
11129
- if (!visible) return null;
11130
- const fileName2 = file?.path.split("/").pop() ?? "File";
11131
- const lang = file ? getLanguageHint(file.path) : "";
11132
- const isPdf = file?.mime_type === "application/pdf" || file?.path.toLowerCase().endsWith(".pdf") || false;
11133
- const isImage = file?.encoding === "base64" && !isPdf;
11134
- const HeaderIcon = isImage ? ImageIcon : FileTextIcon;
11135
- return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col border-t border-line", children: [
11136
- !hideHeader && /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2 border-b border-line px-3 py-2", children: [
11137
- /* @__PURE__ */ jsx(HeaderIcon, { className: "size-3.5 shrink-0 text-muted-foreground" }),
11138
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
11139
- /* @__PURE__ */ jsx("div", { className: "truncate text-xs font-medium", children: fileName2 }),
11140
- /* @__PURE__ */ jsxs("div", { className: "truncate text-[10px] text-muted-foreground", children: [
11141
- file?.path ?? "Loading...",
11142
- file && /* @__PURE__ */ jsxs("span", { className: "ml-1.5", children: [
11143
- formatFileSize(file.size),
11144
- file.truncated && " (truncated)"
11145
- ] })
11146
- ] })
11147
- ] }),
11148
- downloadUrl && /* @__PURE__ */ jsx(
11149
- "a",
11150
- {
11151
- href: downloadUrl,
11152
- download: fileName2,
11153
- className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
11154
- "aria-label": `Download ${fileName2}`,
11155
- title: "Download",
11156
- children: /* @__PURE__ */ jsx(DownloadIcon, { className: "size-3.5" })
11157
- }
11158
- ),
11159
- onDelete && /* @__PURE__ */ jsx(
11160
- "button",
11161
- {
11162
- type: "button",
11163
- onClick: onDelete,
11164
- className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive",
11165
- "aria-label": `Delete ${fileName2}`,
11166
- title: "Delete",
11167
- children: /* @__PURE__ */ jsx(TrashIcon, { className: "size-3.5" })
11443
+ const { adapter, onFileSelect } = useAgentChatAdapterContext();
11444
+ const kind = _fileKindFor(path);
11445
+ const Icon = kind.icon;
11446
+ const downloadUrl = adapter.getWorkspaceDownloadUrl({ sessionId, path });
11447
+ const downloadName = _basename(path) || label;
11448
+ const handleCardClick = useCallback(() => {
11449
+ onFileSelect?.(path);
11450
+ }, [onFileSelect, path]);
11451
+ return /* @__PURE__ */ jsxs(
11452
+ "div",
11453
+ {
11454
+ role: "button",
11455
+ tabIndex: 0,
11456
+ onClick: handleCardClick,
11457
+ onKeyDown: (e) => {
11458
+ if (e.key === "Enter" || e.key === " ") {
11459
+ e.preventDefault();
11460
+ handleCardClick();
11168
11461
  }
11462
+ },
11463
+ className: cn(
11464
+ "group/file flex items-center gap-3 rounded-lg border border-border",
11465
+ "bg-muted/20 px-3 py-2 text-left transition-colors",
11466
+ "cursor-pointer hover:bg-muted/40"
11169
11467
  ),
11170
- /* @__PURE__ */ jsx(
11171
- "button",
11172
- {
11173
- type: "button",
11174
- onClick: onClose,
11175
- className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
11176
- "aria-label": "Close file",
11177
- children: /* @__PURE__ */ jsx(XIcon, { className: "size-3.5" })
11178
- }
11179
- )
11180
- ] }),
11181
- file && isPdf && file.encoding === "base64" ? (
11182
- // PDF has its own scrollable container — bypass ScrollArea so
11183
- // it can grow with the parent's flex height (Radix Viewport's
11184
- // internal table wrapper sizes to content and breaks h-full).
11185
- /* @__PURE__ */ jsx(PdfPreview, { file, fileName: fileName2 })
11186
- ) : /* @__PURE__ */ jsxs(ScrollArea, { className: "min-h-0 flex-1", children: [
11187
- loading && /* @__PURE__ */ jsx("div", { className: "space-y-1.5 p-3", children: Array.from({ length: 10 }).map((_, index) => /* @__PURE__ */ jsx(
11188
- Skeleton,
11189
- {
11190
- className: "h-3.5 rounded",
11191
- style: {
11192
- width: `${SKELETON_WIDTHS[index % SKELETON_WIDTHS.length]}%`
11193
- }
11194
- },
11195
- index
11196
- )) }),
11197
- error && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 p-3 text-xs text-destructive", children: [
11198
- /* @__PURE__ */ jsx(AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
11199
- /* @__PURE__ */ jsx("span", { children: error })
11200
- ] }),
11201
- file && isPdf && file.encoding !== "base64" && /* @__PURE__ */ jsx(FileViewerError, { message: "PDF preview requires base64 file content." }),
11202
- file && isImage && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center p-4", children: /* @__PURE__ */ jsx(
11203
- "img",
11204
- {
11205
- src: `data:${file.mime_type ?? "image/png"};base64,${file.content}`,
11206
- alt: fileName2,
11207
- className: "max-h-[60vh] max-w-full rounded object-contain"
11208
- }
11209
- ) }),
11210
- file && !isImage && !isPdf && /* @__PURE__ */ jsx("pre", { className: "wrap-break-word whitespace-pre-wrap p-3 text-[11px] leading-relaxed text-foreground", children: /* @__PURE__ */ jsx("code", { "data-language": lang, children: file.content }) })
11211
- ] })
11212
- ] });
11468
+ children: [
11469
+ /* @__PURE__ */ jsx("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-background", children: /* @__PURE__ */ jsx(Icon, { className: "size-5 text-muted-foreground", "aria-hidden": true }) }),
11470
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
11471
+ /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium text-foreground", children: label }),
11472
+ /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-muted-foreground", children: kind.typeLabel })
11473
+ ] }),
11474
+ /* @__PURE__ */ jsxs(
11475
+ "a",
11476
+ {
11477
+ href: downloadUrl,
11478
+ download: downloadName,
11479
+ onClick: (e) => e.stopPropagation(),
11480
+ className: cn(
11481
+ "shrink-0 inline-flex items-center gap-1.5 rounded-md border border-border",
11482
+ "bg-background px-2.5 py-1.5 text-xs font-medium text-foreground",
11483
+ "transition-colors hover:bg-muted"
11484
+ ),
11485
+ "aria-label": `Download ${downloadName}`,
11486
+ children: [
11487
+ /* @__PURE__ */ jsx(DownloadIcon, { className: "size-4", "aria-hidden": true }),
11488
+ "Download"
11489
+ ]
11490
+ }
11491
+ )
11492
+ ]
11493
+ }
11494
+ );
11213
11495
  }
11214
- function FileViewerError({ message }) {
11215
- return /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 p-3 text-xs text-destructive", children: [
11216
- /* @__PURE__ */ jsx(AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
11217
- /* @__PURE__ */ jsx("span", { children: message })
11218
- ] });
11496
+ function scrollToArtifactBlock(artifactId) {
11497
+ document.querySelector(`[data-artifact-anchor="${CSS.escape(artifactId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "center" });
11219
11498
  }
11220
- function PdfPreview({
11221
- file,
11222
- fileName: fileName2
11499
+ function ArtifactRefCard({
11500
+ artifactId,
11501
+ name,
11502
+ kind
11223
11503
  }) {
11224
- const containerRef = useRef(null);
11225
- const viewerRef = useRef(null);
11226
- const pdfViewerRef = useRef(null);
11227
- const eventBusRef = useRef(null);
11228
- const findControllerRef = useRef(null);
11229
- const [error, setError] = useState(null);
11230
- const [pageNumber, setPageNumber] = useState(1);
11231
- const [pagesCount, setPagesCount] = useState(0);
11232
- const [scale, setScale] = useState(1);
11233
- const [query, setQuery] = useState("");
11234
- useEffect(() => {
11235
- let cancelled = false;
11236
- let loadingTask = null;
11237
- let pdfDocument = null;
11238
- const renderPdf = async () => {
11239
- setError(null);
11240
- const container = containerRef.current;
11241
- const viewerElement = viewerRef.current;
11242
- if (!container || !viewerElement) return;
11243
- const pdfBytes = decodeBase64(file.content);
11244
- const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
11245
- pdfjs.GlobalWorkerOptions.workerSrc = PDF_WORKER_SRC;
11246
- globalThis.pdfjsLib = pdfjs;
11247
- const pdfViewerModule = await import('pdfjs-dist/web/pdf_viewer.mjs');
11248
- loadingTask = pdfjs.getDocument({ data: pdfBytes });
11249
- const pdf = await loadingTask.promise;
11250
- pdfDocument = pdf;
11251
- if (cancelled) return;
11252
- const eventBus = new pdfViewerModule.EventBus();
11253
- const linkService = new pdfViewerModule.PDFLinkService({ eventBus });
11254
- const findController = new pdfViewerModule.PDFFindController({
11255
- eventBus,
11256
- linkService
11257
- });
11258
- const pdfViewer = new pdfViewerModule.PDFViewer({
11259
- container,
11260
- viewer: viewerElement,
11261
- eventBus,
11262
- linkService,
11263
- findController,
11264
- removePageBorders: true
11265
- });
11266
- linkService.setViewer(pdfViewer);
11267
- linkService.setDocument(pdf);
11268
- findController.setDocument(pdf);
11269
- const onPagesInit = () => {
11270
- pdfViewer.currentScaleValue = "page-width";
11271
- setScale(pdfViewer.currentScale || 1);
11272
- setPagesCount(pdfViewer.pagesCount);
11273
- setPageNumber(pdfViewer.currentPageNumber);
11274
- };
11275
- const onPageChanging = (event) => {
11276
- if (typeof event.pageNumber === "number") {
11277
- setPageNumber(event.pageNumber);
11278
- }
11279
- };
11280
- const onScaleChanging = (event) => {
11281
- if (typeof event.scale === "number") {
11282
- setScale(event.scale);
11283
- }
11284
- };
11285
- eventBus.on("pagesinit", onPagesInit);
11286
- eventBus.on("pagechanging", onPageChanging);
11287
- eventBus.on("scalechanging", onScaleChanging);
11288
- pdfViewerRef.current = pdfViewer;
11289
- eventBusRef.current = eventBus;
11290
- findControllerRef.current = findController;
11291
- setPagesCount(pdf.numPages);
11292
- pdfViewer.setDocument(pdf);
11293
- };
11294
- void renderPdf().catch((nextError) => {
11295
- if (cancelled) return;
11296
- if (nextError instanceof Error && nextError.name === "RenderingCancelledException") {
11297
- return;
11504
+ return /* @__PURE__ */ jsxs(
11505
+ "button",
11506
+ {
11507
+ type: "button",
11508
+ onClick: () => scrollToArtifactBlock(artifactId),
11509
+ className: "flex w-full min-w-0 cursor-pointer items-center gap-2 rounded border border-border bg-background px-3 py-2 text-left text-sm transition-colors hover:bg-muted/60",
11510
+ children: [
11511
+ /* @__PURE__ */ jsx(FileTextIcon, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }),
11512
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-foreground", children: name }),
11513
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: KIND_LABEL[kind] })
11514
+ ]
11515
+ }
11516
+ );
11517
+ }
11518
+ function resolveArtifactRef(ref, messages) {
11519
+ for (const msg of messages) {
11520
+ if (msg.role !== "system" || msg.systemKind !== "artifact") continue;
11521
+ const meta = msg.systemMeta ?? {};
11522
+ if (meta.artifact_id !== ref) continue;
11523
+ const kind = meta.kind ?? "markdown";
11524
+ const version = typeof meta.version === "number" ? meta.version : 1;
11525
+ const name = typeof meta.name === "string" ? meta.name : "";
11526
+ return { artifactId: ref, name, kind, version };
11527
+ }
11528
+ return null;
11529
+ }
11530
+ function TurnSummaryCard({
11531
+ summary,
11532
+ sessionId,
11533
+ messages,
11534
+ onFileSelect,
11535
+ onCommandSelect
11536
+ }) {
11537
+ const hasRecap = summary.recap.trim().length > 0;
11538
+ const hasArtifacts = summary.artifacts.length > 0;
11539
+ if (!hasArtifacts) return null;
11540
+ return /* @__PURE__ */ jsxs("div", { className: "mt-3 rounded border border-border bg-muted/50 px-3 py-2 text-sm", children: [
11541
+ /* @__PURE__ */ jsx("div", { className: "mb-2 text-xs font-semibold uppercase tracking-wide", children: "Summary" }),
11542
+ hasRecap && /* @__PURE__ */ jsx("p", { className: "mb-2 whitespace-pre-wrap text-foreground", children: summary.recap }),
11543
+ hasArtifacts && /* @__PURE__ */ jsx("div", { className: "space-y-2", children: summary.artifacts.map((artifact, i) => {
11544
+ const key = `${artifact.kind}:${artifact.ref}:${i}`;
11545
+ if (artifact.kind === "file" && sessionId) {
11546
+ return /* @__PURE__ */ jsx(
11547
+ WorkspaceFileCard,
11548
+ {
11549
+ sessionId,
11550
+ path: artifact.ref,
11551
+ label: artifact.label
11552
+ },
11553
+ key
11554
+ );
11298
11555
  }
11299
- setError(formatPdfPreviewError(nextError));
11300
- });
11301
- return () => {
11302
- cancelled = true;
11303
- loadingTask?.destroy();
11304
- pdfDocument?.destroy();
11305
- pdfViewerRef.current?.cleanup();
11306
- pdfViewerRef.current = null;
11307
- eventBusRef.current = null;
11308
- findControllerRef.current = null;
11309
- };
11310
- }, [file.content]);
11311
- const setViewerPage = (nextPage) => {
11312
- const viewer = pdfViewerRef.current;
11313
- if (!viewer) return;
11314
- const clampedPage = Math.min(
11315
- Math.max(1, nextPage),
11316
- Math.max(1, viewer.pagesCount)
11317
- );
11318
- viewer.currentPageNumber = clampedPage;
11319
- setPageNumber(clampedPage);
11320
- };
11321
- const setViewerScale = (nextScale) => {
11322
- const viewer = pdfViewerRef.current;
11323
- if (!viewer) return;
11324
- const clampedScale = Math.min(3, Math.max(0.5, nextScale));
11325
- viewer.currentScale = clampedScale;
11326
- setScale(clampedScale);
11327
- };
11328
- const runFind = (findPrevious = false) => {
11329
- const eventBus = eventBusRef.current;
11330
- if (!eventBus || !query.trim()) return;
11331
- eventBus.dispatch("find", {
11332
- source: eventBus,
11333
- type: "again",
11334
- query,
11335
- phraseSearch: true,
11336
- caseSensitive: false,
11337
- entireWord: false,
11338
- highlightAll: true,
11339
- findPrevious,
11340
- matchDiacritics: true
11341
- });
11342
- };
11343
- return /* @__PURE__ */ jsxs("div", { className: "flex min-h-full flex-col", children: [
11344
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-1.5 border-b border-line px-2 py-1.5", children: [
11345
- /* @__PURE__ */ jsx(
11346
- Button,
11347
- {
11348
- type: "button",
11349
- variant: "ghost",
11350
- size: "icon-xs",
11351
- onClick: () => setViewerPage(pageNumber - 1),
11352
- disabled: pageNumber <= 1,
11353
- "aria-label": "Previous PDF page",
11354
- title: "Previous page",
11355
- children: /* @__PURE__ */ jsx(ChevronLeftIcon, { className: "size-3.5" })
11556
+ if (artifact.kind === "artifact") {
11557
+ const resolved = sessionId ? resolveArtifactRef(artifact.ref, messages) : null;
11558
+ if (resolved) {
11559
+ return /* @__PURE__ */ jsx(
11560
+ ArtifactRefCard,
11561
+ {
11562
+ artifactId: resolved.artifactId,
11563
+ name: resolved.name || artifact.label,
11564
+ kind: resolved.kind
11565
+ },
11566
+ key
11567
+ );
11356
11568
  }
11357
- ),
11358
- /* @__PURE__ */ jsx(
11359
- Input,
11569
+ }
11570
+ return /* @__PURE__ */ jsxs(
11571
+ "div",
11360
11572
  {
11361
- type: "number",
11362
- min: 1,
11363
- max: pagesCount || 1,
11364
- value: pageNumber,
11365
- onChange: (event) => setViewerPage(Number(event.target.value)),
11366
- "aria-label": "PDF page number",
11367
- className: "h-7 w-12 border-input px-1 text-center text-xs"
11368
- }
11369
- ),
11370
- /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
11371
- "/ ",
11372
- pagesCount || "-"
11373
- ] }),
11374
- /* @__PURE__ */ jsx(
11375
- Button,
11376
- {
11377
- type: "button",
11378
- variant: "ghost",
11379
- size: "icon-xs",
11380
- onClick: () => setViewerPage(pageNumber + 1),
11381
- disabled: pagesCount > 0 && pageNumber >= pagesCount,
11382
- "aria-label": "Next PDF page",
11383
- title: "Next page",
11384
- children: /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-3.5" })
11385
- }
11386
- ),
11387
- /* @__PURE__ */ jsx("div", { className: "mx-1 h-5 w-px bg-line" }),
11388
- /* @__PURE__ */ jsx(
11389
- Button,
11390
- {
11391
- type: "button",
11392
- variant: "ghost",
11393
- size: "icon-xs",
11394
- onClick: () => setViewerScale(scale - 0.1),
11395
- disabled: scale <= 0.5,
11396
- "aria-label": "Zoom out PDF",
11397
- title: "Zoom out",
11398
- children: /* @__PURE__ */ jsx(MinusIcon, { className: "size-3.5" })
11399
- }
11400
- ),
11401
- /* @__PURE__ */ jsxs(
11402
- "button",
11403
- {
11404
- type: "button",
11405
- className: "h-7 min-w-12 px-1 text-xs text-muted-foreground hover:text-foreground",
11406
- onClick: () => {
11407
- const viewer = pdfViewerRef.current;
11408
- if (!viewer) return;
11409
- viewer.currentScaleValue = "page-width";
11410
- setScale(viewer.currentScale || 1);
11411
- },
11412
- "aria-label": "Fit PDF to width",
11413
- title: "Fit width",
11573
+ className: "flex items-baseline gap-2 text-sm",
11414
11574
  children: [
11415
- Math.round(scale * 100),
11416
- "%"
11575
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", "aria-hidden": true, children: "\u2022" }),
11576
+ /* @__PURE__ */ jsx(
11577
+ ArtifactRow,
11578
+ {
11579
+ artifact,
11580
+ onFileSelect,
11581
+ onCommandSelect
11582
+ }
11583
+ )
11417
11584
  ]
11418
- }
11419
- ),
11420
- /* @__PURE__ */ jsx(
11421
- Button,
11422
- {
11423
- type: "button",
11424
- variant: "ghost",
11425
- size: "icon-xs",
11426
- onClick: () => setViewerScale(scale + 0.1),
11427
- disabled: scale >= 3,
11428
- "aria-label": "Zoom in PDF",
11429
- title: "Zoom in",
11430
- children: /* @__PURE__ */ jsx(PlusIcon, { className: "size-3.5" })
11431
- }
11432
- ),
11433
- /* @__PURE__ */ jsx("div", { className: "mx-1 h-5 w-px bg-line" }),
11434
- /* @__PURE__ */ jsx(SearchIcon, { className: "size-3.5 text-muted-foreground" }),
11435
- /* @__PURE__ */ jsx(
11436
- Input,
11437
- {
11438
- type: "search",
11439
- value: query,
11440
- onChange: (event) => setQuery(event.target.value),
11441
- onKeyDown: (event) => {
11442
- if (event.key === "Enter") runFind(event.shiftKey);
11443
- },
11444
- placeholder: "Find",
11445
- "aria-label": "Find in PDF",
11446
- className: "h-7 w-28 border-input px-1 text-xs"
11447
- }
11448
- ),
11449
- /* @__PURE__ */ jsx(
11450
- Button,
11451
- {
11452
- type: "button",
11453
- variant: "ghost",
11454
- size: "xs",
11455
- onClick: () => runFind(false),
11456
- disabled: !query.trim(),
11457
- children: "Find"
11458
- }
11459
- )
11460
- ] }),
11461
- error && /* @__PURE__ */ jsxs("div", { className: "flex w-full items-start gap-2 p-3 text-xs text-destructive", children: [
11462
- /* @__PURE__ */ jsx(AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
11463
- /* @__PURE__ */ jsx("span", { children: error })
11464
- ] }),
11465
- !error && /* @__PURE__ */ jsx("div", { className: "relative min-h-105 flex-1 bg-muted/20", children: /* @__PURE__ */ jsx(
11466
- "div",
11467
- {
11468
- ref: containerRef,
11469
- "aria-label": `PDF viewer for ${fileName2}`,
11470
- className: "absolute inset-0 overflow-auto",
11471
- children: /* @__PURE__ */ jsx("div", { ref: viewerRef, className: "pdfViewer" })
11472
- }
11473
- ) })
11585
+ },
11586
+ key
11587
+ );
11588
+ }) })
11474
11589
  ] });
11475
11590
  }
11476
- function decodeBase64(content) {
11477
- const binary = globalThis.atob(content);
11478
- const bytes = new Uint8Array(binary.length);
11479
- for (let index = 0; index < binary.length; index += 1) {
11480
- bytes[index] = binary.charCodeAt(index);
11591
+ function ArtifactRow({
11592
+ artifact,
11593
+ onFileSelect,
11594
+ onCommandSelect
11595
+ }) {
11596
+ if (artifact.kind === "file") {
11597
+ if (!onFileSelect) {
11598
+ return /* @__PURE__ */ jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
11599
+ }
11600
+ return /* @__PURE__ */ jsx(
11601
+ "button",
11602
+ {
11603
+ type: "button",
11604
+ onClick: () => onFileSelect(artifact.ref),
11605
+ className: "truncate cursor-pointer text-primary hover:underline",
11606
+ children: artifact.label
11607
+ }
11608
+ );
11481
11609
  }
11482
- return bytes;
11483
- }
11484
- function formatPdfPreviewError(error) {
11485
- if (error instanceof Error && (error.name === "InvalidCharacterError" || error.message === "Invalid character")) {
11486
- return "PDF preview data is not valid base64.";
11610
+ if (artifact.kind === "url") {
11611
+ return /* @__PURE__ */ jsx(
11612
+ "a",
11613
+ {
11614
+ href: artifact.ref,
11615
+ target: "_blank",
11616
+ rel: "noopener noreferrer",
11617
+ className: "truncate text-primary hover:underline",
11618
+ children: artifact.label
11619
+ }
11620
+ );
11487
11621
  }
11488
- return error instanceof Error ? error.message : "Failed to render PDF preview.";
11489
- }
11490
- var _EXTENSION_KIND = {
11491
- // Documents
11492
- pdf: { icon: FileTextIcon, typeLabel: "PDF" },
11493
- doc: { icon: FileTextIcon, typeLabel: "Word document" },
11494
- docx: { icon: FileTextIcon, typeLabel: "Word document" },
11495
- odt: { icon: FileTextIcon, typeLabel: "OpenDocument text" },
11496
- rtf: { icon: FileTextIcon, typeLabel: "Rich text" },
11497
- txt: { icon: FileTextIcon, typeLabel: "Text" },
11498
- md: { icon: FileTextIcon, typeLabel: "Markdown" },
11499
- // Spreadsheets
11500
- xls: { icon: FileSpreadsheetIcon, typeLabel: "Spreadsheet" },
11501
- xlsx: { icon: FileSpreadsheetIcon, typeLabel: "Spreadsheet" },
11502
- ods: { icon: FileSpreadsheetIcon, typeLabel: "OpenDocument spreadsheet" },
11503
- csv: { icon: FileSpreadsheetIcon, typeLabel: "CSV" },
11504
- tsv: { icon: FileSpreadsheetIcon, typeLabel: "TSV" },
11505
- // Presentations / charts
11506
- ppt: { icon: FileChartLineIcon, typeLabel: "Presentation" },
11507
- pptx: { icon: FileChartLineIcon, typeLabel: "Presentation" },
11508
- // Images
11509
- png: { icon: FileImageIcon, typeLabel: "Image" },
11510
- jpg: { icon: FileImageIcon, typeLabel: "Image" },
11511
- jpeg: { icon: FileImageIcon, typeLabel: "Image" },
11512
- gif: { icon: FileImageIcon, typeLabel: "Image" },
11513
- svg: { icon: FileImageIcon, typeLabel: "SVG image" },
11514
- webp: { icon: FileImageIcon, typeLabel: "Image" },
11515
- bmp: { icon: FileImageIcon, typeLabel: "Image" },
11516
- // Audio / video
11517
- mp3: { icon: FileAudioIcon, typeLabel: "Audio" },
11518
- wav: { icon: FileAudioIcon, typeLabel: "Audio" },
11519
- ogg: { icon: FileAudioIcon, typeLabel: "Audio" },
11520
- mp4: { icon: FileVideoIcon, typeLabel: "Video" },
11521
- mov: { icon: FileVideoIcon, typeLabel: "Video" },
11522
- webm: { icon: FileVideoIcon, typeLabel: "Video" },
11523
- // Archives
11524
- zip: { icon: FileArchiveIcon, typeLabel: "Archive" },
11525
- tar: { icon: FileArchiveIcon, typeLabel: "Archive" },
11526
- gz: { icon: FileArchiveIcon, typeLabel: "Archive" },
11527
- bz2: { icon: FileArchiveIcon, typeLabel: "Archive" },
11528
- "7z": { icon: FileArchiveIcon, typeLabel: "Archive" },
11529
- // Code
11530
- py: { icon: FileCodeIcon, typeLabel: "Python" },
11531
- js: { icon: FileCodeIcon, typeLabel: "JavaScript" },
11532
- ts: { icon: FileCodeIcon, typeLabel: "TypeScript" },
11533
- tsx: { icon: FileCodeIcon, typeLabel: "TypeScript" },
11534
- jsx: { icon: FileCodeIcon, typeLabel: "JavaScript" },
11535
- json: { icon: FileCodeIcon, typeLabel: "JSON" },
11536
- yaml: { icon: FileCodeIcon, typeLabel: "YAML" },
11537
- yml: { icon: FileCodeIcon, typeLabel: "YAML" },
11538
- toml: { icon: FileCodeIcon, typeLabel: "TOML" },
11539
- sh: { icon: CodeIcon, typeLabel: "Shell" },
11540
- rs: { icon: FileCodeIcon, typeLabel: "Rust" },
11541
- go: { icon: FileCodeIcon, typeLabel: "Go" },
11542
- rb: { icon: FileCodeIcon, typeLabel: "Ruby" },
11543
- java: { icon: FileCodeIcon, typeLabel: "Java" },
11544
- cpp: { icon: FileCodeIcon, typeLabel: "C++" },
11545
- c: { icon: FileCodeIcon, typeLabel: "C" },
11546
- html: { icon: FileCodeIcon, typeLabel: "HTML" },
11547
- css: { icon: FileCodeIcon, typeLabel: "CSS" },
11548
- scss: { icon: FileCodeIcon, typeLabel: "Sass" },
11549
- sql: { icon: FileCodeIcon, typeLabel: "SQL" }
11550
- };
11551
- function _extension(path) {
11552
- const idx = path.lastIndexOf(".");
11553
- if (idx < 0 || idx === path.length - 1) return "";
11554
- return path.slice(idx + 1).toLowerCase();
11622
+ if (artifact.kind === "command") {
11623
+ if (!onCommandSelect) {
11624
+ return /* @__PURE__ */ jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
11625
+ }
11626
+ return /* @__PURE__ */ jsx(
11627
+ "button",
11628
+ {
11629
+ type: "button",
11630
+ onClick: () => onCommandSelect(artifact.ref),
11631
+ className: "truncate text-left cursor-pointer text-primary hover:underline",
11632
+ children: artifact.label
11633
+ }
11634
+ );
11635
+ }
11636
+ return /* @__PURE__ */ jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
11555
11637
  }
11556
- function _fileKindFor(path) {
11557
- const ext = _extension(path);
11558
- return _EXTENSION_KIND[ext] ?? { icon: FileIcon, typeLabel: "File" };
11638
+ var WORKING_ON_IT_DELAY_MS = 250;
11639
+ function hasUserFacingAskContent(msg) {
11640
+ return !!msg.toolCalls?.some((tc) => tc.toolName === "ask_user_question");
11559
11641
  }
11560
- function _basename(path) {
11561
- const idx = path.lastIndexOf("/");
11562
- return idx >= 0 ? path.slice(idx + 1) : path;
11563
- }
11564
- function WorkspaceFileCard({
11565
- sessionId,
11566
- path,
11567
- label
11568
- }) {
11569
- const { adapter } = useAgentChatAdapterContext();
11570
- const kind = _fileKindFor(path);
11571
- const Icon = kind.icon;
11572
- const downloadUrl = adapter.getWorkspaceDownloadUrl({ sessionId, path });
11573
- const downloadName = _basename(path) || label;
11574
- const [previewOpen, setPreviewOpen] = useState(false);
11575
- const handleCardClick = useCallback(() => {
11576
- setPreviewOpen(true);
11577
- }, []);
11578
- return /* @__PURE__ */ jsxs(Fragment, { children: [
11579
- /* @__PURE__ */ jsxs(
11580
- "div",
11581
- {
11582
- role: "button",
11583
- tabIndex: 0,
11584
- onClick: handleCardClick,
11585
- onKeyDown: (e) => {
11586
- if (e.key === "Enter" || e.key === " ") {
11587
- e.preventDefault();
11588
- handleCardClick();
11589
- }
11590
- },
11591
- className: cn(
11592
- "group/file flex items-center gap-3 rounded-lg border border-border",
11593
- "bg-muted/20 px-3 py-2 text-left transition-colors",
11594
- "cursor-pointer hover:bg-muted/40"
11595
- ),
11596
- children: [
11597
- /* @__PURE__ */ jsx("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-background", children: /* @__PURE__ */ jsx(Icon, { className: "size-5 text-muted-foreground", "aria-hidden": true }) }),
11598
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
11599
- /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium text-foreground", children: label }),
11600
- /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-muted-foreground", children: kind.typeLabel })
11601
- ] }),
11602
- /* @__PURE__ */ jsxs(
11603
- "a",
11604
- {
11605
- href: downloadUrl,
11606
- download: downloadName,
11607
- onClick: (e) => e.stopPropagation(),
11608
- className: cn(
11609
- "shrink-0 inline-flex items-center gap-1.5 rounded-md border border-border",
11610
- "bg-background px-2.5 py-1.5 text-xs font-medium text-foreground",
11611
- "transition-colors hover:bg-muted"
11612
- ),
11613
- "aria-label": `Download ${downloadName}`,
11614
- children: [
11615
- /* @__PURE__ */ jsx(DownloadIcon, { className: "size-3.5", "aria-hidden": true }),
11616
- "Download"
11617
- ]
11618
- }
11619
- )
11620
- ]
11621
- }
11622
- ),
11623
- /* @__PURE__ */ jsx(
11624
- WorkspaceFilePreviewDialog,
11625
- {
11626
- open: previewOpen,
11627
- onOpenChange: setPreviewOpen,
11628
- sessionId,
11629
- path,
11630
- downloadUrl
11631
- }
11632
- )
11633
- ] });
11634
- }
11635
- function WorkspaceFilePreviewDialog({
11636
- open,
11637
- onOpenChange,
11638
- sessionId,
11639
- path,
11640
- downloadUrl
11641
- }) {
11642
- const { adapter } = useAgentChatAdapterContext();
11643
- const [file, setFile] = useState(null);
11644
- const [loading, setLoading] = useState(false);
11645
- const [error, setError] = useState(null);
11646
- useEffect(() => {
11647
- if (!open) return;
11648
- let cancelled = false;
11649
- setLoading(true);
11650
- setError(null);
11651
- setFile(null);
11652
- adapter.getWorkspaceFile({ sessionId, path }).then((result) => {
11653
- if (cancelled) return;
11654
- setFile(result);
11655
- setLoading(false);
11656
- }).catch((err) => {
11657
- if (cancelled) return;
11658
- const message = err instanceof Error ? err.message : "Failed to load file";
11659
- setError(message);
11660
- setLoading(false);
11661
- });
11662
- return () => {
11663
- cancelled = true;
11664
- };
11665
- }, [adapter, sessionId, path, open]);
11666
- return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "flex h-[85vh] w-[95vw] max-w-350! flex-col gap-0 overflow-hidden p-0", children: [
11667
- /* @__PURE__ */ jsxs(DialogHeader, { className: "flex flex-row items-center gap-2 border-b border-line px-4 py-3", children: [
11668
- /* @__PURE__ */ jsx(DialogTitle, { className: "min-w-0 flex-1 truncate text-sm", children: _basename(path) }),
11669
- /* @__PURE__ */ jsxs(
11670
- "a",
11671
- {
11672
- href: downloadUrl,
11673
- download: _basename(path),
11674
- className: "mr-8 inline-flex shrink-0 items-center gap-1.5 rounded-md border border-border bg-background px-2.5 py-1 text-xs font-medium text-foreground transition-colors hover:bg-muted",
11675
- "aria-label": `Download ${_basename(path)}`,
11676
- children: [
11677
- /* @__PURE__ */ jsx(DownloadIcon, { className: "size-3.5", "aria-hidden": true }),
11678
- "Download"
11679
- ]
11680
- }
11681
- )
11682
- ] }),
11683
- /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: /* @__PURE__ */ jsx(
11684
- FileViewer,
11685
- {
11686
- file,
11687
- loading,
11688
- error,
11689
- downloadUrl,
11690
- onDelete: null,
11691
- onClose: () => onOpenChange(false),
11692
- hideHeader: true
11693
- }
11694
- ) })
11695
- ] }) });
11696
- }
11697
- function scrollToArtifactBlock(artifactId) {
11698
- document.querySelector(`[data-artifact-anchor="${CSS.escape(artifactId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "center" });
11699
- }
11700
- function ArtifactRefCard({
11701
- artifactId,
11702
- name,
11703
- kind
11704
- }) {
11705
- return /* @__PURE__ */ jsxs(
11706
- "button",
11707
- {
11708
- type: "button",
11709
- onClick: () => scrollToArtifactBlock(artifactId),
11710
- className: "flex w-full min-w-0 cursor-pointer items-center gap-2 rounded border border-border bg-background px-3 py-2 text-left text-sm transition-colors hover:bg-muted/60",
11711
- children: [
11712
- /* @__PURE__ */ jsx(FileTextIcon, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }),
11713
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-foreground", children: name }),
11714
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: KIND_LABEL[kind] })
11715
- ]
11716
- }
11717
- );
11718
- }
11719
- function resolveArtifactRef(ref, messages) {
11720
- for (const msg of messages) {
11721
- if (msg.role !== "system" || msg.systemKind !== "artifact") continue;
11722
- const meta = msg.systemMeta ?? {};
11723
- if (meta.artifact_id !== ref) continue;
11724
- const kind = meta.kind ?? "markdown";
11725
- const version = typeof meta.version === "number" ? meta.version : 1;
11726
- const name = typeof meta.name === "string" ? meta.name : "";
11727
- return { artifactId: ref, name, kind, version };
11728
- }
11729
- return null;
11730
- }
11731
- function TurnSummaryCard({
11732
- summary,
11733
- sessionId,
11734
- messages,
11735
- onFileSelect,
11736
- onCommandSelect
11737
- }) {
11738
- const hasRecap = summary.recap.trim().length > 0;
11739
- const hasArtifacts = summary.artifacts.length > 0;
11740
- if (!hasArtifacts) return null;
11741
- return /* @__PURE__ */ jsxs("div", { className: "mt-3 rounded border border-border bg-muted/50 px-3 py-2 text-sm", children: [
11742
- /* @__PURE__ */ jsx("div", { className: "mb-2 text-xs font-semibold uppercase tracking-wide", children: "Summary" }),
11743
- hasRecap && /* @__PURE__ */ jsx("p", { className: "mb-2 whitespace-pre-wrap text-foreground", children: summary.recap }),
11744
- hasArtifacts && /* @__PURE__ */ jsx("div", { className: "space-y-2", children: summary.artifacts.map((artifact, i) => {
11745
- const key = `${artifact.kind}:${artifact.ref}:${i}`;
11746
- if (artifact.kind === "file" && sessionId) {
11747
- return /* @__PURE__ */ jsx(
11748
- WorkspaceFileCard,
11749
- {
11750
- sessionId,
11751
- path: artifact.ref,
11752
- label: artifact.label
11753
- },
11754
- key
11755
- );
11756
- }
11757
- if (artifact.kind === "artifact") {
11758
- const resolved = sessionId ? resolveArtifactRef(artifact.ref, messages) : null;
11759
- if (resolved) {
11760
- return /* @__PURE__ */ jsx(
11761
- ArtifactRefCard,
11762
- {
11763
- artifactId: resolved.artifactId,
11764
- name: resolved.name || artifact.label,
11765
- kind: resolved.kind
11766
- },
11767
- key
11768
- );
11769
- }
11770
- }
11771
- return /* @__PURE__ */ jsxs(
11772
- "div",
11773
- {
11774
- className: "flex items-baseline gap-2 text-sm",
11775
- children: [
11776
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", "aria-hidden": true, children: "\u2022" }),
11777
- /* @__PURE__ */ jsx(
11778
- ArtifactRow,
11779
- {
11780
- artifact,
11781
- onFileSelect,
11782
- onCommandSelect
11783
- }
11784
- )
11785
- ]
11786
- },
11787
- key
11788
- );
11789
- }) })
11790
- ] });
11791
- }
11792
- function ArtifactRow({
11793
- artifact,
11794
- onFileSelect,
11795
- onCommandSelect
11796
- }) {
11797
- if (artifact.kind === "file") {
11798
- if (!onFileSelect) {
11799
- return /* @__PURE__ */ jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
11800
- }
11801
- return /* @__PURE__ */ jsx(
11802
- "button",
11803
- {
11804
- type: "button",
11805
- onClick: () => onFileSelect(artifact.ref),
11806
- className: "truncate cursor-pointer text-primary hover:underline",
11807
- children: artifact.label
11808
- }
11809
- );
11810
- }
11811
- if (artifact.kind === "url") {
11812
- return /* @__PURE__ */ jsx(
11813
- "a",
11814
- {
11815
- href: artifact.ref,
11816
- target: "_blank",
11817
- rel: "noopener noreferrer",
11818
- className: "truncate text-primary hover:underline",
11819
- children: artifact.label
11820
- }
11821
- );
11822
- }
11823
- if (artifact.kind === "command") {
11824
- if (!onCommandSelect) {
11825
- return /* @__PURE__ */ jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
11826
- }
11827
- return /* @__PURE__ */ jsx(
11828
- "button",
11829
- {
11830
- type: "button",
11831
- onClick: () => onCommandSelect(artifact.ref),
11832
- className: "truncate text-left cursor-pointer text-primary hover:underline",
11833
- children: artifact.label
11834
- }
11835
- );
11836
- }
11837
- return /* @__PURE__ */ jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
11838
- }
11839
- var WORKING_ON_IT_DELAY_MS = 250;
11840
- function hasUserFacingAskContent(msg) {
11841
- return !!msg.toolCalls?.some((tc) => tc.toolName === "ask_user_question");
11842
- }
11843
- function pendingAskCall(messages) {
11844
- for (let i = messages.length - 1; i >= 0; i--) {
11845
- const m = messages[i];
11846
- if (m.role === "user") return null;
11847
- if (m.role !== "assistant") continue;
11848
- return m.toolCalls?.find(
11849
- (tc) => tc.toolName === "ask_user_question" && tc.status === "running"
11850
- ) ?? null;
11851
- }
11852
- return null;
11642
+ function pendingAskCall(messages) {
11643
+ for (let i = messages.length - 1; i >= 0; i--) {
11644
+ const m = messages[i];
11645
+ if (m.role === "user") return null;
11646
+ if (m.role !== "assistant") continue;
11647
+ return m.toolCalls?.find(
11648
+ (tc) => tc.toolName === "ask_user_question" && tc.status === "running"
11649
+ ) ?? null;
11650
+ }
11651
+ return null;
11853
11652
  }
11854
11653
  function askComposerPolicy(pending) {
11855
11654
  if (!pending) return null;
@@ -12771,7 +12570,7 @@ function RetryBanner({ indicator }) {
12771
12570
  "attempt ",
12772
12571
  indicator.attempt
12773
12572
  ] }),
12774
- indicator.detail && (open ? /* @__PURE__ */ jsx(ChevronDown, { className: "size-3" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "size-3" }))
12573
+ indicator.detail && (open ? /* @__PURE__ */ jsx(ChevronDown, { className: "size-3" }) : /* @__PURE__ */ jsx(ChevronRight$1, { className: "size-3" }))
12775
12574
  ]
12776
12575
  }
12777
12576
  ),
@@ -12814,6 +12613,7 @@ function ChatThread({
12814
12613
  retryIndicator,
12815
12614
  onRetry,
12816
12615
  onComposerError,
12616
+ paneCards,
12817
12617
  showBrowser = false,
12818
12618
  onToggleBrowser,
12819
12619
  showWorkspace = false,
@@ -12893,23 +12693,71 @@ function ChatThread({
12893
12693
  const composerArea = /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-4xl px-3 sm:px-6 pb-3 sm:pb-5 pt-3", children: [
12894
12694
  retryIndicator && /* @__PURE__ */ jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsx(RetryBanner, { indicator: retryIndicator }) }),
12895
12695
  researchSources.length > 0 && /* @__PURE__ */ jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsx(ResearchSourcesPanel, { sources: researchSources }) }),
12896
- /* @__PURE__ */ jsx(
12897
- ChatComposer,
12696
+ (paneCards?.browser || paneCards?.files) && // Layers sliding out from behind the prompt box: each card is
12697
+ // narrower than the one below it and rounded only on top, with a
12698
+ // negative bottom margin so the next layer covers its flat edge. The
12699
+ // composer is the widest and last, so the stack reads as coming out
12700
+ // of it rather than sitting on top of it.
12701
+ /* @__PURE__ */ jsxs(
12702
+ "div",
12898
12703
  {
12899
- onSend,
12900
- onStop,
12901
- isRunning,
12902
- disabled: composerDisabled,
12903
- disabledReason: composerDisabledReason,
12904
- awaitingAnswer: askPolicy !== null && !askPolicy.locked,
12905
- placeholder: askPolicy?.placeholder,
12906
- tokenUsage,
12907
- onComposerError,
12908
- showBrowser,
12909
- onToggleBrowser,
12910
- showWorkspace,
12911
- onToggleWorkspace,
12912
- canShowBrowser,
12704
+ "data-testid": "session-pane-cards",
12705
+ className: "flex flex-col",
12706
+ children: [
12707
+ paneCards.browser && /* @__PURE__ */ jsx(
12708
+ SessionPaneCard,
12709
+ {
12710
+ testId: "session-pane-card-browser",
12711
+ title: "Browser",
12712
+ icon: GlobeIcon,
12713
+ subtitle: paneCards.browser.subtitle,
12714
+ thumbnail: paneCards.browser.thumbnail,
12715
+ onOpen: paneCards.browser.onOpen,
12716
+ className: "w-auto rounded-t-xl border-b-0 -mb-px"
12717
+ }
12718
+ ),
12719
+ paneCards.files && /* @__PURE__ */ jsxs(Fragment, { children: [
12720
+ /* @__PURE__ */ jsx(
12721
+ SessionPaneCard,
12722
+ {
12723
+ testId: "session-pane-card-files",
12724
+ title: "Files",
12725
+ icon: FolderIcon,
12726
+ count: paneCards.files.count,
12727
+ expanded: paneCards.files.open,
12728
+ onOpen: paneCards.files.onToggle,
12729
+ className: "w-auto rounded-t-xl border-b-0 -mb-px"
12730
+ }
12731
+ ),
12732
+ paneCards.files.open && /* @__PURE__ */ jsx(
12733
+ "div",
12734
+ {
12735
+ "data-testid": "files-accordion",
12736
+ className: "-mb-px flex h-72 min-h-0 flex-col overflow-hidden border border-b-0 border-line",
12737
+ children: paneCards.files.panel
12738
+ }
12739
+ )
12740
+ ] })
12741
+ ]
12742
+ }
12743
+ ),
12744
+ /* @__PURE__ */ jsx(
12745
+ ChatComposer,
12746
+ {
12747
+ onSend,
12748
+ onStop,
12749
+ isRunning,
12750
+ disabled: composerDisabled,
12751
+ disabledReason: composerDisabledReason,
12752
+ awaitingAnswer: askPolicy !== null && !askPolicy.locked,
12753
+ placeholder: askPolicy?.placeholder,
12754
+ tokenUsage,
12755
+ onComposerError,
12756
+ showBrowser,
12757
+ onToggleBrowser,
12758
+ showWorkspace,
12759
+ onToggleWorkspace,
12760
+ canShowBrowser,
12913
12761
  browserProfileId,
12914
12762
  onSelectBrowserProfile,
12915
12763
  browserProfilesEnabled,
@@ -14518,17 +14366,25 @@ function useAgentChatRuntime({
14518
14366
  }));
14519
14367
  }
14520
14368
  }).catch(() => void 0);
14521
- void adapter.getBrowserState(sessionId).then((browserState) => {
14522
- if (sessionIdRef.current !== sessionId) return;
14523
- setState((prev) => ({
14524
- ...prev,
14525
- browser: browserState ? {
14526
- status: browserState.status,
14527
- controlOwner: browserState.controlOwner ?? null
14528
- } : null
14529
- }));
14530
- }).catch(() => void 0);
14369
+ const hydrateBrowserState = () => {
14370
+ void adapter.getBrowserState(sessionId).then((browserState) => {
14371
+ if (sessionIdRef.current !== sessionId) return;
14372
+ setState((prev) => ({
14373
+ ...prev,
14374
+ browser: browserState ? {
14375
+ status: browserState.status,
14376
+ controlOwner: browserState.controlOwner ?? null
14377
+ } : null
14378
+ }));
14379
+ }).catch(() => void 0);
14380
+ };
14381
+ hydrateBrowserState();
14382
+ const browserSettleTimer = window.setTimeout(
14383
+ hydrateBrowserState,
14384
+ RECONCILE_FIRST_DELAY
14385
+ );
14531
14386
  return () => {
14387
+ window.clearTimeout(browserSettleTimer);
14532
14388
  clearReconnectTimer();
14533
14389
  closeStream();
14534
14390
  };
@@ -16481,10 +16337,10 @@ async function latexToSvg(latex, display = true) {
16481
16337
  const start = outer.indexOf("<svg");
16482
16338
  const end = outer.lastIndexOf("</svg>");
16483
16339
  if (start === -1 || end === -1) return EMPTY;
16484
- const svg = outer.slice(start, end + "</svg>".length);
16485
- const dims = svg.match(/width="([^"]+)"\s+height="([^"]+)"/);
16340
+ const svg2 = outer.slice(start, end + "</svg>".length);
16341
+ const dims = svg2.match(/width="([^"]+)"\s+height="([^"]+)"/);
16486
16342
  return {
16487
- svg,
16343
+ svg: svg2,
16488
16344
  widthEm: exToEm(dims?.[1]),
16489
16345
  heightEm: exToEm(dims?.[2])
16490
16346
  };
@@ -16512,13 +16368,13 @@ var FormulaCache = class {
16512
16368
  }
16513
16369
  async load(latex) {
16514
16370
  const key = latex;
16515
- const { svg, widthEm, heightEm } = await latexToSvg(latex);
16516
- if (!svg) {
16371
+ const { svg: svg2, widthEm, heightEm } = await latexToSvg(latex);
16372
+ if (!svg2) {
16517
16373
  this.pending.delete(key);
16518
16374
  return;
16519
16375
  }
16520
16376
  const image = new Image();
16521
- image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
16377
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg2)}`;
16522
16378
  const settle = (ok) => {
16523
16379
  this.pending.delete(key);
16524
16380
  if (!ok) return;
@@ -16552,8 +16408,8 @@ var FormulaCache = class {
16552
16408
  };
16553
16409
 
16554
16410
  // src/components/whiteboard/input.ts
16555
- var MIN_ZOOM3 = 0.02;
16556
- var MAX_ZOOM3 = 256;
16411
+ var MIN_ZOOM2 = 0.02;
16412
+ var MAX_ZOOM2 = 256;
16557
16413
  var ZOOM_DIVISOR = 300;
16558
16414
  function screenToLogical(pt, view) {
16559
16415
  return { x: view.x + pt.x / view.zoom, y: view.y + pt.y / view.zoom };
@@ -16563,7 +16419,7 @@ function logicalToScreen(pt, view) {
16563
16419
  }
16564
16420
  var clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
16565
16421
  function zoomAt(view, screenPt, factor) {
16566
- const zoom = clamp2(view.zoom * factor, MIN_ZOOM3, MAX_ZOOM3);
16422
+ const zoom = clamp2(view.zoom * factor, MIN_ZOOM2, MAX_ZOOM2);
16567
16423
  const anchor = screenToLogical(screenPt, view);
16568
16424
  return {
16569
16425
  zoom,
@@ -16587,8 +16443,8 @@ function zoomToFit(bounds, size2, padding = 80) {
16587
16443
  }
16588
16444
  const zoom = clamp2(
16589
16445
  Math.min(size2.w / (bounds.w + padding * 2), size2.h / (bounds.h + padding * 2)),
16590
- MIN_ZOOM3,
16591
- MAX_ZOOM3
16446
+ MIN_ZOOM2,
16447
+ MAX_ZOOM2
16592
16448
  );
16593
16449
  return {
16594
16450
  zoom,
@@ -18026,87 +17882,513 @@ function WhiteboardSurface({
18026
17882
  /* @__PURE__ */ jsx(
18027
17883
  Input,
18028
17884
  {
18029
- className: "w-64 min-w-0 shrink",
18030
- placeholder: "Ask about the board (optional)",
18031
- value: question,
18032
- disabled: busy,
18033
- onChange: (e) => setQuestion(e.target.value),
18034
- onKeyDown: (e) => {
18035
- if (e.key === "Enter" && !e.shiftKey) {
18036
- e.preventDefault();
18037
- void ask("answer");
18038
- }
18039
- }
17885
+ className: "w-64 min-w-0 shrink",
17886
+ placeholder: "Ask about the board (optional)",
17887
+ value: question,
17888
+ disabled: busy,
17889
+ onChange: (e) => setQuestion(e.target.value),
17890
+ onKeyDown: (e) => {
17891
+ if (e.key === "Enter" && !e.shiftKey) {
17892
+ e.preventDefault();
17893
+ void ask("answer");
17894
+ }
17895
+ }
17896
+ }
17897
+ ),
17898
+ /* @__PURE__ */ jsxs(TooltipProvider, { children: [
17899
+ /* @__PURE__ */ jsx(ButtonGroup, { "aria-label": "Actions", children: USER_ACTIONS.map((a) => /* @__PURE__ */ jsxs(Tooltip, { children: [
17900
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
17901
+ Button,
17902
+ {
17903
+ type: "button",
17904
+ variant: a.id === "answer" ? "default" : "outline",
17905
+ disabled: busy,
17906
+ "aria-label": a.label,
17907
+ onClick: () => void ask(a.id),
17908
+ children: a.label
17909
+ }
17910
+ ) }),
17911
+ /* @__PURE__ */ jsx(TooltipContent, { side: "top", children: a.hint })
17912
+ ] }, a.id)) }),
17913
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
17914
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 px-1", children: [
17915
+ /* @__PURE__ */ jsx(
17916
+ Checkbox,
17917
+ {
17918
+ id: "wb-work-it-out",
17919
+ "aria-label": "Work it out",
17920
+ checked: deep,
17921
+ disabled: busy,
17922
+ onCheckedChange: (v) => setDeep(v === true)
17923
+ }
17924
+ ),
17925
+ /* @__PURE__ */ jsx(Label, { htmlFor: "wb-work-it-out", children: "Work it out" })
17926
+ ] }) }),
17927
+ /* @__PURE__ */ jsx(TooltipContent, { side: "top", children: "Let the agent search, compute and build before it draws, over several passes. Slower; for what the board alone cannot settle. Off, it answers your ink in one pass." })
17928
+ ] })
17929
+ ] }),
17930
+ onNewBoard ? /* @__PURE__ */ jsx(
17931
+ Button,
17932
+ {
17933
+ type: "button",
17934
+ variant: "ghost",
17935
+ "aria-label": "New board",
17936
+ disabled: busy,
17937
+ onClick: () => {
17938
+ setWantFresh(true);
17939
+ onNewBoard();
17940
+ },
17941
+ children: "New board"
17942
+ }
17943
+ ) : null,
17944
+ viewMode && onViewModeChange && hasAnswer ? /* @__PURE__ */ jsx("div", { className: "flex items-center rounded-full border p-0.5", children: ["simple", "expert", "whiteboard"].map((mode) => /* @__PURE__ */ jsx(
17945
+ "button",
17946
+ {
17947
+ type: "button",
17948
+ "aria-label": `${VIEW_MODE_LABELS[mode]} view`,
17949
+ "aria-pressed": viewMode === mode,
17950
+ className: cn(
17951
+ "rounded-full px-3 py-1 text-xs",
17952
+ viewMode === mode ? "bg-background font-medium shadow-sm" : "text-muted-foreground"
17953
+ ),
17954
+ onClick: () => onViewModeChange(mode),
17955
+ children: VIEW_MODE_LABELS[mode]
17956
+ },
17957
+ mode
17958
+ )) }) : null
17959
+ ] })
17960
+ ] });
17961
+ }
17962
+ function chunkPairs(flat) {
17963
+ const pairs = [];
17964
+ for (let i = 0; i + 1 < flat.length; i += 2) pairs.push([flat[i], flat[i + 1]]);
17965
+ return pairs;
17966
+ }
17967
+ var SKELETON_WIDTHS = [70, 85, 55, 90, 60, 78, 45, 82, 65, 72];
17968
+ var PDF_WORKER_SRC = new URL(
17969
+ "pdfjs-dist/legacy/build/pdf.worker.mjs",
17970
+ import.meta.url
17971
+ ).toString();
17972
+ function FileViewer({
17973
+ file,
17974
+ loading,
17975
+ error,
17976
+ downloadUrl,
17977
+ onDelete,
17978
+ onClose,
17979
+ hideHeader = false,
17980
+ className
17981
+ }) {
17982
+ const visible = loading || file !== null || error !== null;
17983
+ if (!visible) return null;
17984
+ const fileName2 = file?.path.split("/").pop() ?? "File";
17985
+ const lang = file ? getLanguageHint(file.path) : "";
17986
+ const isPdf = file?.mime_type === "application/pdf" || file?.path.toLowerCase().endsWith(".pdf") || false;
17987
+ const isImage = file?.encoding === "base64" && !isPdf;
17988
+ const HeaderIcon = isImage ? ImageIcon : FileTextIcon;
17989
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex min-h-0 flex-1 flex-col border-t border-line", className), children: [
17990
+ !hideHeader && /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2 border-b border-line px-3 py-2", children: [
17991
+ /* @__PURE__ */ jsx(HeaderIcon, { className: "size-4 shrink-0 text-muted-foreground" }),
17992
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
17993
+ /* @__PURE__ */ jsx("div", { className: "truncate font-medium", children: fileName2 }),
17994
+ /* @__PURE__ */ jsxs("div", { className: "truncate text-sm text-muted-foreground", children: [
17995
+ file?.path ?? "Loading...",
17996
+ file && /* @__PURE__ */ jsxs("span", { className: "ml-1.5", children: [
17997
+ formatFileSize(file.size),
17998
+ file.truncated && " (truncated)"
17999
+ ] })
18000
+ ] })
18001
+ ] }),
18002
+ downloadUrl && /* @__PURE__ */ jsx(
18003
+ "a",
18004
+ {
18005
+ href: downloadUrl,
18006
+ download: fileName2,
18007
+ className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
18008
+ "aria-label": `Download ${fileName2}`,
18009
+ title: "Download",
18010
+ children: /* @__PURE__ */ jsx(DownloadIcon, { className: "size-4" })
18011
+ }
18012
+ ),
18013
+ onDelete && /* @__PURE__ */ jsx(
18014
+ "button",
18015
+ {
18016
+ type: "button",
18017
+ onClick: onDelete,
18018
+ className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive",
18019
+ "aria-label": `Delete ${fileName2}`,
18020
+ title: "Delete",
18021
+ children: /* @__PURE__ */ jsx(TrashIcon, { className: "size-4" })
18022
+ }
18023
+ ),
18024
+ /* @__PURE__ */ jsx(
18025
+ "button",
18026
+ {
18027
+ type: "button",
18028
+ onClick: onClose,
18029
+ className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
18030
+ "aria-label": "Close file",
18031
+ children: /* @__PURE__ */ jsx(XIcon, { className: "size-4" })
18032
+ }
18033
+ )
18034
+ ] }),
18035
+ file && isPdf && file.encoding === "base64" ? (
18036
+ // PDF has its own scrollable container — bypass ScrollArea so
18037
+ // it can grow with the parent's flex height (Radix Viewport's
18038
+ // internal table wrapper sizes to content and breaks h-full).
18039
+ /* @__PURE__ */ jsx(PdfPreview, { file, fileName: fileName2 })
18040
+ ) : /* @__PURE__ */ jsxs(ScrollArea, { className: "min-h-0 flex-1", children: [
18041
+ loading && /* @__PURE__ */ jsx("div", { className: "space-y-1.5 p-3", children: Array.from({ length: 10 }).map((_, index) => /* @__PURE__ */ jsx(
18042
+ Skeleton,
18043
+ {
18044
+ className: "h-3.5 rounded",
18045
+ style: {
18046
+ width: `${SKELETON_WIDTHS[index % SKELETON_WIDTHS.length]}%`
18047
+ }
18048
+ },
18049
+ index
18050
+ )) }),
18051
+ error && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 p-3 text-xs text-destructive", children: [
18052
+ /* @__PURE__ */ jsx(AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
18053
+ /* @__PURE__ */ jsx("span", { children: error })
18054
+ ] }),
18055
+ file && isPdf && file.encoding !== "base64" && /* @__PURE__ */ jsx(FileViewerError, { message: "PDF preview requires base64 file content." }),
18056
+ file && isImage && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center p-4", children: /* @__PURE__ */ jsx(
18057
+ "img",
18058
+ {
18059
+ src: `data:${file.mime_type ?? "image/png"};base64,${file.content}`,
18060
+ alt: fileName2,
18061
+ className: "max-h-[60vh] max-w-full rounded object-contain"
18062
+ }
18063
+ ) }),
18064
+ file && !isImage && !isPdf && /* @__PURE__ */ jsx("pre", { className: "wrap-break-word whitespace-pre-wrap p-3 text-base leading-relaxed text-foreground", children: /* @__PURE__ */ jsx("code", { "data-language": lang, children: file.content }) })
18065
+ ] })
18066
+ ] });
18067
+ }
18068
+ function FileViewerError({ message }) {
18069
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 p-3 text-xs text-destructive", children: [
18070
+ /* @__PURE__ */ jsx(AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
18071
+ /* @__PURE__ */ jsx("span", { children: message })
18072
+ ] });
18073
+ }
18074
+ function PdfPreview({
18075
+ file,
18076
+ fileName: fileName2
18077
+ }) {
18078
+ const containerRef = useRef(null);
18079
+ const viewerRef = useRef(null);
18080
+ const pdfViewerRef = useRef(null);
18081
+ const eventBusRef = useRef(null);
18082
+ const findControllerRef = useRef(null);
18083
+ const [error, setError] = useState(null);
18084
+ const [pageNumber, setPageNumber] = useState(1);
18085
+ const [pagesCount, setPagesCount] = useState(0);
18086
+ const [scale, setScale] = useState(1);
18087
+ const [query, setQuery] = useState("");
18088
+ useEffect(() => {
18089
+ let cancelled = false;
18090
+ let loadingTask = null;
18091
+ let pdfDocument = null;
18092
+ const renderPdf = async () => {
18093
+ setError(null);
18094
+ const container = containerRef.current;
18095
+ const viewerElement = viewerRef.current;
18096
+ if (!container || !viewerElement) return;
18097
+ const pdfBytes = decodeBase64(file.content);
18098
+ const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
18099
+ pdfjs.GlobalWorkerOptions.workerSrc = PDF_WORKER_SRC;
18100
+ globalThis.pdfjsLib = pdfjs;
18101
+ const pdfViewerModule = await import('pdfjs-dist/web/pdf_viewer.mjs');
18102
+ loadingTask = pdfjs.getDocument({ data: pdfBytes });
18103
+ const pdf = await loadingTask.promise;
18104
+ pdfDocument = pdf;
18105
+ if (cancelled) return;
18106
+ const eventBus = new pdfViewerModule.EventBus();
18107
+ const linkService = new pdfViewerModule.PDFLinkService({ eventBus });
18108
+ const findController = new pdfViewerModule.PDFFindController({
18109
+ eventBus,
18110
+ linkService
18111
+ });
18112
+ const pdfViewer = new pdfViewerModule.PDFViewer({
18113
+ container,
18114
+ viewer: viewerElement,
18115
+ eventBus,
18116
+ linkService,
18117
+ findController,
18118
+ removePageBorders: true
18119
+ });
18120
+ linkService.setViewer(pdfViewer);
18121
+ linkService.setDocument(pdf);
18122
+ findController.setDocument(pdf);
18123
+ const onPagesInit = () => {
18124
+ pdfViewer.currentScaleValue = "page-width";
18125
+ setScale(pdfViewer.currentScale || 1);
18126
+ setPagesCount(pdfViewer.pagesCount);
18127
+ setPageNumber(pdfViewer.currentPageNumber);
18128
+ };
18129
+ const onPageChanging = (event) => {
18130
+ if (typeof event.pageNumber === "number") {
18131
+ setPageNumber(event.pageNumber);
18132
+ }
18133
+ };
18134
+ const onScaleChanging = (event) => {
18135
+ if (typeof event.scale === "number") {
18136
+ setScale(event.scale);
18137
+ }
18138
+ };
18139
+ eventBus.on("pagesinit", onPagesInit);
18140
+ eventBus.on("pagechanging", onPageChanging);
18141
+ eventBus.on("scalechanging", onScaleChanging);
18142
+ pdfViewerRef.current = pdfViewer;
18143
+ eventBusRef.current = eventBus;
18144
+ findControllerRef.current = findController;
18145
+ setPagesCount(pdf.numPages);
18146
+ pdfViewer.setDocument(pdf);
18147
+ };
18148
+ void renderPdf().catch((nextError) => {
18149
+ if (cancelled) return;
18150
+ if (nextError instanceof Error && nextError.name === "RenderingCancelledException") {
18151
+ return;
18152
+ }
18153
+ setError(formatPdfPreviewError(nextError));
18154
+ });
18155
+ return () => {
18156
+ cancelled = true;
18157
+ loadingTask?.destroy();
18158
+ pdfDocument?.destroy();
18159
+ pdfViewerRef.current?.cleanup();
18160
+ pdfViewerRef.current = null;
18161
+ eventBusRef.current = null;
18162
+ findControllerRef.current = null;
18163
+ };
18164
+ }, [file.content]);
18165
+ const setViewerPage = (nextPage) => {
18166
+ const viewer = pdfViewerRef.current;
18167
+ if (!viewer) return;
18168
+ const clampedPage = Math.min(
18169
+ Math.max(1, nextPage),
18170
+ Math.max(1, viewer.pagesCount)
18171
+ );
18172
+ viewer.currentPageNumber = clampedPage;
18173
+ setPageNumber(clampedPage);
18174
+ };
18175
+ const setViewerScale = (nextScale) => {
18176
+ const viewer = pdfViewerRef.current;
18177
+ if (!viewer) return;
18178
+ const clampedScale = Math.min(3, Math.max(0.5, nextScale));
18179
+ viewer.currentScale = clampedScale;
18180
+ setScale(clampedScale);
18181
+ };
18182
+ const runFind = (findPrevious = false) => {
18183
+ const eventBus = eventBusRef.current;
18184
+ if (!eventBus || !query.trim()) return;
18185
+ eventBus.dispatch("find", {
18186
+ source: eventBus,
18187
+ type: "again",
18188
+ query,
18189
+ phraseSearch: true,
18190
+ caseSensitive: false,
18191
+ entireWord: false,
18192
+ highlightAll: true,
18193
+ findPrevious,
18194
+ matchDiacritics: true
18195
+ });
18196
+ };
18197
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-h-full flex-col", children: [
18198
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-1.5 border-b border-line px-2 py-1.5", children: [
18199
+ /* @__PURE__ */ jsx(
18200
+ Button,
18201
+ {
18202
+ type: "button",
18203
+ variant: "ghost",
18204
+ size: "icon-xs",
18205
+ onClick: () => setViewerPage(pageNumber - 1),
18206
+ disabled: pageNumber <= 1,
18207
+ "aria-label": "Previous PDF page",
18208
+ title: "Previous page",
18209
+ children: /* @__PURE__ */ jsx(ChevronLeftIcon, { className: "size-3.5" })
18210
+ }
18211
+ ),
18212
+ /* @__PURE__ */ jsx(
18213
+ Input,
18214
+ {
18215
+ type: "number",
18216
+ min: 1,
18217
+ max: pagesCount || 1,
18218
+ value: pageNumber,
18219
+ onChange: (event) => setViewerPage(Number(event.target.value)),
18220
+ "aria-label": "PDF page number",
18221
+ className: "h-7 w-12 border-input px-1 text-center text-xs"
18040
18222
  }
18041
18223
  ),
18042
- /* @__PURE__ */ jsxs(TooltipProvider, { children: [
18043
- /* @__PURE__ */ jsx(ButtonGroup, { "aria-label": "Actions", children: USER_ACTIONS.map((a) => /* @__PURE__ */ jsxs(Tooltip, { children: [
18044
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
18045
- Button,
18046
- {
18047
- type: "button",
18048
- variant: a.id === "answer" ? "default" : "outline",
18049
- disabled: busy,
18050
- "aria-label": a.label,
18051
- onClick: () => void ask(a.id),
18052
- children: a.label
18053
- }
18054
- ) }),
18055
- /* @__PURE__ */ jsx(TooltipContent, { side: "top", children: a.hint })
18056
- ] }, a.id)) }),
18057
- /* @__PURE__ */ jsxs(Tooltip, { children: [
18058
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 px-1", children: [
18059
- /* @__PURE__ */ jsx(
18060
- Checkbox,
18061
- {
18062
- id: "wb-work-it-out",
18063
- "aria-label": "Work it out",
18064
- checked: deep,
18065
- disabled: busy,
18066
- onCheckedChange: (v) => setDeep(v === true)
18067
- }
18068
- ),
18069
- /* @__PURE__ */ jsx(Label, { htmlFor: "wb-work-it-out", children: "Work it out" })
18070
- ] }) }),
18071
- /* @__PURE__ */ jsx(TooltipContent, { side: "top", children: "Let the agent search, compute and build before it draws, over several passes. Slower; for what the board alone cannot settle. Off, it answers your ink in one pass." })
18072
- ] })
18224
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
18225
+ "/ ",
18226
+ pagesCount || "-"
18073
18227
  ] }),
18074
- onNewBoard ? /* @__PURE__ */ jsx(
18228
+ /* @__PURE__ */ jsx(
18075
18229
  Button,
18076
18230
  {
18077
18231
  type: "button",
18078
18232
  variant: "ghost",
18079
- "aria-label": "New board",
18080
- disabled: busy,
18233
+ size: "icon-xs",
18234
+ onClick: () => setViewerPage(pageNumber + 1),
18235
+ disabled: pagesCount > 0 && pageNumber >= pagesCount,
18236
+ "aria-label": "Next PDF page",
18237
+ title: "Next page",
18238
+ children: /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-3.5" })
18239
+ }
18240
+ ),
18241
+ /* @__PURE__ */ jsx("div", { className: "mx-1 h-5 w-px bg-line" }),
18242
+ /* @__PURE__ */ jsx(
18243
+ Button,
18244
+ {
18245
+ type: "button",
18246
+ variant: "ghost",
18247
+ size: "icon-xs",
18248
+ onClick: () => setViewerScale(scale - 0.1),
18249
+ disabled: scale <= 0.5,
18250
+ "aria-label": "Zoom out PDF",
18251
+ title: "Zoom out",
18252
+ children: /* @__PURE__ */ jsx(MinusIcon, { className: "size-3.5" })
18253
+ }
18254
+ ),
18255
+ /* @__PURE__ */ jsxs(
18256
+ "button",
18257
+ {
18258
+ type: "button",
18259
+ className: "h-7 min-w-12 px-1 text-xs text-muted-foreground hover:text-foreground",
18081
18260
  onClick: () => {
18082
- setWantFresh(true);
18083
- onNewBoard();
18261
+ const viewer = pdfViewerRef.current;
18262
+ if (!viewer) return;
18263
+ viewer.currentScaleValue = "page-width";
18264
+ setScale(viewer.currentScale || 1);
18084
18265
  },
18085
- children: "New board"
18266
+ "aria-label": "Fit PDF to width",
18267
+ title: "Fit width",
18268
+ children: [
18269
+ Math.round(scale * 100),
18270
+ "%"
18271
+ ]
18086
18272
  }
18087
- ) : null,
18088
- viewMode && onViewModeChange && hasAnswer ? /* @__PURE__ */ jsx("div", { className: "flex items-center rounded-full border p-0.5", children: ["simple", "expert", "whiteboard"].map((mode) => /* @__PURE__ */ jsx(
18089
- "button",
18273
+ ),
18274
+ /* @__PURE__ */ jsx(
18275
+ Button,
18090
18276
  {
18091
18277
  type: "button",
18092
- "aria-label": `${VIEW_MODE_LABELS[mode]} view`,
18093
- "aria-pressed": viewMode === mode,
18094
- className: cn(
18095
- "rounded-full px-3 py-1 text-xs",
18096
- viewMode === mode ? "bg-background font-medium shadow-sm" : "text-muted-foreground"
18097
- ),
18098
- onClick: () => onViewModeChange(mode),
18099
- children: VIEW_MODE_LABELS[mode]
18100
- },
18101
- mode
18102
- )) }) : null
18103
- ] })
18278
+ variant: "ghost",
18279
+ size: "icon-xs",
18280
+ onClick: () => setViewerScale(scale + 0.1),
18281
+ disabled: scale >= 3,
18282
+ "aria-label": "Zoom in PDF",
18283
+ title: "Zoom in",
18284
+ children: /* @__PURE__ */ jsx(PlusIcon, { className: "size-3.5" })
18285
+ }
18286
+ ),
18287
+ /* @__PURE__ */ jsx("div", { className: "mx-1 h-5 w-px bg-line" }),
18288
+ /* @__PURE__ */ jsx(SearchIcon, { className: "size-3.5 text-muted-foreground" }),
18289
+ /* @__PURE__ */ jsx(
18290
+ Input,
18291
+ {
18292
+ type: "search",
18293
+ value: query,
18294
+ onChange: (event) => setQuery(event.target.value),
18295
+ onKeyDown: (event) => {
18296
+ if (event.key === "Enter") runFind(event.shiftKey);
18297
+ },
18298
+ placeholder: "Find",
18299
+ "aria-label": "Find in PDF",
18300
+ className: "h-7 w-28 border-input px-1 text-xs"
18301
+ }
18302
+ ),
18303
+ /* @__PURE__ */ jsx(
18304
+ Button,
18305
+ {
18306
+ type: "button",
18307
+ variant: "ghost",
18308
+ size: "xs",
18309
+ onClick: () => runFind(false),
18310
+ disabled: !query.trim(),
18311
+ children: "Find"
18312
+ }
18313
+ )
18314
+ ] }),
18315
+ error && /* @__PURE__ */ jsxs("div", { className: "flex w-full items-start gap-2 p-3 text-xs text-destructive", children: [
18316
+ /* @__PURE__ */ jsx(AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
18317
+ /* @__PURE__ */ jsx("span", { children: error })
18318
+ ] }),
18319
+ !error && /* @__PURE__ */ jsx("div", { className: "relative min-h-105 flex-1 bg-muted/20", children: /* @__PURE__ */ jsx(
18320
+ "div",
18321
+ {
18322
+ ref: containerRef,
18323
+ "aria-label": `PDF viewer for ${fileName2}`,
18324
+ className: "absolute inset-0 overflow-auto",
18325
+ children: /* @__PURE__ */ jsx("div", { ref: viewerRef, className: "pdfViewer" })
18326
+ }
18327
+ ) })
18104
18328
  ] });
18105
18329
  }
18106
- function chunkPairs(flat) {
18107
- const pairs = [];
18108
- for (let i = 0; i + 1 < flat.length; i += 2) pairs.push([flat[i], flat[i + 1]]);
18109
- return pairs;
18330
+ function decodeBase64(content) {
18331
+ const binary = globalThis.atob(content);
18332
+ const bytes = new Uint8Array(binary.length);
18333
+ for (let index = 0; index < binary.length; index += 1) {
18334
+ bytes[index] = binary.charCodeAt(index);
18335
+ }
18336
+ return bytes;
18337
+ }
18338
+ function formatPdfPreviewError(error) {
18339
+ if (error instanceof Error && (error.name === "InvalidCharacterError" || error.message === "Invalid character")) {
18340
+ return "PDF preview data is not valid base64.";
18341
+ }
18342
+ return error instanceof Error ? error.message : "Failed to render PDF preview.";
18343
+ }
18344
+ function WorkspaceFileDrawer({
18345
+ adapter,
18346
+ sessionId,
18347
+ path,
18348
+ onClose
18349
+ }) {
18350
+ const [file, setFile] = useState(null);
18351
+ const [loading, setLoading] = useState(true);
18352
+ const [error, setError] = useState(null);
18353
+ const requestRef = useRef(0);
18354
+ useEffect(() => {
18355
+ const request = ++requestRef.current;
18356
+ setFile(null);
18357
+ setLoading(true);
18358
+ setError(null);
18359
+ void (async () => {
18360
+ try {
18361
+ const next = await adapter.getWorkspaceFile({ sessionId, path });
18362
+ if (requestRef.current !== request) return;
18363
+ setFile(next);
18364
+ } catch (fetchError) {
18365
+ if (requestRef.current !== request) return;
18366
+ setFile(null);
18367
+ setError(fetchError.message);
18368
+ } finally {
18369
+ if (requestRef.current === request) setLoading(false);
18370
+ }
18371
+ })();
18372
+ }, [adapter, sessionId, path]);
18373
+ return /* @__PURE__ */ jsx(
18374
+ "aside",
18375
+ {
18376
+ "data-testid": "file-preview-panel",
18377
+ className: "flex h-full min-h-0 w-full flex-col overflow-hidden border-l border-line bg-card",
18378
+ children: /* @__PURE__ */ jsx(
18379
+ FileViewer,
18380
+ {
18381
+ className: "border-t-0",
18382
+ file,
18383
+ loading,
18384
+ error,
18385
+ downloadUrl: file ? adapter.getWorkspaceDownloadUrl({ sessionId, path: file.path }) : null,
18386
+ onDelete: null,
18387
+ onClose
18388
+ }
18389
+ )
18390
+ }
18391
+ );
18110
18392
  }
18111
18393
  var noop = () => {
18112
18394
  };
@@ -18151,7 +18433,7 @@ function FileTree({
18151
18433
  return /* @__PURE__ */ jsx(FileTreeContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
18152
18434
  "div",
18153
18435
  {
18154
- className: cn("rounded-lg border bg-background text-sm", className),
18436
+ className: cn("rounded-lg border bg-card text-base", className),
18155
18437
  role: "tree",
18156
18438
  ...props,
18157
18439
  children: /* @__PURE__ */ jsx("div", { className: "p-2", children })
@@ -18360,7 +18642,7 @@ function RenderEntries({
18360
18642
  onClick: (event) => event.stopPropagation(),
18361
18643
  title: "Download",
18362
18644
  "aria-label": `Download ${fileName2}`,
18363
- children: /* @__PURE__ */ jsx(DownloadIcon, { className: "size-3" })
18645
+ children: /* @__PURE__ */ jsx(DownloadIcon, { className: "size-5" })
18364
18646
  }
18365
18647
  ),
18366
18648
  /* @__PURE__ */ jsx(
@@ -18374,7 +18656,7 @@ function RenderEntries({
18374
18656
  },
18375
18657
  title: "Delete",
18376
18658
  "aria-label": `Delete ${fileName2}`,
18377
- children: /* @__PURE__ */ jsx(TrashIcon, { className: "size-3" })
18659
+ children: /* @__PURE__ */ jsx(TrashIcon, { className: "size-5" })
18378
18660
  }
18379
18661
  )
18380
18662
  ] })
@@ -18386,6 +18668,7 @@ function WorkspacePanel({
18386
18668
  sessionId,
18387
18669
  selectedPath,
18388
18670
  onSelectedPathChange,
18671
+ onOpenFile,
18389
18672
  refreshSignal = 0,
18390
18673
  disabled = false,
18391
18674
  fillParent = false
@@ -18394,9 +18677,6 @@ function WorkspacePanel({
18394
18677
  const [entries, setEntries] = useState([]);
18395
18678
  const [treeLoading, setTreeLoading] = useState(false);
18396
18679
  const [treeError, setTreeError] = useState(null);
18397
- const [file, setFile] = useState(null);
18398
- const [fileLoading, setFileLoading] = useState(false);
18399
- const [fileError, setFileError] = useState(null);
18400
18680
  const [notice, setNotice] = useState(null);
18401
18681
  const [uploading, setUploading] = useState(false);
18402
18682
  const [deleteTarget, setDeleteTarget] = useState(null);
@@ -18436,31 +18716,6 @@ function WorkspacePanel({
18436
18716
  }
18437
18717
  }
18438
18718
  }, [adapter, sessionId]);
18439
- const fetchFile = useCallback(
18440
- async (path) => {
18441
- if (!sessionId) return;
18442
- const requestedSessionId = sessionId;
18443
- setFileLoading(true);
18444
- setFileError(null);
18445
- try {
18446
- const nextFile = await adapter.getWorkspaceFile({
18447
- sessionId: requestedSessionId,
18448
- path
18449
- });
18450
- if (sessionIdRef.current !== requestedSessionId) return;
18451
- setFile(nextFile);
18452
- } catch (error) {
18453
- if (sessionIdRef.current !== requestedSessionId) return;
18454
- setFile(null);
18455
- setFileError(error.message);
18456
- } finally {
18457
- if (sessionIdRef.current === requestedSessionId) {
18458
- setFileLoading(false);
18459
- }
18460
- }
18461
- },
18462
- [adapter, sessionId]
18463
- );
18464
18719
  useEffect(() => {
18465
18720
  void fetchTree();
18466
18721
  }, [fetchTree]);
@@ -18473,8 +18728,6 @@ function WorkspacePanel({
18473
18728
  }, [refreshSignal, sessionId, fetchTree]);
18474
18729
  useEffect(() => {
18475
18730
  if (!sessionId) {
18476
- setFile(null);
18477
- setFileError(null);
18478
18731
  onSelectedPathChange(null);
18479
18732
  }
18480
18733
  }, [onSelectedPathChange, sessionId]);
@@ -18492,22 +18745,15 @@ function WorkspacePanel({
18492
18745
  return next;
18493
18746
  });
18494
18747
  }, [selectedPath]);
18495
- useEffect(() => {
18496
- if (!selectedPath || !sessionId || entries.length === 0) return;
18497
- const entry = findEntry(entries, selectedPath);
18498
- if (entry?.kind === "file" && file?.path !== selectedPath) {
18499
- void fetchFile(selectedPath);
18500
- }
18501
- }, [entries, fetchFile, file?.path, selectedPath, sessionId]);
18502
18748
  const handleSelect = useCallback(
18503
18749
  (path) => {
18504
18750
  onSelectedPathChange(path);
18505
18751
  const entry = findEntry(entries, path);
18506
18752
  if (entry?.kind === "file") {
18507
- void fetchFile(path);
18753
+ onOpenFile?.(path);
18508
18754
  }
18509
18755
  },
18510
- [entries, fetchFile, onSelectedPathChange]
18756
+ [entries, onOpenFile, onSelectedPathChange]
18511
18757
  );
18512
18758
  const handleUpload = useCallback(
18513
18759
  async (files) => {
@@ -18541,8 +18787,6 @@ function WorkspacePanel({
18541
18787
  await adapter.deleteWorkspaceFile({ sessionId, path });
18542
18788
  if (selectedPath === path) {
18543
18789
  onSelectedPathChange(null);
18544
- setFile(null);
18545
- setFileError(null);
18546
18790
  }
18547
18791
  setNotice(`Deleted ${path.split("/").pop() ?? path}`);
18548
18792
  await fetchTree();
@@ -18598,7 +18842,10 @@ function WorkspacePanel({
18598
18842
  {
18599
18843
  "data-testid": "workspace-panel",
18600
18844
  className: cn(
18601
- "relative z-10 flex h-full min-h-0 flex-col overflow-hidden border-l border-muted-foreground/20 bg-card",
18845
+ "relative z-10 flex h-full min-h-0 flex-col overflow-hidden bg-card",
18846
+ // Filling a parent means the parent draws the frame; a border
18847
+ // here would double against it.
18848
+ !fillParent && "border-l border-muted-foreground/20",
18602
18849
  (fillParent || narrow) && "w-full"
18603
18850
  ),
18604
18851
  style: fillParent || narrow ? void 0 : { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
@@ -18623,9 +18870,7 @@ function WorkspacePanel({
18623
18870
  }
18624
18871
  }
18625
18872
  ),
18626
- /* @__PURE__ */ jsxs("div", { className: "flex min-h-14 items-center gap-2 border-b border-line px-3 py-3", children: [
18627
- /* @__PURE__ */ jsx(FolderOpenIcon, { className: "size-4 shrink-0 text-amber-500" }),
18628
- /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium text-foreground", children: disabled ? "Workspace - Read-only" : "Workspace" }) }),
18873
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 border-b border-line px-3 py-1.5", children: [
18629
18874
  /* @__PURE__ */ jsxs(Tooltip, { children: [
18630
18875
  /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
18631
18876
  Button,
@@ -18712,24 +18957,6 @@ function WorkspacePanel({
18712
18957
  }
18713
18958
  )
18714
18959
  ] }) }),
18715
- /* @__PURE__ */ jsx(
18716
- FileViewer,
18717
- {
18718
- file,
18719
- loading: fileLoading,
18720
- error: fileError,
18721
- downloadUrl: file && sessionId ? adapter.getWorkspaceDownloadUrl({
18722
- sessionId,
18723
- path: file.path
18724
- }) : null,
18725
- onDelete: file && !disabled ? () => setDeleteTarget(file.path) : null,
18726
- onClose: () => {
18727
- onSelectedPathChange(null);
18728
- setFile(null);
18729
- setFileError(null);
18730
- }
18731
- }
18732
- ),
18733
18960
  /* @__PURE__ */ jsx(
18734
18961
  ConfirmDialog2,
18735
18962
  {
@@ -18824,12 +19051,15 @@ function readOnlyReasonForSession(session) {
18824
19051
  return { readOnly: false };
18825
19052
  }
18826
19053
  var RIGHT_STACK_STYLE = {
18827
- ["--right-stack-w"]: "440px"
19054
+ // Both the column's width and the chat panel's right offset read this, so
19055
+ // one value keeps them complementary. 50%: the browser shell renders a real
19056
+ // page, and page layouts are unusable in a 440px strip.
19057
+ ["--right-stack-w"]: "50%"
18828
19058
  };
18829
19059
  var MOBILE_PANE_LABELS = {
18830
19060
  chat: "Chat",
18831
19061
  browser: "Browser",
18832
- workspace: "Workspace"
19062
+ file: "Preview"
18833
19063
  };
18834
19064
  function AgentChat({
18835
19065
  adapter,
@@ -18855,9 +19085,10 @@ function AgentChat({
18855
19085
  onOpenBilling
18856
19086
  }) {
18857
19087
  const [workspacePath, setWorkspacePath] = useState(null);
19088
+ const [previewPath, setPreviewPath] = useState(null);
18858
19089
  const [mobileView, setMobileView] = useState("chat");
18859
- const [showBrowser, setShowBrowser] = useState(true);
18860
- const [showWorkspace, setShowWorkspace] = useState(true);
19090
+ const [openPane, setOpenPane] = useState(null);
19091
+ const [filesOpen, setFilesOpen] = useState(false);
18861
19092
  const runtime = useAgentChatRuntime({
18862
19093
  adapter,
18863
19094
  agentId,
@@ -18865,8 +19096,15 @@ function AgentChat({
18865
19096
  onSessionChange
18866
19097
  });
18867
19098
  useEffect(() => {
18868
- setShowWorkspace(runtime.viewMode === "expert");
19099
+ setOpenPane(null);
19100
+ setFilesOpen(false);
18869
19101
  }, [runtime.viewMode]);
19102
+ useEffect(() => {
19103
+ setFilesOpen(false);
19104
+ setWorkspacePath(null);
19105
+ setPreviewPath(null);
19106
+ setOpenPane(null);
19107
+ }, [sessionId]);
18870
19108
  const readOnly = readOnlyReasonForSession(runtime.session);
18871
19109
  const boardAvailable = whiteboardEnabled && isBoardSession(runtime.session);
18872
19110
  const viewMode = runtime.viewMode === "whiteboard" && !boardAvailable ? "simple" : runtime.viewMode;
@@ -18883,35 +19121,54 @@ function AgentChat({
18883
19121
  const hideTurnSummary = isSubAgentSession(runtime.session) || orchestratesDeepResearch || isBoardSession(runtime.session);
18884
19122
  const browserState = runtime.state.browser;
18885
19123
  const browserAvailable = browserState !== null && browserState.status !== "closed" && !!sessionId;
18886
- const browserVisible = browserAvailable && showBrowser;
19124
+ const browserVisible = browserAvailable && openPane === "browser";
18887
19125
  const workspaceAvailable = !!sessionId;
18888
- const workspaceVisible = workspaceAvailable && showWorkspace;
18889
- const rightStackVisible = browserVisible || workspaceVisible;
18890
- const bothPanesVisible = browserVisible && workspaceVisible;
19126
+ const filePreviewVisible = workspaceAvailable && previewPath !== null && openPane === "file";
19127
+ const rightStackVisible = browserVisible || filePreviewVisible;
19128
+ const browserPreview = useBrowserPreview({
19129
+ adapter,
19130
+ sessionId,
19131
+ enabled: browserAvailable
19132
+ });
19133
+ const browserRunning = browserAvailable;
19134
+ useEffect(() => {
19135
+ if (openPane === "browser" && !browserAvailable) setOpenPane(null);
19136
+ if (openPane === "file" && (!workspaceAvailable || previewPath === null))
19137
+ setOpenPane(null);
19138
+ }, [openPane, browserAvailable, workspaceAvailable, previewPath]);
18891
19139
  useEffect(() => {
18892
19140
  onMessagesChange?.(runtime.messages);
18893
19141
  }, [onMessagesChange, runtime.messages]);
19142
+ const handleOpenFilePreview = useCallback((path) => {
19143
+ setWorkspacePath(path);
19144
+ setPreviewPath(path);
19145
+ setOpenPane("file");
19146
+ setMobileView("file");
19147
+ }, []);
19148
+ const handleSelectedPathChange = useCallback((path) => {
19149
+ setWorkspacePath(path);
19150
+ if (path === null) setPreviewPath(null);
19151
+ }, []);
18894
19152
  const handleFileSelect = useCallback(
18895
19153
  (path) => {
18896
- setWorkspacePath(path);
18897
- setMobileView("workspace");
18898
- setShowWorkspace(true);
19154
+ handleOpenFilePreview(path);
18899
19155
  onFileSelect?.(path);
18900
19156
  },
18901
- [onFileSelect]
19157
+ [handleOpenFilePreview, onFileSelect]
18902
19158
  );
18903
19159
  const handleToggleBrowser = useCallback(() => {
18904
- setShowBrowser((prev) => !prev);
19160
+ setOpenPane((prev) => prev === "browser" ? null : "browser");
19161
+ setMobileView("browser");
18905
19162
  }, []);
18906
- const handleToggleWorkspace = useCallback(() => {
18907
- setShowWorkspace((prev) => !prev);
19163
+ const handleToggleFiles = useCallback(() => {
19164
+ setFilesOpen((open) => !open);
18908
19165
  }, []);
18909
19166
  const mobilePanes = useMemo(() => {
18910
19167
  const panes = ["chat"];
18911
19168
  if (browserVisible) panes.push("browser");
18912
- if (workspaceVisible) panes.push("workspace");
19169
+ if (filePreviewVisible) panes.push("file");
18913
19170
  return panes;
18914
- }, [browserVisible, workspaceVisible]);
19171
+ }, [browserVisible, filePreviewVisible]);
18915
19172
  const activeMobilePane = mobilePanes.includes(mobileView) ? mobileView : "chat";
18916
19173
  const showMobileToggle = mobilePanes.length > 1;
18917
19174
  return /* @__PURE__ */ jsx(
@@ -18936,7 +19193,7 @@ function AgentChat({
18936
19193
  // is visible. With both panes, absolute positioning lets the
18937
19194
  // browser/workspace split occupy a fixed width. Without the
18938
19195
  // right stack, the chat takes the full width.
18939
- browserVisible ? "md:relative md:flex-row" : "md:flex-row"
19196
+ rightStackVisible ? "md:relative md:flex-row" : "md:flex-row"
18940
19197
  ),
18941
19198
  style: { direction: "ltr", ...RIGHT_STACK_STYLE },
18942
19199
  children: [
@@ -18969,14 +19226,12 @@ function AgentChat({
18969
19226
  className: cn(
18970
19227
  // Phone: full width column, hidden while another pane is picked.
18971
19228
  "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
18972
- showMobileToggle && "data-[mobile-view=browser]:hidden data-[mobile-view=workspace]:hidden md:flex!",
18973
- // md+: positioning depends on whether the browser/workspace
18974
- // right stack is laid out. When the browser pane is shown
18975
- // we pin a fixed-width column on the right and absolutely
18976
- // position the chat panel beside it. Otherwise the chat
18977
- // panel just flexes alongside the workspace (or fills the
18978
- // space when nothing else is visible).
18979
- browserVisible ? "md:absolute md:inset-y-0 md:left-0 md:right-(--right-stack-w,440px) md:flex" : "md:relative md:flex-1"
19229
+ showMobileToggle && "data-[mobile-view=browser]:hidden data-[mobile-view=file]:hidden md:flex!",
19230
+ // md+: when the right column is laid out (browser pane or file
19231
+ // preview), pin it at the configured width and absolutely
19232
+ // position the chat panel beside it. Otherwise the chat panel
19233
+ // fills the space.
19234
+ rightStackVisible ? "md:absolute md:inset-y-0 md:left-0 md:right-(--right-stack-w,440px) md:flex" : "md:relative md:flex-1"
18980
19235
  ),
18981
19236
  children: viewMode === "whiteboard" ? (
18982
19237
  // The board replaces the transcript, on the same runtime
@@ -19016,12 +19271,38 @@ function AgentChat({
19016
19271
  onSelectBrowserProfile,
19017
19272
  browserProfilesEnabled,
19018
19273
  browserProfileLocked: !!sessionId,
19019
- showBrowser,
19274
+ showBrowser: openPane === "browser",
19020
19275
  onToggleBrowser: handleToggleBrowser,
19021
- showWorkspace,
19022
- onToggleWorkspace: handleToggleWorkspace,
19276
+ showWorkspace: filesOpen,
19277
+ onToggleWorkspace: handleToggleFiles,
19023
19278
  canShowBrowser: browserAvailable,
19024
19279
  canShowWorkspace: workspaceAvailable,
19280
+ paneCards: {
19281
+ browser: browserRunning ? {
19282
+ subtitle: browserState?.controlOwner ? `${browserState.controlOwner} has control` : void 0,
19283
+ thumbnail: browserPreview,
19284
+ // Show and hide on the same card, like the Files
19285
+ // accordion header and the composer's Tools item.
19286
+ onOpen: handleToggleBrowser
19287
+ } : null,
19288
+ files: workspaceAvailable ? {
19289
+ open: filesOpen,
19290
+ onToggle: handleToggleFiles,
19291
+ panel: /* @__PURE__ */ jsx(
19292
+ WorkspacePanel,
19293
+ {
19294
+ adapter,
19295
+ sessionId,
19296
+ selectedPath: workspacePath,
19297
+ onSelectedPathChange: handleSelectedPathChange,
19298
+ onOpenFile: handleOpenFilePreview,
19299
+ refreshSignal: runtime.workspaceRefreshKey,
19300
+ disabled: effectiveDisabled,
19301
+ fillParent: true
19302
+ }
19303
+ )
19304
+ } : null
19305
+ },
19025
19306
  viewMode,
19026
19307
  onViewModeChange: runtime.setViewMode,
19027
19308
  deepResearchEnabled,
@@ -19049,13 +19330,10 @@ function AgentChat({
19049
19330
  // Phone: full width column, hidden when chat tab active.
19050
19331
  "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
19051
19332
  showMobileToggle && "data-[mobile-view=chat]:hidden md:flex!",
19052
- // md+: positioning differs depending on what is inside.
19053
- // - browser visible (with or without workspace): absolute
19054
- // right column at the configured width.
19055
- // - workspace only: relative shrink-0 column letting
19056
- // WorkspacePanel manage its own width via the resize
19057
- // handle.
19058
- browserVisible ? "md:absolute md:inset-y-0 md:right-0 md:w-(--right-stack-w,440px) md:flex-none" : "md:relative md:shrink-0 md:flex-none md:w-auto"
19333
+ // md+: absolute right column at the configured width. The
19334
+ // browser is the only pane that lives here — files expand
19335
+ // as an accordion inside the chat column.
19336
+ "md:absolute md:inset-y-0 md:right-0 md:w-(--right-stack-w,440px) md:flex-none"
19059
19337
  ),
19060
19338
  children: [
19061
19339
  browserVisible && /* @__PURE__ */ jsx(
@@ -19063,42 +19341,25 @@ function AgentChat({
19063
19341
  {
19064
19342
  "data-testid": "browser-panel",
19065
19343
  "data-mobile-view": activeMobilePane,
19066
- className: cn(
19067
- "min-h-0 w-full overflow-hidden",
19068
- // With both panes the desktop splits the column in half.
19069
- // A phone gives whichever pane is selected the whole
19070
- // height instead — see the mobile toggle above.
19071
- bothPanesVisible ? "h-full data-[mobile-view=workspace]:hidden md:block! md:h-1/2 md:border-b md:border-line" : "h-full"
19072
- ),
19344
+ className: "min-h-0 h-full w-full overflow-hidden",
19073
19345
  children: /* @__PURE__ */ jsx(
19074
19346
  BrowserPane,
19075
19347
  {
19076
19348
  sessionId,
19077
19349
  state: browserState,
19078
19350
  adapter,
19079
- onClose: () => setShowBrowser(false)
19351
+ onClose: () => setOpenPane(null)
19080
19352
  }
19081
19353
  )
19082
19354
  }
19083
19355
  ),
19084
- workspaceVisible && /* @__PURE__ */ jsx(
19085
- "div",
19356
+ filePreviewVisible && sessionId && previewPath && /* @__PURE__ */ jsx(
19357
+ WorkspaceFileDrawer,
19086
19358
  {
19087
- "data-testid": "workspace-panel-frame",
19088
- "data-mobile-view": activeMobilePane,
19089
- className: bothPanesVisible ? "h-full min-h-0 w-full overflow-hidden data-[mobile-view=browser]:hidden md:block! md:h-1/2" : "min-h-0 h-full",
19090
- children: /* @__PURE__ */ jsx(
19091
- WorkspacePanel,
19092
- {
19093
- adapter,
19094
- sessionId,
19095
- selectedPath: workspacePath,
19096
- onSelectedPathChange: setWorkspacePath,
19097
- refreshSignal: runtime.workspaceRefreshKey,
19098
- disabled: effectiveDisabled,
19099
- fillParent: bothPanesVisible
19100
- }
19101
- )
19359
+ adapter,
19360
+ sessionId,
19361
+ path: previewPath,
19362
+ onClose: () => setOpenPane(null)
19102
19363
  }
19103
19364
  )
19104
19365
  ]
@@ -23111,6 +23372,6 @@ function SessionTreePanel({
23111
23372
  ] });
23112
23373
  }
23113
23374
 
23114
- export { ACTIVE_MISSION_STATUSES, AgentChat, AgentChatAdapterProvider, AgentWhiteboard, BrandBeam, BrowserLiveView, CitationText, CodingAgentsPanel, ComposioConnectCard, FetchSseEventStream, InboxPanel, IntegrationsBand, IntegrationsPage, MISSION_EVENT_CATEGORIES, MessageResponse, MissionDashboard, MissionsPanel, ORB_STATE_LABELS, OrbShimmerLabel, ResearchSourcesPanel, SIMPLE_MODE_HIDDEN_TOOLS, SIMPLE_MODE_ORB_OPTIONS, ScheduledWorkPanel, SessionTreePanel, WhiteboardSurface, defaultSelectedMissionTaskId, deriveMissionWorkerActivityLabel, deriveOrbActivity, formatMissionTimestamp, groupMissionTasksByBucket, isHiddenSimpleTool, isTerminalMissionStatus, mergeMissionEvents, messageOrbState, missionEventActorLabel, missionEventCategory, missionEventSummary, missionEventTaskId, missionTaskBlocks, missionTaskBucket, missionTaskRailGroups, missionTaskStatusDotClass, missionTaskTitle, missionWorkerTaskCounts, persistChatViewMode, splitCitations, stripMissionControlMarkup, toolOrbState, useAgentChatAdapterContext, useAgentChatRuntime, useChatViewMode, useInboxUnreadCount, useMissionEvents };
23375
+ export { ACTIVE_MISSION_STATUSES, AgentChat, AgentChatAdapterProvider, AgentWhiteboard, BrandBeam, BrowserShell, CitationText, CodingAgentsPanel, ComposioConnectCard, FetchSseEventStream, InboxPanel, IntegrationsBand, IntegrationsPage, MISSION_EVENT_CATEGORIES, MessageResponse, MissionDashboard, MissionsPanel, ORB_STATE_LABELS, OrbShimmerLabel, ResearchSourcesPanel, SIMPLE_MODE_HIDDEN_TOOLS, SIMPLE_MODE_ORB_OPTIONS, ScheduledWorkPanel, SessionTreePanel, WhiteboardSurface, defaultSelectedMissionTaskId, deriveMissionWorkerActivityLabel, deriveOrbActivity, formatMissionTimestamp, groupMissionTasksByBucket, isHiddenSimpleTool, isTerminalMissionStatus, mergeMissionEvents, messageOrbState, missionEventActorLabel, missionEventCategory, missionEventSummary, missionEventTaskId, missionTaskBlocks, missionTaskBucket, missionTaskRailGroups, missionTaskStatusDotClass, missionTaskTitle, missionWorkerTaskCounts, persistChatViewMode, splitCitations, stripMissionControlMarkup, toolOrbState, useAgentChatAdapterContext, useAgentChatRuntime, useBrowserControl, useChatViewMode, useInboxUnreadCount, useMissionEvents };
23115
23376
  //# sourceMappingURL=index.js.map
23116
23377
  //# sourceMappingURL=index.js.map