@invergent/agent-chat-react 1.6.1 → 1.6.2

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
@@ -266,7 +266,9 @@ function Button({
266
266
  function BrowserControlBar({
267
267
  sessionId,
268
268
  hasControl,
269
- adapter
269
+ adapter,
270
+ onControlAcquired,
271
+ onControlReleased
270
272
  }) {
271
273
  const [pending, setPending] = react.useState(false);
272
274
  const [error, setError] = react.useState(null);
@@ -277,8 +279,10 @@ function BrowserControlBar({
277
279
  try {
278
280
  if (hasControl) {
279
281
  await adapter.releaseBrowserControl(sessionId);
282
+ onControlReleased?.();
280
283
  } else {
281
284
  await adapter.acquireBrowserControl(sessionId);
285
+ onControlAcquired?.();
282
286
  }
283
287
  } catch (nextError) {
284
288
  setError(nextError.message);
@@ -526,19 +530,95 @@ function TooltipContent({
526
530
  }
527
531
  function BrowserPane({ sessionId, state, adapter }) {
528
532
  const [fullscreenOpen, setFullscreenOpen] = react.useState(false);
529
- const [inlinePreviewOpen, setInlinePreviewOpen] = react.useState(false);
533
+ const [openFullscreenOnControl, setOpenFullscreenOnControl] = react.useState(false);
534
+ const [localControlActive, setLocalControlActive] = react.useState(false);
535
+ const [previewSnapshot, setPreviewSnapshot] = react.useState(null);
536
+ const [previewLoading, setPreviewLoading] = react.useState(false);
537
+ const [previewError, setPreviewError] = react.useState(null);
530
538
  const hasLiveViewAdapter = typeof adapter.browserLiveViewUrl === "function";
539
+ const hasPreviewAdapter = typeof adapter.getBrowserPreviewSnapshot === "function";
531
540
  const hasControlAdapter = typeof adapter.acquireBrowserControl === "function" && typeof adapter.releaseBrowserControl === "function";
532
541
  const liveViewUrl = react.useMemo(() => {
533
542
  if (!hasLiveViewAdapter) return "";
534
543
  return adapter.browserLiveViewUrl(sessionId);
535
544
  }, [adapter, hasLiveViewAdapter, sessionId]);
536
545
  const hasLiveView = state.status !== "provisioning" && state.status !== "closed";
537
- const canOpenFullscreen = hasLiveView && Boolean(liveViewUrl);
546
+ const hasUserControl = localControlActive;
547
+ const canUseLiveView = hasLiveView && hasUserControl && Boolean(liveViewUrl);
548
+ const canOpenPreview = hasLiveView && (hasPreviewAdapter || canUseLiveView);
549
+ const canOpenFullscreen = canOpenPreview;
550
+ const refreshPreview = react.useCallback(
551
+ async (signal) => {
552
+ if (!hasPreviewAdapter || canUseLiveView) return;
553
+ setPreviewLoading(true);
554
+ setPreviewError(null);
555
+ try {
556
+ const snapshot = await adapter.getBrowserPreviewSnapshot?.(sessionId);
557
+ if (signal?.aborted) return;
558
+ setPreviewSnapshot(snapshot ?? null);
559
+ if (!snapshot) {
560
+ setPreviewError("Browser preview is unavailable.");
561
+ }
562
+ } catch (error) {
563
+ if (signal?.aborted) return;
564
+ setPreviewSnapshot(null);
565
+ setPreviewError(
566
+ error instanceof Error ? error.message : "Browser preview failed."
567
+ );
568
+ } finally {
569
+ if (!signal?.aborted) setPreviewLoading(false);
570
+ }
571
+ },
572
+ [adapter, canUseLiveView, hasPreviewAdapter, sessionId]
573
+ );
538
574
  react.useEffect(() => {
539
575
  setFullscreenOpen(false);
540
- setInlinePreviewOpen(false);
576
+ setOpenFullscreenOnControl(false);
577
+ setLocalControlActive(false);
578
+ setPreviewSnapshot(null);
579
+ setPreviewLoading(false);
580
+ setPreviewError(null);
541
581
  }, [sessionId]);
582
+ react.useEffect(() => {
583
+ if (!openFullscreenOnControl) return;
584
+ if (!canUseLiveView) return;
585
+ setFullscreenOpen(true);
586
+ setOpenFullscreenOnControl(false);
587
+ }, [canUseLiveView, openFullscreenOnControl]);
588
+ const handleFullscreenOpenChange = react.useCallback(
589
+ (open) => {
590
+ setFullscreenOpen(open);
591
+ if (open || !hasUserControl || !hasControlAdapter) return;
592
+ setLocalControlActive(false);
593
+ void adapter.releaseBrowserControl(sessionId).catch((error) => {
594
+ console.error("Failed to release browser control", error);
595
+ });
596
+ },
597
+ [adapter, hasControlAdapter, hasUserControl, sessionId]
598
+ );
599
+ react.useEffect(() => {
600
+ if (!hasLiveView || canUseLiveView) {
601
+ setPreviewLoading(false);
602
+ setPreviewError(null);
603
+ return;
604
+ }
605
+ if (!hasPreviewAdapter) return;
606
+ const abort = new AbortController();
607
+ void refreshPreview(abort.signal);
608
+ const timer = window.setInterval(() => {
609
+ void refreshPreview(abort.signal);
610
+ }, 5e3);
611
+ return () => {
612
+ abort.abort();
613
+ window.clearInterval(timer);
614
+ };
615
+ }, [
616
+ canUseLiveView,
617
+ fullscreenOpen,
618
+ hasLiveView,
619
+ hasPreviewAdapter,
620
+ refreshPreview
621
+ ]);
542
622
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
543
623
  /* @__PURE__ */ jsxRuntime.jsxs(
544
624
  "div",
@@ -568,31 +648,31 @@ function BrowserPane({ sessionId, state, adapter }) {
568
648
  /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "bottom", children: "Full screen" })
569
649
  ] }) }) })
570
650
  ] }),
571
- /* @__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." }) : liveViewUrl && inlinePreviewOpen ? /* @__PURE__ */ jsxRuntime.jsx(BrowserLiveView, { src: liveViewUrl }) : liveViewUrl ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: /* @__PURE__ */ jsxRuntime.jsxs(
572
- "button",
651
+ /* @__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(BrowserLiveView, { src: liveViewUrl }) : previewSnapshot ? /* @__PURE__ */ jsxRuntime.jsx(
652
+ BrowserPreviewImage,
573
653
  {
574
- type: "button",
575
- "aria-label": "Open browser live preview",
576
- 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",
577
- onClick: () => setInlinePreviewOpen(true),
578
- children: [
579
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PlayIcon, { className: "size-3.5", "aria-hidden": "true" }),
580
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Open live preview" })
581
- ]
654
+ src: previewSnapshot.src,
655
+ fit: "contain",
656
+ testId: "browser-preview-image"
582
657
  }
583
- ) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser live view is unavailable." }) }),
658
+ ) : 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." }) }),
584
659
  hasLiveView && hasControlAdapter && /* @__PURE__ */ jsxRuntime.jsx(
585
660
  BrowserControlBar,
586
661
  {
587
662
  sessionId,
588
- hasControl: state.status === "user-control",
589
- adapter
663
+ hasControl: localControlActive,
664
+ adapter,
665
+ onControlAcquired: () => {
666
+ setLocalControlActive(true);
667
+ setOpenFullscreenOnControl(true);
668
+ },
669
+ onControlReleased: () => setLocalControlActive(false)
590
670
  }
591
671
  )
