@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.cjs CHANGED
@@ -215,6 +215,481 @@ function useAgentChatAdapterContext() {
215
215
  }
216
216
  return value;
217
217
  }
218
+ var NAMED_KEYS = /* @__PURE__ */ new Set([
219
+ "Enter",
220
+ "Tab",
221
+ "Escape",
222
+ "Backspace",
223
+ "Delete",
224
+ "ArrowUp",
225
+ "ArrowDown",
226
+ "ArrowLeft",
227
+ "ArrowRight",
228
+ "Home",
229
+ "End",
230
+ "PageUp",
231
+ "PageDown"
232
+ ]);
233
+ function toWsUrl(src) {
234
+ const url = new URL(src, window.location.href);
235
+ if (url.protocol === "http:") url.protocol = "ws:";
236
+ else if (url.protocol === "https:") url.protocol = "wss:";
237
+ return url.toString();
238
+ }
239
+ function normalizePoint(clientX, clientY, rect, naturalWidth, naturalHeight) {
240
+ let { width, height } = rect;
241
+ let offsetX = 0;
242
+ let offsetY = 0;
243
+ if (naturalWidth > 0 && naturalHeight > 0) {
244
+ const scale = Math.min(rect.width / naturalWidth, rect.height / naturalHeight);
245
+ width = naturalWidth * scale;
246
+ height = naturalHeight * scale;
247
+ offsetX = (rect.width - width) / 2;
248
+ offsetY = (rect.height - height) / 2;
249
+ }
250
+ if (width <= 0 || height <= 0) return null;
251
+ const x = (clientX - rect.left - offsetX) / width;
252
+ const y = (clientY - rect.top - offsetY) / height;
253
+ if (x < 0 || x > 1 || y < 0 || y > 1) return null;
254
+ return { x, y };
255
+ }
256
+ function hostOf(url) {
257
+ try {
258
+ const parsed = new URL(url);
259
+ return { origin: parsed.host, rest: `${parsed.pathname}${parsed.search}` };
260
+ } catch {
261
+ return { origin: url, rest: "" };
262
+ }
263
+ }
264
+ 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";
265
+ function BrowserShell({
266
+ src,
267
+ hasControl,
268
+ onToggleControl,
269
+ onClose,
270
+ onMaximize,
271
+ onDisconnect,
272
+ testId = "browser-shell"
273
+ }) {
274
+ const socketRef = react.useRef(null);
275
+ const frameRef = react.useRef(null);
276
+ const onDisconnectRef = react.useRef(onDisconnect);
277
+ onDisconnectRef.current = onDisconnect;
278
+ const objectUrlRef = react.useRef(null);
279
+ const [state, setState] = react.useState("connecting");
280
+ const [frame2, setFrame] = react.useState(null);
281
+ const [tabs, setTabs] = react.useState([]);
282
+ const [nav, setNav] = react.useState({ url: "", title: "" });
283
+ const [menuOpen, setMenuOpen] = react.useState(false);
284
+ react.useEffect(() => {
285
+ setState("connecting");
286
+ let disposed = false;
287
+ const socket = new WebSocket(toWsUrl(src));
288
+ socketRef.current = socket;
289
+ socket.onopen = () => {
290
+ if (!disposed) setState("connected");
291
+ };
292
+ socket.onerror = () => {
293
+ if (!disposed) setState("disconnected");
294
+ };
295
+ socket.onclose = (event) => {
296
+ if (disposed) return;
297
+ setState("disconnected");
298
+ onDisconnectRef.current?.(event?.wasClean ?? false);
299
+ };
300
+ socket.onmessage = (event) => {
301
+ if (disposed) return;
302
+ const data = event.data;
303
+ if (typeof data !== "string") {
304
+ const next = URL.createObjectURL(
305
+ data instanceof Blob ? data : new Blob([data])
306
+ );
307
+ if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);
308
+ objectUrlRef.current = next;
309
+ setFrame(next);
310
+ return;
311
+ }
312
+ let message;
313
+ try {
314
+ message = JSON.parse(data);
315
+ } catch {
316
+ return;
317
+ }
318
+ if (message.t === "tabs") {
319
+ const next = message.tabs ?? [];
320
+ setTabs(next);
321
+ const active = next.find((tab) => tab.active);
322
+ if (active) setNav({ url: active.url, title: active.title });
323
+ } else if (message.t === "nav") {
324
+ setNav({
325
+ url: String(message.url ?? ""),
326
+ title: String(message.title ?? "")
327
+ });
328
+ }
329
+ };
330
+ return () => {
331
+ disposed = true;
332
+ socket.onopen = null;
333
+ socket.onmessage = null;
334
+ socket.onclose = null;
335
+ socket.onerror = null;
336
+ socket.close();
337
+ if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);
338
+ objectUrlRef.current = null;
339
+ socketRef.current = null;
340
+ };
341
+ }, [src]);
342
+ const send = react.useCallback((message) => {
343
+ const socket = socketRef.current;
344
+ if (!socket || socket.readyState !== WebSocket.OPEN) return;
345
+ socket.send(JSON.stringify(message));
346
+ }, []);
347
+ const command = react.useCallback(
348
+ (message) => {
349
+ if (!hasControl) return;
350
+ send(message);
351
+ },
352
+ [hasControl, send]
353
+ );
354
+ const pointerToPage = (event) => {
355
+ const image = frameRef.current;
356
+ if (!image) return null;
357
+ return normalizePoint(
358
+ event.clientX,
359
+ event.clientY,
360
+ image.getBoundingClientRect(),
361
+ image.naturalWidth,
362
+ image.naturalHeight
363
+ );
364
+ };
365
+ const handleKeyDown = (event) => {
366
+ if (!hasControl) return;
367
+ if (NAMED_KEYS.has(event.key)) {
368
+ event.preventDefault();
369
+ command({
370
+ t: "key",
371
+ key: event.key,
372
+ mods: {
373
+ shift: event.shiftKey,
374
+ ctrl: event.ctrlKey,
375
+ alt: event.altKey,
376
+ meta: event.metaKey
377
+ }
378
+ });
379
+ return;
380
+ }
381
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
382
+ event.preventDefault();
383
+ command({ t: "type", text: event.key });
384
+ }
385
+ };
386
+ const { origin, rest } = hostOf(nav.url);
387
+ const showTabs = tabs.length > 1;
388
+ return /* @__PURE__ */ jsxRuntime.jsxs(
389
+ "div",
390
+ {
391
+ "data-testid": testId,
392
+ className: "flex h-full min-h-0 flex-col bg-background",
393
+ children: [
394
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-11 items-center gap-1.5 border-b border-line bg-card px-2.5", children: [
395
+ /* @__PURE__ */ jsxRuntime.jsx(
396
+ "button",
397
+ {
398
+ type: "button",
399
+ "aria-label": "Back",
400
+ className: ICON,
401
+ disabled: !hasControl,
402
+ onClick: () => command({ t: "back" }),
403
+ children: /* @__PURE__ */ jsxRuntime.jsx(ChevronLeft, {})
404
+ }
405
+ ),
406
+ /* @__PURE__ */ jsxRuntime.jsx(
407
+ "button",
408
+ {
409
+ type: "button",
410
+ "aria-label": "Forward",
411
+ className: ICON,
412
+ disabled: !hasControl,
413
+ onClick: () => command({ t: "forward" }),
414
+ children: /* @__PURE__ */ jsxRuntime.jsx(ChevronRight, {})
415
+ }
416
+ ),
417
+ /* @__PURE__ */ jsxRuntime.jsx(
418
+ "button",
419
+ {
420
+ type: "button",
421
+ "aria-label": "Reload",
422
+ className: ICON,
423
+ disabled: !hasControl,
424
+ onClick: () => command({ t: "reload" }),
425
+ children: /* @__PURE__ */ jsxRuntime.jsx(Reload, {})
426
+ }
427
+ ),
428
+ /* @__PURE__ */ jsxRuntime.jsx(
429
+ "button",
430
+ {
431
+ type: "button",
432
+ "data-testid": "browser-shell-control",
433
+ "aria-label": hasControl ? "Return control" : "Take control",
434
+ "aria-pressed": hasControl,
435
+ onClick: onToggleControl,
436
+ className: hasControl ? "flex size-[26px] shrink-0 items-center justify-center rounded-[5px] bg-[#F5A524] text-black" : ICON,
437
+ children: /* @__PURE__ */ jsxRuntime.jsx(Pointer, {})
438
+ }
439
+ ),
440
+ /* @__PURE__ */ jsxRuntime.jsxs(
441
+ "div",
442
+ {
443
+ "data-testid": "browser-shell-url",
444
+ 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",
445
+ children: [
446
+ /* @__PURE__ */ jsxRuntime.jsx(
447
+ "span",
448
+ {
449
+ "aria-hidden": "true",
450
+ className: `size-1.5 shrink-0 rounded-full ${state === "connected" ? "bg-emerald-500" : "bg-muted-foreground"}`
451
+ }
452
+ ),
453
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
454
+ origin,
455
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: rest })
456
+ ] })
457
+ ]
458
+ }
459
+ ),
460
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
461
+ /* @__PURE__ */ jsxRuntime.jsx(
462
+ "button",
463
+ {
464
+ type: "button",
465
+ "aria-label": "More",
466
+ className: ICON,
467
+ onClick: () => setMenuOpen((open) => !open),
468
+ children: /* @__PURE__ */ jsxRuntime.jsx(Ellipsis, {})
469
+ }
470
+ ),
471
+ menuOpen && /* @__PURE__ */ jsxRuntime.jsxs(
472
+ "div",
473
+ {
474
+ "data-testid": "browser-shell-menu",
475
+ className: "absolute right-0 top-8 z-10 min-w-36 rounded-md border border-line bg-card py-1 text-xs shadow-md",
476
+ children: [
477
+ onMaximize && /* @__PURE__ */ jsxRuntime.jsx(
478
+ "button",
479
+ {
480
+ type: "button",
481
+ className: "block w-full px-3 py-1.5 text-left hover:bg-secondary",
482
+ onClick: () => {
483
+ setMenuOpen(false);
484
+ onMaximize();
485
+ },
486
+ children: "Maximize"
487
+ }
488
+ ),
489
+ onClose && /* @__PURE__ */ jsxRuntime.jsx(
490
+ "button",
491
+ {
492
+ type: "button",
493
+ className: "block w-full px-3 py-1.5 text-left hover:bg-secondary",
494
+ onClick: () => {
495
+ setMenuOpen(false);
496
+ onClose();
497
+ },
498
+ children: "Close browser"
499
+ }
500
+ )
501
+ ]
502
+ }
503
+ )
504
+ ] })
505
+ ] }),
506
+ showTabs && /* @__PURE__ */ jsxRuntime.jsx(
507
+ "div",
508
+ {
509
+ "data-testid": "browser-shell-tabs",
510
+ className: "flex min-h-[34px] items-center gap-0.5 border-b border-line bg-card px-1.5",
511
+ children: tabs.map((tab) => (
512
+ // A div, not a button: the close control nests inside, and a
513
+ // button inside a button is invalid markup.
514
+ /* @__PURE__ */ jsxRuntime.jsxs(
515
+ "div",
516
+ {
517
+ role: "tab",
518
+ tabIndex: 0,
519
+ "aria-selected": tab.active,
520
+ "data-testid": "browser-shell-tab",
521
+ onClick: () => send({ t: "switch_tab", id: tab.id }),
522
+ onKeyDown: (event) => {
523
+ if (event.key === "Enter") send({ t: "switch_tab", id: tab.id });
524
+ },
525
+ 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"}`,
526
+ children: [
527
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: tab.title || tab.url }),
528
+ hasControl && /* @__PURE__ */ jsxRuntime.jsx(
529
+ "button",
530
+ {
531
+ type: "button",
532
+ "data-testid": "browser-shell-tab-close",
533
+ "aria-label": `Close ${tab.title || tab.url}`,
534
+ onClick: (event) => {
535
+ event.stopPropagation();
536
+ command({ t: "close_tab", id: tab.id });
537
+ },
538
+ className: "flex size-3.5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-secondary hover:text-foreground",
539
+ children: /* @__PURE__ */ jsxRuntime.jsx(XSmall, {})
540
+ }
541
+ )
542
+ ]
543
+ },
544
+ tab.id
545
+ )
546
+ ))
547
+ }
548
+ ),
549
+ /* @__PURE__ */ jsxRuntime.jsxs(
550
+ "div",
551
+ {
552
+ className: `relative min-h-0 flex-1 bg-black ${hasControl ? "ring-2 ring-inset ring-[#F5A524]" : ""}`,
553
+ tabIndex: hasControl ? 0 : -1,
554
+ onKeyDown: handleKeyDown,
555
+ children: [
556
+ /* @__PURE__ */ jsxRuntime.jsx(
557
+ "img",
558
+ {
559
+ ref: frameRef,
560
+ "data-testid": "browser-shell-frame",
561
+ alt: "",
562
+ src: frame2 ?? void 0,
563
+ draggable: false,
564
+ className: "h-full w-full object-contain",
565
+ onMouseDown: (event) => {
566
+ const point = pointerToPage(event);
567
+ if (point) command({ t: "click", ...point, count: event.detail || 1 });
568
+ },
569
+ onWheel: (event) => {
570
+ const point = pointerToPage(event);
571
+ if (point) {
572
+ command({
573
+ t: "scroll",
574
+ ...point,
575
+ dx: event.deltaX,
576
+ dy: event.deltaY
577
+ });
578
+ }
579
+ }
580
+ }
581
+ ),
582
+ state !== "connected" && /* @__PURE__ */ jsxRuntime.jsx(
583
+ "div",
584
+ {
585
+ "data-testid": "browser-shell-overlay",
586
+ className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/70 text-sm text-muted-foreground",
587
+ children: state === "connecting" ? "Connecting to browser\u2026" : "Browser disconnected."
588
+ }
589
+ ),
590
+ hasControl && /* @__PURE__ */ jsxRuntime.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: [
591
+ /* @__PURE__ */ jsxRuntime.jsx(Pointer, {}),
592
+ "You have control \xB7 click the pointer to return"
593
+ ] })
594
+ ]
595
+ }
596
+ )
597
+ ]
598
+ }
599
+ );
600
+ }
601
+ var svg = {
602
+ width: 14,
603
+ height: 14,
604
+ viewBox: "0 0 24 24",
605
+ fill: "none",
606
+ stroke: "currentColor",
607
+ strokeWidth: 2,
608
+ strokeLinecap: "round",
609
+ strokeLinejoin: "round",
610
+ "aria-hidden": true
611
+ };
612
+ var ChevronLeft = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { ...svg, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m15 18-6-6 6-6" }) });
613
+ var ChevronRight = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { ...svg, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m9 18 6-6-6-6" }) });
614
+ var Reload = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...svg, children: [
615
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
616
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3v5h5" })
617
+ ] });
618
+ var Pointer = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...svg, children: [
619
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12.586 12.586 19 19" }),
620
+ /* @__PURE__ */ jsxRuntime.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" })
621
+ ] });
622
+ var XSmall = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...svg, width: 10, height: 10, children: [
623
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 6 6 18" }),
624
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m6 6 12 12" })
625
+ ] });
626
+ var Ellipsis = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...svg, children: [
627
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "1" }),
628
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "19", cy: "12", r: "1" }),
629
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "5", cy: "12", r: "1" })
630
+ ] });
631
+ function useBrowserControl({
632
+ sessionId,
633
+ hasControl,
634
+ adapter,
635
+ onControlAcquired,
636
+ onControlReleased,
637
+ onClose
638
+ }) {
639
+ const [pending, setPending] = react.useState(false);
640
+ const [confirmOpen, setConfirmOpen] = react.useState(false);
641
+ const [error, setError] = react.useState(null);
642
+ async function toggleControl() {
643
+ if (!sessionId || pending) return;
644
+ setPending(true);
645
+ setError(null);
646
+ try {
647
+ if (hasControl) {
648
+ await adapter.releaseBrowserControl(sessionId);
649
+ onControlReleased?.();
650
+ } else {
651
+ await adapter.acquireBrowserControl(sessionId);
652
+ onControlAcquired?.();
653
+ }
654
+ } catch (nextError) {
655
+ setError(nextError.message);
656
+ } finally {
657
+ setPending(false);
658
+ }
659
+ }
660
+ async function handleConfirmClose() {
661
+ setError(null);
662
+ try {
663
+ if (hasControl) {
664
+ try {
665
+ await adapter.releaseBrowserControl(sessionId);
666
+ onControlReleased?.();
667
+ } catch (releaseError) {
668
+ console.error(
669
+ "Failed to release browser control before close",
670
+ releaseError
671
+ );
672
+ }
673
+ }
674
+ if (adapter.closeBrowserSession) {
675
+ await adapter.closeBrowserSession(sessionId);
676
+ }
677
+ setConfirmOpen(false);
678
+ onClose?.();
679
+ } catch (nextError) {
680
+ setError(nextError.message);
681
+ throw nextError;
682
+ }
683
+ }
684
+ return {
685
+ pending,
686
+ error,
687
+ confirmOpen,
688
+ setConfirmOpen,
689
+ toggleControl,
690
+ handleConfirmClose
691
+ };
692
+ }
218
693
 
219
694
  // src/components/ui/button.tsx
220
695
  init_utils();
@@ -449,375 +924,34 @@ function ConfirmDialog({
449
924
  }
450
925
  );
451
926
  }
