@invergent/agent-chat-react 1.6.1 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { cn } from './chunk-QSC4UIVT.js';
2
2
  import { createContext, memo, useRef, useState, useEffect, useCallback, useMemo, lazy, useContext, Suspense, Children } from 'react';
3
- import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, ChevronRightIcon, GitBranchIcon, XIcon, ThumbsUpIcon, ThumbsDownIcon, ActivityIcon, Loader2Icon, GlobeIcon, SearchIcon, ListTodoIcon, XCircleIcon, CheckCircle2Icon, Code, CheckIcon, CopyIcon, TerminalIcon, Trash2Icon, CircleDotIcon, CircleCheckIcon, CircleXIcon, SkullIcon, ClockIcon, CheckCircleIcon, CircleIcon, CalendarClockIcon, UsersIcon, MessageSquareIcon, ZapIcon, Maximize2Icon, PlayIcon, FolderOpenIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, SquareIcon, ArrowDownIcon, RotateCcwIcon, MousePointer2Icon, DownloadIcon, TrashIcon, ImageIcon, FileTextIcon, FolderIcon, FileIcon, ChevronLeftIcon, MinusIcon, PlusIcon, CornerDownLeftIcon } from 'lucide-react';
3
+ import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, ChevronRightIcon, GitBranchIcon, XIcon, ThumbsUpIcon, ThumbsDownIcon, ActivityIcon, Loader2Icon, EyeIcon, GlobeIcon, SearchIcon, ListTodoIcon, XCircleIcon, CheckCircle2Icon, Code, CheckIcon, CopyIcon, TerminalIcon, Trash2Icon, CircleDotIcon, CircleCheckIcon, CircleXIcon, SkullIcon, ClockIcon, CheckCircleIcon, CircleIcon, InboxIcon, CalendarClockIcon, UsersIcon, MessageSquareIcon, ZapIcon, Maximize2Icon, FolderOpenIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, ExternalLinkIcon, ClipboardCheckIcon, ShieldAlertIcon, TimerIcon, SquareIcon, ArrowDownIcon, RotateCcwIcon, MousePointer2Icon, DownloadIcon, TrashIcon, ImageIcon, FileTextIcon, FolderIcon, FileIcon, ChevronLeftIcon, MinusIcon, PlusIcon, CornerDownLeftIcon } from 'lucide-react';
4
4
  import { cva } from 'class-variance-authority';
5
5
  import { Collapsible as Collapsible$1, Slot, Dialog as Dialog$1, ScrollArea as ScrollArea$1, Tooltip as Tooltip$1, Tabs as Tabs$1, Popover as Popover$1, DropdownMenu as DropdownMenu$1, HoverCard as HoverCard$1, Progress as Progress$1 } from 'radix-ui';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -82,7 +82,9 @@ function Button({
82
82
  function BrowserControlBar({
83
83
  sessionId,
84
84
  hasControl,
85
- adapter
85
+ adapter,
86
+ onControlAcquired,
87
+ onControlReleased
86
88
  }) {
87
89
  const [pending, setPending] = useState(false);
88
90
  const [error, setError] = useState(null);
@@ -93,8 +95,10 @@ function BrowserControlBar({
93
95
  try {
94
96
  if (hasControl) {
95
97
  await adapter.releaseBrowserControl(sessionId);
98
+ onControlReleased?.();
96
99
  } else {
97
100
  await adapter.acquireBrowserControl(sessionId);
101
+ onControlAcquired?.();
98
102
  }
99
103
  } catch (nextError) {
100
104
  setError(nextError.message);
@@ -333,19 +337,95 @@ function TooltipContent({
333
337
  }
334
338
  function BrowserPane({ sessionId, state, adapter }) {
335
339
  const [fullscreenOpen, setFullscreenOpen] = useState(false);
336
- const [inlinePreviewOpen, setInlinePreviewOpen] = useState(false);
340
+ const [openFullscreenOnControl, setOpenFullscreenOnControl] = useState(false);
341
+ const [localControlActive, setLocalControlActive] = useState(false);
342
+ const [previewSnapshot, setPreviewSnapshot] = useState(null);
343
+ const [previewLoading, setPreviewLoading] = useState(false);
344
+ const [previewError, setPreviewError] = useState(null);
337
345
  const hasLiveViewAdapter = typeof adapter.browserLiveViewUrl === "function";
346
+ const hasPreviewAdapter = typeof adapter.getBrowserPreviewSnapshot === "function";
338
347
  const hasControlAdapter = typeof adapter.acquireBrowserControl === "function" && typeof adapter.releaseBrowserControl === "function";
339
348
  const liveViewUrl = useMemo(() => {
340
349
  if (!hasLiveViewAdapter) return "";
341
350
  return adapter.browserLiveViewUrl(sessionId);
342
351
  }, [adapter, hasLiveViewAdapter, sessionId]);
343
352
  const hasLiveView = state.status !== "provisioning" && state.status !== "closed";
344
- const canOpenFullscreen = hasLiveView && Boolean(liveViewUrl);
353
+ const hasUserControl = localControlActive;
354
+ const canUseLiveView = hasLiveView && hasUserControl && Boolean(liveViewUrl);
355
+ const canOpenPreview = hasLiveView && (hasPreviewAdapter || canUseLiveView);
356
+ const canOpenFullscreen = canOpenPreview;
357
+ const refreshPreview = useCallback(
358
+ async (signal) => {
359
+ if (!hasPreviewAdapter || canUseLiveView) return;
360
+ setPreviewLoading(true);
361
+ setPreviewError(null);
362
+ try {
363
+ const snapshot = await adapter.getBrowserPreviewSnapshot?.(sessionId);
364
+ if (signal?.aborted) return;
365
+ setPreviewSnapshot(snapshot ?? null);
366
+ if (!snapshot) {
367
+ setPreviewError("Browser preview is unavailable.");
368
+ }
369
+ } catch (error) {
370
+ if (signal?.aborted) return;
371
+ setPreviewSnapshot(null);
372
+ setPreviewError(
373
+ error instanceof Error ? error.message : "Browser preview failed."
374
+ );
375
+ } finally {
376
+ if (!signal?.aborted) setPreviewLoading(false);
377
+ }
378
+ },
379
+ [adapter, canUseLiveView, hasPreviewAdapter, sessionId]
380
+ );
345
381
  useEffect(() => {
346
382
  setFullscreenOpen(false);
347
- setInlinePreviewOpen(false);
383
+ setOpenFullscreenOnControl(false);
384
+ setLocalControlActive(false);
385
+ setPreviewSnapshot(null);
386
+ setPreviewLoading(false);
387
+ setPreviewError(null);
348
388
  }, [sessionId]);
389
+ useEffect(() => {
390
+ if (!openFullscreenOnControl) return;
391
+ if (!canUseLiveView) return;
392
+ setFullscreenOpen(true);
393
+ setOpenFullscreenOnControl(false);
394
+ }, [canUseLiveView, openFullscreenOnControl]);
395
+ const handleFullscreenOpenChange = useCallback(
396
+ (open) => {
397
+ setFullscreenOpen(open);
398
+ if (open || !hasUserControl || !hasControlAdapter) return;
399
+ setLocalControlActive(false);
400
+ void adapter.releaseBrowserControl(sessionId).catch((error) => {
401
+ console.error("Failed to release browser control", error);
402
+ });
403
+ },
404
+ [adapter, hasControlAdapter, hasUserControl, sessionId]
405
+ );
406
+ useEffect(() => {
407
+ if (!hasLiveView || canUseLiveView) {
408
+ setPreviewLoading(false);
409
+ setPreviewError(null);
410
+ return;
411
+ }
412
+ if (!hasPreviewAdapter) return;
413
+ const abort = new AbortController();
414
+ void refreshPreview(abort.signal);
415
+ const timer = window.setInterval(() => {
416
+ void refreshPreview(abort.signal);
417
+ }, 5e3);
418
+ return () => {
419
+ abort.abort();
420
+ window.clearInterval(timer);
421
+ };
422
+ }, [
423
+ canUseLiveView,
424
+ fullscreenOpen,
425
+ hasLiveView,
426
+ hasPreviewAdapter,
427
+ refreshPreview
428
+ ]);
349
429
  return /* @__PURE__ */ jsxs(Fragment, { children: [
350
430
  /* @__PURE__ */ jsxs(
351
431
  "div",
@@ -375,31 +455,31 @@ function BrowserPane({ sessionId, state, adapter }) {
375
455
  /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: "Full screen" })
376
456
  ] }) }) })
377
457
  ] }),
378
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-black", children: state.status === "provisioning" ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Starting browser..." }) : state.status === "closed" ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser closed." }) : liveViewUrl && inlinePreviewOpen ? /* @__PURE__ */ jsx(BrowserLiveView, { src: liveViewUrl }) : liveViewUrl ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: /* @__PURE__ */ jsxs(
379
- "button",
458
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-black", children: state.status === "provisioning" ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Starting browser..." }) : state.status === "closed" ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser closed." }) : canUseLiveView ? /* @__PURE__ */ jsx(BrowserLiveView, { src: liveViewUrl }) : previewSnapshot ? /* @__PURE__ */ jsx(
459
+ BrowserPreviewImage,
380
460
  {
381
- type: "button",
382
- "aria-label": "Open browser live preview",
383
- className: "inline-flex items-center gap-2 border border-line bg-card px-3 py-2 text-xs font-medium text-foreground transition-colors hover:bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
384
- onClick: () => setInlinePreviewOpen(true),
385
- children: [
386
- /* @__PURE__ */ jsx(PlayIcon, { className: "size-3.5", "aria-hidden": "true" }),
387
- /* @__PURE__ */ jsx("span", { children: "Open live preview" })
388
- ]
461
+ src: previewSnapshot.src,
462
+ fit: "contain",
463
+ testId: "browser-preview-image"
389
464
  }