592
672
  ]
593
673
  }
594
674
  ),
595
- /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open: fullscreenOpen, onOpenChange: setFullscreenOpen, children: /* @__PURE__ */ jsxRuntime.jsxs(
675
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open: fullscreenOpen, onOpenChange: handleFullscreenOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(
596
676
  DialogContent,
597
677
  {
598
678
  "aria-describedby": void 0,
@@ -607,18 +687,41 @@ function BrowserPane({ sessionId, state, adapter }) {
607
687
  " has control"
608
688
  ] })
609
689
  ] }),
610
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 bg-black", children: liveViewUrl ? /* @__PURE__ */ jsxRuntime.jsx(
690
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 bg-black", children: canUseLiveView ? /* @__PURE__ */ jsxRuntime.jsx(
611
691
  BrowserLiveView,
612
692
  {
613
693
  src: liveViewUrl,
614
694
  testId: "browser-fullscreen-iframe"
615
695
  }
616
- ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser live view is unavailable." }) })
696
+ ) : previewSnapshot ? /* @__PURE__ */ jsxRuntime.jsx(
697
+ BrowserPreviewImage,
698
+ {
699
+ src: previewSnapshot.src,
700
+ fit: "contain",
701
+ testId: "browser-fullscreen-preview-image"
702
+ }
703
+ ) : 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." }) })
617
704
  ]
618
705
  }
619
706
  ) })
620
707
  ] });
621
708
  }
709
+ function BrowserPreviewImage({
710
+ src,
711
+ fit,
712
+ testId
713
+ }) {
714
+ return /* @__PURE__ */ jsxRuntime.jsx(
715
+ "img",
716
+ {
717
+ "data-testid": testId,
718
+ src,
719
+ alt: "Browser preview",
720
+ className: `h-full w-full bg-black ${fit === "cover" ? "object-cover object-top" : "object-contain"}`,
721
+ draggable: false
722
+ }
723
+ );
724
+ }
622
725
 
623
726
  // src/components/ai-elements/conversation.tsx
624
727
  init_utils();
@@ -1075,66 +1178,82 @@ function TimelineSeparator({
1075
1178
  );
1076
1179
  }
1077
1180
  function BrowserActivityGroup({ calls }) {
1078
- const [open, setOpen] = react.useState(false);
1079
- const latest = calls[calls.length - 1];
1080
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-none border border-line bg-card text-xs", children: [
1081
- /* @__PURE__ */ jsxRuntime.jsxs(
1082
- "button",
1083
- {
1084
- type: "button",
1085
- onClick: () => setOpen((current) => !current),
1086
- className: "flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted/40",
1087
- children: [
1088
- open ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDownIcon, { className: "size-3 shrink-0", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRightIcon, { className: "size-3 shrink-0", "aria-hidden": "true" }),
1089
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ZapIcon, { className: "size-3 shrink-0 text-amber-500", "aria-hidden": "true" }),
1090
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-foreground", children: "browser" }),
1091
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 truncate text-muted-foreground", children: [
1092
- "(",
1093
- calls.length,
1094
- " ",
1095
- calls.length === 1 ? "action" : "actions",
1096
- " - latest:",
1097
- " ",
1098
- latest ? summarise(latest) : "none",
1099
- ")"
1100
- ] })
1101
- ]
1102
- }
1103
- ),
1104
- open && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "border-t border-line py-1", children: calls.map((call) => {
1181
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border border-line bg-card px-3 py-2 text-xs", children: [
1182
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ZapIcon, { className: "size-3 shrink-0 text-amber-500", "aria-hidden": "true" }),
1183
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 font-semibold text-foreground", children: "Browser:" }),
1184
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate text-muted-foreground", children: calls.map((call, index) => {
1105
1185
  const error = stringValue(parseJson(call.result).error);
1106
- return /* @__PURE__ */ jsxRuntime.jsxs(
1107
- "li",
1108
- {
1109
- className: "flex items-center gap-2 px-6 py-1 font-mono text-[11px]",
1110
- children: [
1111
- error && /* @__PURE__ */ jsxRuntime.jsx(
1112
- lucideReact.AlertCircleIcon,
1113
- {
1114
- "data-testid": `activity-error-${call.id}`,
1115
- className: "size-3 shrink-0 text-destructive",
1116
- "aria-hidden": "true"
1117
- }
1118
- ),
1119
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: summarise(call) }),
1120
- error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto max-w-40 truncate text-destructive", children: error })
1121
- ]
1122
- },
1123
- call.id
1124
- );
1186
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1187
+ index > 0 && ", ",
1188
+ /* @__PURE__ */ jsxRuntime.jsx(
1189
+ "span",
1190
+ {
1191
+ className: error ? "text-destructive" : void 0,
1192
+ "data-testid": error ? `activity-error-${call.id}` : void 0,
1193
+ title: error || void 0,
1194
+ children: summarize(call)
1195
+ }
1196
+ )
1197
+ ] }, call.id);
1125
1198
  }) })
1126
1199
  ] });
1127
1200
  }
1128
- function summarise(call) {
1129
- const verb = call.toolName.replace(/^browser_/, "");
1201
+ function summarize(call) {
1130
1202
  const args = parseJson(call.args);
1131
- const ref = stringValue(args.ref);
1132
- const url = stringValue(args.url);
1133
- const text = stringValue(args.text);
1134
- if (url) return `${verb} ${url}`;
1135
- if (ref) return `${verb} ${ref}`;
1136
- if (text) return `${verb} "${text.slice(0, 24)}"`;
1137
- return verb;
1203
+ const handler = ACTION_SUMMARIES[call.toolName];
1204
+ return handler ? handler(args) : call.toolName.replace(/^browser_/, "");
1205
+ }
1206
+ var ACTION_SUMMARIES = {
1207
+ browser_navigate: (args) => `navigate to ${stringValue(args.url) || "?"}`,
1208
+ browser_get_state: () => "get state",
1209
+ browser_close: () => "close",
1210
+ browser_click: (args) => {
1211
+ const ref = stringValue(args.ref);
1212
+ if (ref) return `click ${ref}`;
1213
+ const x = asNumber(args.x);
1214
+ const y = asNumber(args.y);
1215
+ if (x !== null && y !== null) return `click (${x}, ${y})`;
1216
+ return "click";
1217
+ },
1218
+ browser_type: (args) => {
1219
+ const text = stringValue(args.text);
1220
+ const ref = stringValue(args.ref);
1221
+ const quoted = text ? `"${truncateText(text)}"` : "";
1222
+ if (!quoted) return ref ? `type into ${ref}` : "type";
1223
+ return ref ? `type ${quoted} into ${ref}` : `type ${quoted}`;
1224
+ },
1225
+ browser_press_key: (args) => {
1226
+ const keys = Array.isArray(args.keys) ? args.keys.map(String) : [];
1227
+ return keys.length ? `press ${keys.join("+")}` : "press key";
1228
+ },
1229
+ browser_scroll: (args) => {
1230
+ const x = asNumber(args.x);
1231
+ const y = asNumber(args.y);
1232
+ const dx = asNumber(args.delta_x) ?? 0;
1233
+ const dy = asNumber(args.delta_y) ?? 0;
1234
+ const delta = dx || dy ? `\u0394(${dx}, ${dy})` : "";
1235
+ const origin = x !== null && y !== null ? `at (${x}, ${y})` : "";
1236
+ return ["scroll", delta, origin].filter(Boolean).join(" ");
1237
+ },
1238
+ browser_drag: (args) => {
1239
+ const path = Array.isArray(args.path) ? args.path : [];
1240
+ if (path.length < 2) return "drag";
1241
+ const first = path[0];
1242
+ const last = path[path.length - 1];
1243
+ const point = (p) => `(${asNumber(p[0]) ?? "?"}, ${asNumber(p[1]) ?? "?"})`;
1244
+ return `drag ${point(first)} \u2192 ${point(last)}`;
1245
+ },
1246
+ browser_wait: (args) => {
1247
+ const ms = asNumber(args.ms);
1248
+ return ms === null ? "wait" : `wait ${ms}ms`;
1249
+ },
1250
+ browser_screenshot: (args) => args.annotate === true ? "screenshot (annotated)" : "screenshot"
1251
+ };
1252
+ function truncateText(text, max = 32) {
1253
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
1254
+ }
1255
+ function asNumber(value) {
1256
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1138
1257
  }
1139
1258
  function parseJson(raw) {
1140
1259
  if (!raw) return {};
@@ -2193,6 +2312,20 @@ function WebToolBlock({ tc }) {
2193
2312
  displayText && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground/70 truncate text-xs", children: displayText })
2194
2313
  ] });