452
- function BrowserControlBar({
927
+ function BrowserPane({
453
928
  sessionId,
454
- hasControl,
455
- adapter,
456
- onControlAcquired,
457
- onControlReleased,
458
- onClose
459
- }) {
460
- const [pending, setPending] = react.useState(false);
461
- const [confirmOpen, setConfirmOpen] = react.useState(false);
462
- const [error, setError] = react.useState(null);
463
- async function toggleControl() {
464
- if (!sessionId || pending) return;
465
- setPending(true);
466
- setError(null);
467
- try {
468
- if (hasControl) {
469
- await adapter.releaseBrowserControl(sessionId);
470
- onControlReleased?.();
471
- } else {
472
- await adapter.acquireBrowserControl(sessionId);
473
- onControlAcquired?.();
474
- }
475
- } catch (nextError) {
476
- setError(nextError.message);
477
- } finally {
478
- setPending(false);
479
- }
480
- }
481
- async function handleConfirmClose() {
482
- setError(null);
483
- try {
484
- if (hasControl) {
485
- try {
486
- await adapter.releaseBrowserControl(sessionId);
487
- onControlReleased?.();
488
- } catch (releaseError) {
489
- console.error(
490
- "Failed to release browser control before close",
491
- releaseError
492
- );
493
- }
494
- }
495
- if (adapter.closeBrowserSession) {
496
- await adapter.closeBrowserSession(sessionId);
497
- }
498
- setConfirmOpen(false);
499
- onClose?.();
500
- } catch (nextError) {
501
- setError(nextError.message);
502
- throw nextError;
503
- }
504
- }
505
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-11 items-center gap-2 border-t border-line bg-card px-3 py-2", children: [
506
- /* @__PURE__ */ jsxRuntime.jsxs(
507
- Button,
508
- {
509
- type: "button",
510
- size: "xs",
511
- variant: hasControl ? "secondary" : "default",
512
- disabled: !sessionId || pending,
513
- onClick: () => void toggleControl(),
514
- children: [
515
- hasControl ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcwIcon, { className: "size-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MousePointer2Icon, { className: "size-3", "aria-hidden": "true" }),
516
- hasControl ? "Return control" : "Take control"
517
- ]
518
- }
519
- ),
520
- onClose && /* @__PURE__ */ jsxRuntime.jsxs(
521
- Button,
522
- {
523
- type: "button",
524
- size: "xs",
525
- variant: "secondary",
526
- disabled: !sessionId,
527
- onClick: () => {
528
- setError(null);
529
- setConfirmOpen(true);
530
- },
531
- children: [
532
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.XIcon, { className: "size-3", "aria-hidden": "true" }),
533
- "Close"
534
- ]
535
- }
536
- ),
537
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-auto flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [
538
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-1.5 rounded-full bg-red-500", "aria-hidden": "true" }),
539
- "Live"
540
- ] }),
541
- error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "max-w-40 truncate text-[11px] text-destructive", children: error }),
542
- /* @__PURE__ */ jsxRuntime.jsx(
543
- ConfirmDialog,
544
- {
545
- open: confirmOpen,
546
- title: "Close browser session?",
547
- 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.",
548
- confirmLabel: "Close browser",
549
- variant: "destructive",
550
- confirmIcon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.XIcon, { className: "size-3.5", "aria-hidden": "true" }),
551
- onConfirm: handleConfirmClose,
552
- onCancel: () => setConfirmOpen(false)
553
- }
554
- )
555
- ] });
556
- }
557
- var MIN_ZOOM = 1;
558
- var MAX_ZOOM = 3;
559
- var ZOOM_STEP = 0.5;
560
- function clampZoom(zoom) {
561
- const bounded = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
562
- return Math.round(bounded * 100) / 100;
563
- }
564
- function toWsUrl(src) {
565
- const url = new URL(src, window.location.href);
566
- if (url.protocol === "http:") {
567
- url.protocol = "ws:";
568
- } else if (url.protocol === "https:") {
569
- url.protocol = "wss:";
570
- }
571
- return url.toString();
572
- }
573
- function BrowserLiveView({
574
- src,
575
- testId = "browser-rfb",
576
- onDisconnect
577
- }) {
578
- const containerRef = react.useRef(null);
579
- const onDisconnectRef = react.useRef(onDisconnect);
580
- onDisconnectRef.current = onDisconnect;
581
- const [state, setState] = react.useState("connecting");
582
- const [zoom, setZoom] = react.useState(MIN_ZOOM);
583
- react.useEffect(() => {
584
- const container = containerRef.current;
585
- if (!container) return;
586
- setState("connecting");
587
- setZoom(MIN_ZOOM);
588
- let rfb;
589
- let disposed = false;
590
- const handleConnect = () => setState("connected");
591
- const handleDisconnect = (event) => {
592
- const detail = event.detail;
593
- setState("disconnected");
594
- onDisconnectRef.current?.(detail?.clean ?? false);
595
- };
596
- void import('@novnc/novnc').then((mod) => {
597
- if (disposed || !containerRef.current) return;
598
- const instance = new mod.default(containerRef.current, toWsUrl(src), {
599
- wsProtocols: ["binary"]
600
- });
601
- instance.viewOnly = false;
602
- instance.scaleViewport = true;
603
- instance.addEventListener("connect", handleConnect);
604
- instance.addEventListener("disconnect", handleDisconnect);
605
- rfb = instance;
606
- }).catch(() => {
607
- if (!disposed) setState("disconnected");
608
- });
609
- return () => {
610
- disposed = true;
611
- if (rfb) {
612
- rfb.removeEventListener("connect", handleConnect);
613
- rfb.removeEventListener("disconnect", handleDisconnect);
614
- rfb.disconnect();
615
- }
616
- };
617
- }, [src]);
618
- const keepCanvasFocus = (event) => event.preventDefault();
619
- const zoomTo = (next) => setZoom((current) => clampZoom(next || current));
620
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative h-full w-full bg-black", children: [
621
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsx(
622
- "div",
623
- {
624
- ref: containerRef,
625
- "data-testid": testId,
626
- style: { width: `${zoom * 100}%`, height: `${zoom * 100}%` }
627
- }
628
- ) }),
629
- state === "connected" && /* @__PURE__ */ jsxRuntime.jsxs(
630
- "div",
631
- {
632
- "data-testid": "browser-rfb-zoom",
633
- 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",
634
- children: [
635
- /* @__PURE__ */ jsxRuntime.jsx(
636
- "button",
637
- {
638
- type: "button",
639
- tabIndex: -1,
640
- "aria-label": "Zoom out",
641
- disabled: zoom <= MIN_ZOOM,
642
- onMouseDown: keepCanvasFocus,
643
- onClick: () => zoomTo(zoom - ZOOM_STEP),
644
- 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",
645
- children: "\u2212"
646
- }
647
- ),
648
- /* @__PURE__ */ jsxRuntime.jsxs(
649
- "button",
650
- {
651
- type: "button",
652
- tabIndex: -1,
653
- "aria-label": "Reset zoom",
654
- disabled: zoom === MIN_ZOOM,
655
- onMouseDown: keepCanvasFocus,
656
- onClick: () => setZoom(MIN_ZOOM),
657
- className: "min-w-10 rounded px-1 text-center text-xs tabular-nums hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none",
658
- children: [
659
- Math.round(zoom * 100),
660
- "%"
661
- ]
662
- }
663
- ),
664
- /* @__PURE__ */ jsxRuntime.jsx(
665
- "button",
666
- {
667
- type: "button",
668
- tabIndex: -1,
669
- "aria-label": "Zoom in",
670
- disabled: zoom >= MAX_ZOOM,
671
- onMouseDown: keepCanvasFocus,
672
- onClick: () => zoomTo(zoom + ZOOM_STEP),
673
- 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",
674
- children: "+"
675
- }
676
- )
677
- ]
678
- }
679
- ),
680
- state !== "connected" && /* @__PURE__ */ jsxRuntime.jsx(
681
- "div",
682
- {
683
- "data-testid": "browser-rfb-overlay",
684
- className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/70 text-sm text-muted-foreground",
685
- children: state === "connecting" ? "Connecting to browser\u2026" : "Live view disconnected \u2014 take control again to reconnect."
686
- }
687
- )
688
- ] });
689
- }
690
-
691
- // src/components/browser/browser-status-dot.tsx
692
- init_utils();
693
- function BrowserStatusDot({
694
- status
695
- }) {
696
- return /* @__PURE__ */ jsxRuntime.jsx(
697
- "span",
698
- {
699
- "aria-hidden": "true",
700
- className: cn(
701
- "inline-block size-2 shrink-0 rounded-full",
702
- status === "live" && "bg-emerald-500",
703
- status === "user-control" && "bg-amber-500",
704
- status === "provisioning" && "animate-pulse bg-sky-500",
705
- status === "closed" && "bg-muted-foreground"
706
- )
707
- }
708
- );
709
- }
710
-
711
- // src/components/ui/tooltip.tsx
712
- init_utils();
713
- function TooltipProvider({
714
- delayDuration = 0,
715
- ...props
716
- }) {
717
- return /* @__PURE__ */ jsxRuntime.jsx(
718
- radixUi.Tooltip.Provider,
719
- {
720
- "data-slot": "tooltip-provider",
721
- delayDuration,
722
- ...props
723
- }
724
- );
725
- }
726
- function Tooltip({
727
- ...props
728
- }) {
729
- return /* @__PURE__ */ jsxRuntime.jsx(radixUi.Tooltip.Root, { "data-slot": "tooltip", ...props });
730
- }
731
- function TooltipTrigger({
732
- ...props
733
- }) {
734
- return /* @__PURE__ */ jsxRuntime.jsx(radixUi.Tooltip.Trigger, { "data-slot": "tooltip-trigger", ...props });
735
- }
736
- function TooltipContent({
737
- className,
738
- sideOffset = 0,
739
- children,
740
- ...props
741
- }) {
742
- return /* @__PURE__ */ jsxRuntime.jsx(radixUi.Tooltip.Portal, { children: /* @__PURE__ */ jsxRuntime.jsxs(
743
- radixUi.Tooltip.Content,
744
- {
745
- "data-slot": "tooltip-content",
746
- sideOffset,
747
- className: cn(
748
- "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",
749
- className
750
- ),
751
- ...props,
752
- children: [
753
- children,
754
- /* @__PURE__ */ jsxRuntime.jsx(radixUi.Tooltip.Arrow, { className: "z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-none bg-foreground fill-foreground" })
755
- ]
756
- }
757
- ) });
758
- }
759
- function BrowserPane({
760
- sessionId,
761
- state,
929
+ state,
762
930
  adapter,
763
931
  onClose
764
932
  }) {
765
933
  const [fullscreenOpen, setFullscreenOpen] = react.useState(false);
766
934
  const [openFullscreenOnControl, setOpenFullscreenOnControl] = react.useState(false);
767
935
  const [localControlActive, setLocalControlActive] = react.useState(false);
768
- const [previewSnapshot, setPreviewSnapshot] = react.useState(null);
769
- const [previewLoading, setPreviewLoading] = react.useState(false);
770
- const [previewError, setPreviewError] = react.useState(null);
771
- const hasLiveViewAdapter = typeof adapter.browserLiveViewUrl === "function";
772
- const hasPreviewAdapter = typeof adapter.getBrowserPreviewSnapshot === "function";
936
+ const hasShellAdapter = typeof adapter.browserShellUrl === "function";
773
937
  const hasControlAdapter = typeof adapter.acquireBrowserControl === "function" && typeof adapter.releaseBrowserControl === "function";
774
- const liveViewUrl = react.useMemo(() => {
775
- if (!hasLiveViewAdapter) return "";
776
- return adapter.browserLiveViewUrl(sessionId);
777
- }, [adapter, hasLiveViewAdapter, sessionId]);
938
+ const shellUrl = react.useMemo(() => {
939
+ if (!hasShellAdapter) return "";
940
+ return adapter.browserShellUrl(sessionId);
941
+ }, [adapter, hasShellAdapter, sessionId]);
778
942
  const hasLiveView = state.status !== "provisioning" && state.status !== "closed";
779
- const hasUserControl = localControlActive;
780
- const canUseLiveView = hasLiveView && hasUserControl && Boolean(liveViewUrl);
781
- const canOpenPreview = hasLiveView && (hasPreviewAdapter || canUseLiveView);
782
- const canOpenFullscreen = canOpenPreview;
783
- const refreshPreview = react.useCallback(
784
- async (signal) => {
785
- if (!hasPreviewAdapter || canUseLiveView) return;
786
- setPreviewLoading(true);
787
- setPreviewError(null);
788
- try {
789
- const snapshot = await adapter.getBrowserPreviewSnapshot?.(sessionId);
790
- if (signal?.aborted) return;
791
- setPreviewSnapshot(snapshot ?? null);
792
- if (!snapshot) {
793
- setPreviewError("Browser preview is unavailable.");
794
- }
795
- } catch (error) {
796
- if (signal?.aborted) return;
797
- setPreviewSnapshot(null);
798
- setPreviewError(
799
- error instanceof Error ? error.message : "Browser preview failed."
800
- );
801
- } finally {
802
- if (!signal?.aborted) setPreviewLoading(false);
803
- }
804
- },
805
- [adapter, canUseLiveView, hasPreviewAdapter, sessionId]
806
- );
943
+ const canUseShell = hasLiveView && Boolean(shellUrl);
807
944
  react.useEffect(() => {
808
945
  setFullscreenOpen(false);
809
946
  setOpenFullscreenOnControl(false);
810
947
  setLocalControlActive(false);
811
- setPreviewSnapshot(null);
812
- setPreviewLoading(false);
813
- setPreviewError(null);
814
948
  }, [sessionId]);
815
949
  react.useEffect(() => {
816
950
  if (!openFullscreenOnControl) return;
817
- if (!canUseLiveView) return;
951
+ if (!canUseShell) return;
818
952
  setFullscreenOpen(true);
819
953
  setOpenFullscreenOnControl(false);
820
- }, [canUseLiveView, openFullscreenOnControl]);
954
+ }, [canUseShell, openFullscreenOnControl]);
821
955
  const handleFullscreenOpenChange = react.useCallback(
822
956
  (open) => {
823
957
  setFullscreenOpen(open);
@@ -825,7 +959,7 @@ function BrowserPane({
825
959
  []
826
960
  );
827
961
  react.useEffect(() => {
828
- if (!canUseLiveView || !hasControlAdapter) return;
962
+ if (!localControlActive || !hasControlAdapter) return;
829
963
  const handle = window.setInterval(() => {
830
964
  void adapter.acquireBrowserControl(sessionId).catch((error) => {
831
965
  console.error("Failed to refresh browser control", error);
@@ -833,30 +967,31 @@ function BrowserPane({
833
967
  });
834
968
  }, 25e3);
835
969
  return () => window.clearInterval(handle);
836
- }, [adapter, canUseLiveView, hasControlAdapter, sessionId]);
837
- react.useEffect(() => {
838
- if (!hasLiveView || canUseLiveView) {
839
- setPreviewLoading(false);
840
- setPreviewError(null);
841
- return;
970
+ }, [adapter, localControlActive, hasControlAdapter, sessionId]);
971
+ const control = useBrowserControl({
972
+ sessionId,
973
+ hasControl: localControlActive,
974
+ adapter,
975
+ onControlAcquired: () => {
976
+ setLocalControlActive(true);
977
+ setOpenFullscreenOnControl(true);
978
+ },
979
+ onControlReleased: () => setLocalControlActive(false),
980
+ onClose
981
+ });
982
+ const shell = (testId) => /* @__PURE__ */ jsxRuntime.jsx(
983
+ BrowserShell,
984
+ {
985
+ src: shellUrl,
986
+ testId,
987
+ hasControl: localControlActive,
988
+ onToggleControl: hasControlAdapter ? () => void control.toggleControl() : void 0,
989
+ onClose: onClose ? () => control.setConfirmOpen(true) : void 0,
990
+ onMaximize: fullscreenOpen ? void 0 : () => setFullscreenOpen(true),
991
+ onDisconnect: () => setLocalControlActive(false)
842
992
  }
843
- if (!hasPreviewAdapter) return;
844
- const abort = new AbortController();
845
- void refreshPreview(abort.signal);
846
- const timer = window.setInterval(() => {
847
- void refreshPreview(abort.signal);
848
- }, 5e3);
849
- return () => {
850
- abort.abort();
851
- window.clearInterval(timer);
852
- };
853
- }, [
854
- canUseLiveView,
855
- fullscreenOpen,
856
- hasLiveView,
857
- hasPreviewAdapter,
858
- refreshPreview
859
- ]);
993
+ );
994
+ const placeholder = (message) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: message });
860
995
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
861
996
  /* @__PURE__ */ jsxRuntime.jsxs(
862
997
  "div",
@@ -864,56 +999,8 @@ function BrowserPane({
864
999
  "data-testid": "browser-pane",
865
1000
  className: "flex h-full min-h-0 flex-col bg-background",
866
1001
  children: [
867
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex min-h-10 items-center gap-2 border-b border-line bg-card px-3 text-xs text-foreground", children: [
868
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ZapIcon, { className: "size-3.5 text-muted-foreground", "aria-hidden": "true" }),
869
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: "Browser" }),
870
- /* @__PURE__ */ jsxRuntime.jsx(BrowserStatusDot, { status: state.status }),
871
- state.controlOwner && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 truncate text-amber-500", children: [
872
- state.controlOwner,
873
- " has control"
874
- ] }),
875
- canOpenFullscreen && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ml-auto flex items-center", children: /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { children: /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
876
- /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
877
- "button",
878
- {
879
- type: "button",
880
- "aria-label": "Maximize browser",
881
- 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",
882
- onClick: () => setFullscreenOpen(true),
883
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Maximize2Icon, { className: "size-3.5", "aria-hidden": "true" })
884
- }
885
- ) }),
886
- /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "bottom", children: "Full screen" })
887
- ] }) }) })
888
- ] }),
889
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 bg-black", children: state.status === "provisioning" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Starting browser..." }) : state.status === "closed" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser closed." }) : canUseLiveView ? /* @__PURE__ */ jsxRuntime.jsx(
890
- BrowserLiveView,
891
- {
892
- src: liveViewUrl,
893
- onDisconnect: () => setLocalControlActive(false)
894
- }
895
- ) : previewSnapshot ? /* @__PURE__ */ jsxRuntime.jsx(
896
- BrowserPreviewImage,
897
- {
898
- src: previewSnapshot.src,
899
- fit: "contain",
900
- testId: "browser-preview-image"
901
- }
902
- ) : previewLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Loading browser preview..." }) : previewError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: previewError }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser preview is unavailable." }) }),
903
- hasLiveView && hasControlAdapter && /* @__PURE__ */ jsxRuntime.jsx(
904
- BrowserControlBar,
905
- {
906
- sessionId,
907
- hasControl: localControlActive,
908
- adapter,
909
- onControlAcquired: () => {
910
- setLocalControlActive(true);
911
- setOpenFullscreenOnControl(true);
912
- },
913
- onControlReleased: () => setLocalControlActive(false),
914
- onClose
915
- }
916
- )
1002
+ /* @__PURE__ */ jsxRuntime.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.") }),
1003
+ control.error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-line bg-card px-3 py-1.5 text-[11px] text-destructive", children: control.error })
917
1004
  ]
918
1005
  }
919
1006
  ),
@@ -923,50 +1010,72 @@ function BrowserPane({
923
1010
  "aria-describedby": void 0,
924
1011
  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",
925
1012
  children: [
926
- /* @__PURE__ */ jsxRuntime.jsxs(DialogHeader, { className: "h-10 shrink-0 flex-row items-center gap-2 border-b border-line bg-card px-4 py-0", children: [
927
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ZapIcon, { className: "size-3.5 text-muted-foreground", "aria-hidden": "true" }),
928
- /* @__PURE__ */ jsxRuntime.jsx(DialogTitle, { className: "text-xs normal-case tracking-normal", children: "Browser" }),
929
- /* @__PURE__ */ jsxRuntime.jsx(BrowserStatusDot, { status: state.status }),
930
- state.controlOwner && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 truncate text-xs text-amber-500", children: [
931
- state.controlOwner,
932
- " has control"
933
- ] })
934
- ] }),
935
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 bg-black", children: canUseLiveView ? /* @__PURE__ */ jsxRuntime.jsx(
936
- BrowserLiveView,
937
- {
938
- src: liveViewUrl,
939
- testId: "browser-fullscreen-rfb",
940
- onDisconnect: () => setLocalControlActive(false)
941
- }
942
- ) : previewSnapshot ? /* @__PURE__ */ jsxRuntime.jsx(
943
- BrowserPreviewImage,
944
- {
945
- src: previewSnapshot.src,
946
- fit: "contain",
947
- testId: "browser-fullscreen-preview-image"
948
- }
949
- ) : previewLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Loading browser preview..." }) : previewError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: previewError }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser preview is unavailable." }) })
1013
+ /* @__PURE__ */ jsxRuntime.jsx(DialogHeader, { className: "sr-only", children: /* @__PURE__ */ jsxRuntime.jsx(DialogTitle, { children: "Browser" }) }),
1014
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1", children: canUseShell ? shell("browser-fullscreen-shell") : placeholder("Browser view is unavailable.") })
950
1015
  ]