390
- ) }) : /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser live view is unavailable." }) }),
465
+ ) : previewLoading ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Loading browser preview..." }) : previewError ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: previewError }) : /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser preview is unavailable." }) }),
391
466
  hasLiveView && hasControlAdapter && /* @__PURE__ */ jsx(
392
467
  BrowserControlBar,
393
468
  {
394
469
  sessionId,
395
- hasControl: state.status === "user-control",
396
- adapter
470
+ hasControl: localControlActive,
471
+ adapter,
472
+ onControlAcquired: () => {
473
+ setLocalControlActive(true);
474
+ setOpenFullscreenOnControl(true);
475
+ },
476
+ onControlReleased: () => setLocalControlActive(false)
397
477
  }
398
478
  )
399
479
  ]
400
480
  }
401
481
  ),
402
- /* @__PURE__ */ jsx(Dialog, { open: fullscreenOpen, onOpenChange: setFullscreenOpen, children: /* @__PURE__ */ jsxs(
482
+ /* @__PURE__ */ jsx(Dialog, { open: fullscreenOpen, onOpenChange: handleFullscreenOpenChange, children: /* @__PURE__ */ jsxs(
403
483
  DialogContent,
404
484
  {
405
485
  "aria-describedby": void 0,
@@ -414,18 +494,41 @@ function BrowserPane({ sessionId, state, adapter }) {
414
494
  " has control"
415
495
  ] })
416
496
  ] }),
417
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-black", children: liveViewUrl ? /* @__PURE__ */ jsx(
497
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-black", children: canUseLiveView ? /* @__PURE__ */ jsx(
418
498
  BrowserLiveView,
419
499
  {
420
500
  src: liveViewUrl,
421
501
  testId: "browser-fullscreen-iframe"
422
502
  }
423
- ) : /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser live view is unavailable." }) })
503
+ ) : previewSnapshot ? /* @__PURE__ */ jsx(
504
+ BrowserPreviewImage,
505
+ {
506
+ src: previewSnapshot.src,
507
+ fit: "contain",
508
+ testId: "browser-fullscreen-preview-image"
509
+ }
510
+ ) : previewLoading ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Loading browser preview..." }) : previewError ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: previewError }) : /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser preview is unavailable." }) })
424
511
  ]
425
512
  }
426
513
  ) })
427
514
  ] });
428
515
  }
516
+ function BrowserPreviewImage({
517
+ src,
518
+ fit,
519
+ testId
520
+ }) {
521
+ return /* @__PURE__ */ jsx(
522
+ "img",
523
+ {
524
+ "data-testid": testId,
525
+ src,
526
+ alt: "Browser preview",
527
+ className: `h-full w-full bg-black ${fit === "cover" ? "object-cover object-top" : "object-contain"}`,
528
+ draggable: false
529
+ }
530
+ );
531
+ }
429
532
  var Conversation = ({ className, ...props }) => /* @__PURE__ */ jsx(
430
533
  StickToBottom,
431
534
  {
@@ -867,66 +970,82 @@ function TimelineSeparator({
867
970
  );
868
971
  }
869
972
  function BrowserActivityGroup({ calls }) {
870
- const [open, setOpen] = useState(false);
871
- const latest = calls[calls.length - 1];
872
- return /* @__PURE__ */ jsxs("div", { className: "rounded-none border border-line bg-card text-xs", children: [
873
- /* @__PURE__ */ jsxs(
874
- "button",
875
- {
876
- type: "button",
877
- onClick: () => setOpen((current) => !current),
878
- className: "flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted/40",
879
- children: [
880
- open ? /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3 shrink-0", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-3 shrink-0", "aria-hidden": "true" }),
881
- /* @__PURE__ */ jsx(ZapIcon, { className: "size-3 shrink-0 text-amber-500", "aria-hidden": "true" }),
882
- /* @__PURE__ */ jsx("span", { className: "font-semibold text-foreground", children: "browser" }),
883
- /* @__PURE__ */ jsxs("span", { className: "min-w-0 truncate text-muted-foreground", children: [
884
- "(",
885
- calls.length,
886
- " ",
887
- calls.length === 1 ? "action" : "actions",
888
- " - latest:",
889
- " ",
890
- latest ? summarise(latest) : "none",
891
- ")"
892
- ] })
893
- ]
894
- }
895
- ),
896
- open && /* @__PURE__ */ jsx("ul", { className: "border-t border-line py-1", children: calls.map((call) => {
973
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border border-line bg-card px-3 py-2 text-xs", children: [
974
+ /* @__PURE__ */ jsx(ZapIcon, { className: "size-3 shrink-0 text-amber-500", "aria-hidden": "true" }),
975
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 font-semibold text-foreground", children: "Browser:" }),
976
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate text-muted-foreground", children: calls.map((call, index) => {
897
977
  const error = stringValue(parseJson(call.result).error);
898
- return /* @__PURE__ */ jsxs(
899
- "li",
900
- {
901
- className: "flex items-center gap-2 px-6 py-1 font-mono text-[11px]",
902
- children: [
903
- error && /* @__PURE__ */ jsx(
904
- AlertCircleIcon,
905
- {
906
- "data-testid": `activity-error-${call.id}`,
907
- className: "size-3 shrink-0 text-destructive",
908
- "aria-hidden": "true"
909
- }
910
- ),
911
- /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: summarise(call) }),
912
- error && /* @__PURE__ */ jsx("span", { className: "ml-auto max-w-40 truncate text-destructive", children: error })
913
- ]
914
- },
915
- call.id
916
- );
978
+ return /* @__PURE__ */ jsxs("span", { children: [
979
+ index > 0 && ", ",
980
+ /* @__PURE__ */ jsx(
981
+ "span",
982
+ {
983
+ className: error ? "text-destructive" : void 0,
984
+ "data-testid": error ? `activity-error-${call.id}` : void 0,
985
+ title: error || void 0,
986
+ children: summarize(call)
987
+ }
988
+ )
989
+ ] }, call.id);
917
990
  }) })
918
991
  ] });
919
992
  }