2195
2314
  }
2315
+ function VisionAnalyzeBlock({ tc }) {
2316
+ let image = "";
2317
+ try {
2318
+ const args = JSON.parse(tc.args);
2319
+ const ref = String(args.image ?? args.image_url ?? args.image_path ?? "");
2320
+ image = ref.split("/").pop() ?? ref;
2321
+ } catch {
2322
+ }
2323
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm py-0.5", children: [
2324
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeIcon, { className: "size-3.5 text-muted-foreground/60 shrink-0" }),
2325
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: "Vision Analyze" }),
2326
+ image && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground/70 truncate text-xs", children: image })
2327
+ ] });
2328
+ }
2196
2329
  init_utils();
2197
2330
 
2198
2331
  // src/components/chat/diff-viewer.tsx
@@ -4159,6 +4292,8 @@ function ToolCallBlock({
4159
4292
  case "web_search":
4160
4293
  case "web_crawl":
4161
4294
  return /* @__PURE__ */ jsxRuntime.jsx(WebToolBlock, { tc });
4295
+ case "vision_analyze":
4296
+ return /* @__PURE__ */ jsxRuntime.jsx(VisionAnalyzeBlock, { tc });
4162
4297
  case "read_file":
4163
4298
  return /* @__PURE__ */ jsxRuntime.jsx(ReadFileBlock, { tc, onFileSelect });
4164
4299
  case "write_file":
@@ -5679,18 +5814,21 @@ function ChatComposerInner({
5679
5814
  [menuOpen, filteredCommands, selectedIndex, handleCommandSelect, textInput]
5680
5815
  );
5681
5816
  const handleSubmit = react.useCallback(
5682
- (message) => {
5817
+ async (message) => {
5683
5818
  const text = message.text.trim();
5684
- if (!text || isRunning || disabled) return;
5819
+ if (!text || disabled) return;
5685
5820
  const images = [];
5686
5821
  for (const file of message.files) {
5687
5822
  if (file.mediaType?.startsWith("image/") && file.url) {
5688
5823
  images.push({ data: file.url, mimeType: file.mediaType });
5689
5824
  }
5690
5825
  }
5691
- onSend(text, images.length > 0 ? images : void 0);
5826
+ if (isRunning) {
5827
+ await onStop();
5828
+ }
5829
+ await onSend(text, images.length > 0 ? images : void 0);
5692
5830
  },
5693
- [onSend, isRunning, disabled]
5831
+ [onSend, onStop, isRunning, disabled]
5694
5832
  );
5695
5833
  return /* @__PURE__ */ jsxRuntime.jsxs(Popover, { open: menuOpen, onOpenChange: (open) => {
5696
5834
  if (!open) {
@@ -7551,10 +7689,9 @@ function WorkspacePanel({
7551
7689
  sessionId,
7552
7690
  selectedPath,
7553
7691
  onSelectedPathChange,
7554
- collapsed = false,
7555
- onCollapsedChange,
7556
7692
  refreshSignal = 0,
7557
- disabled = false
7693
+ disabled = false,
7694
+ fillParent = false
7558
7695
  }) {
7559
7696
  const fileInputRef = react.useRef(null);
7560
7697
  const [entries, setEntries] = react.useState([]);
@@ -7755,34 +7892,17 @@ function WorkspacePanel({
7755
7892
  window.removeEventListener("mouseup", onMouseUp);
7756
7893
  };
7757
7894
  }, []);
7758
- if (collapsed) {
7759
- return /* @__PURE__ */ jsxRuntime.jsx(
7760
- "aside",
7761
- {
7762
- "data-testid": "workspace-panel",
7763
- role: "button",
7764
- tabIndex: 0,
7765
- 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",
7766
- "aria-label": "Expand workspace",
7767
- onClick: () => onCollapsedChange?.(false),
7768
- onKeyDown: (event) => {
7769
- if (event.key === "Enter" || event.key === " ") {
7770
- event.preventDefault();
7771
- onCollapsedChange?.(false);
7772
- }
7773
- },
7774
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FolderOpenIcon, { className: "size-4 text-amber-500" })
7775
- }
7776
- );
7777
- }
7778
7895
  return /* @__PURE__ */ jsxRuntime.jsxs(
7779
7896
  "aside",
7780
7897
  {
7781
7898
  "data-testid": "workspace-panel",
7782
- className: "relative z-10 flex min-h-0 flex-col overflow-hidden border-l border-muted-foreground/20 bg-card",
7783
- style: { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
7899
+ className: cn(
7900
+ "relative z-10 flex h-full min-h-0 flex-col overflow-hidden border-l border-muted-foreground/20 bg-card",
7901
+ fillParent && "w-full"
7902
+ ),
7903
+ style: fillParent ? void 0 : { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
7784
7904
  children: [
7785
- /* @__PURE__ */ jsxRuntime.jsx(
7905
+ !fillParent && /* @__PURE__ */ jsxRuntime.jsx(
7786
7906
  "div",
7787
7907
  {
7788
7908
  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",
@@ -7808,19 +7928,6 @@ function WorkspacePanel({
7808
7928
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-sm font-medium text-foreground", children: rootName }),
7809
7929
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-xs text-faint", children: disabled ? "Workspace - Read-only" : "Workspace" })
7810
7930
  ] }),
7811
- /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
7812
- /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
7813
- Button,
7814
- {
7815
- variant: "ghost",
7816
- size: "icon-sm",
7817
- onClick: () => onCollapsedChange?.(true),
7818
- "aria-label": "Collapse workspace",
7819
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRightIcon, { className: "size-4" })
7820
- }
7821
- ) }),
7822
- /* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { side: "bottom", children: "Collapse" })
7823
- ] }),
7824
7931
  /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
7825
7932
  /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
7826
7933
  Button,
@@ -7993,7 +8100,8 @@ var WORKSPACE_MUTATING_TOOLS = /* @__PURE__ */ new Set([
7993
8100
  "terminal",
7994
8101
  "write_file",
7995
8102
  "patch",
7996
- "execute_code"
8103
+ "execute_code",
8104
+ "browser_screenshot"
7997
8105
  ]);
7998
8106
  var AGENT_CHAT_LISTENED_EVENTS = [
7999
8107
  "user.message",
@@ -8919,7 +9027,6 @@ function AgentChat({
8919
9027
  disabled
8920
9028
  }) {
8921
9029
  const [workspacePath, setWorkspacePath] = react.useState(null);
8922
- const [workspaceCollapsed, setWorkspaceCollapsed] = react.useState(false);
8923
9030
  const runtime = useAgentChatRuntime({
8924
9031
  adapter,
8925
9032
  agentId,
@@ -8930,6 +9037,7 @@ function AgentChat({
8930
9037
  const effectiveDisabled = disabled || isReadOnlySession;
8931
9038
  const disabledReason = isReadOnlySession ? "Scheduled run is read-only" : void 0;
8932
9039
  const browserState = runtime.state.browser;
9040
+ const hasBrowserPanel = browserState !== null && sessionId;
8933
9041
  react.useEffect(() => {
8934
9042
  onMessagesChange?.(runtime.messages);
8935
9043
  }, [onMessagesChange, runtime.messages]);
@@ -8948,53 +9056,769 @@ function AgentChat({
8948
9056
  sessionId,
8949
9057
  onFileSelect: handleFileSelect
8950
9058
  },
8951
- children: /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { children: /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "flex flex-1 min-h-0 overflow-hidden bg-background text-sm text-foreground", children: [
8952
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
8953
- ChatThread,
9059
+ children: /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { children: /* @__PURE__ */ jsxRuntime.jsxs(
9060
+ "section",
9061
+ {
9062
+ "data-testid": "agent-chat-layout",
9063
+ 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",
9064
+ style: { direction: "ltr" },
9065
+ children: [
9066
+ /* @__PURE__ */ jsxRuntime.jsx(
9067
+ "div",
9068
+ {
9069
+ "data-testid": "chat-panel",
9070
+ 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",
9071
+ style: hasBrowserPanel ? { right: 440 } : void 0,
9072
+ children: /* @__PURE__ */ jsxRuntime.jsx(
9073
+ ChatThread,
9074
+ {
9075
+ sessionId,
9076
+ messages: runtime.messages,
9077
+ isRunning: runtime.isRunning,
9078
+ isLoadingHistory: runtime.isLoadingHistory,
9079
+ onSend: (content, images) => runtime.send(content, images),
9080
+ onStop: () => runtime.stop(),
9081
+ onRetry: runtime.retry,
9082
+ onFileSelect: handleFileSelect,
9083
+ disabled: effectiveDisabled,
9084
+ disabledReason,
9085
+ tokenUsage: runtime.tokenUsage,
9086
+ retryIndicator: runtime.retryIndicator
9087
+ }
9088
+ )
9089
+ }
9090
+ ),
9091
+ /* @__PURE__ */ jsxRuntime.jsxs(
9092
+ "div",
9093
+ {
9094
+ "data-testid": "right-stack",
9095
+ className: hasBrowserPanel ? "absolute inset-y-0 flex min-h-0 flex-col overflow-hidden" : "flex min-h-0 shrink-0 flex-col",
9096
+ style: hasBrowserPanel ? { right: 0, width: 440 } : void 0,
9097
+ children: [
9098
+ hasBrowserPanel && /* @__PURE__ */ jsxRuntime.jsx(
9099
+ "div",
9100
+ {
9101
+ "data-testid": "browser-panel",
9102
+ className: "h-1/2 min-h-0 w-full overflow-hidden border-b border-line",
9103
+ children: /* @__PURE__ */ jsxRuntime.jsx(
9104
+ BrowserPane,
9105
+ {
9106
+ sessionId,
9107
+ state: browserState,
9108
+ adapter
9109
+ }
9110
+ )
9111
+ }
9112
+ ),
9113
+ /* @__PURE__ */ jsxRuntime.jsx(
9114
+ "div",
9115
+ {
9116
+ "data-testid": "workspace-panel-frame",
9117
+ className: hasBrowserPanel ? "h-1/2 min-h-0 w-full overflow-hidden" : "min-h-0",
9118
+ children: /* @__PURE__ */ jsxRuntime.jsx(
9119
+ WorkspacePanel,
9120
+ {
9121
+ adapter,
9122
+ sessionId,
9123
+ selectedPath: workspacePath,
9124
+ onSelectedPathChange: setWorkspacePath,
9125
+ refreshSignal: runtime.workspaceRefreshKey,
9126
+ disabled: effectiveDisabled,
9127
+ fillParent: Boolean(hasBrowserPanel)
9128
+ }
9129
+ )
9130
+ }
9131
+ )
9132
+ ]
9133
+ }
9134
+ )
9135
+ ]
9136
+ }
9137
+ ) })
9138
+ }
9139
+ );
9140
+ }
9141
+ init_utils();
9142
+ var DEFAULT_LIMIT = 50;
9143
+ function requireInboxAdapter(adapter) {
9144
+ if (!adapter.listInbox || !adapter.getInboxItem || !adapter.markInboxItemRead || !adapter.acknowledgeInboxItem || !adapter.respondGovernanceInboxItem || !adapter.openInboxStream) {
9145
+ throw new Error("Inbox is not supported by this adapter.");
9146
+ }
9147
+ return adapter;
9148
+ }
9149
+ var KIND_LABEL2 = {
9150
+ input_required: "Input needed",
9151
+ action_required: "Action needed",
9152
+ task_complete: "Task complete",
9153
+ governance_gate: "Approval",
9154
+ progress_checkin: "Progress"
9155
+ };
9156
+ function kindLabel(kind) {
9157
+ return KIND_LABEL2[kind] ?? kind.replace(/[_-]+/g, " ");
9158
+ }
9159
+ function statusLabel(status) {
9160
+ return status.split(/[_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
9161
+ }
9162
+ function formatRelative(value) {
9163
+ const date = new Date(value);
9164
+ if (Number.isNaN(date.getTime())) return "";
9165
+ return dateFns.formatDistanceToNow(date, { addSuffix: true });
9166
+ }
9167
+ function kindIcon(kind) {
9168
+ if (kind === "input_required") return lucideReact.MessageSquareIcon;
9169
+ if (kind === "action_required") return lucideReact.ExternalLinkIcon;
9170
+ if (kind === "task_complete") return lucideReact.ClipboardCheckIcon;
9171
+ if (kind === "governance_gate") return lucideReact.ShieldAlertIcon;
9172
+ if (kind === "progress_checkin") return lucideReact.TimerIcon;
9173
+ return lucideReact.CircleDotIcon;
9174
+ }
9175
+ function sortItems(items) {
9176
+ return [...items].sort((a, b) => {
9177
+ if (a.createdAt !== b.createdAt) return a.createdAt > b.createdAt ? -1 : 1;
9178
+ return b.id - a.id;
9179
+ });
9180
+ }
9181
+ function QuestionInput({
9182
+ prompt,
9183
+ choices,
9184
+ disabled,
9185
+ value,
9186
+ onChange
9187
+ }) {
9188
+ if (choices && choices.length > 0) {
9189
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9190
+ "select",
9191
+ {
9192
+ "aria-label": prompt,
9193
+ className: "h-9 w-full border border-line bg-background px-2 text-sm text-foreground outline-none focus:border-primary disabled:opacity-50",
9194
+ disabled,
9195
+ value,
9196
+ onChange: (event) => onChange(event.target.value),
9197
+ children: [
9198
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select" }),
9199
+ choices.map((choice) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: choice.label, children: choice.label }, choice.label))
9200
+ ]
9201
+ }
9202
+ );
9203
+ }
9204
+ return /* @__PURE__ */ jsxRuntime.jsx(
9205
+ "input",
9206
+ {
9207
+ "aria-label": prompt,
9208
+ className: "h-9 w-full border border-line bg-background px-2 text-sm text-foreground outline-none focus:border-primary disabled:opacity-50",
9209
+ disabled,
9210
+ value,
9211
+ onChange: (event) => onChange(event.target.value)
9212
+ }
9213
+ );
9214
+ }
9215
+ function InboxDetailActions({
9216
+ item,
9217
+ adapter,
9218
+ onDeleted,
9219
+ onSessionSelect,
9220
+ children
9221
+ }) {
9222
+ const [deleting, setDeleting] = react.useState(false);
9223
+ async function deleteItem() {
9224
+ if (!adapter.deleteInboxItem) return;
9225
+ setDeleting(true);
9226
+ try {
9227
+ await onDeleted(item.id);
9228
+ } finally {
9229
+ setDeleting(false);
9230
+ }
9231
+ }
9232
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
9233
+ /* @__PURE__ */ jsxRuntime.jsxs(
9234
+ Button,
9235
+ {
9236
+ type: "button",
9237
+ size: "sm",
9238
+ onClick: () => onSessionSelect?.(item.sessionId),
9239
+ "aria-label": "Open session",
9240
+ children: [
9241
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLinkIcon, { className: "size-3.5" }),
9242
+ "Open session"
9243
+ ]
9244
+ }
9245
+ ),
9246
+ children,
9247
+ adapter.deleteInboxItem && /* @__PURE__ */ jsxRuntime.jsxs(
9248
+ Button,
9249
+ {
9250
+ type: "button",
9251
+ size: "sm",
9252
+ variant: "ghost",
9253
+ onClick: () => void deleteItem(),
9254
+ disabled: deleting,
9255
+ "aria-label": "Delete inbox item",
9256
+ title: "Delete inbox item",
9257
+ children: [
9258
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2Icon, { className: "size-3.5" }),
9259
+ deleting ? "Deleting" : "Delete"
9260
+ ]
9261
+ }
9262
+ )
9263
+ ] });
9264
+ }
9265
+ function InputRequiredDetail({
9266
+ item,
9267
+ adapter,
9268
+ onUpdated,
9269
+ onDeleted,
9270
+ onSessionSelect
9271
+ }) {
9272
+ const questions = Array.isArray(item.payload.questions) ? item.payload.questions.map((question) => ({
9273
+ prompt: typeof question.prompt === "string" ? question.prompt : "",
9274
+ choices: Array.isArray(question.choices) ? question.choices.map((choice) => ({
9275
+ label: typeof choice.label === "string" ? choice.label : "",
9276
+ description: typeof choice.description === "string" ? choice.description : void 0
9277
+ })).filter((choice) => choice.label) : void 0
9278
+ })).filter((question) => question.prompt) : [];
9279
+ const [answers, setAnswers] = react.useState({});
9280
+ const [submitting, setSubmitting] = react.useState(false);
9281
+ const disabled = item.status !== "pending" || submitting;
9282
+ const canSubmit = questions.every((question) => answers[question.prompt]?.trim());
9283
+ async function submit() {
9284
+ const toolCallId = typeof item.payload.tool_call_id === "string" ? item.payload.tool_call_id : "";
9285
+ if (!toolCallId || !canSubmit) return;
9286
+ setSubmitting(true);
9287
+ try {
9288
+ const responses = questions.map((question) => ({
9289
+ question: question.prompt,
9290
+ answer: answers[question.prompt]?.trim() ?? "",
9291
+ is_other: false
9292
+ }));
9293
+ await adapter.submitClarifyResponse({
9294
+ sessionId: item.sessionId,
9295
+ toolCallId,
9296
+ responses
9297
+ });
9298
+ onUpdated(await adapter.getInboxItem({ itemId: item.id }));
9299
+ } finally {
9300
+ setSubmitting(false);
9301
+ }
9302
+ }
9303
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
9304
+ item.body && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "whitespace-pre-wrap text-sm text-muted-foreground", children: item.body }),
9305
+ questions.map((question) => /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block space-y-1.5", children: [
9306
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-foreground", children: question.prompt }),
9307
+ /* @__PURE__ */ jsxRuntime.jsx(
9308
+ QuestionInput,
9309
+ {
9310
+ prompt: question.prompt,
9311
+ choices: question.choices,
9312
+ disabled,
9313
+ value: answers[question.prompt] ?? "",
9314
+ onChange: (value) => setAnswers((current) => ({
9315
+ ...current,
9316
+ [question.prompt]: value
9317
+ }))
9318
+ }
9319
+ )
9320
+ ] }, question.prompt)),
9321
+ /* @__PURE__ */ jsxRuntime.jsx(
9322
+ InboxDetailActions,
9323
+ {
9324
+ item,
9325
+ adapter,
9326
+ onDeleted,
9327
+ onSessionSelect,
9328
+ children: /* @__PURE__ */ jsxRuntime.jsx(
9329
+ Button,
8954
9330
  {
8955
- sessionId,
8956
- messages: runtime.messages,
8957
- isRunning: runtime.isRunning,
8958
- isLoadingHistory: runtime.isLoadingHistory,
8959
- onSend: (content, images) => void runtime.send(content, images),
8960
- onStop: () => void runtime.stop(),
8961
- onRetry: runtime.retry,
8962
- onFileSelect: handleFileSelect,
8963
- disabled: effectiveDisabled,
8964
- disabledReason,
8965
- tokenUsage: runtime.tokenUsage,
8966
- retryIndicator: runtime.retryIndicator
9331
+ type: "button",
9332
+ size: "sm",
9333
+ onClick: () => void submit(),
9334
+ disabled: disabled || !canSubmit,
9335
+ "aria-label": "Submit inbox response",
9336
+ children: submitting ? "Submitting" : "Submit"
9337
+ }
9338
+ )
9339
+ }
9340
+ )
9341
+ ] });
9342
+ }
9343
+ function AckDetail({
9344
+ item,
9345
+ adapter,
9346
+ onUpdated,
9347
+ onDeleted,
9348
+ onSessionSelect
9349
+ }) {
9350
+ async function acknowledge() {
9351
+ onUpdated(await adapter.acknowledgeInboxItem({ itemId: item.id }));
9352
+ }
9353
+ const outcome = typeof item.payload.outcome === "string" ? item.payload.outcome : "";
9354
+ const duration = typeof item.payload.duration_seconds === "number" ? item.payload.duration_seconds : null;
9355
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
9356
+ outcome && /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "secondary", children: outcome }),
9357
+ item.body && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "whitespace-pre-wrap text-sm", children: item.body }),
9358
+ typeof item.payload.error === "string" && /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "overflow-x-auto bg-destructive/10 p-3 text-xs text-destructive", children: item.payload.error }),
9359
+ duration !== null && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
9360
+ "Duration: ",
9361
+ Math.round(duration / 60),
9362
+ " min (",
9363
+ duration,
9364
+ " s)"
9365
+ ] }),
9366
+ /* @__PURE__ */ jsxRuntime.jsx(
9367
+ InboxDetailActions,
9368
+ {
9369
+ item,
9370
+ adapter,
9371
+ onDeleted,
9372
+ onSessionSelect,
9373
+ children: item.status === "pending" && /* @__PURE__ */ jsxRuntime.jsxs(
9374
+ Button,
9375
+ {
9376
+ type: "button",
9377
+ size: "sm",
9378
+ onClick: () => void acknowledge(),
9379
+ "aria-label": "Acknowledge inbox item",
9380
+ children: [
9381
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckIcon, { className: "size-3.5" }),
9382
+ "Acknowledge"
9383
+ ]
8967
9384
  }
8968
- ) }),
8969
- /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "right-stack", className: "flex min-h-0 shrink-0 flex-col", children: [
8970
- browserState !== null && sessionId && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 border-b border-line", children: /* @__PURE__ */ jsxRuntime.jsx(
8971
- BrowserPane,
9385
+ )
9386
+ }
9387
+ )
9388
+ ] });
9389
+ }
9390
+ function GovernanceDetail({
9391
+ item,
9392
+ adapter,
9393
+ onUpdated,
9394
+ onDeleted,
9395
+ onSessionSelect
9396
+ }) {
9397
+ async function decide(decision) {
9398
+ onUpdated(
9399
+ await adapter.respondGovernanceInboxItem({ itemId: item.id, decision })
9400
+ );
9401
+ }
9402
+ const toolName = typeof item.payload.tool_name === "string" ? item.payload.tool_name : "tool";
9403
+ const args = typeof item.payload.arguments_excerpt === "string" ? item.payload.arguments_excerpt : "";
9404
+ const reason = typeof item.payload.deny_reason === "string" ? item.payload.deny_reason : "";
9405
+ const disabled = item.status !== "pending";
9406
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
9407
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
9408
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-base font-semibold text-foreground", children: [
9409
+ "Approve ",
9410
+ toolName,
9411
+ "?"
9412
+ ] }),
9413
+ reason && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: reason })
9414
+ ] }),
9415
+ args && /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "overflow-x-auto border border-line bg-muted p-3 text-xs", children: args }),
9416
+ /* @__PURE__ */ jsxRuntime.jsxs(
9417
+ InboxDetailActions,
9418
+ {
9419
+ item,
9420
+ adapter,
9421
+ onDeleted,
9422
+ onSessionSelect,
9423
+ children: [
9424
+ /* @__PURE__ */ jsxRuntime.jsx(
9425
+ Button,
8972
9426
  {
8973
- sessionId,
8974
- state: browserState,
8975
- adapter
9427
+ type: "button",
9428
+ size: "sm",
9429
+ disabled,
9430
+ onClick: () => void decide("approve"),
9431
+ children: "Approve"
8976
9432
  }
8977
- ) }),
9433
+ ),
8978
9434
  /* @__PURE__ */ jsxRuntime.jsx(
8979
- WorkspacePanel,
9435
+ Button,
8980
9436
  {
8981
- adapter,
8982
- sessionId,
8983
- selectedPath: workspacePath,
8984
- onSelectedPathChange: setWorkspacePath,
8985
- collapsed: workspaceCollapsed,
8986
- onCollapsedChange: setWorkspaceCollapsed,
8987
- refreshSignal: runtime.workspaceRefreshKey,
8988
- disabled: effectiveDisabled
9437
+ type: "button",
9438
+ size: "sm",
9439
+ variant: "destructive",
9440
+ disabled,
9441
+ onClick: () => void decide("reject"),
9442
+ children: "Reject"
8989
9443
  }
8990
9444
  )