951
1016
  }
952
- ) })
1017
+ ) }),
1018
+ /* @__PURE__ */ jsxRuntime.jsx(
1019
+ ConfirmDialog,
1020
+ {
1021
+ open: control.confirmOpen,
1022
+ title: "Close browser session?",
1023
+ 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.",
1024
+ confirmLabel: "Close browser",
1025
+ variant: "destructive",
1026
+ onConfirm: control.handleConfirmClose,
1027
+ onCancel: () => control.setConfirmOpen(false)
1028
+ }
1029
+ )
953
1030
  ] });
954
1031
  }
955
- function BrowserPreviewImage({
956
- src,
957
- fit,
958
- testId
1032
+ var POLL_MS = 15e3;
1033
+ var MAX_FAILURES = 3;
1034
+ function useBrowserPreview({
1035
+ adapter,
1036
+ sessionId,
1037
+ enabled
959
1038
  }) {
960
- return /* @__PURE__ */ jsxRuntime.jsx(
961
- "img",
962
- {
963
- "data-testid": testId,
964
- src,
965
- alt: "Browser preview",
966
- className: `h-full w-full bg-black ${fit === "cover" ? "object-cover object-top" : "object-contain"}`,
967
- draggable: false
1039
+ const [src, setSrc] = react.useState(null);
1040
+ const adapterRef = react.useRef(adapter);
1041
+ adapterRef.current = adapter;
1042
+ react.useEffect(() => {
1043
+ if (!enabled || !sessionId) {
1044
+ setSrc(null);
1045
+ return;
968
1046
  }
969
- );
1047
+ let cancelled = false;
1048
+ let failures = 0;
1049
+ let timer = 0;
1050
+ const refresh = async () => {
1051
+ if (document.hidden) return;
1052
+ const fetchSnapshot = adapterRef.current.getBrowserPreviewSnapshot;
1053
+ if (typeof fetchSnapshot !== "function") return;
1054
+ try {
1055
+ const snapshot = await fetchSnapshot(sessionId);
1056
+ if (cancelled) return;
1057
+ if (!snapshot) {
1058
+ window.clearInterval(timer);
1059
+ return;
1060
+ }
1061
+ failures = 0;
1062
+ if (snapshot.src) setSrc(snapshot.src);
1063
+ } catch {
1064
+ if (cancelled) return;
1065
+ failures += 1;
1066
+ if (failures >= MAX_FAILURES) {
1067
+ window.clearInterval(timer);
1068
+ }
1069
+ }
1070
+ };
1071
+ void refresh();
1072
+ timer = window.setInterval(() => void refresh(), POLL_MS);
1073
+ return () => {
1074
+ cancelled = true;
1075
+ window.clearInterval(timer);
1076
+ };
1077
+ }, [enabled, sessionId]);
1078
+ return src;
970
1079
  }
971
1080
 
972
1081
  // src/components/ai-elements/conversation.tsx
@@ -1073,15 +1182,64 @@ function ButtonGroup({
1073
1182
  );
1074
1183
  }
1075
1184
 
1076
- // src/components/ai-elements/message.tsx
1185
+ // src/components/ui/tooltip.tsx
1077
1186
  init_utils();
1078
-
1079
- // src/lib/oauth-popup.ts
1080
- function openOAuthPopup(url) {
1081
- const w = 600;
1082
- const h = 720;
1083
- const dualLeft = window.screenLeft ?? window.screenX ?? 0;
1084
- const dualTop = window.screenTop ?? window.screenY ?? 0;
1187
+ function TooltipProvider({
1188
+ delayDuration = 0,
1189
+ ...props
1190
+ }) {
1191
+ return /* @__PURE__ */ jsxRuntime.jsx(
1192
+ radixUi.Tooltip.Provider,
1193
+ {
1194
+ "data-slot": "tooltip-provider",
1195
+ delayDuration,
1196
+ ...props
1197
+ }
1198
+ );
1199
+ }
1200
+ function Tooltip({
1201
+ ...props
1202
+ }) {
1203
+ return /* @__PURE__ */ jsxRuntime.jsx(radixUi.Tooltip.Root, { "data-slot": "tooltip", ...props });
1204
+ }
1205
+ function TooltipTrigger({
1206
+ ...props
1207
+ }) {
1208
+ return /* @__PURE__ */ jsxRuntime.jsx(radixUi.Tooltip.Trigger, { "data-slot": "tooltip-trigger", ...props });
1209
+ }
1210
+ function TooltipContent({
1211
+ className,
1212
+ sideOffset = 0,
1213
+ children,
1214
+ ...props
1215
+ }) {
1216
+ return /* @__PURE__ */ jsxRuntime.jsx(radixUi.Tooltip.Portal, { children: /* @__PURE__ */ jsxRuntime.jsxs(
1217
+ radixUi.Tooltip.Content,
1218
+ {
1219
+ "data-slot": "tooltip-content",
1220
+ sideOffset,
1221
+ className: cn(
1222
+ "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",
1223
+ className
1224
+ ),
1225
+ ...props,
1226
+ children: [
1227
+ children,
1228
+ /* @__PURE__ */ jsxRuntime.jsx(radixUi.Tooltip.Arrow, { className: "z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-none bg-foreground fill-foreground" })
1229
+ ]
1230
+ }
1231
+ ) });
1232
+ }
1233
+
1234
+ // src/components/ai-elements/message.tsx
1235
+ init_utils();
1236
+
1237
+ // src/lib/oauth-popup.ts
1238
+ function openOAuthPopup(url) {
1239
+ const w = 600;
1240
+ const h = 720;
1241
+ const dualLeft = window.screenLeft ?? window.screenX ?? 0;
1242
+ const dualTop = window.screenTop ?? window.screenY ?? 0;
1085
1243
  const width = window.innerWidth || document.documentElement.clientWidth || w;
1086
1244
  const height = window.innerHeight || document.documentElement.clientHeight || h;
1087
1245
  const left = dualLeft + Math.max(0, (width - w) / 2);
@@ -2883,6 +3041,25 @@ function hostname(url) {
2883
3041
  function truncate2(s, n) {
2884
3042
  return s.length > n ? `${s.slice(0, n)}\u2026` : s;
2885
3043
  }
3044
+ function KnowledgeBaseToolBlock({ tc }) {
3045
+ const args = parseArgs(tc.args) ?? {};
3046
+ const label = {
3047
+ kb_search_pages: "KB Search",
3048
+ kb_list_pages: "KB Browse",
3049
+ kb_read_page: "KB Read"
3050
+ }[tc.toolName] ?? tc.toolName;
3051
+ let detail = "";
3052
+ if (tc.toolName === "kb_search_pages" && args.query) {
3053
+ detail = `\u201C${truncate2(args.query, 60)}\u201D`;
3054
+ } else if (tc.toolName === "kb_read_page" && args.path) {
3055
+ detail = args.pages ? `${args.path} \xB7 pages ${args.pages}` : args.path;
3056
+ }
3057
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm py-0.5", children: [
3058
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LibraryIcon, { className: "size-3.5 text-muted-foreground/60 shrink-0" }),
3059
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: label }),
3060
+ detail && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground/70 truncate text-xs", children: detail })
3061
+ ] });
3062
+ }
2886
3063
  function VisionAnalyzeBlock({ tc }) {
2887
3064
  let image = "";
2888
3065
  try {
@@ -5197,6 +5374,10 @@ function ToolCallBlock({
5197
5374
  case "web_search":
5198
5375
  case "web_crawl":
5199
5376
  return /* @__PURE__ */ jsxRuntime.jsx(WebToolBlock, { tc });
5377
+ case "kb_search_pages":
5378
+ case "kb_list_pages":
5379
+ case "kb_read_page":
5380
+ return /* @__PURE__ */ jsxRuntime.jsx(KnowledgeBaseToolBlock, { tc });
5200
5381
  case "vision_analyze":
5201
5382
  return /* @__PURE__ */ jsxRuntime.jsx(VisionAnalyzeBlock, { tc });
5202
5383
  case "read_file":
@@ -10250,6 +10431,75 @@ function ChatComposerInner({
10250
10431
  }
10251
10432
  );
10252
10433
  }
10434
+
10435
+ // src/components/chat/session-pane-card.tsx
10436
+ init_utils();
10437
+ function SessionPaneCard({
10438
+ title,
10439
+ subtitle,
10440
+ thumbnail,
10441
+ count,
10442
+ icon: Icon = lucideReact.GlobeIcon,
10443
+ onOpen,
10444
+ testId = "session-pane-card",
10445
+ className,
10446
+ expanded
10447
+ }) {
10448
+ return /* @__PURE__ */ jsxRuntime.jsxs(
10449
+ "button",
10450
+ {
10451
+ type: "button",
10452
+ "data-testid": testId,
10453
+ "aria-label": `Open ${title}`,
10454
+ "aria-expanded": expanded === void 0 ? void 0 : expanded,
10455
+ onClick: onOpen,
10456
+ className: cn(
10457
+ // No radius here: the caller sets it. Declaring rounded-xl in the base
10458
+ // and cancelling the bottom from a className leaves both rules live,
10459
+ // and which corner wins comes down to stylesheet order rather than
10460
+ // intent -- which is how the stacked cards kept round bottoms.
10461
+ "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",
10462
+ className
10463
+ ),
10464
+ children: [
10465
+ thumbnail ? /* @__PURE__ */ jsxRuntime.jsx(
10466
+ "img",
10467
+ {
10468
+ "data-testid": "session-pane-card-thumb",
10469
+ src: thumbnail,
10470
+ alt: "",
10471
+ className: "h-11 w-16 shrink-0 rounded-md border border-line object-cover object-top"
10472
+ }
10473
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
10474
+ Icon,
10475
+ {
10476
+ "data-testid": "session-pane-card-icon",
10477
+ className: "size-5 shrink-0 text-muted-foreground",
10478
+ "aria-hidden": "true"
10479
+ }
10480
+ ),
10481
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col", children: [
10482
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5 text-sm font-medium text-foreground", children: [
10483
+ title,
10484
+ count !== void 0 && count > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-full bg-secondary px-1.5 text-[11px] font-normal text-muted-foreground", children: count })
10485
+ ] }),
10486
+ subtitle && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-xs text-muted-foreground", children: subtitle })
10487
+ ] }),
10488
+ expanded !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
10489
+ lucideReact.ChevronDownIcon,
10490
+ {
10491
+ "data-testid": "session-pane-card-chevron",
10492
+ className: cn(
10493
+ "ml-auto size-4 shrink-0 text-muted-foreground transition-transform",
10494
+ expanded && "rotate-180"
10495
+ ),
10496
+ "aria-hidden": "true"
10497
+ }
10498
+ )
10499
+ ]
10500
+ }
10501
+ );
10502
+ }
10253
10503
  var MAX_LOGOS = 10;
10254
10504
  function IntegrationsBand({
10255
10505
  agentId,
@@ -10836,9 +11086,9 @@ function ArtifactTable({ spec }) {
10836
11086
  init_utils();
10837
11087
  var DEFAULT_HEIGHT_PX = 480;
10838
11088
  var EXPANDED_HEIGHT_PX = 800;
10839
- var MIN_ZOOM2 = 0.5;
10840
- var MAX_ZOOM2 = 2;
10841
- var ZOOM_STEP2 = 0.1;
11089
+ var MIN_ZOOM = 0.5;
11090
+ var MAX_ZOOM = 2;
11091
+ var ZOOM_STEP = 0.1;
10842
11092
  var SAFETY_STYLE_PREAMBLE = "<style>html,body{max-height:100vh}canvas{max-height:80vh!important}</style>";
10843
11093
  function injectSafetyStyle(html) {
10844
11094
  const headCloseIdx = html.search(/<\/head\s*>/i);
@@ -10855,7 +11105,7 @@ function ArtifactHtml({
10855
11105
  const [zoom, setZoom] = react.useState(1);
10856
11106
  const safeSrcdoc = react.useMemo(() => injectSafetyStyle(spec.html), [spec.html]);
10857
11107
  const viewportHeight = expanded ? EXPANDED_HEIGHT_PX : DEFAULT_HEIGHT_PX;
10858
- const clamp4 = (v) => Math.round(Math.min(MAX_ZOOM2, Math.max(MIN_ZOOM2, v)) * 100) / 100;
11108
+ const clamp4 = (v) => Math.round(Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, v)) * 100) / 100;
10859
11109
  return /* @__PURE__ */ jsxRuntime.jsxs(
10860
11110
  "div",
10861
11111
  {
@@ -10901,8 +11151,8 @@ function ArtifactHtml({
10901
11151
  "button",
10902
11152
  {
10903
11153
  type: "button",
10904
- onClick: () => setZoom((z) => clamp4(z - ZOOM_STEP2)),
10905
- disabled: zoom <= MIN_ZOOM2,
11154
+ onClick: () => setZoom((z) => clamp4(z - ZOOM_STEP)),
11155
+ disabled: zoom <= MIN_ZOOM,
10906
11156
  className: "hover:text-foreground disabled:opacity-40 cursor-pointer",
10907
11157
  "aria-label": "zoom out",
10908
11158
  children: "\u2212"
@@ -10925,8 +11175,8 @@ function ArtifactHtml({
10925
11175
  "button",
10926
11176
  {
10927
11177
  type: "button",
10928
- onClick: () => setZoom((z) => clamp4(z + ZOOM_STEP2)),
10929
- disabled: zoom >= MAX_ZOOM2,
11178
+ onClick: () => setZoom((z) => clamp4(z + ZOOM_STEP)),
11179
+ disabled: zoom >= MAX_ZOOM,
10930
11180
  className: "hover:text-foreground disabled:opacity-40",
10931
11181
  "aria-label": "zoom in",
10932
11182
  children: "+"
@@ -11427,748 +11677,297 @@ var InsufficientCreditsCard = react.memo(function InsufficientCreditsCard2({
11427
11677
  );
11428
11678
  });
11429
11679
  init_utils();
11430
- var SKELETON_WIDTHS = [70, 85, 55, 90, 60, 78, 45, 82, 65, 72];
11431
- var PDF_WORKER_SRC = new URL(
11432
- "pdfjs-dist/legacy/build/pdf.worker.mjs",
11433
- (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))
11434
- ).toString();
11435
- function FileViewer({
11436
- file,
11437
- loading,
11438
- error,
11439
- downloadUrl,
11440
- onDelete,
11441
- onClose,
11442
- hideHeader = false
11680
+ var _EXTENSION_KIND = {
11681
+ // Documents
11682
+ pdf: { icon: lucideReact.FileTextIcon, typeLabel: "PDF" },
11683
+ doc: { icon: lucideReact.FileTextIcon, typeLabel: "Word document" },
11684
+ docx: { icon: lucideReact.FileTextIcon, typeLabel: "Word document" },
11685
+ odt: { icon: lucideReact.FileTextIcon, typeLabel: "OpenDocument text" },
11686
+ rtf: { icon: lucideReact.FileTextIcon, typeLabel: "Rich text" },
11687
+ txt: { icon: lucideReact.FileTextIcon, typeLabel: "Text" },
11688
+ md: { icon: lucideReact.FileTextIcon, typeLabel: "Markdown" },
11689
+ // Spreadsheets
11690
+ xls: { icon: lucideReact.FileSpreadsheetIcon, typeLabel: "Spreadsheet" },
11691
+ xlsx: { icon: lucideReact.FileSpreadsheetIcon, typeLabel: "Spreadsheet" },
11692
+ ods: { icon: lucideReact.FileSpreadsheetIcon, typeLabel: "OpenDocument spreadsheet" },
11693
+ csv: { icon: lucideReact.FileSpreadsheetIcon, typeLabel: "CSV" },
11694
+ tsv: { icon: lucideReact.FileSpreadsheetIcon, typeLabel: "TSV" },
11695
+ // Presentations / charts
11696
+ ppt: { icon: lucideReact.FileChartLineIcon, typeLabel: "Presentation" },
11697
+ pptx: { icon: lucideReact.FileChartLineIcon, typeLabel: "Presentation" },
11698
+ // Images
11699
+ png: { icon: lucideReact.FileImageIcon, typeLabel: "Image" },
11700
+ jpg: { icon: lucideReact.FileImageIcon, typeLabel: "Image" },
11701
+ jpeg: { icon: lucideReact.FileImageIcon, typeLabel: "Image" },
11702
+ gif: { icon: lucideReact.FileImageIcon, typeLabel: "Image" },
11703
+ svg: { icon: lucideReact.FileImageIcon, typeLabel: "SVG image" },
11704
+ webp: { icon: lucideReact.FileImageIcon, typeLabel: "Image" },
11705
+ bmp: { icon: lucideReact.FileImageIcon, typeLabel: "Image" },
11706
+ // Audio / video
11707
+ mp3: { icon: lucideReact.FileAudioIcon, typeLabel: "Audio" },
11708
+ wav: { icon: lucideReact.FileAudioIcon, typeLabel: "Audio" },
11709
+ ogg: { icon: lucideReact.FileAudioIcon, typeLabel: "Audio" },
11710
+ mp4: { icon: lucideReact.FileVideoIcon, typeLabel: "Video" },
11711
+ mov: { icon: lucideReact.FileVideoIcon, typeLabel: "Video" },
11712
+ webm: { icon: lucideReact.FileVideoIcon, typeLabel: "Video" },
11713
+ // Archives
11714
+ zip: { icon: lucideReact.FileArchiveIcon, typeLabel: "Archive" },
11715
+ tar: { icon: lucideReact.FileArchiveIcon, typeLabel: "Archive" },
11716
+ gz: { icon: lucideReact.FileArchiveIcon, typeLabel: "Archive" },
11717
+ bz2: { icon: lucideReact.FileArchiveIcon, typeLabel: "Archive" },
11718
+ "7z": { icon: lucideReact.FileArchiveIcon, typeLabel: "Archive" },
11719
+ // Code
11720
+ py: { icon: lucideReact.FileCodeIcon, typeLabel: "Python" },
11721
+ js: { icon: lucideReact.FileCodeIcon, typeLabel: "JavaScript" },
11722
+ ts: { icon: lucideReact.FileCodeIcon, typeLabel: "TypeScript" },
11723
+ tsx: { icon: lucideReact.FileCodeIcon, typeLabel: "TypeScript" },
11724
+ jsx: { icon: lucideReact.FileCodeIcon, typeLabel: "JavaScript" },
11725
+ json: { icon: lucideReact.FileCodeIcon, typeLabel: "JSON" },
11726
+ yaml: { icon: lucideReact.FileCodeIcon, typeLabel: "YAML" },
11727
+ yml: { icon: lucideReact.FileCodeIcon, typeLabel: "YAML" },
11728
+ toml: { icon: lucideReact.FileCodeIcon, typeLabel: "TOML" },
11729
+ sh: { icon: lucideReact.CodeIcon, typeLabel: "Shell" },
11730
+ rs: { icon: lucideReact.FileCodeIcon, typeLabel: "Rust" },
11731
+ go: { icon: lucideReact.FileCodeIcon, typeLabel: "Go" },
11732
+ rb: { icon: lucideReact.FileCodeIcon, typeLabel: "Ruby" },
11733
+ java: { icon: lucideReact.FileCodeIcon, typeLabel: "Java" },
11734
+ cpp: { icon: lucideReact.FileCodeIcon, typeLabel: "C++" },
11735
+ c: { icon: lucideReact.FileCodeIcon, typeLabel: "C" },
11736
+ html: { icon: lucideReact.FileCodeIcon, typeLabel: "HTML" },
11737
+ css: { icon: lucideReact.FileCodeIcon, typeLabel: "CSS" },
11738
+ scss: { icon: lucideReact.FileCodeIcon, typeLabel: "Sass" },
11739
+ sql: { icon: lucideReact.FileCodeIcon, typeLabel: "SQL" }
11740
+ };
11741
+ function _extension(path) {
11742
+ const idx = path.lastIndexOf(".");
11743
+ if (idx < 0 || idx === path.length - 1) return "";
11744
+ return path.slice(idx + 1).toLowerCase();
11745
+ }
11746
+ function _fileKindFor(path) {
11747
+ const ext = _extension(path);
11748
+ return _EXTENSION_KIND[ext] ?? { icon: lucideReact.FileIcon, typeLabel: "File" };
11749
+ }
11750
+ function _basename(path) {
11751
+ const idx = path.lastIndexOf("/");
11752
+ return idx >= 0 ? path.slice(idx + 1) : path;
11753
+ }
11754
+ function WorkspaceFileCard({
11755
+ sessionId,
11756
+ path,
11757
+ label
11443
11758
  }) {
11444
- const visible = loading || file !== null || error !== null;
11445
- if (!visible) return null;
11446
- const fileName2 = file?.path.split("/").pop() ?? "File";
11447
- const lang = file ? getLanguageHint(file.path) : "";
11448
- const isPdf = file?.mime_type === "application/pdf" || file?.path.toLowerCase().endsWith(".pdf") || false;
11449
- const isImage = file?.encoding === "base64" && !isPdf;
11450
- const HeaderIcon = isImage ? lucideReact.ImageIcon : lucideReact.FileTextIcon;
11451
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 flex-col border-t border-line", children: [
11452
- !hideHeader && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-2 border-b border-line px-3 py-2", children: [
11453
- /* @__PURE__ */ jsxRuntime.jsx(HeaderIcon, { className: "size-3.5 shrink-0 text-muted-foreground" }),
11454
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
11455
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-xs font-medium", children: fileName2 }),
11456
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "truncate text-[10px] text-muted-foreground", children: [
11457
- file?.path ?? "Loading...",
11458
- file && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-1.5", children: [
11459
- formatFileSize(file.size),
11460
- file.truncated && " (truncated)"
11461
- ] })
11462
- ] })
11463
- ] }),
11464
- downloadUrl && /* @__PURE__ */ jsxRuntime.jsx(
11465
- "a",
11466
- {
11467
- href: downloadUrl,
11468
- download: fileName2,
11469
- className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
11470
- "aria-label": `Download ${fileName2}`,
11471
- title: "Download",
11472
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.DownloadIcon, { className: "size-3.5" })
11473
- }
11474
- ),
11475
- onDelete && /* @__PURE__ */ jsxRuntime.jsx(
11476
- "button",
11477
- {
11478
- type: "button",
11479
- onClick: onDelete,
11480
- className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive",
11481
- "aria-label": `Delete ${fileName2}`,
11482
- title: "Delete",
11483
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TrashIcon, { className: "size-3.5" })
11759
+ const { adapter, onFileSelect } = useAgentChatAdapterContext();
11760
+ const kind = _fileKindFor(path);
11761
+ const Icon = kind.icon;
11762
+ const downloadUrl = adapter.getWorkspaceDownloadUrl({ sessionId, path });
11763
+ const downloadName = _basename(path) || label;
11764
+ const handleCardClick = react.useCallback(() => {
11765
+ onFileSelect?.(path);
11766
+ }, [onFileSelect, path]);
11767
+ return /* @__PURE__ */ jsxRuntime.jsxs(
11768
+ "div",
11769
+ {
11770
+ role: "button",
11771
+ tabIndex: 0,
11772
+ onClick: handleCardClick,
11773
+ onKeyDown: (e) => {
11774
+ if (e.key === "Enter" || e.key === " ") {
11775
+ e.preventDefault();
11776
+ handleCardClick();
11484
11777
  }
11778
+ },
11779
+ className: cn(
11780
+ "group/file flex items-center gap-3 rounded-lg border border-border",
11781
+ "bg-muted/20 px-3 py-2 text-left transition-colors",
11782
+ "cursor-pointer hover:bg-muted/40"
11485
11783
  ),
11486
- /* @__PURE__ */ jsxRuntime.jsx(
11487
- "button",
11488
- {
11489
- type: "button",
11490
- onClick: onClose,
11491
- className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
11492
- "aria-label": "Close file",
11493
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.XIcon, { className: "size-3.5" })
11494
- }
11495
- )
11496
- ] }),
11497
- file && isPdf && file.encoding === "base64" ? (
11498
- // PDF has its own scrollable container — bypass ScrollArea so
11499
- // it can grow with the parent's flex height (Radix Viewport's
11500
- // internal table wrapper sizes to content and breaks h-full).
11501
- /* @__PURE__ */ jsxRuntime.jsx(PdfPreview, { file, fileName: fileName2 })
11502
- ) : /* @__PURE__ */ jsxRuntime.jsxs(ScrollArea, { className: "min-h-0 flex-1", children: [
11503
- loading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1.5 p-3", children: Array.from({ length: 10 }).map((_, index) => /* @__PURE__ */ jsxRuntime.jsx(
11504
- Skeleton,
11505
- {
11506
- className: "h-3.5 rounded",
11507
- style: {
11508
- width: `${SKELETON_WIDTHS[index % SKELETON_WIDTHS.length]}%`
11509
- }
11510
- },
11511
- index
11512
- )) }),
11513
- error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 p-3 text-xs text-destructive", children: [
11514
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
11515
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: error })
11516
- ] }),
11517
- file && isPdf && file.encoding !== "base64" && /* @__PURE__ */ jsxRuntime.jsx(FileViewerError, { message: "PDF preview requires base64 file content." }),
11518
- file && isImage && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center p-4", children: /* @__PURE__ */ jsxRuntime.jsx(
11519
- "img",
11520
- {
11521
- src: `data:${file.mime_type ?? "image/png"};base64,${file.content}`,
11522
- alt: fileName2,
11523
- className: "max-h-[60vh] max-w-full rounded object-contain"
11524
- }
11525
- ) }),
11526
- file && !isImage && !isPdf && /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "wrap-break-word whitespace-pre-wrap p-3 text-[11px] leading-relaxed text-foreground", children: /* @__PURE__ */ jsxRuntime.jsx("code", { "data-language": lang, children: file.content }) })
11527
- ] })
11528
- ] });
11784
+ children: [
11785
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-background", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "size-5 text-muted-foreground", "aria-hidden": true }) }),
11786
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
11787
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-sm font-medium text-foreground", children: label }),
11788
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-xs text-muted-foreground", children: kind.typeLabel })
11789
+ ] }),
11790
+ /* @__PURE__ */ jsxRuntime.jsxs(
11791
+ "a",
11792
+ {
11793
+ href: downloadUrl,
11794
+ download: downloadName,
11795
+ onClick: (e) => e.stopPropagation(),
11796
+ className: cn(
11797
+ "shrink-0 inline-flex items-center gap-1.5 rounded-md border border-border",
11798
+ "bg-background px-2.5 py-1.5 text-xs font-medium text-foreground",
11799
+ "transition-colors hover:bg-muted"
11800
+ ),
11801
+ "aria-label": `Download ${downloadName}`,
11802
+ children: [
11803
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.DownloadIcon, { className: "size-4", "aria-hidden": true }),
11804
+ "Download"
11805
+ ]
11806
+ }
11807
+ )
11808
+ ]
11809
+ }
11810
+ );
11529
11811
  }