920
- function summarise(call) {
921
- const verb = call.toolName.replace(/^browser_/, "");
993
+ function summarize(call) {
922
994
  const args = parseJson(call.args);
923
- const ref = stringValue(args.ref);
924
- const url = stringValue(args.url);
925
- const text = stringValue(args.text);
926
- if (url) return `${verb} ${url}`;
927
- if (ref) return `${verb} ${ref}`;
928
- if (text) return `${verb} "${text.slice(0, 24)}"`;
929
- return verb;
995
+ const handler = ACTION_SUMMARIES[call.toolName];
996
+ return handler ? handler(args) : call.toolName.replace(/^browser_/, "");
997
+ }
998
+ var ACTION_SUMMARIES = {
999
+ browser_navigate: (args) => `navigate to ${stringValue(args.url) || "?"}`,
1000
+ browser_get_state: () => "get state",
1001
+ browser_close: () => "close",
1002
+ browser_click: (args) => {
1003
+ const ref = stringValue(args.ref);
1004
+ if (ref) return `click ${ref}`;
1005
+ const x = asNumber(args.x);
1006
+ const y = asNumber(args.y);
1007
+ if (x !== null && y !== null) return `click (${x}, ${y})`;
1008
+ return "click";
1009
+ },
1010
+ browser_type: (args) => {
1011
+ const text = stringValue(args.text);
1012
+ const ref = stringValue(args.ref);
1013
+ const quoted = text ? `"${truncateText(text)}"` : "";
1014
+ if (!quoted) return ref ? `type into ${ref}` : "type";
1015
+ return ref ? `type ${quoted} into ${ref}` : `type ${quoted}`;
1016
+ },
1017
+ browser_press_key: (args) => {
1018
+ const keys = Array.isArray(args.keys) ? args.keys.map(String) : [];
1019
+ return keys.length ? `press ${keys.join("+")}` : "press key";
1020
+ },
1021
+ browser_scroll: (args) => {
1022
+ const x = asNumber(args.x);
1023
+ const y = asNumber(args.y);
1024
+ const dx = asNumber(args.delta_x) ?? 0;
1025
+ const dy = asNumber(args.delta_y) ?? 0;
1026
+ const delta = dx || dy ? `\u0394(${dx}, ${dy})` : "";
1027
+ const origin = x !== null && y !== null ? `at (${x}, ${y})` : "";
1028
+ return ["scroll", delta, origin].filter(Boolean).join(" ");
1029
+ },
1030
+ browser_drag: (args) => {
1031
+ const path = Array.isArray(args.path) ? args.path : [];
1032
+ if (path.length < 2) return "drag";
1033
+ const first = path[0];
1034
+ const last = path[path.length - 1];
1035
+ const point = (p) => `(${asNumber(p[0]) ?? "?"}, ${asNumber(p[1]) ?? "?"})`;
1036
+ return `drag ${point(first)} \u2192 ${point(last)}`;
1037
+ },
1038
+ browser_wait: (args) => {
1039
+ const ms = asNumber(args.ms);
1040
+ return ms === null ? "wait" : `wait ${ms}ms`;
1041
+ },
1042
+ browser_screenshot: (args) => args.annotate === true ? "screenshot (annotated)" : "screenshot"
1043
+ };
1044
+ function truncateText(text, max = 32) {
1045
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1046
+ }
1047
+ function asNumber(value) {
1048
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
930
1049
  }
931
1050
  function parseJson(raw) {
932
1051
  if (!raw) return {};
@@ -1966,6 +2085,20 @@ function WebToolBlock({ tc }) {
1966
2085
  displayText && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/70 truncate text-xs", children: displayText })
1967
2086
  ] });
1968
2087
  }