8991
- ] })
8992
- ] }) })
9445
+ ]
9446
+ }
9447
+ )
9448
+ ] });
9449
+ }
9450
+ function ActionRequiredDetail({
9451
+ item,
9452
+ adapter,
9453
+ onUpdated,
9454
+ onDeleted,
9455
+ onSessionSelect
9456
+ }) {
9457
+ const [submitting, setSubmitting] = react.useState(false);
9458
+ const actionType = typeof item.payload.action_type === "string" ? item.payload.action_type : "";
9459
+ const context = typeof item.payload.context === "string" ? item.payload.context : "";
9460
+ const disabled = item.status !== "pending" || submitting || !adapter.respondActionRequiredInboxItem;
9461
+ async function complete() {
9462
+ if (!adapter.respondActionRequiredInboxItem) return;
9463
+ setSubmitting(true);
9464
+ try {
9465
+ onUpdated(await adapter.respondActionRequiredInboxItem({ itemId: item.id }));
9466
+ } finally {
9467
+ setSubmitting(false);
8993
9468
  }
9469
+ }
9470
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
9471
+ item.body && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "whitespace-pre-wrap text-sm", children: item.body }),
9472
+ context && context !== item.body && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "whitespace-pre-wrap text-sm text-muted-foreground", children: context }),
9473
+ actionType && /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "secondary", children: actionType }),
9474
+ /* @__PURE__ */ jsxRuntime.jsx(
9475
+ InboxDetailActions,
9476
+ {
9477
+ item,
9478
+ adapter,
9479
+ onDeleted,
9480
+ onSessionSelect,
9481
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
9482
+ Button,
9483
+ {
9484
+ type: "button",
9485
+ size: "sm",
9486
+ onClick: () => void complete(),
9487
+ disabled,
9488
+ "aria-label": "Mark action complete",
9489
+ children: [
9490
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckIcon, { className: "size-3.5" }),
9491
+ submitting ? "Marking" : "I completed this"
9492
+ ]
9493
+ }
9494
+ )
9495
+ }
9496
+ )
9497
+ ] });
9498
+ }
9499
+ function ProgressDetail({
9500
+ item,
9501
+ adapter,
9502
+ onUpdated,
9503
+ onDeleted,
9504
+ onSessionSelect
9505
+ }) {
9506
+ async function acknowledge() {
9507
+ onUpdated(await adapter.acknowledgeInboxItem({ itemId: item.id }));
9508
+ }
9509
+ const rows = [];
9510
+ for (const row of [
9511
+ ["Iterations", item.payload.iterations],
9512
+ ["Last tool", item.payload.last_tool],
9513
+ ["Elapsed", item.payload.elapsed_seconds]
9514
+ ]) {
9515
+ if (row[1] !== void 0 && row[1] !== null && row[1] !== "") {
9516
+ rows.push(row);
9517
+ }
9518
+ }
9519
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
9520
+ item.body && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "whitespace-pre-wrap text-sm", children: item.body }),
9521
+ rows.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("dl", { className: "space-y-1 text-xs text-muted-foreground", children: rows.map(([label, value]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
9522
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "w-20 shrink-0", children: String(label) }),
9523
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "min-w-0", children: String(value) })
9524
+ ] }, String(label))) }),
9525
+ /* @__PURE__ */ jsxRuntime.jsx(
9526
+ InboxDetailActions,
9527
+ {
9528
+ item,
9529
+ adapter,
9530
+ onDeleted,
9531
+ onSessionSelect,
9532
+ children: item.status === "pending" && /* @__PURE__ */ jsxRuntime.jsxs(
9533
+ Button,
9534
+ {
9535
+ type: "button",
9536
+ size: "sm",
9537
+ onClick: () => void acknowledge(),
9538
+ "aria-label": "Acknowledge inbox item",
9539
+ children: [
9540
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckIcon, { className: "size-3.5" }),
9541
+ "Acknowledge"
9542
+ ]
9543
+ }
9544
+ )
9545
+ }
9546
+ )
9547
+ ] });
9548
+ }
9549
+ function InboxDetail({
9550
+ item,
9551
+ adapter,
9552
+ onUpdated,
9553
+ onDeleted,
9554
+ onSessionSelect
9555
+ }) {
9556
+ const Icon = kindIcon(item.kind);
9557
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "min-w-0 flex-1 overflow-y-auto p-6", children: [
9558
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-5 flex min-w-0 items-start gap-3", children: [
9559
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-0.5 flex size-8 shrink-0 items-center justify-center border border-line text-muted-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "size-4" }) }),
9560
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
9561
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
9562
+ /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "secondary", children: kindLabel(item.kind) }),
9563
+ /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: item.status === "pending" ? "default" : "secondary", children: statusLabel(item.status) })
9564
+ ] }),
9565
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "mt-2 text-lg font-semibold leading-snug text-foreground", children: item.title })
9566
+ ] })
9567
+ ] }),
9568
+ item.kind === "input_required" ? /* @__PURE__ */ jsxRuntime.jsx(
9569
+ InputRequiredDetail,
9570
+ {
9571
+ item,
9572
+ adapter,
9573
+ onUpdated,
9574
+ onDeleted,
9575
+ onSessionSelect
9576
+ }
9577
+ ) : item.kind === "action_required" ? /* @__PURE__ */ jsxRuntime.jsx(
9578
+ ActionRequiredDetail,
9579
+ {
9580
+ item,
9581
+ adapter,
9582
+ onUpdated,
9583
+ onDeleted,
9584
+ onSessionSelect
9585
+ }
9586
+ ) : item.kind === "governance_gate" ? /* @__PURE__ */ jsxRuntime.jsx(
9587
+ GovernanceDetail,
9588
+ {
9589
+ item,
9590
+ adapter,
9591
+ onUpdated,
9592
+ onDeleted,
9593
+ onSessionSelect
9594
+ }
9595
+ ) : item.kind === "progress_checkin" ? /* @__PURE__ */ jsxRuntime.jsx(
9596
+ ProgressDetail,
9597
+ {
9598
+ item,
9599
+ adapter,
9600
+ onUpdated,
9601
+ onDeleted,
9602
+ onSessionSelect
9603
+ }
9604
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
9605
+ AckDetail,
9606
+ {
9607
+ item,
9608
+ adapter,
9609
+ onUpdated,
9610
+ onDeleted,
9611
+ onSessionSelect
9612
+ }
9613
+ )
9614
+ ] });
9615
+ }
9616
+ function InboxPanel({
9617
+ adapter,
9618
+ title = "Inbox",
9619
+ selectedId,
9620
+ onSelectedIdChange,
9621
+ onSessionSelect,
9622
+ hideHeader = false,
9623
+ limit = DEFAULT_LIMIT
9624
+ }) {
9625
+ const inboxAdapter = react.useMemo(() => requireInboxAdapter(adapter), [adapter]);
9626
+ const [items, setItems] = react.useState([]);
9627
+ const [nextCursor, setNextCursor] = react.useState(null);
9628
+ const [internalSelectedId, setInternalSelectedId] = react.useState(null);
9629
+ const [loading, setLoading] = react.useState(false);
9630
+ const [error, setError] = react.useState(null);
9631
+ const requestId = react.useRef(0);
9632
+ const selectedItemId = selectedId ?? internalSelectedId;
9633
+ const selectedItem = react.useMemo(
9634
+ () => items.find((item) => item.id === selectedItemId) ?? null,
9635
+ [items, selectedItemId]
9636
+ );
9637
+ const applyItem = react.useCallback((nextItem) => {
9638
+ setItems(
9639
+ (current) => sortItems(
9640
+ current.map((item) => item.id === nextItem.id ? nextItem : item)
9641
+ )
9642
+ );
9643
+ }, []);
9644
+ const selectItem = react.useCallback(
9645
+ async (itemId) => {
9646
+ if (selectedId === void 0) setInternalSelectedId(itemId);
9647
+ onSelectedIdChange?.(itemId);
9648
+ const item = await inboxAdapter.getInboxItem({ itemId });
9649
+ applyItem(item);
9650
+ if (!item.readAt) {
9651
+ applyItem(await inboxAdapter.markInboxItemRead({ itemId }));
9652
+ }
9653
+ },
9654
+ [applyItem, inboxAdapter, onSelectedIdChange, selectedId]
9655
+ );
9656
+ const load = react.useCallback(
9657
+ async (cursor) => {
9658
+ const id = ++requestId.current;
9659
+ setLoading(true);
9660
+ setError(null);
9661
+ try {
9662
+ const response = await inboxAdapter.listInbox({
9663
+ cursor: cursor ?? void 0,
9664
+ limit
9665
+ });
9666
+ if (id !== requestId.current) return;
9667
+ setItems(
9668
+ (current) => sortItems(cursor ? [...current, ...response.items] : response.items)
9669
+ );
9670
+ setNextCursor(response.nextCursor);
9671
+ } catch (err) {
9672
+ if (id === requestId.current) {
9673
+ setError(err instanceof Error ? err.message : "Failed to load inbox");
9674
+ }
9675
+ } finally {
9676
+ if (id === requestId.current) setLoading(false);
9677
+ }
9678
+ },
9679
+ [inboxAdapter, limit]
8994
9680
  );
9681
+ react.useEffect(() => {
9682
+ void load(null);
9683
+ }, [load]);
9684
+ react.useEffect(() => {
9685
+ const stream = inboxAdapter.openInboxStream();
9686
+ stream.addEventListener("item", (event) => {
9687
+ const payload = JSON.parse(event.data);
9688
+ if (typeof payload.item_id !== "number") return;
9689
+ void inboxAdapter.getInboxItem({ itemId: payload.item_id }).then(applyItem);
9690
+ });
9691
+ return () => stream.close();
9692
+ }, [applyItem, inboxAdapter]);
9693
+ function updateSelectedItem(item) {
9694
+ applyItem(item);
9695
+ }
9696
+ const deleteItem = react.useCallback(
9697
+ async (itemId) => {
9698
+ if (!inboxAdapter.deleteInboxItem) return;
9699
+ await inboxAdapter.deleteInboxItem({ itemId });
9700
+ setItems((current) => current.filter((item) => item.id !== itemId));
9701
+ if (selectedItemId === itemId) {
9702
+ if (selectedId === void 0) setInternalSelectedId(null);
9703
+ onSelectedIdChange?.(null);
9704
+ }
9705
+ },
9706
+ [inboxAdapter, onSelectedIdChange, selectedId, selectedItemId]
9707
+ );
9708
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full min-h-0 min-w-0 bg-background text-foreground", children: [
9709
+ /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: "flex w-80 min-w-72 max-w-sm shrink-0 flex-col border-r border-line", children: [
9710
+ !hideHeader && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-14 items-center gap-2 border-b border-line px-4", children: [
9711
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.InboxIcon, { className: "size-4 text-muted-foreground" }),
9712
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "font-semibold", children: title })
9713
+ ] }),
9714
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
9715
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-3 text-sm text-destructive", children: error }),
9716
+ !error && items.length === 0 && !loading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-8 text-sm text-muted-foreground", children: "No inbox items" }),
9717
+ items.map((item) => {
9718
+ const Icon = kindIcon(item.kind);
9719
+ const selected = item.id === selectedItemId;
9720
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9721
+ "button",
9722
+ {
9723
+ type: "button",
9724
+ "aria-label": `Open inbox item ${item.title}`,
9725
+ className: cn(
9726
+ "flex w-full min-w-0 gap-3 border-l-2 px-3 py-3 text-left transition-colors hover:bg-input",
9727
+ selected ? "border-l-primary bg-line text-foreground" : "border-l-transparent text-subtle",
9728
+ !item.readAt && "font-medium text-foreground"
9729
+ ),
9730
+ onClick: () => void selectItem(item.id),
9731
+ children: [
9732
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "mt-0.5 size-4 shrink-0 text-muted-foreground" }),
9733
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 flex-1", children: [
9734
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-center justify-between gap-2", children: [
9735
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-xs uppercase tracking-wide text-muted-foreground", children: kindLabel(item.kind) }),
9736
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: formatRelative(item.createdAt) })
9737
+ ] }),
9738
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 block truncate text-sm", children: item.title }),
9739
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 block text-xs text-muted-foreground", children: statusLabel(item.status) })
9740
+ ] })
9741
+ ]
9742
+ },
9743
+ item.id
9744
+ );
9745
+ }),
9746
+ nextCursor && /* @__PURE__ */ jsxRuntime.jsx(
9747
+ "button",
9748
+ {
9749
+ type: "button",
9750
+ className: "w-full px-3 py-3 text-sm text-muted-foreground hover:bg-input",
9751
+ onClick: () => void load(nextCursor),
9752
+ disabled: loading,
9753
+ children: loading ? "Loading" : "Load more"
9754
+ }
9755
+ )
9756
+ ] })
9757
+ ] }),
9758
+ selectedItem ? /* @__PURE__ */ jsxRuntime.jsx(
9759
+ InboxDetail,
9760
+ {
9761
+ item: selectedItem,
9762
+ adapter: inboxAdapter,
9763
+ onUpdated: updateSelectedItem,
9764
+ onDeleted: deleteItem,
9765
+ onSessionSelect
9766
+ }
9767
+ ) : /* @__PURE__ */ jsxRuntime.jsx("main", { className: "flex min-w-0 flex-1 items-center justify-center px-4 text-sm text-muted-foreground", children: "Select an item" })
9768
+ ] });
9769
+ }
9770
+ function useInboxUnreadCount(adapter) {
9771
+ const [unreadCount, setUnreadCount] = react.useState(0);
9772
+ const [hasLoaded, setHasLoaded] = react.useState(false);
9773
+ const [error, setError] = react.useState(null);
9774
+ react.useEffect(() => {
9775
+ let cancelled = false;
9776
+ let stream = null;
9777
+ if (!adapter.listInbox || !adapter.openInboxStream) {
9778
+ setError("Inbox is not supported by this adapter.");
9779
+ setHasLoaded(true);
9780
+ return () => {
9781
+ cancelled = true;
9782
+ };
9783
+ }
9784
+ adapter.listInbox({ status: "pending", limit: 200 }).then((response) => {
9785
+ if (cancelled) return;
9786
+ setUnreadCount(response.items.filter((item) => !item.readAt).length);
9787
+ setHasLoaded(true);
9788
+ }).catch((err) => {
9789
+ if (cancelled) return;
9790
+ setError(err instanceof Error ? err.message : "Failed to load inbox");
9791
+ setHasLoaded(true);
9792
+ });
9793
+ try {
9794
+ stream = adapter.openInboxStream();
9795
+ stream.addEventListener("snapshot", (event) => {
9796
+ if (cancelled) return;
9797
+ const payload = JSON.parse(event.data);
9798
+ const ids = Array.isArray(payload.unread_ids) ? payload.unread_ids : [];
9799
+ setUnreadCount(ids.length);
9800
+ setHasLoaded(true);
9801
+ });
9802
+ stream.addEventListener("item", () => {
9803
+ if (cancelled) return;
9804
+ setUnreadCount((count) => count + 1);
9805
+ setHasLoaded(true);
9806
+ });
9807
+ stream.onerror = () => {
9808
+ if (!cancelled) setError("Inbox stream disconnected.");
9809
+ };
9810
+ } catch (err) {
9811
+ setError(err instanceof Error ? err.message : "Failed to open inbox stream");
9812
+ }
9813
+ return () => {
9814
+ cancelled = true;
9815
+ stream?.close();
9816
+ };
9817
+ }, [adapter]);
9818
+ return { unreadCount, hasLoaded, error };
8995
9819
  }