11530
- function FileViewerError({ message }) {
11531
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 p-3 text-xs text-destructive", children: [
11532
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
11533
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: message })
11534
- ] });
11812
+ function scrollToArtifactBlock(artifactId) {
11813
+ document.querySelector(`[data-artifact-anchor="${CSS.escape(artifactId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "center" });
11535
11814
  }
11536
- function PdfPreview({
11537
- file,
11538
- fileName: fileName2
11815
+ function ArtifactRefCard({
11816
+ artifactId,
11817
+ name,
11818
+ kind
11539
11819
  }) {
11540
- const containerRef = react.useRef(null);
11541
- const viewerRef = react.useRef(null);
11542
- const pdfViewerRef = react.useRef(null);
11543
- const eventBusRef = react.useRef(null);
11544
- const findControllerRef = react.useRef(null);
11545
- const [error, setError] = react.useState(null);
11546
- const [pageNumber, setPageNumber] = react.useState(1);
11547
- const [pagesCount, setPagesCount] = react.useState(0);
11548
- const [scale, setScale] = react.useState(1);
11549
- const [query, setQuery] = react.useState("");
11550
- react.useEffect(() => {
11551
- let cancelled = false;
11552
- let loadingTask = null;
11553
- let pdfDocument = null;
11554
- const renderPdf = async () => {
11555
- setError(null);
11556
- const container = containerRef.current;
11557
- const viewerElement = viewerRef.current;
11558
- if (!container || !viewerElement) return;
11559
- const pdfBytes = decodeBase64(file.content);
11560
- const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
11561
- pdfjs.GlobalWorkerOptions.workerSrc = PDF_WORKER_SRC;
11562
- globalThis.pdfjsLib = pdfjs;
11563
- const pdfViewerModule = await import('pdfjs-dist/web/pdf_viewer.mjs');
11564
- loadingTask = pdfjs.getDocument({ data: pdfBytes });
11565
- const pdf = await loadingTask.promise;
11566
- pdfDocument = pdf;
11567
- if (cancelled) return;
11568
- const eventBus = new pdfViewerModule.EventBus();
11569
- const linkService = new pdfViewerModule.PDFLinkService({ eventBus });
11570
- const findController = new pdfViewerModule.PDFFindController({
11571
- eventBus,
11572
- linkService
11573
- });
11574
- const pdfViewer = new pdfViewerModule.PDFViewer({
11575
- container,
11576
- viewer: viewerElement,
11577
- eventBus,
11578
- linkService,
11579
- findController,
11580
- removePageBorders: true
11581
- });
11582
- linkService.setViewer(pdfViewer);
11583
- linkService.setDocument(pdf);
11584
- findController.setDocument(pdf);
11585
- const onPagesInit = () => {
11586
- pdfViewer.currentScaleValue = "page-width";
11587
- setScale(pdfViewer.currentScale || 1);
11588
- setPagesCount(pdfViewer.pagesCount);
11589
- setPageNumber(pdfViewer.currentPageNumber);
11590
- };
11591
- const onPageChanging = (event) => {
11592
- if (typeof event.pageNumber === "number") {
11593
- setPageNumber(event.pageNumber);
11594
- }
11595
- };
11596
- const onScaleChanging = (event) => {
11597
- if (typeof event.scale === "number") {
11598
- setScale(event.scale);
11599
- }
11600
- };
11601
- eventBus.on("pagesinit", onPagesInit);
11602
- eventBus.on("pagechanging", onPageChanging);
11603
- eventBus.on("scalechanging", onScaleChanging);
11604
- pdfViewerRef.current = pdfViewer;
11605
- eventBusRef.current = eventBus;
11606
- findControllerRef.current = findController;
11607
- setPagesCount(pdf.numPages);
11608
- pdfViewer.setDocument(pdf);
11609
- };
11610
- void renderPdf().catch((nextError) => {
11611
- if (cancelled) return;
11612
- if (nextError instanceof Error && nextError.name === "RenderingCancelledException") {
11613
- return;
11820
+ return /* @__PURE__ */ jsxRuntime.jsxs(
11821
+ "button",
11822
+ {
11823
+ type: "button",
11824
+ onClick: () => scrollToArtifactBlock(artifactId),
11825
+ 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",
11826
+ children: [
11827
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileTextIcon, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }),
11828
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-foreground", children: name }),
11829
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: KIND_LABEL[kind] })
11830
+ ]
11831
+ }
11832
+ );
11833
+ }
11834
+ function resolveArtifactRef(ref, messages) {
11835
+ for (const msg of messages) {
11836
+ if (msg.role !== "system" || msg.systemKind !== "artifact") continue;
11837
+ const meta = msg.systemMeta ?? {};
11838
+ if (meta.artifact_id !== ref) continue;
11839
+ const kind = meta.kind ?? "markdown";
11840
+ const version = typeof meta.version === "number" ? meta.version : 1;
11841
+ const name = typeof meta.name === "string" ? meta.name : "";
11842
+ return { artifactId: ref, name, kind, version };
11843
+ }
11844
+ return null;
11845
+ }
11846
+ function TurnSummaryCard({
11847
+ summary,
11848
+ sessionId,
11849
+ messages,
11850
+ onFileSelect,
11851
+ onCommandSelect
11852
+ }) {
11853
+ const hasRecap = summary.recap.trim().length > 0;
11854
+ const hasArtifacts = summary.artifacts.length > 0;
11855
+ if (!hasArtifacts) return null;
11856
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 rounded border border-border bg-muted/50 px-3 py-2 text-sm", children: [
11857
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2 text-xs font-semibold uppercase tracking-wide", children: "Summary" }),
11858
+ hasRecap && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 whitespace-pre-wrap text-foreground", children: summary.recap }),
11859
+ hasArtifacts && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: summary.artifacts.map((artifact, i) => {
11860
+ const key = `${artifact.kind}:${artifact.ref}:${i}`;
11861
+ if (artifact.kind === "file" && sessionId) {
11862
+ return /* @__PURE__ */ jsxRuntime.jsx(
11863
+ WorkspaceFileCard,
11864
+ {
11865
+ sessionId,
11866
+ path: artifact.ref,
11867
+ label: artifact.label
11868
+ },
11869
+ key
11870
+ );
11614
11871
  }
11615
- setError(formatPdfPreviewError(nextError));
11616
- });
11617
- return () => {
11618
- cancelled = true;
11619
- loadingTask?.destroy();
11620
- pdfDocument?.destroy();
11621
- pdfViewerRef.current?.cleanup();
11622
- pdfViewerRef.current = null;
11623
- eventBusRef.current = null;
11624
- findControllerRef.current = null;
11625
- };
11626
- }, [file.content]);
11627
- const setViewerPage = (nextPage) => {
11628
- const viewer = pdfViewerRef.current;
11629
- if (!viewer) return;
11630
- const clampedPage = Math.min(
11631
- Math.max(1, nextPage),
11632
- Math.max(1, viewer.pagesCount)
11633
- );
11634
- viewer.currentPageNumber = clampedPage;
11635
- setPageNumber(clampedPage);
11636
- };
11637
- const setViewerScale = (nextScale) => {
11638
- const viewer = pdfViewerRef.current;
11639
- if (!viewer) return;
11640
- const clampedScale = Math.min(3, Math.max(0.5, nextScale));
11641
- viewer.currentScale = clampedScale;
11642
- setScale(clampedScale);
11643
- };
11644
- const runFind = (findPrevious = false) => {
11645
- const eventBus = eventBusRef.current;
11646
- if (!eventBus || !query.trim()) return;
11647
- eventBus.dispatch("find", {
11648
- source: eventBus,
11649
- type: "again",
11650
- query,
11651
- phraseSearch: true,
11652
- caseSensitive: false,
11653
- entireWord: false,
11654
- highlightAll: true,
11655
- findPrevious,
11656
- matchDiacritics: true
11657
- });
11658
- };
11659
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-full flex-col", children: [
11660
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-1.5 border-b border-line px-2 py-1.5", children: [
11661
- /* @__PURE__ */ jsxRuntime.jsx(
11662
- Button,
11663
- {
11664
- type: "button",
11665
- variant: "ghost",
11666
- size: "icon-xs",
11667
- onClick: () => setViewerPage(pageNumber - 1),
11668
- disabled: pageNumber <= 1,
11669
- "aria-label": "Previous PDF page",
11670
- title: "Previous page",
11671
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeftIcon, { className: "size-3.5" })
11872
+ if (artifact.kind === "artifact") {
11873
+ const resolved = sessionId ? resolveArtifactRef(artifact.ref, messages) : null;
11874
+ if (resolved) {
11875
+ return /* @__PURE__ */ jsxRuntime.jsx(
11876
+ ArtifactRefCard,
11877
+ {
11878
+ artifactId: resolved.artifactId,
11879
+ name: resolved.name || artifact.label,
11880
+ kind: resolved.kind
11881
+ },
11882
+ key
11883
+ );
11672
11884
  }
11673
- ),
11674
- /* @__PURE__ */ jsxRuntime.jsx(
11675
- Input,
11885
+ }
11886
+ return /* @__PURE__ */ jsxRuntime.jsxs(
11887
+ "div",
11676
11888
  {
11677
- type: "number",
11678
- min: 1,
11679
- max: pagesCount || 1,
11680
- value: pageNumber,
11681
- onChange: (event) => setViewerPage(Number(event.target.value)),
11682
- "aria-label": "PDF page number",
11683
- className: "h-7 w-12 border-input px-1 text-center text-xs"
11684
- }
11685
- ),
11686
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
11687
- "/ ",
11688
- pagesCount || "-"
11689
- ] }),
11690
- /* @__PURE__ */ jsxRuntime.jsx(
11691
- Button,
11692
- {
11693
- type: "button",
11694
- variant: "ghost",
11695
- size: "icon-xs",
11696
- onClick: () => setViewerPage(pageNumber + 1),
11697
- disabled: pagesCount > 0 && pageNumber >= pagesCount,
11698
- "aria-label": "Next PDF page",
11699
- title: "Next page",
11700
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRightIcon, { className: "size-3.5" })
11701
- }
11702
- ),
11703
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-1 h-5 w-px bg-line" }),
11704
- /* @__PURE__ */ jsxRuntime.jsx(
11705
- Button,
11706
- {
11707
- type: "button",
11708
- variant: "ghost",
11709
- size: "icon-xs",
11710
- onClick: () => setViewerScale(scale - 0.1),
11711
- disabled: scale <= 0.5,
11712
- "aria-label": "Zoom out PDF",
11713
- title: "Zoom out",
11714
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MinusIcon, { className: "size-3.5" })
11715
- }
11716
- ),
11717
- /* @__PURE__ */ jsxRuntime.jsxs(
11718
- "button",
11719
- {
11720
- type: "button",
11721
- className: "h-7 min-w-12 px-1 text-xs text-muted-foreground hover:text-foreground",
11722
- onClick: () => {
11723
- const viewer = pdfViewerRef.current;
11724
- if (!viewer) return;
11725
- viewer.currentScaleValue = "page-width";
11726
- setScale(viewer.currentScale || 1);
11727
- },
11728
- "aria-label": "Fit PDF to width",
11729
- title: "Fit width",
11889
+ className: "flex items-baseline gap-2 text-sm",
11730
11890
  children: [
11731
- Math.round(scale * 100),
11732
- "%"
11891
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", "aria-hidden": true, children: "\u2022" }),
11892
+ /* @__PURE__ */ jsxRuntime.jsx(
11893
+ ArtifactRow,
11894
+ {
11895
+ artifact,
11896
+ onFileSelect,
11897
+ onCommandSelect
11898
+ }
11899
+ )
11733
11900
  ]
11734
- }
11735
- ),
11736
- /* @__PURE__ */ jsxRuntime.jsx(
11737
- Button,
11738
- {
11739
- type: "button",
11740
- variant: "ghost",
11741
- size: "icon-xs",
11742
- onClick: () => setViewerScale(scale + 0.1),
11743
- disabled: scale >= 3,
11744
- "aria-label": "Zoom in PDF",
11745
- title: "Zoom in",
11746
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PlusIcon, { className: "size-3.5" })
11747
- }
11748
- ),
11749
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-1 h-5 w-px bg-line" }),
11750
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.SearchIcon, { className: "size-3.5 text-muted-foreground" }),
11751
- /* @__PURE__ */ jsxRuntime.jsx(
11752
- Input,
11753
- {
11754
- type: "search",
11755
- value: query,
11756
- onChange: (event) => setQuery(event.target.value),
11757
- onKeyDown: (event) => {
11758
- if (event.key === "Enter") runFind(event.shiftKey);
11759
- },
11760
- placeholder: "Find",
11761
- "aria-label": "Find in PDF",
11762
- className: "h-7 w-28 border-input px-1 text-xs"
11763
- }
11764
- ),
11765
- /* @__PURE__ */ jsxRuntime.jsx(
11766
- Button,
11767
- {
11768
- type: "button",
11769
- variant: "ghost",
11770
- size: "xs",
11771
- onClick: () => runFind(false),
11772
- disabled: !query.trim(),
11773
- children: "Find"
11774
- }
11775
- )
11776
- ] }),
11777
- error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full items-start gap-2 p-3 text-xs text-destructive", children: [
11778
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
11779
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: error })
11780
- ] }),
11781
- !error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative min-h-105 flex-1 bg-muted/20", children: /* @__PURE__ */ jsxRuntime.jsx(
11782
- "div",
11783
- {
11784
- ref: containerRef,
11785
- "aria-label": `PDF viewer for ${fileName2}`,
11786
- className: "absolute inset-0 overflow-auto",
11787
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: viewerRef, className: "pdfViewer" })
11788
- }
11789
- ) })
11901
+ },
11902
+ key
11903
+ );
11904
+ }) })
11790
11905
  ] });
11791
11906
  }
11792
- function decodeBase64(content) {
11793
- const binary = globalThis.atob(content);
11794
- const bytes = new Uint8Array(binary.length);
11795
- for (let index = 0; index < binary.length; index += 1) {
11796
- bytes[index] = binary.charCodeAt(index);
11907
+ function ArtifactRow({
11908
+ artifact,
11909
+ onFileSelect,
11910
+ onCommandSelect
11911
+ }) {
11912
+ if (artifact.kind === "file") {
11913
+ if (!onFileSelect) {
11914
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
11915
+ }
11916
+ return /* @__PURE__ */ jsxRuntime.jsx(
11917
+ "button",
11918
+ {
11919
+ type: "button",
11920
+ onClick: () => onFileSelect(artifact.ref),
11921
+ className: "truncate cursor-pointer text-primary hover:underline",
11922
+ children: artifact.label
11923
+ }
11924
+ );
11797
11925
  }
11798
- return bytes;
11799
- }
11800
- function formatPdfPreviewError(error) {
11801
- if (error instanceof Error && (error.name === "InvalidCharacterError" || error.message === "Invalid character")) {
11802
- return "PDF preview data is not valid base64.";
11926
+ if (artifact.kind === "url") {
11927
+ return /* @__PURE__ */ jsxRuntime.jsx(
11928
+ "a",
11929
+ {
11930
+ href: artifact.ref,
11931
+ target: "_blank",
11932
+ rel: "noopener noreferrer",
11933
+ className: "truncate text-primary hover:underline",
11934
+ children: artifact.label
11935
+ }
11936
+ );
11803
11937
  }
11804
- return error instanceof Error ? error.message : "Failed to render PDF preview.";
11938
+ if (artifact.kind === "command") {
11939
+ if (!onCommandSelect) {
11940
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
11941
+ }
11942
+ return /* @__PURE__ */ jsxRuntime.jsx(
11943
+ "button",
11944
+ {
11945
+ type: "button",
11946
+ onClick: () => onCommandSelect(artifact.ref),
11947
+ className: "truncate text-left cursor-pointer text-primary hover:underline",
11948
+ children: artifact.label
11949
+ }
11950
+ );
11951
+ }
11952
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
11805
11953
  }
11806
- var _EXTENSION_KIND = {
11807
- // Documents
11808
- pdf: { icon: lucideReact.FileTextIcon, typeLabel: "PDF" },
11809
- doc: { icon: lucideReact.FileTextIcon, typeLabel: "Word document" },
11810
- docx: { icon: lucideReact.FileTextIcon, typeLabel: "Word document" },
11811
- odt: { icon: lucideReact.FileTextIcon, typeLabel: "OpenDocument text" },
11812
- rtf: { icon: lucideReact.FileTextIcon, typeLabel: "Rich text" },
11813
- txt: { icon: lucideReact.FileTextIcon, typeLabel: "Text" },
11814
- md: { icon: lucideReact.FileTextIcon, typeLabel: "Markdown" },
11815
- // Spreadsheets
11816
- xls: { icon: lucideReact.FileSpreadsheetIcon, typeLabel: "Spreadsheet" },
11817
- xlsx: { icon: lucideReact.FileSpreadsheetIcon, typeLabel: "Spreadsheet" },
11818
- ods: { icon: lucideReact.FileSpreadsheetIcon, typeLabel: "OpenDocument spreadsheet" },
11819
- csv: { icon: lucideReact.FileSpreadsheetIcon, typeLabel: "CSV" },
11820
- tsv: { icon: lucideReact.FileSpreadsheetIcon, typeLabel: "TSV" },
11821
- // Presentations / charts
11822
- ppt: { icon: lucideReact.FileChartLineIcon, typeLabel: "Presentation" },
11823
- pptx: { icon: lucideReact.FileChartLineIcon, typeLabel: "Presentation" },
11824
- // Images
11825
- png: { icon: lucideReact.FileImageIcon, typeLabel: "Image" },
11826
- jpg: { icon: lucideReact.FileImageIcon, typeLabel: "Image" },
11827
- jpeg: { icon: lucideReact.FileImageIcon, typeLabel: "Image" },
11828
- gif: { icon: lucideReact.FileImageIcon, typeLabel: "Image" },
11829
- svg: { icon: lucideReact.FileImageIcon, typeLabel: "SVG image" },
11830
- webp: { icon: lucideReact.FileImageIcon, typeLabel: "Image" },
11831
- bmp: { icon: lucideReact.FileImageIcon, typeLabel: "Image" },
11832
- // Audio / video
11833
- mp3: { icon: lucideReact.FileAudioIcon, typeLabel: "Audio" },
11834
- wav: { icon: lucideReact.FileAudioIcon, typeLabel: "Audio" },
11835
- ogg: { icon: lucideReact.FileAudioIcon, typeLabel: "Audio" },
11836
- mp4: { icon: lucideReact.FileVideoIcon, typeLabel: "Video" },
11837
- mov: { icon: lucideReact.FileVideoIcon, typeLabel: "Video" },
11838
- webm: { icon: lucideReact.FileVideoIcon, typeLabel: "Video" },
11839
- // Archives
11840
- zip: { icon: lucideReact.FileArchiveIcon, typeLabel: "Archive" },
11841
- tar: { icon: lucideReact.FileArchiveIcon, typeLabel: "Archive" },
11842
- gz: { icon: lucideReact.FileArchiveIcon, typeLabel: "Archive" },
11843
- bz2: { icon: lucideReact.FileArchiveIcon, typeLabel: "Archive" },
11844
- "7z": { icon: lucideReact.FileArchiveIcon, typeLabel: "Archive" },
11845
- // Code
11846
- py: { icon: lucideReact.FileCodeIcon, typeLabel: "Python" },
11847
- js: { icon: lucideReact.FileCodeIcon, typeLabel: "JavaScript" },
11848
- ts: { icon: lucideReact.FileCodeIcon, typeLabel: "TypeScript" },
11849
- tsx: { icon: lucideReact.FileCodeIcon, typeLabel: "TypeScript" },
11850
- jsx: { icon: lucideReact.FileCodeIcon, typeLabel: "JavaScript" },
11851
- json: { icon: lucideReact.FileCodeIcon, typeLabel: "JSON" },
11852
- yaml: { icon: lucideReact.FileCodeIcon, typeLabel: "YAML" },
11853
- yml: { icon: lucideReact.FileCodeIcon, typeLabel: "YAML" },
11854
- toml: { icon: lucideReact.FileCodeIcon, typeLabel: "TOML" },
11855
- sh: { icon: lucideReact.CodeIcon, typeLabel: "Shell" },
11856
- rs: { icon: lucideReact.FileCodeIcon, typeLabel: "Rust" },
11857
- go: { icon: lucideReact.FileCodeIcon, typeLabel: "Go" },
11858
- rb: { icon: lucideReact.FileCodeIcon, typeLabel: "Ruby" },
11859
- java: { icon: lucideReact.FileCodeIcon, typeLabel: "Java" },
11860
- cpp: { icon: lucideReact.FileCodeIcon, typeLabel: "C++" },
11861
- c: { icon: lucideReact.FileCodeIcon, typeLabel: "C" },
11862
- html: { icon: lucideReact.FileCodeIcon, typeLabel: "HTML" },
11863
- css: { icon: lucideReact.FileCodeIcon, typeLabel: "CSS" },
11864
- scss: { icon: lucideReact.FileCodeIcon, typeLabel: "Sass" },
11865
- sql: { icon: lucideReact.FileCodeIcon, typeLabel: "SQL" }
11866
- };
11867
- function _extension(path) {
11868
- const idx = path.lastIndexOf(".");
11869
- if (idx < 0 || idx === path.length - 1) return "";
11870
- return path.slice(idx + 1).toLowerCase();
11954
+
11955
+ // src/components/chat/chat-thread.tsx
11956
+ init_utils();
11957
+ var WORKING_ON_IT_DELAY_MS = 250;
11958
+ function hasUserFacingAskContent(msg) {
11959
+ return !!msg.toolCalls?.some((tc) => tc.toolName === "ask_user_question");
11871
11960
  }
11872
- function _fileKindFor(path) {
11873
- const ext = _extension(path);
11874
- return _EXTENSION_KIND[ext] ?? { icon: lucideReact.FileIcon, typeLabel: "File" };
11875
- }
11876
- function _basename(path) {
11877
- const idx = path.lastIndexOf("/");
11878
- return idx >= 0 ? path.slice(idx + 1) : path;
11879
- }
11880
- function WorkspaceFileCard({
11881
- sessionId,
11882
- path,
11883
- label
11884
- }) {
11885
- const { adapter } = useAgentChatAdapterContext();
11886
- const kind = _fileKindFor(path);
11887
- const Icon = kind.icon;
11888
- const downloadUrl = adapter.getWorkspaceDownloadUrl({ sessionId, path });
11889
- const downloadName = _basename(path) || label;
11890
- const [previewOpen, setPreviewOpen] = react.useState(false);
11891
- const handleCardClick = react.useCallback(() => {
11892
- setPreviewOpen(true);
11893
- }, []);
11894
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11895
- /* @__PURE__ */ jsxRuntime.jsxs(
11896
- "div",
11897
- {
11898
- role: "button",
11899
- tabIndex: 0,
11900
- onClick: handleCardClick,
11901
- onKeyDown: (e) => {
11902
- if (e.key === "Enter" || e.key === " ") {
11903
- e.preventDefault();
11904
- handleCardClick();
11905
- }
11906
- },
11907
- className: cn(
11908
- "group/file flex items-center gap-3 rounded-lg border border-border",
11909
- "bg-muted/20 px-3 py-2 text-left transition-colors",
11910
- "cursor-pointer hover:bg-muted/40"
11911
- ),
11912
- children: [
11913
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-md border border-border bg-background", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "size-5 text-muted-foreground", "aria-hidden": true }) }),
11914
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
11915
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-sm font-medium text-foreground", children: label }),
11916
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-xs text-muted-foreground", children: kind.typeLabel })
11917
- ] }),
11918
- /* @__PURE__ */ jsxRuntime.jsxs(
11919
- "a",
11920
- {
11921
- href: downloadUrl,
11922
- download: downloadName,
11923
- onClick: (e) => e.stopPropagation(),
11924
- className: cn(
11925
- "shrink-0 inline-flex items-center gap-1.5 rounded-md border border-border",
11926
- "bg-background px-2.5 py-1.5 text-xs font-medium text-foreground",
11927
- "transition-colors hover:bg-muted"
11928
- ),
11929
- "aria-label": `Download ${downloadName}`,
11930
- children: [
11931
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.DownloadIcon, { className: "size-3.5", "aria-hidden": true }),
11932
- "Download"
11933
- ]
11934
- }
11935
- )
11936
- ]
11937
- }
11938
- ),
11939
- /* @__PURE__ */ jsxRuntime.jsx(
11940
- WorkspaceFilePreviewDialog,
11941
- {
11942
- open: previewOpen,
11943
- onOpenChange: setPreviewOpen,
11944
- sessionId,
11945
- path,
11946
- downloadUrl
11947
- }
11948
- )
11949
- ] });
11950
- }
11951
- function WorkspaceFilePreviewDialog({
11952
- open,
11953
- onOpenChange,
11954
- sessionId,
11955
- path,
11956
- downloadUrl
11957
- }) {
11958
- const { adapter } = useAgentChatAdapterContext();
11959
- const [file, setFile] = react.useState(null);
11960
- const [loading, setLoading] = react.useState(false);
11961
- const [error, setError] = react.useState(null);
11962
- react.useEffect(() => {
11963
- if (!open) return;
11964
- let cancelled = false;
11965
- setLoading(true);
11966
- setError(null);
11967
- setFile(null);
11968
- adapter.getWorkspaceFile({ sessionId, path }).then((result) => {
11969
- if (cancelled) return;
11970
- setFile(result);
11971
- setLoading(false);
11972
- }).catch((err) => {
11973
- if (cancelled) return;
11974
- const message = err instanceof Error ? err.message : "Failed to load file";
11975
- setError(message);
11976
- setLoading(false);
11977
- });
11978
- return () => {
11979
- cancelled = true;
11980
- };
11981
- }, [adapter, sessionId, path, open]);
11982
- return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(DialogContent, { className: "flex h-[85vh] w-[95vw] max-w-350! flex-col gap-0 overflow-hidden p-0", children: [
11983
- /* @__PURE__ */ jsxRuntime.jsxs(DialogHeader, { className: "flex flex-row items-center gap-2 border-b border-line px-4 py-3", children: [
11984
- /* @__PURE__ */ jsxRuntime.jsx(DialogTitle, { className: "min-w-0 flex-1 truncate text-sm", children: _basename(path) }),
11985
- /* @__PURE__ */ jsxRuntime.jsxs(
11986
- "a",
11987
- {
11988
- href: downloadUrl,
11989
- download: _basename(path),
11990
- 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",
11991
- "aria-label": `Download ${_basename(path)}`,
11992
- children: [
11993
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.DownloadIcon, { className: "size-3.5", "aria-hidden": true }),
11994
- "Download"
11995
- ]
11996
- }
11997
- )
11998
- ] }),
11999
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: /* @__PURE__ */ jsxRuntime.jsx(
12000
- FileViewer,
12001
- {
12002
- file,
12003
- loading,
12004
- error,
12005
- downloadUrl,
12006
- onDelete: null,
12007
- onClose: () => onOpenChange(false),
12008
- hideHeader: true
12009
- }
12010
- ) })
12011
- ] }) });
12012
- }
12013
- function scrollToArtifactBlock(artifactId) {
12014
- document.querySelector(`[data-artifact-anchor="${CSS.escape(artifactId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "center" });
12015
- }
12016
- function ArtifactRefCard({
12017
- artifactId,
12018
- name,
12019
- kind
12020
- }) {
12021
- return /* @__PURE__ */ jsxRuntime.jsxs(
12022
- "button",
12023
- {
12024
- type: "button",
12025
- onClick: () => scrollToArtifactBlock(artifactId),
12026
- 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",
12027
- children: [
12028
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileTextIcon, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }),
12029
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-foreground", children: name }),
12030
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: KIND_LABEL[kind] })
12031
- ]
12032
- }
12033
- );
12034
- }
12035
- function resolveArtifactRef(ref, messages) {
12036
- for (const msg of messages) {
12037
- if (msg.role !== "system" || msg.systemKind !== "artifact") continue;
12038
- const meta = msg.systemMeta ?? {};
12039
- if (meta.artifact_id !== ref) continue;
12040
- const kind = meta.kind ?? "markdown";
12041
- const version = typeof meta.version === "number" ? meta.version : 1;
12042
- const name = typeof meta.name === "string" ? meta.name : "";
12043
- return { artifactId: ref, name, kind, version };
12044
- }
12045
- return null;
12046
- }
12047
- function TurnSummaryCard({
12048
- summary,
12049
- sessionId,
12050
- messages,
12051
- onFileSelect,
12052
- onCommandSelect
12053
- }) {
12054
- const hasRecap = summary.recap.trim().length > 0;
12055
- const hasArtifacts = summary.artifacts.length > 0;
12056
- if (!hasArtifacts) return null;
12057
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 rounded border border-border bg-muted/50 px-3 py-2 text-sm", children: [
12058
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2 text-xs font-semibold uppercase tracking-wide", children: "Summary" }),
12059
- hasRecap && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 whitespace-pre-wrap text-foreground", children: summary.recap }),
12060
- hasArtifacts && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: summary.artifacts.map((artifact, i) => {
12061
- const key = `${artifact.kind}:${artifact.ref}:${i}`;
12062
- if (artifact.kind === "file" && sessionId) {
12063
- return /* @__PURE__ */ jsxRuntime.jsx(
12064
- WorkspaceFileCard,
12065
- {
12066
- sessionId,
12067
- path: artifact.ref,
12068
- label: artifact.label
12069
- },
12070
- key
12071
- );
12072
- }
12073
- if (artifact.kind === "artifact") {
12074
- const resolved = sessionId ? resolveArtifactRef(artifact.ref, messages) : null;
12075
- if (resolved) {
12076
- return /* @__PURE__ */ jsxRuntime.jsx(
12077
- ArtifactRefCard,
12078
- {
12079
- artifactId: resolved.artifactId,
12080
- name: resolved.name || artifact.label,
12081
- kind: resolved.kind
12082
- },
12083
- key
12084
- );
12085
- }
12086
- }
12087
- return /* @__PURE__ */ jsxRuntime.jsxs(
12088
- "div",
12089
- {
12090
- className: "flex items-baseline gap-2 text-sm",
12091
- children: [
12092
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", "aria-hidden": true, children: "\u2022" }),
12093
- /* @__PURE__ */ jsxRuntime.jsx(
12094
- ArtifactRow,
12095
- {
12096
- artifact,
12097
- onFileSelect,
12098
- onCommandSelect
12099
- }
12100
- )
12101
- ]
12102
- },
12103
- key
12104
- );
12105
- }) })
12106
- ] });
12107
- }
12108
- function ArtifactRow({
12109
- artifact,
12110
- onFileSelect,
12111
- onCommandSelect
12112
- }) {
12113
- if (artifact.kind === "file") {
12114
- if (!onFileSelect) {
12115
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
12116
- }
12117
- return /* @__PURE__ */ jsxRuntime.jsx(
12118
- "button",
12119
- {
12120
- type: "button",
12121
- onClick: () => onFileSelect(artifact.ref),
12122
- className: "truncate cursor-pointer text-primary hover:underline",
12123
- children: artifact.label
12124
- }
12125
- );
12126
- }
12127
- if (artifact.kind === "url") {
12128
- return /* @__PURE__ */ jsxRuntime.jsx(
12129
- "a",
12130
- {
12131
- href: artifact.ref,
12132
- target: "_blank",
12133
- rel: "noopener noreferrer",
12134
- className: "truncate text-primary hover:underline",
12135
- children: artifact.label
12136
- }
12137
- );
12138
- }
12139
- if (artifact.kind === "command") {
12140
- if (!onCommandSelect) {
12141
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
12142
- }
12143
- return /* @__PURE__ */ jsxRuntime.jsx(
12144
- "button",
12145
- {
12146
- type: "button",
12147
- onClick: () => onCommandSelect(artifact.ref),
12148
- className: "truncate text-left cursor-pointer text-primary hover:underline",
12149
- children: artifact.label
12150
- }
12151
- );
12152
- }
12153
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-muted-foreground", children: artifact.label });
12154
- }
12155
-
12156
- // src/components/chat/chat-thread.tsx
12157
- init_utils();
12158
- var WORKING_ON_IT_DELAY_MS = 250;
12159
- function hasUserFacingAskContent(msg) {
12160
- return !!msg.toolCalls?.some((tc) => tc.toolName === "ask_user_question");
12161
- }
12162
- function pendingAskCall(messages) {
12163
- for (let i = messages.length - 1; i >= 0; i--) {
12164
- const m = messages[i];
12165
- if (m.role === "user") return null;
12166
- if (m.role !== "assistant") continue;
12167
- return m.toolCalls?.find(
12168
- (tc) => tc.toolName === "ask_user_question" && tc.status === "running"
12169
- ) ?? null;
12170
- }
12171
- return null;
11961
+ function pendingAskCall(messages) {
11962
+ for (let i = messages.length - 1; i >= 0; i--) {
11963
+ const m = messages[i];
11964
+ if (m.role === "user") return null;
11965
+ if (m.role !== "assistant") continue;
11966
+ return m.toolCalls?.find(
11967
+ (tc) => tc.toolName === "ask_user_question" && tc.status === "running"
11968
+ ) ?? null;
11969
+ }
11970
+ return null;
12172
11971
  }
12173
11972
  function askComposerPolicy(pending) {
12174
11973
  if (!pending) return null;
@@ -13133,6 +12932,7 @@ function ChatThread({
13133
12932
  retryIndicator,
13134
12933
  onRetry,
13135
12934
  onComposerError,
12935
+ paneCards,
13136
12936
  showBrowser = false,
13137
12937
  onToggleBrowser,
13138
12938
  showWorkspace = false,
@@ -13212,17 +13012,65 @@ function ChatThread({
13212
13012
  const composerArea = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-4xl px-3 sm:px-6 pb-3 sm:pb-5 pt-3", children: [
13213
13013
  retryIndicator && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsxRuntime.jsx(RetryBanner, { indicator: retryIndicator }) }),
13214
13014
  researchSources.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsxRuntime.jsx(ResearchSourcesPanel, { sources: researchSources }) }),
13215
- /* @__PURE__ */ jsxRuntime.jsx(
13216
- ChatComposer,
13015
+ (paneCards?.browser || paneCards?.files) && // Layers sliding out from behind the prompt box: each card is
13016
+ // narrower than the one below it and rounded only on top, with a
13017
+ // negative bottom margin so the next layer covers its flat edge. The
13018
+ // composer is the widest and last, so the stack reads as coming out
13019
+ // of it rather than sitting on top of it.
13020
+ /* @__PURE__ */ jsxRuntime.jsxs(
13021
+ "div",
13217
13022
  {
13218
- onSend,
13219
- onStop,
13220
- isRunning,
13221
- disabled: composerDisabled,
13222
- disabledReason: composerDisabledReason,
13223
- awaitingAnswer: askPolicy !== null && !askPolicy.locked,
13224
- placeholder: askPolicy?.placeholder,
13225
- tokenUsage,
13023
+ "data-testid": "session-pane-cards",
13024
+ className: "flex flex-col",
13025
+ children: [
13026
+ paneCards.browser && /* @__PURE__ */ jsxRuntime.jsx(
13027
+ SessionPaneCard,
13028
+ {
13029
+ testId: "session-pane-card-browser",
13030
+ title: "Browser",
13031
+ icon: lucideReact.GlobeIcon,
13032
+ subtitle: paneCards.browser.subtitle,
13033
+ thumbnail: paneCards.browser.thumbnail,
13034
+ onOpen: paneCards.browser.onOpen,
13035
+ className: "w-auto rounded-t-xl border-b-0 -mb-px"
13036
+ }
13037
+ ),
13038
+ paneCards.files && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
13039
+ /* @__PURE__ */ jsxRuntime.jsx(
13040
+ SessionPaneCard,
13041
+ {
13042
+ testId: "session-pane-card-files",
13043
+ title: "Files",
13044
+ icon: lucideReact.FolderIcon,
13045
+ count: paneCards.files.count,
13046
+ expanded: paneCards.files.open,
13047
+ onOpen: paneCards.files.onToggle,
13048
+ className: "w-auto rounded-t-xl border-b-0 -mb-px"
13049
+ }
13050
+ ),
13051
+ paneCards.files.open && /* @__PURE__ */ jsxRuntime.jsx(
13052
+ "div",
13053
+ {
13054
+ "data-testid": "files-accordion",
13055
+ className: "-mb-px flex h-72 min-h-0 flex-col overflow-hidden border border-b-0 border-line",
13056
+ children: paneCards.files.panel
13057
+ }
13058
+ )
13059
+ ] })
13060
+ ]
13061
+ }
13062
+ ),
13063
+ /* @__PURE__ */ jsxRuntime.jsx(
13064
+ ChatComposer,
13065
+ {
13066
+ onSend,
13067
+ onStop,
13068
+ isRunning,
13069
+ disabled: composerDisabled,
13070
+ disabledReason: composerDisabledReason,
13071
+ awaitingAnswer: askPolicy !== null && !askPolicy.locked,
13072
+ placeholder: askPolicy?.placeholder,
13073
+ tokenUsage,
13226
13074
  onComposerError,
13227
13075
  showBrowser,
13228
13076
  onToggleBrowser,
@@ -14840,17 +14688,25 @@ function useAgentChatRuntime({
14840
14688
  }));
14841
14689
  }