2088
+ function VisionAnalyzeBlock({ tc }) {
2089
+ let image = "";
2090
+ try {
2091
+ const args = JSON.parse(tc.args);
2092
+ const ref = String(args.image ?? args.image_url ?? args.image_path ?? "");
2093
+ image = ref.split("/").pop() ?? ref;
2094
+ } catch {
2095
+ }
2096
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm py-0.5", children: [
2097
+ /* @__PURE__ */ jsx(EyeIcon, { className: "size-3.5 text-muted-foreground/60 shrink-0" }),
2098
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: "Vision Analyze" }),
2099
+ image && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/70 truncate text-xs", children: image })
2100
+ ] });
2101
+ }
1969
2102
  function splitChangeLines(change) {
1970
2103
  const v = change.value;
1971
2104
  if (v === "") return [];
@@ -3903,6 +4036,8 @@ function ToolCallBlock({
3903
4036
  case "web_search":
3904
4037
  case "web_crawl":
3905
4038
  return /* @__PURE__ */ jsx(WebToolBlock, { tc });
4039
+ case "vision_analyze":
4040
+ return /* @__PURE__ */ jsx(VisionAnalyzeBlock, { tc });
3906
4041
  case "read_file":
3907
4042
  return /* @__PURE__ */ jsx(ReadFileBlock, { tc, onFileSelect });
3908
4043
  case "write_file":
@@ -5396,18 +5531,21 @@ function ChatComposerInner({
5396
5531
  [menuOpen, filteredCommands, selectedIndex, handleCommandSelect, textInput]
5397
5532
  );
5398
5533
  const handleSubmit = useCallback(
5399
- (message) => {
5534
+ async (message) => {
5400
5535
  const text = message.text.trim();
5401
- if (!text || isRunning || disabled) return;
5536
+ if (!text || disabled) return;
5402
5537
  const images = [];
5403
5538
  for (const file of message.files) {
5404
5539
  if (file.mediaType?.startsWith("image/") && file.url) {
5405
5540
  images.push({ data: file.url, mimeType: file.mediaType });
5406
5541
  }
5407
5542
  }
5408
- onSend(text, images.length > 0 ? images : void 0);
5543
+ if (isRunning) {
5544
+ await onStop();
5545
+ }
5546
+ await onSend(text, images.length > 0 ? images : void 0);
5409
5547
  },
5410
- [onSend, isRunning, disabled]
5548
+ [onSend, onStop, isRunning, disabled]
5411
5549
  );
5412
5550
  return /* @__PURE__ */ jsxs(Popover, { open: menuOpen, onOpenChange: (open) => {
5413
5551
  if (!open) {
@@ -7249,10 +7387,9 @@ function WorkspacePanel({
7249
7387
  sessionId,
7250
7388
  selectedPath,
7251
7389
  onSelectedPathChange,
7252
- collapsed = false,
7253
- onCollapsedChange,
7254
7390
  refreshSignal = 0,
7255
- disabled = false
7391
+ disabled = false,
7392
+ fillParent = false
7256
7393
  }) {
7257
7394
  const fileInputRef = useRef(null);
7258
7395
  const [entries, setEntries] = useState([]);
@@ -7453,34 +7590,17 @@ function WorkspacePanel({
7453
7590
  window.removeEventListener("mouseup", onMouseUp);
7454
7591
  };
7455
7592
  }, []);
7456
- if (collapsed) {
7457
- return /* @__PURE__ */ jsx(
7458
- "aside",
7459
- {
7460
- "data-testid": "workspace-panel",
7461
- role: "button",
7462
- tabIndex: 0,
7463
- className: "relative z-10 flex min-h-0 w-10 shrink-0 cursor-pointer items-center justify-center border-l border-muted-foreground/20 bg-card text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30",
7464
- "aria-label": "Expand workspace",
7465
- onClick: () => onCollapsedChange?.(false),
7466
- onKeyDown: (event) => {
7467
- if (event.key === "Enter" || event.key === " ") {
7468
- event.preventDefault();
7469
- onCollapsedChange?.(false);
7470
- }
7471
- },
7472
- children: /* @__PURE__ */ jsx(FolderOpenIcon, { className: "size-4 text-amber-500" })
7473
- }
7474
- );
7475
- }
7476
7593
  return /* @__PURE__ */ jsxs(
7477
7594
  "aside",
7478
7595
  {
7479
7596
  "data-testid": "workspace-panel",
7480
- className: "relative z-10 flex min-h-0 flex-col overflow-hidden border-l border-muted-foreground/20 bg-card",
7481
- style: { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
7597
+ className: cn(
7598
+ "relative z-10 flex h-full min-h-0 flex-col overflow-hidden border-l border-muted-foreground/20 bg-card",
7599
+ fillParent && "w-full"
7600
+ ),
7601
+ style: fillParent ? void 0 : { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
7482
7602
  children: [
7483
- /* @__PURE__ */ jsx(
7603
+ !fillParent && /* @__PURE__ */ jsx(
7484
7604
  "div",
7485
7605
  {
7486
7606
  className: "absolute inset-y-0 left-0 z-20 w-1.5 cursor-col-resize transition-colors hover:bg-primary/20 active:bg-primary/30",
@@ -7506,19 +7626,6 @@ function WorkspacePanel({
7506
7626
  /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium text-foreground", children: rootName }),
7507
7627
  /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-faint", children: disabled ? "Workspace - Read-only" : "Workspace" })
7508
7628
  ] }),
7509
- /* @__PURE__ */ jsxs(Tooltip, { children: [
7510
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
7511
- Button,
7512
- {
7513
- variant: "ghost",
7514
- size: "icon-sm",
7515
- onClick: () => onCollapsedChange?.(true),
7516
- "aria-label": "Collapse workspace",
7517
- children: /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-4" })
7518
- }
7519
- ) }),
7520
- /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: "Collapse" })
7521
- ] }),
7522
7629
  /* @__PURE__ */ jsxs(Tooltip, { children: [
7523
7630
  /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
7524
7631
  Button,
@@ -7691,7 +7798,8 @@ var WORKSPACE_MUTATING_TOOLS = /* @__PURE__ */ new Set([
7691
7798
  "terminal",
7692
7799
  "write_file",
7693
7800
  "patch",
7694
- "execute_code"
7801
+ "execute_code",
7802
+ "browser_screenshot"
7695
7803
  ]);
7696
7804
  var AGENT_CHAT_LISTENED_EVENTS = [
7697
7805
  "user.message",
@@ -8617,7 +8725,6 @@ function AgentChat({
8617
8725
  disabled
8618
8726
  }) {
8619
8727
  const [workspacePath, setWorkspacePath] = useState(null);
8620
- const [workspaceCollapsed, setWorkspaceCollapsed] = useState(false);
8621
8728
  const runtime = useAgentChatRuntime({
8622
8729
  adapter,
8623
8730
  agentId,
@@ -8628,6 +8735,7 @@ function AgentChat({
8628
8735
  const effectiveDisabled = disabled || isReadOnlySession;
8629
8736
  const disabledReason = isReadOnlySession ? "Scheduled run is read-only" : void 0;
8630
8737
  const browserState = runtime.state.browser;
8738
+ const hasBrowserPanel = browserState !== null && sessionId;
8631
8739
  useEffect(() => {
8632
8740
  onMessagesChange?.(runtime.messages);
8633
8741
  }, [onMessagesChange, runtime.messages]);
@@ -8646,52 +8754,767 @@ function AgentChat({
8646
8754
  sessionId,
8647
8755
  onFileSelect: handleFileSelect
8648
8756
  },
8649
- children: /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs("section", { className: "flex flex-1 min-h-0 overflow-hidden bg-background text-sm text-foreground", children: [
8650
- /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: /* @__PURE__ */ jsx(
8651
- ChatThread,
8757
+ children: /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(
8758
+ "section",
8759
+ {
8760
+ "data-testid": "agent-chat-layout",
8761
+ className: hasBrowserPanel ? "relative min-h-0 flex-1 overflow-hidden bg-background text-sm text-foreground" : "flex min-h-0 flex-1 overflow-hidden bg-background text-sm text-foreground",
8762
+ style: { direction: "ltr" },
8763
+ children: [
8764
+ /* @__PURE__ */ jsx(
8765
+ "div",
8766
+ {
8767
+ "data-testid": "chat-panel",
8768
+ className: hasBrowserPanel ? "absolute inset-y-0 left-0 flex min-h-0 min-w-0 flex-col overflow-hidden" : "flex min-w-0 flex-1 flex-col overflow-hidden",
8769
+ style: hasBrowserPanel ? { right: 440 } : void 0,
8770
+ children: /* @__PURE__ */ jsx(
8771
+ ChatThread,
8772
+ {
8773
+ sessionId,
8774
+ messages: runtime.messages,
8775
+ isRunning: runtime.isRunning,
8776
+ isLoadingHistory: runtime.isLoadingHistory,
8777
+ onSend: (content, images) => runtime.send(content, images),
8778
+ onStop: () => runtime.stop(),
8779
+ onRetry: runtime.retry,
8780
+ onFileSelect: handleFileSelect,
8781
+ disabled: effectiveDisabled,
8782
+ disabledReason,
8783
+ tokenUsage: runtime.tokenUsage,
8784
+ retryIndicator: runtime.retryIndicator
8785
+ }
8786
+ )
8787
+ }
8788
+ ),
8789
+ /* @__PURE__ */ jsxs(
8790
+ "div",
8791
+ {
8792
+ "data-testid": "right-stack",
8793
+ className: hasBrowserPanel ? "absolute inset-y-0 flex min-h-0 flex-col overflow-hidden" : "flex min-h-0 shrink-0 flex-col",
8794
+ style: hasBrowserPanel ? { right: 0, width: 440 } : void 0,
8795
+ children: [
8796
+ hasBrowserPanel && /* @__PURE__ */ jsx(
8797
+ "div",
8798
+ {
8799
+ "data-testid": "browser-panel",
8800
+ className: "h-1/2 min-h-0 w-full overflow-hidden border-b border-line",
8801
+ children: /* @__PURE__ */ jsx(
8802
+ BrowserPane,
8803
+ {
8804
+ sessionId,
8805
+ state: browserState,
8806
+ adapter
8807
+ }
8808
+ )
8809
+ }
8810
+ ),
8811
+ /* @__PURE__ */ jsx(
8812
+ "div",
8813
+ {
8814
+ "data-testid": "workspace-panel-frame",
8815
+ className: hasBrowserPanel ? "h-1/2 min-h-0 w-full overflow-hidden" : "min-h-0",
8816
+ children: /* @__PURE__ */ jsx(
8817
+ WorkspacePanel,
8818
+ {
8819
+ adapter,
8820
+ sessionId,
8821
+ selectedPath: workspacePath,
8822
+ onSelectedPathChange: setWorkspacePath,
8823
+ refreshSignal: runtime.workspaceRefreshKey,
8824
+ disabled: effectiveDisabled,
8825
+ fillParent: Boolean(hasBrowserPanel)
8826
+ }
8827
+ )
8828
+ }
8829
+ )
8830
+ ]
8831
+ }
8832
+ )
8833
+ ]
8834
+ }
8835
+ ) })
8836
+ }
8837
+ );
8838
+ }
8839
+ var DEFAULT_LIMIT = 50;
8840
+ function requireInboxAdapter(adapter) {
8841
+ if (!adapter.listInbox || !adapter.getInboxItem || !adapter.markInboxItemRead || !adapter.acknowledgeInboxItem || !adapter.respondGovernanceInboxItem || !adapter.openInboxStream) {
8842
+ throw new Error("Inbox is not supported by this adapter.");
8843
+ }
8844
+ return adapter;
8845
+ }
8846
+ var KIND_LABEL2 = {
8847
+ input_required: "Input needed",
8848
+ action_required: "Action needed",
8849
+ task_complete: "Task complete",
8850
+ governance_gate: "Approval",
8851
+ progress_checkin: "Progress"
8852
+ };
8853
+ function kindLabel(kind) {
8854
+ return KIND_LABEL2[kind] ?? kind.replace(/[_-]+/g, " ");
8855
+ }
8856
+ function statusLabel(status) {
8857
+ return status.split(/[_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
8858
+ }
8859
+ function formatRelative(value) {
8860
+ const date = new Date(value);
8861
+ if (Number.isNaN(date.getTime())) return "";
8862
+ return formatDistanceToNow(date, { addSuffix: true });
8863
+ }
8864
+ function kindIcon(kind) {
8865
+ if (kind === "input_required") return MessageSquareIcon;
8866
+ if (kind === "action_required") return ExternalLinkIcon;
8867
+ if (kind === "task_complete") return ClipboardCheckIcon;
8868
+ if (kind === "governance_gate") return ShieldAlertIcon;
8869
+ if (kind === "progress_checkin") return TimerIcon;
8870
+ return CircleDotIcon;
8871
+ }
8872
+ function sortItems(items) {
8873
+ return [...items].sort((a, b) => {
8874
+ if (a.createdAt !== b.createdAt) return a.createdAt > b.createdAt ? -1 : 1;
8875
+ return b.id - a.id;
8876
+ });
8877
+ }
8878
+ function QuestionInput({
8879
+ prompt,
8880
+ choices,
8881
+ disabled,
8882
+ value,
8883
+ onChange
8884
+ }) {
8885
+ if (choices && choices.length > 0) {
8886
+ return /* @__PURE__ */ jsxs(
8887
+ "select",
8888
+ {
8889
+ "aria-label": prompt,
8890
+ className: "h-9 w-full border border-line bg-background px-2 text-sm text-foreground outline-none focus:border-primary disabled:opacity-50",
8891
+ disabled,
8892
+ value,
8893
+ onChange: (event) => onChange(event.target.value),
8894
+ children: [
8895
+ /* @__PURE__ */ jsx("option", { value: "", children: "Select" }),
8896
+ choices.map((choice) => /* @__PURE__ */ jsx("option", { value: choice.label, children: choice.label }, choice.label))
8897
+ ]
8898
+ }
8899
+ );
8900
+ }
8901
+ return /* @__PURE__ */ jsx(
8902
+ "input",
8903
+ {
8904
+ "aria-label": prompt,
8905
+ className: "h-9 w-full border border-line bg-background px-2 text-sm text-foreground outline-none focus:border-primary disabled:opacity-50",
8906
+ disabled,
8907
+ value,
8908
+ onChange: (event) => onChange(event.target.value)
8909
+ }
8910
+ );
8911
+ }
8912
+ function InboxDetailActions({
8913
+ item,
8914
+ adapter,
8915
+ onDeleted,
8916
+ onSessionSelect,
8917
+ children
8918
+ }) {
8919
+ const [deleting, setDeleting] = useState(false);
8920
+ async function deleteItem() {
8921
+ if (!adapter.deleteInboxItem) return;
8922
+ setDeleting(true);
8923
+ try {
8924
+ await onDeleted(item.id);
8925
+ } finally {
8926
+ setDeleting(false);
8927
+ }
8928
+ }
8929
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
8930
+ /* @__PURE__ */ jsxs(
8931
+ Button,
8932
+ {
8933
+ type: "button",
8934
+ size: "sm",
8935
+ onClick: () => onSessionSelect?.(item.sessionId),
8936
+ "aria-label": "Open session",
8937
+ children: [
8938
+ /* @__PURE__ */ jsx(ExternalLinkIcon, { className: "size-3.5" }),
8939
+ "Open session"
8940
+ ]
8941
+ }
8942
+ ),
8943
+ children,
8944
+ adapter.deleteInboxItem && /* @__PURE__ */ jsxs(
8945
+ Button,
8946
+ {
8947
+ type: "button",
8948
+ size: "sm",
8949
+ variant: "ghost",
8950
+ onClick: () => void deleteItem(),
8951
+ disabled: deleting,
8952
+ "aria-label": "Delete inbox item",
8953
+ title: "Delete inbox item",
8954
+ children: [
8955
+ /* @__PURE__ */ jsx(Trash2Icon, { className: "size-3.5" }),
8956
+ deleting ? "Deleting" : "Delete"
8957
+ ]
8958
+ }
8959
+ )
8960
+ ] });
8961
+ }
8962
+ function InputRequiredDetail({
8963
+ item,
8964
+ adapter,
8965
+ onUpdated,
8966
+ onDeleted,
8967
+ onSessionSelect
8968
+ }) {
8969
+ const questions = Array.isArray(item.payload.questions) ? item.payload.questions.map((question) => ({
8970
+ prompt: typeof question.prompt === "string" ? question.prompt : "",
8971
+ choices: Array.isArray(question.choices) ? question.choices.map((choice) => ({
8972
+ label: typeof choice.label === "string" ? choice.label : "",
8973
+ description: typeof choice.description === "string" ? choice.description : void 0
8974
+ })).filter((choice) => choice.label) : void 0
8975
+ })).filter((question) => question.prompt) : [];
8976
+ const [answers, setAnswers] = useState({});
8977
+ const [submitting, setSubmitting] = useState(false);
8978
+ const disabled = item.status !== "pending" || submitting;
8979
+ const canSubmit = questions.every((question) => answers[question.prompt]?.trim());
8980
+ async function submit() {
8981
+ const toolCallId = typeof item.payload.tool_call_id === "string" ? item.payload.tool_call_id : "";
8982
+ if (!toolCallId || !canSubmit) return;
8983
+ setSubmitting(true);
8984
+ try {
8985
+ const responses = questions.map((question) => ({
8986
+ question: question.prompt,
8987
+ answer: answers[question.prompt]?.trim() ?? "",
8988
+ is_other: false
8989
+ }));
8990
+ await adapter.submitClarifyResponse({
8991
+ sessionId: item.sessionId,
8992
+ toolCallId,
8993
+ responses
8994
+ });
8995
+ onUpdated(await adapter.getInboxItem({ itemId: item.id }));
8996
+ } finally {
8997
+ setSubmitting(false);
8998
+ }
8999
+ }
9000
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
9001
+ item.body && /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-sm text-muted-foreground", children: item.body }),
9002
+ questions.map((question) => /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
9003
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: question.prompt }),
9004
+ /* @__PURE__ */ jsx(
9005
+ QuestionInput,
9006
+ {
9007
+ prompt: question.prompt,
9008
+ choices: question.choices,
9009
+ disabled,
9010
+ value: answers[question.prompt] ?? "",
9011
+ onChange: (value) => setAnswers((current) => ({
9012
+ ...current,
9013
+ [question.prompt]: value
9014
+ }))
9015
+ }
9016
+ )
9017
+ ] }, question.prompt)),
9018
+ /* @__PURE__ */ jsx(
9019
+ InboxDetailActions,
9020
+ {
9021
+ item,
9022
+ adapter,
9023
+ onDeleted,
9024
+ onSessionSelect,
9025
+ children: /* @__PURE__ */ jsx(
9026
+ Button,
8652
9027
  {
8653
- sessionId,
8654
- messages: runtime.messages,
8655
- isRunning: runtime.isRunning,
8656
- isLoadingHistory: runtime.isLoadingHistory,
8657
- onSend: (content, images) => void runtime.send(content, images),
8658
- onStop: () => void runtime.stop(),
8659
- onRetry: runtime.retry,
8660
- onFileSelect: handleFileSelect,
8661
- disabled: effectiveDisabled,
8662
- disabledReason,
8663
- tokenUsage: runtime.tokenUsage,
8664
- retryIndicator: runtime.retryIndicator
9028
+ type: "button",
9029
+ size: "sm",
9030
+ onClick: () => void submit(),
9031
+ disabled: disabled || !canSubmit,
9032
+ "aria-label": "Submit inbox response",
9033
+ children: submitting ? "Submitting" : "Submit"
9034
+ }
9035
+ )
9036
+ }
9037
+ )
9038
+ ] });
9039
+ }
9040
+ function AckDetail({
9041
+ item,
9042
+ adapter,
9043
+ onUpdated,
9044
+ onDeleted,
9045
+ onSessionSelect
9046
+ }) {
9047
+ async function acknowledge() {
9048
+ onUpdated(await adapter.acknowledgeInboxItem({ itemId: item.id }));
9049
+ }
9050
+ const outcome = typeof item.payload.outcome === "string" ? item.payload.outcome : "";
9051
+ const duration = typeof item.payload.duration_seconds === "number" ? item.payload.duration_seconds : null;
9052
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
9053
+ outcome && /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: outcome }),
9054
+ item.body && /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-sm", children: item.body }),
9055
+ typeof item.payload.error === "string" && /* @__PURE__ */ jsx("pre", { className: "overflow-x-auto bg-destructive/10 p-3 text-xs text-destructive", children: item.payload.error }),
9056
+ duration !== null && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
9057
+ "Duration: ",
9058
+ Math.round(duration / 60),
9059
+ " min (",
9060
+ duration,
9061
+ " s)"
9062
+ ] }),
9063
+ /* @__PURE__ */ jsx(
9064
+ InboxDetailActions,
9065
+ {
9066
+ item,
9067
+ adapter,
9068
+ onDeleted,
9069
+ onSessionSelect,
9070
+ children: item.status === "pending" && /* @__PURE__ */ jsxs(
9071
+ Button,
9072
+ {
9073
+ type: "button",
9074
+ size: "sm",
9075
+ onClick: () => void acknowledge(),
9076
+ "aria-label": "Acknowledge inbox item",
9077
+ children: [
9078
+ /* @__PURE__ */ jsx(CheckIcon, { className: "size-3.5" }),
9079
+ "Acknowledge"
9080
+ ]
8665
9081
  }
8666
- ) }),
8667
- /* @__PURE__ */ jsxs("div", { "data-testid": "right-stack", className: "flex min-h-0 shrink-0 flex-col", children: [
8668
- browserState !== null && sessionId && /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 border-b border-line", children: /* @__PURE__ */ jsx(
8669
- BrowserPane,
9082
+ )
9083
+ }
9084
+ )
9085
+ ] });
9086
+ }
9087
+ function GovernanceDetail({
9088
+ item,
9089
+ adapter,
9090
+ onUpdated,
9091
+ onDeleted,
9092
+ onSessionSelect
9093
+ }) {
9094
+ async function decide(decision) {
9095
+ onUpdated(
9096
+ await adapter.respondGovernanceInboxItem({ itemId: item.id, decision })
9097
+ );
9098
+ }
9099
+ const toolName = typeof item.payload.tool_name === "string" ? item.payload.tool_name : "tool";
9100
+ const args = typeof item.payload.arguments_excerpt === "string" ? item.payload.arguments_excerpt : "";
9101
+ const reason = typeof item.payload.deny_reason === "string" ? item.payload.deny_reason : "";
9102
+ const disabled = item.status !== "pending";
9103
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
9104
+ /* @__PURE__ */ jsxs("div", { children: [
9105
+ /* @__PURE__ */ jsxs("div", { className: "text-base font-semibold text-foreground", children: [
9106
+ "Approve ",
9107
+ toolName,
9108
+ "?"
9109
+ ] }),
9110
+ reason && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: reason })
9111
+ ] }),
9112
+ args && /* @__PURE__ */ jsx("pre", { className: "overflow-x-auto border border-line bg-muted p-3 text-xs", children: args }),
9113
+ /* @__PURE__ */ jsxs(
9114
+ InboxDetailActions,
9115
+ {
9116
+ item,
9117
+ adapter,
9118
+ onDeleted,
9119
+ onSessionSelect,
9120
+ children: [
9121
+ /* @__PURE__ */ jsx(
9122
+ Button,
8670
9123
  {
8671
- sessionId,
8672
- state: browserState,
8673
- adapter
9124
+ type: "button",
9125
+ size: "sm",
9126
+ disabled,
9127
+ onClick: () => void decide("approve"),
9128
+ children: "Approve"
8674
9129
  }
8675
- ) }),
9130
+ ),
8676
9131
  /* @__PURE__ */ jsx(
8677
- WorkspacePanel,
9132
+ Button,
8678
9133
  {
8679
- adapter,
8680
- sessionId,
8681
- selectedPath: workspacePath,
8682
- onSelectedPathChange: setWorkspacePath,
8683
- collapsed: workspaceCollapsed,
8684
- onCollapsedChange: setWorkspaceCollapsed,
8685
- refreshSignal: runtime.workspaceRefreshKey,
8686
- disabled: effectiveDisabled
9134
+ type: "button",
9135
+ size: "sm",
9136
+ variant: "destructive",
9137
+ disabled,
9138
+ onClick: () => void decide("reject"),
9139
+ children: "Reject"
8687
9140
  }
8688
9141
  )
8689
- ] })
8690
- ] }) })
9142
+ ]
9143
+ }
9144
+ )
9145
+ ] });
9146
+ }
9147
+ function ActionRequiredDetail({
9148
+ item,
9149
+ adapter,
9150
+ onUpdated,
9151
+ onDeleted,
9152
+ onSessionSelect
9153
+ }) {
9154
+ const [submitting, setSubmitting] = useState(false);
9155
+ const actionType = typeof item.payload.action_type === "string" ? item.payload.action_type : "";
9156
+ const context = typeof item.payload.context === "string" ? item.payload.context : "";
9157
+ const disabled = item.status !== "pending" || submitting || !adapter.respondActionRequiredInboxItem;
9158
+ async function complete() {
9159
+ if (!adapter.respondActionRequiredInboxItem) return;
9160
+ setSubmitting(true);
9161
+ try {
9162
+ onUpdated(await adapter.respondActionRequiredInboxItem({ itemId: item.id }));
9163
+ } finally {
9164
+ setSubmitting(false);
8691
9165
  }
9166
+ }
9167
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
9168
+ item.body && /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-sm", children: item.body }),
9169
+ context && context !== item.body && /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-sm text-muted-foreground", children: context }),
9170
+ actionType && /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: actionType }),
9171
+ /* @__PURE__ */ jsx(
9172
+ InboxDetailActions,
9173
+ {
9174
+ item,
9175
+ adapter,
9176
+ onDeleted,
9177
+ onSessionSelect,
9178
+ children: /* @__PURE__ */ jsxs(
9179
+ Button,
9180
+ {
9181
+ type: "button",
9182
+ size: "sm",
9183
+ onClick: () => void complete(),
9184
+ disabled,
9185
+ "aria-label": "Mark action complete",
9186
+ children: [
9187
+ /* @__PURE__ */ jsx(CheckIcon, { className: "size-3.5" }),
9188
+ submitting ? "Marking" : "I completed this"
9189
+ ]
9190
+ }
9191
+ )
9192
+ }
9193
+ )
9194
+ ] });
9195
+ }
9196
+ function ProgressDetail({
9197
+ item,
9198
+ adapter,
9199
+ onUpdated,
9200
+ onDeleted,
9201
+ onSessionSelect
9202
+ }) {
9203
+ async function acknowledge() {
9204
+ onUpdated(await adapter.acknowledgeInboxItem({ itemId: item.id }));
9205
+ }
9206
+ const rows = [];
9207
+ for (const row of [
9208
+ ["Iterations", item.payload.iterations],
9209
+ ["Last tool", item.payload.last_tool],
9210
+ ["Elapsed", item.payload.elapsed_seconds]
9211
+ ]) {
9212
+ if (row[1] !== void 0 && row[1] !== null && row[1] !== "") {
9213
+ rows.push(row);
9214
+ }
9215
+ }
9216
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
9217
+ item.body && /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-sm", children: item.body }),
9218
+ rows.length > 0 && /* @__PURE__ */ jsx("dl", { className: "space-y-1 text-xs text-muted-foreground", children: rows.map(([label, value]) => /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
9219
+ /* @__PURE__ */ jsx("dt", { className: "w-20 shrink-0", children: String(label) }),
9220
+ /* @__PURE__ */ jsx("dd", { className: "min-w-0", children: String(value) })
9221
+ ] }, String(label))) }),
9222
+ /* @__PURE__ */ jsx(
9223
+ InboxDetailActions,
9224
+ {
9225
+ item,
9226
+ adapter,
9227
+ onDeleted,
9228
+ onSessionSelect,
9229
+ children: item.status === "pending" && /* @__PURE__ */ jsxs(
9230
+ Button,
9231
+ {
9232
+ type: "button",
9233
+ size: "sm",
9234
+ onClick: () => void acknowledge(),
9235
+ "aria-label": "Acknowledge inbox item",
9236
+ children: [
9237
+ /* @__PURE__ */ jsx(CheckIcon, { className: "size-3.5" }),
9238
+ "Acknowledge"
9239
+ ]
9240
+ }
9241
+ )
9242
+ }
9243
+ )
9244
+ ] });
9245
+ }
9246
+ function InboxDetail({
9247
+ item,
9248
+ adapter,
9249
+ onUpdated,
9250
+ onDeleted,
9251
+ onSessionSelect
9252
+ }) {
9253
+ const Icon = kindIcon(item.kind);
9254
+ return /* @__PURE__ */ jsxs("section", { className: "min-w-0 flex-1 overflow-y-auto p-6", children: [
9255
+ /* @__PURE__ */ jsxs("div", { className: "mb-5 flex min-w-0 items-start gap-3", children: [
9256
+ /* @__PURE__ */ jsx("div", { className: "mt-0.5 flex size-8 shrink-0 items-center justify-center border border-line text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { className: "size-4" }) }),
9257
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
9258
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
9259
+ /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: kindLabel(item.kind) }),
9260
+ /* @__PURE__ */ jsx(Badge, { variant: item.status === "pending" ? "default" : "secondary", children: statusLabel(item.status) })
9261
+ ] }),
9262
+ /* @__PURE__ */ jsx("h2", { className: "mt-2 text-lg font-semibold leading-snug text-foreground", children: item.title })
9263
+ ] })
9264
+ ] }),
9265
+ item.kind === "input_required" ? /* @__PURE__ */ jsx(
9266
+ InputRequiredDetail,
9267
+ {
9268
+ item,
9269
+ adapter,
9270
+ onUpdated,
9271
+ onDeleted,
9272
+ onSessionSelect
9273
+ }
9274
+ ) : item.kind === "action_required" ? /* @__PURE__ */ jsx(
9275
+ ActionRequiredDetail,
9276
+ {
9277
+ item,
9278
+ adapter,
9279
+ onUpdated,
9280
+ onDeleted,
9281
+ onSessionSelect
9282
+ }
9283
+ ) : item.kind === "governance_gate" ? /* @__PURE__ */ jsx(
9284
+ GovernanceDetail,
9285
+ {
9286
+ item,
9287
+ adapter,
9288
+ onUpdated,
9289
+ onDeleted,
9290
+ onSessionSelect
9291
+ }
9292
+ ) : item.kind === "progress_checkin" ? /* @__PURE__ */ jsx(
9293
+ ProgressDetail,
9294
+ {
9295
+ item,
9296
+ adapter,
9297
+ onUpdated,
9298
+ onDeleted,
9299
+ onSessionSelect
9300
+ }
9301
+ ) : /* @__PURE__ */ jsx(
9302
+ AckDetail,
9303
+ {
9304
+ item,
9305
+ adapter,
9306
+ onUpdated,
9307
+ onDeleted,
9308
+ onSessionSelect
9309
+ }
9310
+ )
9311
+ ] });
9312
+ }
9313
+ function InboxPanel({
9314
+ adapter,
9315
+ title = "Inbox",
9316
+ selectedId,
9317
+ onSelectedIdChange,
9318
+ onSessionSelect,
9319
+ hideHeader = false,
9320
+ limit = DEFAULT_LIMIT
9321
+ }) {
9322
+ const inboxAdapter = useMemo(() => requireInboxAdapter(adapter), [adapter]);
9323
+ const [items, setItems] = useState([]);
9324
+ const [nextCursor, setNextCursor] = useState(null);
9325
+ const [internalSelectedId, setInternalSelectedId] = useState(null);
9326
+ const [loading, setLoading] = useState(false);
9327
+ const [error, setError] = useState(null);
9328
+ const requestId = useRef(0);
9329
+ const selectedItemId = selectedId ?? internalSelectedId;
9330
+ const selectedItem = useMemo(
9331
+ () => items.find((item) => item.id === selectedItemId) ?? null,
9332
+ [items, selectedItemId]
9333
+ );
9334
+ const applyItem = useCallback((nextItem) => {
9335
+ setItems(
9336
+ (current) => sortItems(
9337
+ current.map((item) => item.id === nextItem.id ? nextItem : item)
9338
+ )
9339
+ );
9340
+ }, []);
9341
+ const selectItem = useCallback(
9342
+ async (itemId) => {
9343
+ if (selectedId === void 0) setInternalSelectedId(itemId);
9344
+ onSelectedIdChange?.(itemId);
9345
+ const item = await inboxAdapter.getInboxItem({ itemId });
9346
+ applyItem(item);
9347
+ if (!item.readAt) {
9348
+ applyItem(await inboxAdapter.markInboxItemRead({ itemId }));
9349
+ }
9350
+ },
9351
+ [applyItem, inboxAdapter, onSelectedIdChange, selectedId]
9352
+ );
9353
+ const load = useCallback(
9354
+ async (cursor) => {
9355
+ const id = ++requestId.current;
9356
+ setLoading(true);
9357
+ setError(null);
9358
+ try {
9359
+ const response = await inboxAdapter.listInbox({
9360
+ cursor: cursor ?? void 0,
9361
+ limit
9362
+ });
9363
+ if (id !== requestId.current) return;
9364
+ setItems(
9365
+ (current) => sortItems(cursor ? [...current, ...response.items] : response.items)
9366
+ );
9367
+ setNextCursor(response.nextCursor);
9368
+ } catch (err) {
9369
+ if (id === requestId.current) {
9370
+ setError(err instanceof Error ? err.message : "Failed to load inbox");
9371
+ }
9372
+ } finally {
9373
+ if (id === requestId.current) setLoading(false);
9374
+ }
9375
+ },
9376
+ [inboxAdapter, limit]
8692
9377
  );
9378
+ useEffect(() => {
9379
+ void load(null);
9380
+ }, [load]);
9381
+ useEffect(() => {
9382
+ const stream = inboxAdapter.openInboxStream();
9383
+ stream.addEventListener("item", (event) => {
9384
+ const payload = JSON.parse(event.data);
9385
+ if (typeof payload.item_id !== "number") return;
9386
+ void inboxAdapter.getInboxItem({ itemId: payload.item_id }).then(applyItem);
9387
+ });
9388
+ return () => stream.close();
9389
+ }, [applyItem, inboxAdapter]);
9390
+ function updateSelectedItem(item) {
9391
+ applyItem(item);
9392
+ }
9393
+ const deleteItem = useCallback(
9394
+ async (itemId) => {
9395
+ if (!inboxAdapter.deleteInboxItem) return;
9396
+ await inboxAdapter.deleteInboxItem({ itemId });
9397
+ setItems((current) => current.filter((item) => item.id !== itemId));
9398
+ if (selectedItemId === itemId) {
9399
+ if (selectedId === void 0) setInternalSelectedId(null);
9400
+ onSelectedIdChange?.(null);
9401
+ }
9402
+ },
9403
+ [inboxAdapter, onSelectedIdChange, selectedId, selectedItemId]
9404
+ );
9405
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 min-w-0 bg-background text-foreground", children: [
9406
+ /* @__PURE__ */ jsxs("aside", { className: "flex w-80 min-w-72 max-w-sm shrink-0 flex-col border-r border-line", children: [
9407
+ !hideHeader && /* @__PURE__ */ jsxs("div", { className: "flex min-h-14 items-center gap-2 border-b border-line px-4", children: [
9408
+ /* @__PURE__ */ jsx(InboxIcon, { className: "size-4 text-muted-foreground" }),
9409
+ /* @__PURE__ */ jsx("h1", { className: "font-semibold", children: title })
9410
+ ] }),
9411
+ /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
9412
+ error && /* @__PURE__ */ jsx("div", { className: "px-4 py-3 text-sm text-destructive", children: error }),
9413
+ !error && items.length === 0 && !loading && /* @__PURE__ */ jsx("div", { className: "px-4 py-8 text-sm text-muted-foreground", children: "No inbox items" }),
9414
+ items.map((item) => {
9415
+ const Icon = kindIcon(item.kind);
9416
+ const selected = item.id === selectedItemId;
9417
+ return /* @__PURE__ */ jsxs(
9418
+ "button",
9419
+ {
9420
+ type: "button",
9421
+ "aria-label": `Open inbox item ${item.title}`,
9422
+ className: cn(
9423
+ "flex w-full min-w-0 gap-3 border-l-2 px-3 py-3 text-left transition-colors hover:bg-input",
9424
+ selected ? "border-l-primary bg-line text-foreground" : "border-l-transparent text-subtle",
9425
+ !item.readAt && "font-medium text-foreground"
9426
+ ),
9427
+ onClick: () => void selectItem(item.id),
9428
+ children: [
9429
+ /* @__PURE__ */ jsx(Icon, { className: "mt-0.5 size-4 shrink-0 text-muted-foreground" }),
9430
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
9431
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center justify-between gap-2", children: [
9432
+ /* @__PURE__ */ jsx("span", { className: "truncate text-xs uppercase tracking-wide text-muted-foreground", children: kindLabel(item.kind) }),
9433
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: formatRelative(item.createdAt) })
9434
+ ] }),
9435
+ /* @__PURE__ */ jsx("span", { className: "mt-1 block truncate text-sm", children: item.title }),
9436
+ /* @__PURE__ */ jsx("span", { className: "mt-1 block text-xs text-muted-foreground", children: statusLabel(item.status) })
9437
+ ] })
9438
+ ]
9439
+ },
9440
+ item.id
9441
+ );
9442
+ }),
9443
+ nextCursor && /* @__PURE__ */ jsx(
9444
+ "button",
9445
+ {
9446
+ type: "button",
9447
+ className: "w-full px-3 py-3 text-sm text-muted-foreground hover:bg-input",
9448
+ onClick: () => void load(nextCursor),
9449
+ disabled: loading,
9450
+ children: loading ? "Loading" : "Load more"
9451
+ }
9452
+ )
9453
+ ] })
9454
+ ] }),
9455
+ selectedItem ? /* @__PURE__ */ jsx(
9456
+ InboxDetail,
9457
+ {
9458
+ item: selectedItem,
9459
+ adapter: inboxAdapter,
9460
+ onUpdated: updateSelectedItem,
9461
+ onDeleted: deleteItem,
9462
+ onSessionSelect
9463
+ }
9464
+ ) : /* @__PURE__ */ jsx("main", { className: "flex min-w-0 flex-1 items-center justify-center px-4 text-sm text-muted-foreground", children: "Select an item" })
9465
+ ] });
8693
9466
  }