8996
9820
  init_utils();
8997
- var DEFAULT_LIMIT = 50;
9821
+ var DEFAULT_LIMIT2 = 50;
8998
9822
  var DEFAULT_POLL_INTERVAL_MS = 3e4;
8999
9823
  function fingerprint(items) {
9000
9824
  return items.map(
@@ -9009,7 +9833,7 @@ function formatKind(value) {
9009
9833
  if (!value) return "Scheduled";
9010
9834
  return value.split(/[_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
9011
9835
  }
9012
- function formatRelative(value, label) {
9836
+ function formatRelative2(value, label) {
9013
9837
  if (!value) return null;
9014
9838
  const date = new Date(value);
9015
9839
  if (Number.isNaN(date.getTime())) return null;
@@ -9059,10 +9883,10 @@ function ScheduledWorkRow({
9059
9883
  const lastSessionId = item.lastSessionId ?? null;
9060
9884
  const meta = [
9061
9885
  scheduleDisplay(item),
9062
- formatRelative(item.nextRunAt, "Next"),
9063
- formatRelative(item.lastRunAt, "Last"),
9886
+ formatRelative2(item.nextRunAt, "Next"),
9887
+ formatRelative2(item.lastRunAt, "Last"),
9064
9888
  formatRuns(item.runCount),
9065
- item.expiresAt ? formatRelative(item.expiresAt, "Expires") : null
9889
+ item.expiresAt ? formatRelative2(item.expiresAt, "Expires") : null
9066
9890
  ].filter(Boolean);
9067
9891
  const openLastRun = () => {
9068
9892
  if (lastSessionId) onOpenLastRun(lastSessionId);
@@ -9142,7 +9966,7 @@ function ScheduledWorkPanel({
9142
9966
  adapter,
9143
9967
  agentId,
9144
9968
  title = "Scheduled Work",
9145
- limit = DEFAULT_LIMIT,
9969
+ limit = DEFAULT_LIMIT2,
9146
9970
  status = "active",
9147
9971
  hideHeader = false,
9148
9972
  pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
@@ -9665,10 +10489,12 @@ function SessionTreePanel({
9665
10489
 
9666
10490
  exports.AgentChat = AgentChat;
9667
10491
  exports.AgentChatAdapterProvider = AgentChatAdapterProvider;
10492
+ exports.InboxPanel = InboxPanel;
9668
10493
  exports.MessageResponse = MessageResponse;
9669
10494
  exports.ScheduledWorkPanel = ScheduledWorkPanel;
9670
10495
  exports.SessionTreePanel = SessionTreePanel;
9671
10496
  exports.useAgentChatAdapterContext = useAgentChatAdapterContext;
9672
10497
  exports.useAgentChatRuntime = useAgentChatRuntime;
10498
+ exports.useInboxUnreadCount = useInboxUnreadCount;
9673
10499
  //# sourceMappingURL=index.cjs.map
9674
10500
  //# sourceMappingURL=index.cjs.map