14842
14690
  }).catch(() => void 0);
14843
- void adapter.getBrowserState(sessionId).then((browserState) => {
14844
- if (sessionIdRef.current !== sessionId) return;
14845
- setState((prev) => ({
14846
- ...prev,
14847
- browser: browserState ? {
14848
- status: browserState.status,
14849
- controlOwner: browserState.controlOwner ?? null
14850
- } : null
14851
- }));
14852
- }).catch(() => void 0);
14691
+ const hydrateBrowserState = () => {
14692
+ void adapter.getBrowserState(sessionId).then((browserState) => {
14693
+ if (sessionIdRef.current !== sessionId) return;
14694
+ setState((prev) => ({
14695
+ ...prev,
14696
+ browser: browserState ? {
14697
+ status: browserState.status,
14698
+ controlOwner: browserState.controlOwner ?? null
14699
+ } : null
14700
+ }));
14701
+ }).catch(() => void 0);
14702
+ };
14703
+ hydrateBrowserState();
14704
+ const browserSettleTimer = window.setTimeout(
14705
+ hydrateBrowserState,
14706
+ RECONCILE_FIRST_DELAY
14707
+ );
14853
14708
  return () => {
14709
+ window.clearTimeout(browserSettleTimer);
14854
14710
  clearReconnectTimer();
14855
14711
  closeStream();
14856
14712
  };