8694
- var DEFAULT_LIMIT = 50;
9467
+ function useInboxUnreadCount(adapter) {
9468
+ const [unreadCount, setUnreadCount] = useState(0);
9469
+ const [hasLoaded, setHasLoaded] = useState(false);
9470
+ const [error, setError] = useState(null);
9471
+ useEffect(() => {
9472
+ let cancelled = false;
9473
+ let stream = null;
9474
+ if (!adapter.listInbox || !adapter.openInboxStream) {
9475
+ setError("Inbox is not supported by this adapter.");
9476
+ setHasLoaded(true);
9477
+ return () => {
9478
+ cancelled = true;
9479
+ };
9480
+ }
9481
+ adapter.listInbox({ status: "pending", limit: 200 }).then((response) => {
9482
+ if (cancelled) return;
9483
+ setUnreadCount(response.items.filter((item) => !item.readAt).length);
9484
+ setHasLoaded(true);
9485
+ }).catch((err) => {
9486
+ if (cancelled) return;
9487
+ setError(err instanceof Error ? err.message : "Failed to load inbox");
9488
+ setHasLoaded(true);
9489
+ });
9490
+ try {
9491
+ stream = adapter.openInboxStream();
9492
+ stream.addEventListener("snapshot", (event) => {
9493
+ if (cancelled) return;
9494
+ const payload = JSON.parse(event.data);
9495
+ const ids = Array.isArray(payload.unread_ids) ? payload.unread_ids : [];
9496
+ setUnreadCount(ids.length);
9497
+ setHasLoaded(true);
9498
+ });
9499
+ stream.addEventListener("item", () => {
9500
+ if (cancelled) return;
9501
+ setUnreadCount((count) => count + 1);
9502
+ setHasLoaded(true);
9503
+ });
9504
+ stream.onerror = () => {
9505
+ if (!cancelled) setError("Inbox stream disconnected.");
9506
+ };
9507
+ } catch (err) {
9508
+ setError(err instanceof Error ? err.message : "Failed to open inbox stream");
9509
+ }
9510
+ return () => {
9511
+ cancelled = true;
9512
+ stream?.close();
9513
+ };
9514
+ }, [adapter]);
9515
+ return { unreadCount, hasLoaded, error };
9516
+ }
9517
+ var DEFAULT_LIMIT2 = 50;
8695
9518
  var DEFAULT_POLL_INTERVAL_MS = 3e4;
8696
9519
  function fingerprint(items) {
8697
9520
  return items.map(
@@ -8706,7 +9529,7 @@ function formatKind(value) {
8706
9529
  if (!value) return "Scheduled";
8707
9530
  return value.split(/[_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
8708
9531
  }
8709
- function formatRelative(value, label) {
9532
+ function formatRelative2(value, label) {
8710
9533
  if (!value) return null;
8711
9534
  const date = new Date(value);
8712
9535
  if (Number.isNaN(date.getTime())) return null;
@@ -8756,10 +9579,10 @@ function ScheduledWorkRow({
8756
9579
  const lastSessionId = item.lastSessionId ?? null;
8757
9580
  const meta = [
8758
9581
  scheduleDisplay(item),
8759
- formatRelative(item.nextRunAt, "Next"),
8760
- formatRelative(item.lastRunAt, "Last"),
9582
+ formatRelative2(item.nextRunAt, "Next"),
9583
+ formatRelative2(item.lastRunAt, "Last"),
8761
9584
  formatRuns(item.runCount),
8762
- item.expiresAt ? formatRelative(item.expiresAt, "Expires") : null
9585
+ item.expiresAt ? formatRelative2(item.expiresAt, "Expires") : null
8763
9586
  ].filter(Boolean);
8764
9587
  const openLastRun = () => {
8765
9588
  if (lastSessionId) onOpenLastRun(lastSessionId);
@@ -8839,7 +9662,7 @@ function ScheduledWorkPanel({
8839
9662
  adapter,
8840
9663
  agentId,
8841
9664
  title = "Scheduled Work",
8842
- limit = DEFAULT_LIMIT,
9665
+ limit = DEFAULT_LIMIT2,
8843
9666
  status = "active",
8844
9667
  hideHeader = false,
8845
9668
  pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
@@ -9359,6 +10182,6 @@ function SessionTreePanel({
9359
10182
  ] });
9360
10183
  }
9361
10184
 
9362
- export { AgentChat, AgentChatAdapterProvider, MessageResponse, ScheduledWorkPanel, SessionTreePanel, useAgentChatAdapterContext, useAgentChatRuntime };
10185
+ export { AgentChat, AgentChatAdapterProvider, InboxPanel, MessageResponse, ScheduledWorkPanel, SessionTreePanel, useAgentChatAdapterContext, useAgentChatRuntime, useInboxUnreadCount };
9363
10186
  //# sourceMappingURL=index.js.map
9364
10187
  //# sourceMappingURL=index.js.map