@@ -16809,10 +16665,10 @@ async function latexToSvg(latex, display = true) {
16809
16665
  const start = outer.indexOf("<svg");
16810
16666
  const end = outer.lastIndexOf("</svg>");
16811
16667
  if (start === -1 || end === -1) return EMPTY;
16812
- const svg = outer.slice(start, end + "</svg>".length);
16813
- const dims = svg.match(/width="([^"]+)"\s+height="([^"]+)"/);
16668
+ const svg2 = outer.slice(start, end + "</svg>".length);
16669
+ const dims = svg2.match(/width="([^"]+)"\s+height="([^"]+)"/);
16814
16670
  return {
16815
- svg,
16671
+ svg: svg2,
16816
16672
  widthEm: exToEm(dims?.[1]),
16817
16673
  heightEm: exToEm(dims?.[2])
16818
16674
  };
@@ -16840,13 +16696,13 @@ var FormulaCache = class {
16840
16696
  }
16841
16697
  async load(latex) {
16842
16698
  const key = latex;
16843
- const { svg, widthEm, heightEm } = await latexToSvg(latex);
16844
- if (!svg) {
16699
+ const { svg: svg2, widthEm, heightEm } = await latexToSvg(latex);
16700
+ if (!svg2) {
16845
16701
  this.pending.delete(key);
16846
16702
  return;
16847
16703
  }
16848
16704
  const image = new Image();
16849
- image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
16705
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg2)}`;
16850
16706
  const settle = (ok) => {
16851
16707
  this.pending.delete(key);
16852
16708
  if (!ok) return;
@@ -16880,8 +16736,8 @@ var FormulaCache = class {
16880
16736
  };
16881
16737
 
16882
16738
  // src/components/whiteboard/input.ts
16883
- var MIN_ZOOM3 = 0.02;
16884
- var MAX_ZOOM3 = 256;
16739
+ var MIN_ZOOM2 = 0.02;
16740
+ var MAX_ZOOM2 = 256;
16885
16741
  var ZOOM_DIVISOR = 300;
16886
16742
  function screenToLogical(pt, view) {
16887
16743
  return { x: view.x + pt.x / view.zoom, y: view.y + pt.y / view.zoom };
@@ -16891,7 +16747,7 @@ function logicalToScreen(pt, view) {
16891
16747
  }
16892
16748
  var clamp2 = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
16893
16749
  function zoomAt(view, screenPt, factor) {
16894
- const zoom = clamp2(view.zoom * factor, MIN_ZOOM3, MAX_ZOOM3);
16750
+ const zoom = clamp2(view.zoom * factor, MIN_ZOOM2, MAX_ZOOM2);
16895
16751
  const anchor = screenToLogical(screenPt, view);
16896
16752
  return {
16897
16753
  zoom,
@@ -16915,8 +16771,8 @@ function zoomToFit(bounds, size2, padding = 80) {
16915
16771
  }
16916
16772
  const zoom = clamp2(
16917
16773
  Math.min(size2.w / (bounds.w + padding * 2), size2.h / (bounds.h + padding * 2)),
16918
- MIN_ZOOM3,
16919
- MAX_ZOOM3
16774
+ MIN_ZOOM2,
16775
+ MAX_ZOOM2
16920
16776
  );
16921
16777
  return {
16922
16778
  zoom,
@@ -18357,87 +18213,514 @@ function WhiteboardSurface({
18357
18213
  /* @__PURE__ */ jsxRuntime.jsx(
18358
18214
  Input,
18359
18215
  {
18360
- className: "w-64 min-w-0 shrink",
18361
- placeholder: "Ask about the board (optional)",
18362
- value: question,
18363
- disabled: busy,
18364
- onChange: (e) => setQuestion(e.target.value),
18365
- onKeyDown: (e) => {
18366
- if (e.key === "Enter" && !e.shiftKey) {
18367
- e.preventDefault();
18368
- void ask("answer");
18369
- }
18370
- }
18216
+ className: "w-64 min-w-0 shrink",
18217
+ placeholder: "Ask about the board (optional)",
18218
+ value: question,
18219
+ disabled: busy,
18220
+ onChange: (e) => setQuestion(e.target.value),
18221
+ onKeyDown: (e) => {
18222
+ if (e.key === "Enter" && !e.shiftKey) {
18223
+ e.preventDefault();
18224
+ void ask("answer");
18225
+ }
18226
+ }
18227
+ }
18228
+ ),
18229
+ /* @__PURE__ */ jsxRuntime.jsxs(TooltipProvider, { children: [
18230
+ /* @__PURE__ */ jsxRuntime.jsx(ButtonGroup, { "aria-label": "Actions", children: USER_ACTIONS.map((a) => /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
18231
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
18232
+ Button,
18233
+ {
18234
+ type: "button",
18235
+ variant: a.id === "answer" ? "default" : "outline",
18236
+ disabled: busy,
18237
+ "aria-label": a.label,
18238
+ onClick: () => void ask(a.id),
18239
+ children: a.label
18240
+ }
18241
+ ) }),
18242
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "top", children: a.hint })
18243
+ ] }, a.id)) }),
18244
+ /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
18245
+ /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2 px-1", children: [
18246
+ /* @__PURE__ */ jsxRuntime.jsx(
18247
+ Checkbox,
18248
+ {
18249
+ id: "wb-work-it-out",
18250
+ "aria-label": "Work it out",
18251
+ checked: deep,
18252
+ disabled: busy,
18253
+ onCheckedChange: (v) => setDeep(v === true)
18254
+ }
18255
+ ),
18256
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "wb-work-it-out", children: "Work it out" })
18257
+ ] }) }),
18258
+ /* @__PURE__ */ jsxRuntime.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." })
18259
+ ] })
18260
+ ] }),
18261
+ onNewBoard ? /* @__PURE__ */ jsxRuntime.jsx(
18262
+ Button,
18263
+ {
18264
+ type: "button",
18265
+ variant: "ghost",
18266
+ "aria-label": "New board",
18267
+ disabled: busy,
18268
+ onClick: () => {
18269
+ setWantFresh(true);
18270
+ onNewBoard();
18271
+ },
18272
+ children: "New board"
18273
+ }
18274
+ ) : null,
18275
+ viewMode && onViewModeChange && hasAnswer ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center rounded-full border p-0.5", children: ["simple", "expert", "whiteboard"].map((mode) => /* @__PURE__ */ jsxRuntime.jsx(
18276
+ "button",
18277
+ {
18278
+ type: "button",
18279
+ "aria-label": `${VIEW_MODE_LABELS[mode]} view`,
18280
+ "aria-pressed": viewMode === mode,
18281
+ className: cn(
18282
+ "rounded-full px-3 py-1 text-xs",
18283
+ viewMode === mode ? "bg-background font-medium shadow-sm" : "text-muted-foreground"
18284
+ ),
18285
+ onClick: () => onViewModeChange(mode),
18286
+ children: VIEW_MODE_LABELS[mode]
18287
+ },
18288
+ mode
18289
+ )) }) : null
18290
+ ] })
18291
+ ] });
18292
+ }
18293
+ function chunkPairs(flat) {
18294
+ const pairs = [];
18295
+ for (let i = 0; i + 1 < flat.length; i += 2) pairs.push([flat[i], flat[i + 1]]);
18296
+ return pairs;
18297
+ }
18298
+ init_utils();
18299
+ var SKELETON_WIDTHS = [70, 85, 55, 90, 60, 78, 45, 82, 65, 72];
18300
+ var PDF_WORKER_SRC = new URL(
18301
+ "pdfjs-dist/legacy/build/pdf.worker.mjs",
18302
+ (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))
18303
+ ).toString();
18304
+ function FileViewer({
18305
+ file,
18306
+ loading,
18307
+ error,
18308
+ downloadUrl,
18309
+ onDelete,
18310
+ onClose,
18311
+ hideHeader = false,
18312
+ className
18313
+ }) {
18314
+ const visible = loading || file !== null || error !== null;
18315
+ if (!visible) return null;
18316
+ const fileName2 = file?.path.split("/").pop() ?? "File";
18317
+ const lang = file ? getLanguageHint(file.path) : "";
18318
+ const isPdf = file?.mime_type === "application/pdf" || file?.path.toLowerCase().endsWith(".pdf") || false;
18319
+ const isImage = file?.encoding === "base64" && !isPdf;
18320
+ const HeaderIcon = isImage ? lucideReact.ImageIcon : lucideReact.FileTextIcon;
18321
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex min-h-0 flex-1 flex-col border-t border-line", className), children: [
18322
+ !hideHeader && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-2 border-b border-line px-3 py-2", children: [
18323
+ /* @__PURE__ */ jsxRuntime.jsx(HeaderIcon, { className: "size-4 shrink-0 text-muted-foreground" }),
18324
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
18325
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate font-medium", children: fileName2 }),
18326
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "truncate text-sm text-muted-foreground", children: [
18327
+ file?.path ?? "Loading...",
18328
+ file && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-1.5", children: [
18329
+ formatFileSize(file.size),
18330
+ file.truncated && " (truncated)"
18331
+ ] })
18332
+ ] })
18333
+ ] }),
18334
+ downloadUrl && /* @__PURE__ */ jsxRuntime.jsx(
18335
+ "a",
18336
+ {
18337
+ href: downloadUrl,
18338
+ download: fileName2,
18339
+ className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
18340
+ "aria-label": `Download ${fileName2}`,
18341
+ title: "Download",
18342
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.DownloadIcon, { className: "size-4" })
18343
+ }
18344
+ ),
18345
+ onDelete && /* @__PURE__ */ jsxRuntime.jsx(
18346
+ "button",
18347
+ {
18348
+ type: "button",
18349
+ onClick: onDelete,
18350
+ className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive",
18351
+ "aria-label": `Delete ${fileName2}`,
18352
+ title: "Delete",
18353
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TrashIcon, { className: "size-4" })
18354
+ }
18355
+ ),
18356
+ /* @__PURE__ */ jsxRuntime.jsx(
18357
+ "button",
18358
+ {
18359
+ type: "button",
18360
+ onClick: onClose,
18361
+ className: "shrink-0 rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
18362
+ "aria-label": "Close file",
18363
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.XIcon, { className: "size-4" })
18364
+ }
18365
+ )
18366
+ ] }),
18367
+ file && isPdf && file.encoding === "base64" ? (
18368
+ // PDF has its own scrollable container — bypass ScrollArea so
18369
+ // it can grow with the parent's flex height (Radix Viewport's
18370
+ // internal table wrapper sizes to content and breaks h-full).
18371
+ /* @__PURE__ */ jsxRuntime.jsx(PdfPreview, { file, fileName: fileName2 })
18372
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(ScrollArea, { className: "min-h-0 flex-1", children: [
18373
+ loading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1.5 p-3", children: Array.from({ length: 10 }).map((_, index) => /* @__PURE__ */ jsxRuntime.jsx(
18374
+ Skeleton,
18375
+ {
18376
+ className: "h-3.5 rounded",
18377
+ style: {
18378
+ width: `${SKELETON_WIDTHS[index % SKELETON_WIDTHS.length]}%`
18379
+ }
18380
+ },
18381
+ index
18382
+ )) }),
18383
+ error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 p-3 text-xs text-destructive", children: [
18384
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
18385
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: error })
18386
+ ] }),
18387
+ file && isPdf && file.encoding !== "base64" && /* @__PURE__ */ jsxRuntime.jsx(FileViewerError, { message: "PDF preview requires base64 file content." }),
18388
+ file && isImage && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center p-4", children: /* @__PURE__ */ jsxRuntime.jsx(
18389
+ "img",
18390
+ {
18391
+ src: `data:${file.mime_type ?? "image/png"};base64,${file.content}`,
18392
+ alt: fileName2,
18393
+ className: "max-h-[60vh] max-w-full rounded object-contain"
18394
+ }
18395
+ ) }),
18396
+ file && !isImage && !isPdf && /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "wrap-break-word whitespace-pre-wrap p-3 text-base leading-relaxed text-foreground", children: /* @__PURE__ */ jsxRuntime.jsx("code", { "data-language": lang, children: file.content }) })
18397
+ ] })
18398
+ ] });
18399
+ }
18400
+ function FileViewerError({ message }) {
18401
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 p-3 text-xs text-destructive", children: [
18402
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
18403
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: message })
18404
+ ] });
18405
+ }
18406
+ function PdfPreview({
18407
+ file,
18408
+ fileName: fileName2
18409
+ }) {
18410
+ const containerRef = react.useRef(null);
18411
+ const viewerRef = react.useRef(null);
18412
+ const pdfViewerRef = react.useRef(null);
18413
+ const eventBusRef = react.useRef(null);
18414
+ const findControllerRef = react.useRef(null);
18415
+ const [error, setError] = react.useState(null);
18416
+ const [pageNumber, setPageNumber] = react.useState(1);
18417
+ const [pagesCount, setPagesCount] = react.useState(0);
18418
+ const [scale, setScale] = react.useState(1);
18419
+ const [query, setQuery] = react.useState("");
18420
+ react.useEffect(() => {
18421
+ let cancelled = false;
18422
+ let loadingTask = null;
18423
+ let pdfDocument = null;
18424
+ const renderPdf = async () => {
18425
+ setError(null);
18426
+ const container = containerRef.current;
18427
+ const viewerElement = viewerRef.current;
18428
+ if (!container || !viewerElement) return;
18429
+ const pdfBytes = decodeBase64(file.content);
18430
+ const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
18431
+ pdfjs.GlobalWorkerOptions.workerSrc = PDF_WORKER_SRC;
18432
+ globalThis.pdfjsLib = pdfjs;
18433
+ const pdfViewerModule = await import('pdfjs-dist/web/pdf_viewer.mjs');
18434
+ loadingTask = pdfjs.getDocument({ data: pdfBytes });
18435
+ const pdf = await loadingTask.promise;
18436
+ pdfDocument = pdf;
18437
+ if (cancelled) return;
18438
+ const eventBus = new pdfViewerModule.EventBus();
18439
+ const linkService = new pdfViewerModule.PDFLinkService({ eventBus });
18440
+ const findController = new pdfViewerModule.PDFFindController({
18441
+ eventBus,
18442
+ linkService
18443
+ });
18444
+ const pdfViewer = new pdfViewerModule.PDFViewer({
18445
+ container,
18446
+ viewer: viewerElement,
18447
+ eventBus,
18448
+ linkService,
18449
+ findController,
18450
+ removePageBorders: true
18451
+ });
18452
+ linkService.setViewer(pdfViewer);
18453
+ linkService.setDocument(pdf);
18454
+ findController.setDocument(pdf);
18455
+ const onPagesInit = () => {
18456
+ pdfViewer.currentScaleValue = "page-width";
18457
+ setScale(pdfViewer.currentScale || 1);
18458
+ setPagesCount(pdfViewer.pagesCount);
18459
+ setPageNumber(pdfViewer.currentPageNumber);
18460
+ };
18461
+ const onPageChanging = (event) => {
18462
+ if (typeof event.pageNumber === "number") {
18463
+ setPageNumber(event.pageNumber);
18464
+ }
18465
+ };
18466
+ const onScaleChanging = (event) => {
18467
+ if (typeof event.scale === "number") {
18468
+ setScale(event.scale);
18469
+ }
18470
+ };
18471
+ eventBus.on("pagesinit", onPagesInit);
18472
+ eventBus.on("pagechanging", onPageChanging);
18473
+ eventBus.on("scalechanging", onScaleChanging);
18474
+ pdfViewerRef.current = pdfViewer;
18475
+ eventBusRef.current = eventBus;
18476
+ findControllerRef.current = findController;
18477
+ setPagesCount(pdf.numPages);
18478
+ pdfViewer.setDocument(pdf);
18479
+ };
18480
+ void renderPdf().catch((nextError) => {
18481
+ if (cancelled) return;
18482
+ if (nextError instanceof Error && nextError.name === "RenderingCancelledException") {
18483
+ return;
18484
+ }
18485
+ setError(formatPdfPreviewError(nextError));
18486
+ });
18487
+ return () => {
18488
+ cancelled = true;
18489
+ loadingTask?.destroy();
18490
+ pdfDocument?.destroy();
18491
+ pdfViewerRef.current?.cleanup();
18492
+ pdfViewerRef.current = null;
18493
+ eventBusRef.current = null;
18494
+ findControllerRef.current = null;
18495
+ };
18496
+ }, [file.content]);
18497
+ const setViewerPage = (nextPage) => {
18498
+ const viewer = pdfViewerRef.current;
18499
+ if (!viewer) return;
18500
+ const clampedPage = Math.min(
18501
+ Math.max(1, nextPage),
18502
+ Math.max(1, viewer.pagesCount)
18503
+ );
18504
+ viewer.currentPageNumber = clampedPage;
18505
+ setPageNumber(clampedPage);
18506
+ };
18507
+ const setViewerScale = (nextScale) => {
18508
+ const viewer = pdfViewerRef.current;
18509
+ if (!viewer) return;
18510
+ const clampedScale = Math.min(3, Math.max(0.5, nextScale));
18511
+ viewer.currentScale = clampedScale;
18512
+ setScale(clampedScale);
18513
+ };
18514
+ const runFind = (findPrevious = false) => {
18515
+ const eventBus = eventBusRef.current;
18516
+ if (!eventBus || !query.trim()) return;
18517
+ eventBus.dispatch("find", {
18518
+ source: eventBus,
18519
+ type: "again",
18520
+ query,
18521
+ phraseSearch: true,
18522
+ caseSensitive: false,
18523
+ entireWord: false,
18524
+ highlightAll: true,
18525
+ findPrevious,
18526
+ matchDiacritics: true
18527
+ });
18528
+ };
18529
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-full flex-col", children: [
18530
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-1.5 border-b border-line px-2 py-1.5", children: [
18531
+ /* @__PURE__ */ jsxRuntime.jsx(
18532
+ Button,
18533
+ {
18534
+ type: "button",
18535
+ variant: "ghost",
18536
+ size: "icon-xs",
18537
+ onClick: () => setViewerPage(pageNumber - 1),
18538
+ disabled: pageNumber <= 1,
18539
+ "aria-label": "Previous PDF page",
18540
+ title: "Previous page",
18541
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeftIcon, { className: "size-3.5" })
18542
+ }
18543
+ ),
18544
+ /* @__PURE__ */ jsxRuntime.jsx(
18545
+ Input,
18546
+ {
18547
+ type: "number",
18548
+ min: 1,
18549
+ max: pagesCount || 1,
18550
+ value: pageNumber,
18551
+ onChange: (event) => setViewerPage(Number(event.target.value)),
18552
+ "aria-label": "PDF page number",
18553
+ className: "h-7 w-12 border-input px-1 text-center text-xs"
18371
18554
  }
18372
18555
  ),
18373
- /* @__PURE__ */ jsxRuntime.jsxs(TooltipProvider, { children: [
18374
- /* @__PURE__ */ jsxRuntime.jsx(ButtonGroup, { "aria-label": "Actions", children: USER_ACTIONS.map((a) => /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
18375
- /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
18376
- Button,
18377
- {
18378
- type: "button",
18379
- variant: a.id === "answer" ? "default" : "outline",
18380
- disabled: busy,
18381
- "aria-label": a.label,
18382
- onClick: () => void ask(a.id),
18383
- children: a.label
18384
- }
18385
- ) }),
18386
- /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "top", children: a.hint })
18387
- ] }, a.id)) }),
18388
- /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
18389
- /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2 px-1", children: [
18390
- /* @__PURE__ */ jsxRuntime.jsx(
18391
- Checkbox,
18392
- {
18393
- id: "wb-work-it-out",
18394
- "aria-label": "Work it out",
18395
- checked: deep,
18396
- disabled: busy,
18397
- onCheckedChange: (v) => setDeep(v === true)
18398
- }
18399
- ),
18400
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "wb-work-it-out", children: "Work it out" })
18401
- ] }) }),
18402
- /* @__PURE__ */ jsxRuntime.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." })
18403
- ] })
18556
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
18557
+ "/ ",
18558
+ pagesCount || "-"
18404
18559
  ] }),
18405
- onNewBoard ? /* @__PURE__ */ jsxRuntime.jsx(
18560
+ /* @__PURE__ */ jsxRuntime.jsx(
18406
18561
  Button,
18407
18562
  {
18408
18563
  type: "button",
18409
18564
  variant: "ghost",
18410
- "aria-label": "New board",
18411
- disabled: busy,
18565
+ size: "icon-xs",
18566
+ onClick: () => setViewerPage(pageNumber + 1),
18567
+ disabled: pagesCount > 0 && pageNumber >= pagesCount,
18568
+ "aria-label": "Next PDF page",
18569
+ title: "Next page",
18570
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRightIcon, { className: "size-3.5" })
18571
+ }
18572
+ ),
18573
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-1 h-5 w-px bg-line" }),
18574
+ /* @__PURE__ */ jsxRuntime.jsx(
18575
+ Button,
18576
+ {
18577
+ type: "button",
18578
+ variant: "ghost",
18579
+ size: "icon-xs",
18580
+ onClick: () => setViewerScale(scale - 0.1),
18581
+ disabled: scale <= 0.5,
18582
+ "aria-label": "Zoom out PDF",
18583
+ title: "Zoom out",
18584
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MinusIcon, { className: "size-3.5" })
18585
+ }
18586
+ ),
18587
+ /* @__PURE__ */ jsxRuntime.jsxs(
18588
+ "button",
18589
+ {
18590
+ type: "button",
18591
+ className: "h-7 min-w-12 px-1 text-xs text-muted-foreground hover:text-foreground",
18412
18592
  onClick: () => {
18413
- setWantFresh(true);
18414
- onNewBoard();
18593
+ const viewer = pdfViewerRef.current;
18594
+ if (!viewer) return;
18595
+ viewer.currentScaleValue = "page-width";
18596
+ setScale(viewer.currentScale || 1);
18415
18597
  },
18416
- children: "New board"
18598
+ "aria-label": "Fit PDF to width",
18599
+ title: "Fit width",
18600
+ children: [
18601
+ Math.round(scale * 100),
18602
+ "%"
18603
+ ]
18417
18604
  }
18418
- ) : null,
18419
- viewMode && onViewModeChange && hasAnswer ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center rounded-full border p-0.5", children: ["simple", "expert", "whiteboard"].map((mode) => /* @__PURE__ */ jsxRuntime.jsx(
18420
- "button",
18605
+ ),
18606
+ /* @__PURE__ */ jsxRuntime.jsx(
18607
+ Button,
18421
18608
  {
18422
18609
  type: "button",
18423
- "aria-label": `${VIEW_MODE_LABELS[mode]} view`,
18424
- "aria-pressed": viewMode === mode,
18425
- className: cn(
18426
- "rounded-full px-3 py-1 text-xs",
18427
- viewMode === mode ? "bg-background font-medium shadow-sm" : "text-muted-foreground"
18428
- ),
18429
- onClick: () => onViewModeChange(mode),
18430
- children: VIEW_MODE_LABELS[mode]
18431
- },
18432
- mode
18433
- )) }) : null
18434
- ] })
18610
+ variant: "ghost",
18611
+ size: "icon-xs",
18612
+ onClick: () => setViewerScale(scale + 0.1),
18613
+ disabled: scale >= 3,
18614
+ "aria-label": "Zoom in PDF",
18615
+ title: "Zoom in",
18616
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PlusIcon, { className: "size-3.5" })
18617
+ }
18618
+ ),
18619
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-1 h-5 w-px bg-line" }),
18620
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.SearchIcon, { className: "size-3.5 text-muted-foreground" }),
18621
+ /* @__PURE__ */ jsxRuntime.jsx(
18622
+ Input,
18623
+ {
18624
+ type: "search",
18625
+ value: query,
18626
+ onChange: (event) => setQuery(event.target.value),
18627
+ onKeyDown: (event) => {
18628
+ if (event.key === "Enter") runFind(event.shiftKey);
18629
+ },
18630
+ placeholder: "Find",
18631
+ "aria-label": "Find in PDF",
18632
+ className: "h-7 w-28 border-input px-1 text-xs"
18633
+ }
18634
+ ),
18635
+ /* @__PURE__ */ jsxRuntime.jsx(
18636
+ Button,
18637
+ {
18638
+ type: "button",
18639
+ variant: "ghost",
18640
+ size: "xs",
18641
+ onClick: () => runFind(false),
18642
+ disabled: !query.trim(),
18643
+ children: "Find"
18644
+ }
18645
+ )
18646
+ ] }),
18647
+ error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full items-start gap-2 p-3 text-xs text-destructive", children: [
18648
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircleIcon, { className: "mt-0.5 size-3.5 shrink-0" }),
18649
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: error })
18650
+ ] }),
18651
+ !error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative min-h-105 flex-1 bg-muted/20", children: /* @__PURE__ */ jsxRuntime.jsx(
18652
+ "div",
18653
+ {
18654
+ ref: containerRef,
18655
+ "aria-label": `PDF viewer for ${fileName2}`,
18656
+ className: "absolute inset-0 overflow-auto",
18657
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: viewerRef, className: "pdfViewer" })
18658
+ }
18659
+ ) })
18435
18660
  ] });
18436
18661
  }
18437
- function chunkPairs(flat) {
18438
- const pairs = [];
18439
- for (let i = 0; i + 1 < flat.length; i += 2) pairs.push([flat[i], flat[i + 1]]);
18440
- return pairs;
18662
+ function decodeBase64(content) {
18663
+ const binary = globalThis.atob(content);
18664
+ const bytes = new Uint8Array(binary.length);
18665
+ for (let index = 0; index < binary.length; index += 1) {
18666
+ bytes[index] = binary.charCodeAt(index);
18667
+ }
18668
+ return bytes;
18669
+ }
18670
+ function formatPdfPreviewError(error) {
18671
+ if (error instanceof Error && (error.name === "InvalidCharacterError" || error.message === "Invalid character")) {
18672
+ return "PDF preview data is not valid base64.";
18673
+ }
18674
+ return error instanceof Error ? error.message : "Failed to render PDF preview.";
18675
+ }
18676
+ function WorkspaceFileDrawer({
18677
+ adapter,
18678
+ sessionId,
18679
+ path,
18680
+ onClose
18681
+ }) {
18682
+ const [file, setFile] = react.useState(null);
18683
+ const [loading, setLoading] = react.useState(true);
18684
+ const [error, setError] = react.useState(null);
18685
+ const requestRef = react.useRef(0);
18686
+ react.useEffect(() => {
18687
+ const request = ++requestRef.current;
18688
+ setFile(null);
18689
+ setLoading(true);
18690
+ setError(null);
18691
+ void (async () => {
18692
+ try {
18693
+ const next = await adapter.getWorkspaceFile({ sessionId, path });
18694
+ if (requestRef.current !== request) return;
18695
+ setFile(next);
18696
+ } catch (fetchError) {
18697
+ if (requestRef.current !== request) return;
18698
+ setFile(null);
18699
+ setError(fetchError.message);
18700
+ } finally {
18701
+ if (requestRef.current === request) setLoading(false);
18702
+ }
18703
+ })();
18704
+ }, [adapter, sessionId, path]);
18705
+ return /* @__PURE__ */ jsxRuntime.jsx(
18706
+ "aside",
18707
+ {
18708
+ "data-testid": "file-preview-panel",
18709
+ className: "flex h-full min-h-0 w-full flex-col overflow-hidden border-l border-line bg-card",
18710
+ children: /* @__PURE__ */ jsxRuntime.jsx(
18711
+ FileViewer,
18712
+ {
18713
+ className: "border-t-0",
18714
+ file,
18715
+ loading,
18716
+ error,
18717
+ downloadUrl: file ? adapter.getWorkspaceDownloadUrl({ sessionId, path: file.path }) : null,
18718
+ onDelete: null,
18719
+ onClose
18720
+ }
18721
+ )
18722
+ }
18723
+ );
18441
18724
  }
18442
18725
  init_utils();
18443
18726
 
@@ -18486,7 +18769,7 @@ function FileTree({
18486
18769
  return /* @__PURE__ */ jsxRuntime.jsx(FileTreeContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(
18487
18770
  "div",
18488
18771
  {
18489
- className: cn("rounded-lg border bg-background text-sm", className),
18772
+ className: cn("rounded-lg border bg-card text-base", className),
18490
18773
  role: "tree",
18491
18774
  ...props,
18492
18775
  children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2", children })
@@ -18695,7 +18978,7 @@ function RenderEntries({
18695
18978
  onClick: (event) => event.stopPropagation(),
18696
18979
  title: "Download",
18697
18980
  "aria-label": `Download ${fileName2}`,
18698
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.DownloadIcon, { className: "size-3" })
18981
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.DownloadIcon, { className: "size-5" })
18699
18982
  }
18700
18983
  ),
18701
18984
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -18709,7 +18992,7 @@ function RenderEntries({
18709
18992
  },
18710
18993
  title: "Delete",
18711
18994
  "aria-label": `Delete ${fileName2}`,
18712
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TrashIcon, { className: "size-3" })
18995
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TrashIcon, { className: "size-5" })
18713
18996
  }
18714
18997
  )
18715
18998
  ] })
@@ -18721,6 +19004,7 @@ function WorkspacePanel({
18721
19004
  sessionId,
18722
19005
  selectedPath,
18723
19006
  onSelectedPathChange,
19007
+ onOpenFile,
18724
19008
  refreshSignal = 0,
18725
19009
  disabled = false,
18726
19010
  fillParent = false
@@ -18729,9 +19013,6 @@ function WorkspacePanel({
18729
19013
  const [entries, setEntries] = react.useState([]);
18730
19014
  const [treeLoading, setTreeLoading] = react.useState(false);
18731
19015
  const [treeError, setTreeError] = react.useState(null);
18732
- const [file, setFile] = react.useState(null);
18733
- const [fileLoading, setFileLoading] = react.useState(false);
18734
- const [fileError, setFileError] = react.useState(null);
18735
19016
  const [notice, setNotice] = react.useState(null);
18736
19017
  const [uploading, setUploading] = react.useState(false);
18737
19018
  const [deleteTarget, setDeleteTarget] = react.useState(null);
@@ -18771,31 +19052,6 @@ function WorkspacePanel({
18771
19052
  }
18772
19053
  }
18773
19054
  }, [adapter, sessionId]);
18774
- const fetchFile = react.useCallback(
18775
- async (path) => {
18776
- if (!sessionId) return;
18777
- const requestedSessionId = sessionId;
18778
- setFileLoading(true);
18779
- setFileError(null);
18780
- try {
18781
- const nextFile = await adapter.getWorkspaceFile({
18782
- sessionId: requestedSessionId,
18783
- path
18784
- });
18785
- if (sessionIdRef.current !== requestedSessionId) return;
18786
- setFile(nextFile);
18787
- } catch (error) {
18788
- if (sessionIdRef.current !== requestedSessionId) return;
18789
- setFile(null);
18790
- setFileError(error.message);
18791
- } finally {
18792
- if (sessionIdRef.current === requestedSessionId) {
18793
- setFileLoading(false);
18794
- }
18795
- }
18796
- },
18797
- [adapter, sessionId]
18798
- );
18799
19055
  react.useEffect(() => {
18800
19056
  void fetchTree();
18801
19057
  }, [fetchTree]);
@@ -18808,8 +19064,6 @@ function WorkspacePanel({
18808
19064
  }, [refreshSignal, sessionId, fetchTree]);
18809
19065
  react.useEffect(() => {
18810
19066
  if (!sessionId) {
18811
- setFile(null);
18812
- setFileError(null);
18813
19067
  onSelectedPathChange(null);
18814
19068
  }
18815
19069
  }, [onSelectedPathChange, sessionId]);
@@ -18827,22 +19081,15 @@ function WorkspacePanel({
18827
19081
  return next;
18828
19082
  });
18829
19083
  }, [selectedPath]);
18830
- react.useEffect(() => {
18831
- if (!selectedPath || !sessionId || entries.length === 0) return;
18832
- const entry = findEntry(entries, selectedPath);
18833
- if (entry?.kind === "file" && file?.path !== selectedPath) {
18834
- void fetchFile(selectedPath);
18835
- }
18836
- }, [entries, fetchFile, file?.path, selectedPath, sessionId]);
18837
19084
  const handleSelect = react.useCallback(
18838
19085
  (path) => {
18839
19086
  onSelectedPathChange(path);
18840
19087
  const entry = findEntry(entries, path);
18841
19088
  if (entry?.kind === "file") {
18842
- void fetchFile(path);
19089
+ onOpenFile?.(path);
18843
19090
  }
18844
19091
  },
18845
- [entries, fetchFile, onSelectedPathChange]
19092
+ [entries, onOpenFile, onSelectedPathChange]
18846
19093
  );
18847
19094
  const handleUpload = react.useCallback(
18848
19095
  async (files) => {
@@ -18876,8 +19123,6 @@ function WorkspacePanel({
18876
19123
  await adapter.deleteWorkspaceFile({ sessionId, path });
18877
19124
  if (selectedPath === path) {
18878
19125
  onSelectedPathChange(null);
18879
- setFile(null);
18880
- setFileError(null);
18881
19126
  }
18882
19127
  setNotice(`Deleted ${path.split("/").pop() ?? path}`);
18883
19128
  await fetchTree();
@@ -18933,7 +19178,10 @@ function WorkspacePanel({
18933
19178
  {
18934
19179
  "data-testid": "workspace-panel",
18935
19180
  className: cn(
18936
- "relative z-10 flex h-full min-h-0 flex-col overflow-hidden border-l border-muted-foreground/20 bg-card",
19181
+ "relative z-10 flex h-full min-h-0 flex-col overflow-hidden bg-card",
19182
+ // Filling a parent means the parent draws the frame; a border
19183
+ // here would double against it.
19184
+ !fillParent && "border-l border-muted-foreground/20",
18937
19185
  (fillParent || narrow) && "w-full"
18938
19186
  ),
18939
19187
  style: fillParent || narrow ? void 0 : { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
@@ -18958,9 +19206,7 @@ function WorkspacePanel({
18958
19206
  }
18959
19207
  }
18960
19208
  ),
18961
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-14 items-center gap-2 border-b border-line px-3 py-3", children: [
18962
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FolderOpenIcon, { className: "size-4 shrink-0 text-amber-500" }),
18963
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-sm font-medium text-foreground", children: disabled ? "Workspace - Read-only" : "Workspace" }) }),
19209
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-2 border-b border-line px-3 py-1.5", children: [
18964
19210
  /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
18965
19211
  /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
18966
19212
  Button,
@@ -19047,24 +19293,6 @@ function WorkspacePanel({
19047
19293
  }
19048
19294
  )
19049
19295
  ] }) }),
19050
- /* @__PURE__ */ jsxRuntime.jsx(
19051
- FileViewer,
19052
- {
19053
- file,
19054
- loading: fileLoading,
19055
- error: fileError,
19056
- downloadUrl: file && sessionId ? adapter.getWorkspaceDownloadUrl({
19057
- sessionId,
19058
- path: file.path
19059
- }) : null,
19060
- onDelete: file && !disabled ? () => setDeleteTarget(file.path) : null,
19061
- onClose: () => {
19062
- onSelectedPathChange(null);
19063
- setFile(null);
19064
- setFileError(null);
19065
- }
19066
- }
19067
- ),
19068
19296
  /* @__PURE__ */ jsxRuntime.jsx(
19069
19297
  ConfirmDialog2,
19070
19298
  {
@@ -19162,12 +19390,15 @@ function readOnlyReasonForSession(session) {
19162
19390
  return { readOnly: false };
19163
19391
  }
19164
19392
  var RIGHT_STACK_STYLE = {
19165
- ["--right-stack-w"]: "440px"
19393
+ // Both the column's width and the chat panel's right offset read this, so
19394
+ // one value keeps them complementary. 50%: the browser shell renders a real
19395
+ // page, and page layouts are unusable in a 440px strip.
19396
+ ["--right-stack-w"]: "50%"
19166
19397
  };
19167
19398
  var MOBILE_PANE_LABELS = {
19168
19399
  chat: "Chat",
19169
19400
  browser: "Browser",
19170
- workspace: "Workspace"
19401
+ file: "Preview"
19171
19402
  };
19172
19403
  function AgentChat({
19173
19404
  adapter,
@@ -19193,9 +19424,10 @@ function AgentChat({
19193
19424
  onOpenBilling
19194
19425
  }) {
19195
19426
  const [workspacePath, setWorkspacePath] = react.useState(null);
19427
+ const [previewPath, setPreviewPath] = react.useState(null);
19196
19428
  const [mobileView, setMobileView] = react.useState("chat");
19197
- const [showBrowser, setShowBrowser] = react.useState(true);
19198
- const [showWorkspace, setShowWorkspace] = react.useState(true);
19429
+ const [openPane, setOpenPane] = react.useState(null);
19430
+ const [filesOpen, setFilesOpen] = react.useState(false);
19199
19431
  const runtime = useAgentChatRuntime({
19200
19432
  adapter,
19201
19433
  agentId,
@@ -19203,8 +19435,15 @@ function AgentChat({
19203
19435
  onSessionChange
19204
19436
  });
19205
19437
  react.useEffect(() => {
19206
- setShowWorkspace(runtime.viewMode === "expert");
19438
+ setOpenPane(null);
19439
+ setFilesOpen(false);
19207
19440
  }, [runtime.viewMode]);
19441
+ react.useEffect(() => {
19442
+ setFilesOpen(false);
19443
+ setWorkspacePath(null);
19444
+ setPreviewPath(null);
19445
+ setOpenPane(null);
19446
+ }, [sessionId]);
19208
19447
  const readOnly = readOnlyReasonForSession(runtime.session);
19209
19448
  const boardAvailable = whiteboardEnabled && isBoardSession(runtime.session);
19210
19449
  const viewMode = runtime.viewMode === "whiteboard" && !boardAvailable ? "simple" : runtime.viewMode;
@@ -19221,35 +19460,54 @@ function AgentChat({
19221
19460
  const hideTurnSummary = isSubAgentSession(runtime.session) || orchestratesDeepResearch || isBoardSession(runtime.session);
19222
19461
  const browserState = runtime.state.browser;
19223
19462
  const browserAvailable = browserState !== null && browserState.status !== "closed" && !!sessionId;
19224
- const browserVisible = browserAvailable && showBrowser;
19463
+ const browserVisible = browserAvailable && openPane === "browser";
19225
19464
  const workspaceAvailable = !!sessionId;
19226
- const workspaceVisible = workspaceAvailable && showWorkspace;
19227
- const rightStackVisible = browserVisible || workspaceVisible;
19228
- const bothPanesVisible = browserVisible && workspaceVisible;
19465
+ const filePreviewVisible = workspaceAvailable && previewPath !== null && openPane === "file";
19466
+ const rightStackVisible = browserVisible || filePreviewVisible;
19467
+ const browserPreview = useBrowserPreview({
19468
+ adapter,
19469
+ sessionId,
19470
+ enabled: browserAvailable
19471
+ });
19472
+ const browserRunning = browserAvailable;
19473
+ react.useEffect(() => {
19474
+ if (openPane === "browser" && !browserAvailable) setOpenPane(null);
19475
+ if (openPane === "file" && (!workspaceAvailable || previewPath === null))
19476
+ setOpenPane(null);
19477
+ }, [openPane, browserAvailable, workspaceAvailable, previewPath]);
19229
19478
  react.useEffect(() => {
19230
19479
  onMessagesChange?.(runtime.messages);
19231
19480
  }, [onMessagesChange, runtime.messages]);
19481
+ const handleOpenFilePreview = react.useCallback((path) => {
19482
+ setWorkspacePath(path);
19483
+ setPreviewPath(path);
19484
+ setOpenPane("file");
19485
+ setMobileView("file");
19486
+ }, []);
19487
+ const handleSelectedPathChange = react.useCallback((path) => {
19488
+ setWorkspacePath(path);
19489
+ if (path === null) setPreviewPath(null);
19490
+ }, []);
19232
19491
  const handleFileSelect = react.useCallback(
19233
19492
  (path) => {
19234
- setWorkspacePath(path);
19235
- setMobileView("workspace");
19236
- setShowWorkspace(true);
19493
+ handleOpenFilePreview(path);
19237
19494
  onFileSelect?.(path);
19238
19495
  },
19239
- [onFileSelect]
19496
+ [handleOpenFilePreview, onFileSelect]
19240
19497
  );
19241
19498
  const handleToggleBrowser = react.useCallback(() => {
19242
- setShowBrowser((prev) => !prev);
19499
+ setOpenPane((prev) => prev === "browser" ? null : "browser");
19500
+ setMobileView("browser");
19243
19501
  }, []);
19244
- const handleToggleWorkspace = react.useCallback(() => {
19245
- setShowWorkspace((prev) => !prev);
19502
+ const handleToggleFiles = react.useCallback(() => {
19503
+ setFilesOpen((open) => !open);
19246
19504
  }, []);
19247
19505
  const mobilePanes = react.useMemo(() => {
19248
19506
  const panes = ["chat"];
19249
19507
  if (browserVisible) panes.push("browser");
19250
- if (workspaceVisible) panes.push("workspace");
19508
+ if (filePreviewVisible) panes.push("file");
19251
19509
  return panes;
19252
- }, [browserVisible, workspaceVisible]);
19510
+ }, [browserVisible, filePreviewVisible]);
19253
19511
  const activeMobilePane = mobilePanes.includes(mobileView) ? mobileView : "chat";
19254
19512
  const showMobileToggle = mobilePanes.length > 1;
19255
19513
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -19274,7 +19532,7 @@ function AgentChat({
19274
19532
  // is visible. With both panes, absolute positioning lets the
19275
19533
  // browser/workspace split occupy a fixed width. Without the
19276
19534
  // right stack, the chat takes the full width.
19277
- browserVisible ? "md:relative md:flex-row" : "md:flex-row"
19535
+ rightStackVisible ? "md:relative md:flex-row" : "md:flex-row"
19278
19536
  ),
19279
19537
  style: { direction: "ltr", ...RIGHT_STACK_STYLE },
19280
19538
  children: [
@@ -19307,14 +19565,12 @@ function AgentChat({
19307
19565
  className: cn(
19308
19566
  // Phone: full width column, hidden while another pane is picked.
19309
19567
  "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
19310
- showMobileToggle && "data-[mobile-view=browser]:hidden data-[mobile-view=workspace]:hidden md:flex!",
19311
- // md+: positioning depends on whether the browser/workspace
19312
- // right stack is laid out. When the browser pane is shown
19313
- // we pin a fixed-width column on the right and absolutely
19314
- // position the chat panel beside it. Otherwise the chat
19315
- // panel just flexes alongside the workspace (or fills the
19316
- // space when nothing else is visible).
19317
- browserVisible ? "md:absolute md:inset-y-0 md:left-0 md:right-(--right-stack-w,440px) md:flex" : "md:relative md:flex-1"
19568
+ showMobileToggle && "data-[mobile-view=browser]:hidden data-[mobile-view=file]:hidden md:flex!",
19569
+ // md+: when the right column is laid out (browser pane or file
19570
+ // preview), pin it at the configured width and absolutely
19571
+ // position the chat panel beside it. Otherwise the chat panel
19572
+ // fills the space.
19573
+ rightStackVisible ? "md:absolute md:inset-y-0 md:left-0 md:right-(--right-stack-w,440px) md:flex" : "md:relative md:flex-1"
19318
19574
  ),
19319
19575
  children: viewMode === "whiteboard" ? (
19320
19576
  // The board replaces the transcript, on the same runtime
@@ -19354,12 +19610,38 @@ function AgentChat({
19354
19610
  onSelectBrowserProfile,
19355
19611
  browserProfilesEnabled,
19356
19612
  browserProfileLocked: !!sessionId,
19357
- showBrowser,
19613
+ showBrowser: openPane === "browser",
19358
19614
  onToggleBrowser: handleToggleBrowser,
19359
- showWorkspace,
19360
- onToggleWorkspace: handleToggleWorkspace,
19615
+ showWorkspace: filesOpen,
19616
+ onToggleWorkspace: handleToggleFiles,
19361
19617
  canShowBrowser: browserAvailable,
19362
19618
  canShowWorkspace: workspaceAvailable,
19619
+ paneCards: {
19620
+ browser: browserRunning ? {
19621
+ subtitle: browserState?.controlOwner ? `${browserState.controlOwner} has control` : void 0,
19622
+ thumbnail: browserPreview,
19623
+ // Show and hide on the same card, like the Files
19624
+ // accordion header and the composer's Tools item.
19625
+ onOpen: handleToggleBrowser
19626
+ } : null,
19627
+ files: workspaceAvailable ? {
19628
+ open: filesOpen,
19629
+ onToggle: handleToggleFiles,
19630
+ panel: /* @__PURE__ */ jsxRuntime.jsx(
19631
+ WorkspacePanel,
19632
+ {
19633
+ adapter,
19634
+ sessionId,
19635
+ selectedPath: workspacePath,
19636
+ onSelectedPathChange: handleSelectedPathChange,
19637
+ onOpenFile: handleOpenFilePreview,
19638
+ refreshSignal: runtime.workspaceRefreshKey,
19639
+ disabled: effectiveDisabled,
19640
+ fillParent: true
19641
+ }
19642
+ )
19643
+ } : null
19644
+ },
19363
19645
  viewMode,
19364
19646
  onViewModeChange: runtime.setViewMode,
19365
19647
  deepResearchEnabled,
@@ -19387,13 +19669,10 @@ function AgentChat({
19387
19669
  // Phone: full width column, hidden when chat tab active.
19388
19670
  "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
19389
19671
  showMobileToggle && "data-[mobile-view=chat]:hidden md:flex!",
19390
- // md+: positioning differs depending on what is inside.
19391
- // - browser visible (with or without workspace): absolute
19392
- // right column at the configured width.
19393
- // - workspace only: relative shrink-0 column letting
19394
- // WorkspacePanel manage its own width via the resize
19395
- // handle.
19396
- 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"
19672
+ // md+: absolute right column at the configured width. The
19673
+ // browser is the only pane that lives here — files expand
19674
+ // as an accordion inside the chat column.
19675
+ "md:absolute md:inset-y-0 md:right-0 md:w-(--right-stack-w,440px) md:flex-none"
19397
19676
  ),
19398
19677
  children: [
19399
19678
  browserVisible && /* @__PURE__ */ jsxRuntime.jsx(
@@ -19401,42 +19680,25 @@ function AgentChat({
19401
19680
  {
19402
19681
  "data-testid": "browser-panel",
19403
19682
  "data-mobile-view": activeMobilePane,
19404
- className: cn(
19405
- "min-h-0 w-full overflow-hidden",
19406
- // With both panes the desktop splits the column in half.
19407
- // A phone gives whichever pane is selected the whole
19408
- // height instead — see the mobile toggle above.
19409
- bothPanesVisible ? "h-full data-[mobile-view=workspace]:hidden md:block! md:h-1/2 md:border-b md:border-line" : "h-full"
19410
- ),
19683
+ className: "min-h-0 h-full w-full overflow-hidden",
19411
19684
  children: /* @__PURE__ */ jsxRuntime.jsx(
19412
19685
  BrowserPane,
19413
19686
  {
19414
19687
  sessionId,
19415
19688
  state: browserState,
19416
19689
  adapter,
19417
- onClose: () => setShowBrowser(false)
19690
+ onClose: () => setOpenPane(null)
19418
19691
  }
19419
19692
  )
19420
19693
  }
19421
19694
  ),
19422
- workspaceVisible && /* @__PURE__ */ jsxRuntime.jsx(
19423
- "div",
19695
+ filePreviewVisible && sessionId && previewPath && /* @__PURE__ */ jsxRuntime.jsx(
19696
+ WorkspaceFileDrawer,
19424
19697
  {
19425
- "data-testid": "workspace-panel-frame",
19426
- "data-mobile-view": activeMobilePane,
19427
- 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",
19428
- children: /* @__PURE__ */ jsxRuntime.jsx(
19429
- WorkspacePanel,
19430
- {
19431
- adapter,
19432
- sessionId,
19433
- selectedPath: workspacePath,
19434
- onSelectedPathChange: setWorkspacePath,
19435
- refreshSignal: runtime.workspaceRefreshKey,
19436
- disabled: effectiveDisabled,
19437
- fillParent: bothPanesVisible
19438
- }
19439
- )
19698
+ adapter,
19699
+ sessionId,
19700
+ path: previewPath,
19701
+ onClose: () => setOpenPane(null)
19440
19702
  }
19441
19703
  )
19442
19704
  ]
@@ -23463,7 +23725,7 @@ exports.AgentChat = AgentChat;
23463
23725
  exports.AgentChatAdapterProvider = AgentChatAdapterProvider;
23464
23726
  exports.AgentWhiteboard = AgentWhiteboard;
23465
23727
  exports.BrandBeam = BrandBeam;
23466
- exports.BrowserLiveView = BrowserLiveView;
23728
+ exports.BrowserShell = BrowserShell;
23467
23729
  exports.CitationText = CitationText;
23468
23730
  exports.CodingAgentsPanel = CodingAgentsPanel;
23469
23731
  exports.ComposioConnectCard = ComposioConnectCard;
@@ -23508,6 +23770,7 @@ exports.stripMissionControlMarkup = stripMissionControlMarkup;
23508
23770
  exports.toolOrbState = toolOrbState;
23509
23771
  exports.useAgentChatAdapterContext = useAgentChatAdapterContext;
23510
23772
  exports.useAgentChatRuntime = useAgentChatRuntime;
23773
+ exports.useBrowserControl = useBrowserControl;
23511
23774
  exports.useChatViewMode = useChatViewMode;
23512
23775
  exports.useInboxUnreadCount = useInboxUnreadCount;
23513
23776
  exports.useMissionEvents = useMissionEvents;