@invergent/agent-chat-react 1.5.9 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6,9 +6,9 @@ var react = require('react');
6
6
  var auto = require('chart.js/auto');
7
7
  var nextThemes = require('next-themes');
8
8
  var jsxRuntime = require('react/jsx-runtime');
9
+ var lucideReact = require('lucide-react');
9
10
  var classVarianceAuthority = require('class-variance-authority');
10
11
  var radixUi = require('radix-ui');
11
- var lucideReact = require('lucide-react');
12
12
  var useStickToBottom = require('use-stick-to-bottom');
13
13
  var cjk = require('@streamdown/cjk');
14
14
  var code = require('@streamdown/code');
@@ -263,6 +263,117 @@ function Button({
263
263
  }
264
264
  );
265
265
  }
266
+ function BrowserControlBar({
267
+ sessionId,
268
+ hasControl,
269
+ adapter
270
+ }) {
271
+ const [pending, setPending] = react.useState(false);
272
+ const [error, setError] = react.useState(null);
273
+ async function toggleControl() {
274
+ if (!sessionId || pending) return;
275
+ setPending(true);
276
+ setError(null);
277
+ try {
278
+ if (hasControl) {
279
+ await adapter.releaseBrowserControl(sessionId);
280
+ } else {
281
+ await adapter.acquireBrowserControl(sessionId);
282
+ }
283
+ } catch (nextError) {
284
+ setError(nextError.message);
285
+ } finally {
286
+ setPending(false);
287
+ }
288
+ }
289
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-11 items-center gap-2 border-t border-line bg-card px-3 py-2", children: [
290
+ /* @__PURE__ */ jsxRuntime.jsxs(
291
+ Button,
292
+ {
293
+ type: "button",
294
+ size: "xs",
295
+ variant: hasControl ? "secondary" : "default",
296
+ disabled: !sessionId || pending,
297
+ onClick: () => void toggleControl(),
298
+ children: [
299
+ hasControl ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcwIcon, { className: "size-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MousePointer2Icon, { className: "size-3", "aria-hidden": "true" }),
300
+ hasControl ? "Return control" : "Take control"
301
+ ]
302
+ }
303
+ ),
304
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-auto flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [
305
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "size-1.5 rounded-full bg-red-500", "aria-hidden": "true" }),
306
+ "Live"
307
+ ] }),
308
+ error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "max-w-40 truncate text-[11px] text-destructive", children: error })
309
+ ] });
310
+ }
311
+ function BrowserLiveView({ src }) {
312
+ return /* @__PURE__ */ jsxRuntime.jsx(
313
+ "iframe",
314
+ {
315
+ "data-testid": "browser-iframe",
316
+ title: "Browser live view",
317
+ src,
318
+ className: "h-full w-full border-0 bg-black",
319
+ allow: "clipboard-read; clipboard-write"
320
+ }
321
+ );
322
+ }
323
+
324
+ // src/components/browser/browser-status-dot.tsx
325
+ init_utils();
326
+ function BrowserStatusDot({
327
+ status
328
+ }) {
329
+ return /* @__PURE__ */ jsxRuntime.jsx(
330
+ "span",
331
+ {
332
+ "aria-hidden": "true",
333
+ className: cn(
334
+ "inline-block size-2 shrink-0 rounded-full",
335
+ status === "live" && "bg-emerald-500",
336
+ status === "user-control" && "bg-amber-500",
337
+ status === "provisioning" && "animate-pulse bg-sky-500",
338
+ status === "closed" && "bg-muted-foreground"
339
+ )
340
+ }
341
+ );
342
+ }
343
+ function BrowserPane({ sessionId, state, adapter }) {
344
+ const liveViewUrl = react.useMemo(
345
+ () => adapter.browserLiveViewUrl(sessionId),
346
+ [adapter, sessionId]
347
+ );
348
+ const hasLiveView = state.status !== "provisioning" && state.status !== "closed";
349
+ return /* @__PURE__ */ jsxRuntime.jsxs(
350
+ "div",
351
+ {
352
+ "data-testid": "browser-pane",
353
+ className: "flex h-full min-h-0 flex-col bg-background",
354
+ children: [
355
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex min-h-10 items-center gap-2 border-b border-line bg-card px-3 text-xs text-foreground", children: [
356
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ZapIcon, { className: "size-3.5 text-muted-foreground", "aria-hidden": "true" }),
357
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: "Browser" }),
358
+ /* @__PURE__ */ jsxRuntime.jsx(BrowserStatusDot, { status: state.status }),
359
+ state.controlOwner && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 truncate text-amber-500", children: [
360
+ state.controlOwner,
361
+ " has control"
362
+ ] })
363
+ ] }),
364
+ /* @__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." }) : /* @__PURE__ */ jsxRuntime.jsx(BrowserLiveView, { src: liveViewUrl }) }),
365
+ hasLiveView && /* @__PURE__ */ jsxRuntime.jsx(
366
+ BrowserControlBar,
367
+ {
368
+ sessionId,
369
+ hasControl: state.status === "user-control",
370
+ adapter
371
+ }
372
+ )
373
+ ]
374
+ }
375
+ );
376
+ }
266
377
 
267
378
  // src/components/ai-elements/conversation.tsx
268
379
  init_utils();
@@ -767,6 +878,80 @@ function TimelineSeparator({
767
878
  }
768
879
  );
769
880
  }
881
+ function BrowserActivityGroup({ calls }) {
882
+ const [open, setOpen] = react.useState(false);
883
+ const latest = calls[calls.length - 1];
884
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-none border border-line bg-card text-xs", children: [
885
+ /* @__PURE__ */ jsxRuntime.jsxs(
886
+ "button",
887
+ {
888
+ type: "button",
889
+ onClick: () => setOpen((current) => !current),
890
+ className: "flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted/40",
891
+ children: [
892
+ 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" }),
893
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ZapIcon, { className: "size-3 shrink-0 text-amber-500", "aria-hidden": "true" }),
894
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-foreground", children: "browser" }),
895
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0 truncate text-muted-foreground", children: [
896
+ "(",
897
+ calls.length,
898
+ " ",
899
+ calls.length === 1 ? "action" : "actions",
900
+ " - latest:",
901
+ " ",
902
+ latest ? summarise(latest) : "none",
903
+ ")"
904
+ ] })
905
+ ]
906
+ }
907
+ ),
908
+ open && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "border-t border-line py-1", children: calls.map((call) => {
909
+ const error = stringValue(parseJson(call.result).error);
910
+ return /* @__PURE__ */ jsxRuntime.jsxs(
911
+ "li",
912
+ {
913
+ className: "flex items-center gap-2 px-6 py-1 font-mono text-[11px]",
914
+ children: [
915
+ error && /* @__PURE__ */ jsxRuntime.jsx(
916
+ lucideReact.AlertCircleIcon,
917
+ {
918
+ "data-testid": `activity-error-${call.id}`,
919
+ className: "size-3 shrink-0 text-destructive",
920
+ "aria-hidden": "true"
921
+ }
922
+ ),
923
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: summarise(call) }),
924
+ error && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto max-w-40 truncate text-destructive", children: error })
925
+ ]
926
+ },
927
+ call.id
928
+ );
929
+ }) })
930
+ ] });
931
+ }
932
+ function summarise(call) {
933
+ const verb = call.toolName.replace(/^browser_/, "");
934
+ const args = parseJson(call.args);
935
+ const ref = stringValue(args.ref);
936
+ const url = stringValue(args.url);
937
+ const text = stringValue(args.text);
938
+ if (url) return `${verb} ${url}`;
939
+ if (ref) return `${verb} ${ref}`;
940
+ if (text) return `${verb} "${text.slice(0, 24)}"`;
941
+ return verb;
942
+ }
943
+ function parseJson(raw) {
944
+ if (!raw) return {};
945
+ try {
946
+ const parsed = JSON.parse(raw);
947
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
948
+ } catch {
949
+ return {};
950
+ }
951
+ }
952
+ function stringValue(value) {
953
+ return typeof value === "string" ? value : "";
954
+ }
770
955
  init_utils();
771
956
 
772
957
  // src/components/chat/tools/shared.ts
@@ -3552,13 +3737,16 @@ function ClarifyLocked({
3552
3737
  }) })
3553
3738
  ] });
3554
3739
  }
3555
- function ArtifactToolBlock({ tc }) {
3556
- const args = parseArgs(tc.args) ?? {};
3740
+ function ArtifactToolBlock({
3741
+ tc,
3742
+ resolvedName
3743
+ }) {
3557
3744
  const status = effectiveStatus(tc);
3558
3745
  const label = status === "running" ? "Creating artifact\u2026" : status === "error" ? "Creating artifact\u2026" : "Created";
3746
+ const name = resolvedName ?? parseArgs(tc.args)?.name;
3559
3747
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-sm ", children: [
3560
3748
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-foreground", children: label }),
3561
- args.name && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground truncate", children: args.name })
3749
+ name && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground truncate", children: name })
3562
3750
  ] });
3563
3751
  }
3564
3752
 
@@ -3891,7 +4079,11 @@ function DefaultToolBlock({ tc }) {
3891
4079
  ] })
3892
4080
  ] });
3893
4081
  }
3894
- function ToolCallBlock({ tc, onFileSelect }) {
4082
+ function ToolCallBlock({
4083
+ tc,
4084
+ resolvedArtifactName,
4085
+ onFileSelect
4086
+ }) {
3895
4087
  switch (tc.toolName) {
3896
4088
  case "terminal":
3897
4089
  return /* @__PURE__ */ jsxRuntime.jsx(TerminalToolBlock, { tc });
@@ -3928,7 +4120,7 @@ function ToolCallBlock({ tc, onFileSelect }) {
3928
4120
  case "clarify":
3929
4121
  return /* @__PURE__ */ jsxRuntime.jsx(ClarifyToolBlock, { tc });
3930
4122
  case "create_artifact":
3931
- return /* @__PURE__ */ jsxRuntime.jsx(ArtifactToolBlock, { tc });
4123
+ return /* @__PURE__ */ jsxRuntime.jsx(ArtifactToolBlock, { tc, resolvedName: resolvedArtifactName });
3932
4124
  case "delegate_task":
3933
4125
  return /* @__PURE__ */ jsxRuntime.jsx(DelegateToolBlock, { tc });
3934
4126
  case "memory":
@@ -5337,6 +5529,7 @@ function ChatComposerInner({
5337
5529
  onStop,
5338
5530
  isRunning,
5339
5531
  disabled = false,
5532
+ disabledReason,
5340
5533
  tokenUsage
5341
5534
  }) {
5342
5535
  const { adapter } = useAgentChatAdapterContext();
@@ -5365,7 +5558,10 @@ function ChatComposerInner({
5365
5558
  const slashCommands = react.useMemo(() => {
5366
5559
  const builtin = [
5367
5560
  { value: "/clear", label: "/clear", description: "Clear conversation" },
5368
- { value: "/compress", label: "/compress", description: "Compress context" }
5561
+ { value: "/compress", label: "/compress", description: "Compress context" },
5562
+ { value: "/loop", label: "/loop", description: "Schedule recurring prompt" },
5563
+ { value: "/loop list", label: "/loop list", description: "List active loops" },
5564
+ { value: "/loop cancel", label: "/loop cancel", description: "Cancel a loop by ID" }
5369
5565
  ];
5370
5566
  return [...adapterCommands, ...builtin];
5371
5567
  }, [adapterCommands]);
@@ -5444,7 +5640,7 @@ function ChatComposerInner({
5444
5640
  /* @__PURE__ */ jsxRuntime.jsx(PromptInputBody, { children: /* @__PURE__ */ jsxRuntime.jsx(
5445
5641
  PromptInputTextarea,
5446
5642
  {
5447
- placeholder: disabled ? "Session disabled" : "Send a message...",
5643
+ placeholder: disabled ? disabledReason ?? "Session disabled" : "Send a message...",
5448
5644
  disabled,
5449
5645
  onKeyDown: handleKeyDown
5450
5646
  }
@@ -6099,7 +6295,7 @@ var ErrorMessage = react.memo(function ErrorMessage2({
6099
6295
 
6100
6296
  // src/components/chat/chat-thread.tsx
6101
6297
  init_utils();
6102
- function messageToEntries(msg, isLast) {
6298
+ function messageToEntries(msg, isLast, artifactFallbacks) {
6103
6299
  if (msg.role === "system") {
6104
6300
  if (msg.systemKind === "skill_invoked") {
6105
6301
  return [{
@@ -6141,6 +6337,18 @@ function messageToEntries(msg, isLast) {
6141
6337
  }
6142
6338
  if (hasToolCalls) {
6143
6339
  for (const tc of msg.toolCalls) {
6340
+ if (tc.toolName === "create_artifact") {
6341
+ const resolvedName = parseArgs(tc.args)?.name ?? artifactFallbacks[tc.id];
6342
+ const status = effectiveStatus(tc);
6343
+ if (!resolvedName && status !== "error") continue;
6344
+ entries.push({
6345
+ kind: "tool",
6346
+ key: tc.id,
6347
+ tc,
6348
+ resolvedArtifactName: resolvedName
6349
+ });
6350
+ continue;
6351
+ }
6144
6352
  entries.push({ kind: "tool", key: tc.id, tc });
6145
6353
  }
6146
6354
  }
@@ -6155,6 +6363,32 @@ function messageToEntries(msg, isLast) {
6155
6363
  }
6156
6364
  return entries;
6157
6365
  }
6366
+ function groupBrowserActivityEntries(entries) {
6367
+ const grouped = [];
6368
+ let buffer = [];
6369
+ const flush = () => {
6370
+ if (buffer.length === 0) return;
6371
+ grouped.push({
6372
+ kind: "browser_activity",
6373
+ key: `browser-activity-${buffer[0]?.id}-${buffer[buffer.length - 1]?.id}`,
6374
+ calls: buffer
6375
+ });
6376
+ buffer = [];
6377
+ };
6378
+ for (const entry of entries) {
6379
+ if (entry.kind === "tool" && isBrowserToolCall(entry.tc)) {
6380
+ buffer.push(entry.tc);
6381
+ continue;
6382
+ }
6383
+ flush();
6384
+ grouped.push(entry);
6385
+ }
6386
+ flush();
6387
+ return grouped;
6388
+ }
6389
+ function isBrowserToolCall(call) {
6390
+ return call.toolName.startsWith("browser_");
6391
+ }
6158
6392
  function groupMessages(messages) {
6159
6393
  const groups = [];
6160
6394
  let pendingSystem = [];
@@ -6299,11 +6533,27 @@ function TimelineEntryItem({
6299
6533
  )
6300
6534
  ] }),
6301
6535
  /* @__PURE__ */ jsxRuntime.jsx(TimelineContent, { children: entry.tc.cancelled ? /* @__PURE__ */ jsxRuntime.jsx(CancelledToolRow, { tc: entry.tc }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
6302
- /* @__PURE__ */ jsxRuntime.jsx(ToolCallBlock, { tc: entry.tc, onFileSelect }),
6536
+ /* @__PURE__ */ jsxRuntime.jsx(
6537
+ ToolCallBlock,
6538
+ {
6539
+ tc: entry.tc,
6540
+ resolvedArtifactName: entry.resolvedArtifactName,
6541
+ onFileSelect
6542
+ }
6543
+ ),
6303
6544
  failureSummary ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-w-full truncate text-xs text-destructive", title: failureSummary, children: failureSummary }) : null
6304
6545
  ] }) })
6305
6546
  ] });
6306
6547
  }
6548
+ if (entry.kind === "browser_activity") {
6549
+ return /* @__PURE__ */ jsxRuntime.jsxs(TimelineItem, { step, children: [
6550
+ /* @__PURE__ */ jsxRuntime.jsxs(TimelineHeader, { children: [
6551
+ /* @__PURE__ */ jsxRuntime.jsx(TimelineSeparator, { style: { backgroundColor: "var(--color-border)" } }),
6552
+ /* @__PURE__ */ jsxRuntime.jsx(TimelineIndicator, { className: "size-2 border-none bg-amber-500" })
6553
+ ] }),
6554
+ /* @__PURE__ */ jsxRuntime.jsx(TimelineContent, { children: /* @__PURE__ */ jsxRuntime.jsx(BrowserActivityGroup, { calls: entry.calls }) })
6555
+ ] });
6556
+ }
6307
6557
  if (entry.kind === "text") {
6308
6558
  return /* @__PURE__ */ jsxRuntime.jsxs(TimelineItem, { step, children: [
6309
6559
  /* @__PURE__ */ jsxRuntime.jsxs(TimelineHeader, { children: [
@@ -6361,14 +6611,16 @@ function AssistantGroup({
6361
6611
  totalMessages,
6362
6612
  isRunning,
6363
6613
  sessionId,
6614
+ artifactFallbacks,
6364
6615
  onFileSelect,
6365
6616
  onRetry
6366
6617
  }) {
6367
- const entries = [];
6618
+ let entries = [];
6368
6619
  for (let i = 0; i < messages.length; i++) {
6369
6620
  const isLast = i === messages.length - 1 && lastGlobalIndex === totalMessages - 1;
6370
- entries.push(...messageToEntries(messages[i], isLast));
6621
+ entries.push(...messageToEntries(messages[i], isLast, artifactFallbacks));
6371
6622
  }
6623
+ entries = groupBrowserActivityEntries(entries);
6372
6624
  const isTailGroup = lastGlobalIndex === totalMessages - 1;
6373
6625
  const lastEntry = entries[entries.length - 1];
6374
6626
  const hasRunningTool = entries.some(
@@ -6434,11 +6686,34 @@ function ChatThread({
6434
6686
  onStop,
6435
6687
  onFileSelect,
6436
6688
  disabled = false,
6689
+ disabledReason,
6437
6690
  tokenUsage,
6438
6691
  retryIndicator,
6439
6692
  onRetry
6440
6693
  }) {
6441
6694
  const groups = react.useMemo(() => groupMessages(messages), [messages]);
6695
+ const artifactFallbacks = react.useMemo(() => {
6696
+ const fallbacks = {};
6697
+ const createArtifactToolCallIds = [];
6698
+ let artifactIdx = 0;
6699
+ for (const msg of messages) {
6700
+ if (msg.role === "assistant" && msg.toolCalls) {
6701
+ for (const tc of msg.toolCalls) {
6702
+ if (tc.toolName === "create_artifact" && !tc.cancelled && tc.status !== "error") {
6703
+ createArtifactToolCallIds.push(tc.id);
6704
+ }
6705
+ }
6706
+ } else if (msg.role === "system" && msg.systemKind === "artifact") {
6707
+ const tcId = createArtifactToolCallIds[artifactIdx];
6708
+ if (tcId) {
6709
+ const { name } = unpackArtifactMeta(msg.systemMeta, msg.content);
6710
+ if (name) fallbacks[tcId] = name;
6711
+ }
6712
+ artifactIdx += 1;
6713
+ }
6714
+ }
6715
+ return fallbacks;
6716
+ }, [messages]);
6442
6717
  const activeFailureId = react.useMemo(() => {
6443
6718
  if (messages.length === 0) return null;
6444
6719
  const tail = messages[messages.length - 1];
@@ -6503,6 +6778,7 @@ function ChatThread({
6503
6778
  totalMessages: messages.length,
6504
6779
  isRunning,
6505
6780
  sessionId,
6781
+ artifactFallbacks,
6506
6782
  onFileSelect,
6507
6783
  onRetry: groupRetry
6508
6784
  },
@@ -6515,13 +6791,21 @@ function ChatThread({
6515
6791
  ] }),
6516
6792
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-4xl px-6 pb-5 pt-3", children: [
6517
6793
  retryIndicator && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsxRuntime.jsx(RetryBanner, { indicator: retryIndicator }) }),
6518
- /* @__PURE__ */ jsxRuntime.jsx(
6794
+ disabled && disabledReason ? /* @__PURE__ */ jsxRuntime.jsx(
6795
+ "div",
6796
+ {
6797
+ className: "rounded border border-line bg-muted/40 px-3 py-2 text-sm text-muted-foreground",
6798
+ role: "status",
6799
+ children: disabledReason
6800
+ }
6801
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
6519
6802
  ChatComposer,
6520
6803
  {
6521
6804
  onSend,
6522
6805
  onStop,
6523
6806
  isRunning,
6524
6807
  disabled,
6808
+ disabledReason,
6525
6809
  tokenUsage
6526
6810
  }
6527
6811
  )
@@ -7359,7 +7643,7 @@ function WorkspacePanel({
7359
7643
  );
7360
7644
  const handleDelete = react.useCallback(
7361
7645
  async (path) => {
7362
- if (!sessionId) return;
7646
+ if (disabled || !sessionId) return;
7363
7647
  try {
7364
7648
  await adapter.deleteWorkspaceFile({ sessionId, path });
7365
7649
  if (selectedPath === path) {
@@ -7375,7 +7659,7 @@ function WorkspacePanel({
7375
7659
  setDeleteTarget(null);
7376
7660
  }
7377
7661
  },
7378
- [adapter, fetchTree, onSelectedPathChange, selectedPath, sessionId]
7662
+ [adapter, disabled, fetchTree, onSelectedPathChange, selectedPath, sessionId]
7379
7663
  );
7380
7664
  const onResizeStart = react.useCallback(
7381
7665
  (event) => {
@@ -7413,6 +7697,7 @@ function WorkspacePanel({
7413
7697
  return /* @__PURE__ */ jsxRuntime.jsx(
7414
7698
  "aside",
7415
7699
  {
7700
+ "data-testid": "workspace-panel",
7416
7701
  role: "button",
7417
7702
  tabIndex: 0,
7418
7703
  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",
@@ -7431,6 +7716,7 @@ function WorkspacePanel({
7431
7716
  return /* @__PURE__ */ jsxRuntime.jsxs(
7432
7717
  "aside",
7433
7718
  {
7719
+ "data-testid": "workspace-panel",
7434
7720
  className: "relative z-10 flex min-h-0 flex-col overflow-hidden border-l border-muted-foreground/20 bg-card",
7435
7721
  style: { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
7436
7722
  children: [
@@ -7458,7 +7744,7 @@ function WorkspacePanel({
7458
7744
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FolderOpenIcon, { className: "size-4 shrink-0 text-amber-500" }),
7459
7745
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
7460
7746
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-sm font-medium text-foreground", children: rootName }),
7461
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-xs text-faint", children: "Workspace" })
7747
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-xs text-faint", children: disabled ? "Workspace - Read-only" : "Workspace" })
7462
7748
  ] }),
7463
7749
  /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
7464
7750
  /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -7569,7 +7855,7 @@ function WorkspacePanel({
7569
7855
  sessionId,
7570
7856
  path: file.path
7571
7857
  }) : null,
7572
- onDelete: file ? () => setDeleteTarget(file.path) : null,
7858
+ onDelete: file && !disabled ? () => setDeleteTarget(file.path) : null,
7573
7859
  onClose: () => {
7574
7860
  onSelectedPathChange(null);
7575
7861
  setFile(null);
@@ -7633,6 +7919,13 @@ function ConfirmDialog({
7633
7919
  );
7634
7920
  }
7635
7921
 
7922
+ // src/lib/sessions.ts
7923
+ function isScheduledRunSession(session) {
7924
+ return Boolean(
7925
+ session?.channel === "scheduled" || session?.config?.scheduled_session_id
7926
+ );
7927
+ }
7928
+
7636
7929
  // src/runtime/events.ts
7637
7930
  var WORKSPACE_MUTATING_TOOLS = /* @__PURE__ */ new Set([
7638
7931
  "terminal",
@@ -7665,6 +7958,10 @@ var AGENT_CHAT_LISTENED_EVENTS = [
7665
7958
  "expert.override",
7666
7959
  "artifact.created",
7667
7960
  "artifact.updated",
7961
+ "browser.provisioned",
7962
+ "browser.destroyed",
7963
+ "browser.control_granted",
7964
+ "browser.control_returned",
7668
7965
  "clarify.response"
7669
7966
  ];
7670
7967
 
@@ -7689,7 +7986,8 @@ function createInitialAgentChatState(options = {}) {
7689
7986
  sessionDone: false,
7690
7987
  hadDeltas: false,
7691
7988
  terminal: false,
7692
- workspaceRefreshKey: 0
7989
+ workspaceRefreshKey: 0,
7990
+ browser: null
7693
7991
  };
7694
7992
  }
7695
7993
  function applyAgentChatEvent(state, event) {
@@ -7713,7 +8011,7 @@ function applyAgentChatEvent(state, event) {
7713
8011
  {
7714
8012
  id: `evt-${event.eventId}`,
7715
8013
  role: "system",
7716
- content: stringValue(event.data.skill),
8014
+ content: stringValue2(event.data.skill),
7717
8015
  createdAt: /* @__PURE__ */ new Date(),
7718
8016
  status: "complete",
7719
8017
  systemKind: "skill_invoked",
@@ -7730,7 +8028,7 @@ function applyAgentChatEvent(state, event) {
7730
8028
  {
7731
8029
  id: `evt-${event.eventId}`,
7732
8030
  role: "system",
7733
- content: stringValue(event.data.name),
8031
+ content: stringValue2(event.data.name),
7734
8032
  createdAt: /* @__PURE__ */ new Date(),
7735
8033
  status: "complete",
7736
8034
  systemKind: "artifact",
@@ -7743,6 +8041,23 @@ function applyAgentChatEvent(state, event) {
7743
8041
  }
7744
8042
  }
7745
8043
  ]);
8044
+ case "browser.provisioned":
8045
+ return applyBrowserEvent(nextState, event, {
8046
+ status: "live",
8047
+ controlOwner: null
8048
+ });
8049
+ case "browser.control_granted":
8050
+ return applyBrowserEvent(nextState, event, {
8051
+ status: "user-control",
8052
+ controlOwner: stringValue2(event.data.owner_user_id) || null
8053
+ });
8054
+ case "browser.control_returned":
8055
+ return applyBrowserEvent(nextState, event, {
8056
+ status: "live",
8057
+ controlOwner: null
8058
+ });
8059
+ case "browser.destroyed":
8060
+ return applyBrowserEvent(nextState, event, null);
7746
8061
  case "llm.delta":
7747
8062
  return applyLlmDelta(nextState, event);
7748
8063
  case "llm.response":
@@ -7752,7 +8067,7 @@ function applyAgentChatEvent(state, event) {
7752
8067
  case "tool.call":
7753
8068
  return applyToolCall(nextState, event);
7754
8069
  case "tool.result": {
7755
- const toolCallId = stringValue(event.data.tool_call_id);
8070
+ const toolCallId = stringValue2(event.data.tool_call_id);
7756
8071
  const toolName = findToolNameById(nextState.messages, toolCallId);
7757
8072
  const messages = applyToolResult(nextState.messages, event.data);
7758
8073
  const mutatesWorkspace = toolName !== null && WORKSPACE_MUTATING_TOOLS.has(toolName);
@@ -7822,8 +8137,8 @@ function applyRetryIndicator(state, event) {
7822
8137
  case "session.done":
7823
8138
  return { ...state, retryIndicator: null };
7824
8139
  case "harness.crash": {
7825
- const title = stringValue(event.data.error_title) || "A transient error occurred, retrying...";
7826
- const detail = stringValue(event.data.error_detail) || stringValue(event.data.error);
8140
+ const title = stringValue2(event.data.error_title) || "A transient error occurred, retrying...";
8141
+ const detail = stringValue2(event.data.error_detail) || stringValue2(event.data.error);
7827
8142
  return {
7828
8143
  ...state,
7829
8144
  retryIndicator: {
@@ -7838,7 +8153,7 @@ function applyRetryIndicator(state, event) {
7838
8153
  }
7839
8154
  }
7840
8155
  function applyUserMessage(messages, eventId, data) {
7841
- const content = stringValue(data.content);
8156
+ const content = stringValue2(data.content);
7842
8157
  const next = [...messages];
7843
8158
  const localIdx = next.findIndex(
7844
8159
  (m) => m.role === "user" && m.id.startsWith("local-") && m.content === content
@@ -7856,9 +8171,47 @@ function applyUserMessage(messages, eventId, data) {
7856
8171
  });
7857
8172
  return next;
7858
8173
  }
8174
+ function applyBrowserEvent(state, event, browser) {
8175
+ return withMessages(
8176
+ { ...state, browser },
8177
+ [...state.messages, browserMarker(event)]
8178
+ );
8179
+ }
8180
+ function browserMarker(event) {
8181
+ const labels = {
8182
+ "browser.provisioned": {
8183
+ content: "Browser ready.",
8184
+ warning: false
8185
+ },
8186
+ "browser.control_granted": {
8187
+ content: "A user took control of the browser.",
8188
+ warning: true
8189
+ },
8190
+ "browser.control_returned": {
8191
+ content: "Browser control returned to the agent.",
8192
+ warning: false
8193
+ },
8194
+ "browser.destroyed": {
8195
+ content: "Browser closed.",
8196
+ warning: false
8197
+ }
8198
+ };
8199
+ const label = labels[event.type] ?? {
8200
+ content: "Browser updated.",
8201
+ warning: false
8202
+ };
8203
+ return {
8204
+ id: `browser-marker-${event.eventId}`,
8205
+ role: "system",
8206
+ content: label.content,
8207
+ createdAt: /* @__PURE__ */ new Date(),
8208
+ status: "complete",
8209
+ systemKind: label.warning ? "browser_marker_warning" : "browser_marker"
8210
+ };
8211
+ }
7859
8212
  function applyLlmDelta(state, event) {
7860
- const deltaContent = stringValue(event.data.content);
7861
- const deltaReasoning = stringValue(event.data.reasoning);
8213
+ const deltaContent = stringValue2(event.data.content);
8214
+ const deltaReasoning = stringValue2(event.data.reasoning);
7862
8215
  const messages = [...state.messages];
7863
8216
  const lastIdx = findLastAssistantIndex(messages);
7864
8217
  const lastMsg = lastIdx >= 0 ? messages[lastIdx] : null;
@@ -7895,7 +8248,7 @@ function applyLlmDelta(state, event) {
7895
8248
  function applyLlmResponse(state, event) {
7896
8249
  const messages = [...state.messages];
7897
8250
  const message = objectValue(event.data.message);
7898
- const responseContent = stringValue(message?.content) || stringValue(event.data.content);
8251
+ const responseContent = stringValue2(message?.content) || stringValue2(event.data.content);
7899
8252
  const toolCalls = Array.isArray(message?.tool_calls) ? message?.tool_calls : [];
7900
8253
  const hasToolCalls = toolCalls.length > 0;
7901
8254
  const idx = findLastAssistantIndex(messages);
@@ -7953,12 +8306,12 @@ function applyLlmResponse(state, event) {
7953
8306
  cachedInputTokens: numberValue(event.data.cache_read_tokens),
7954
8307
  totalTokens: inputTokens + outputTokens,
7955
8308
  contextWindow: numberValue(event.data.context_window),
7956
- model: stringValue(event.data.model)
8309
+ model: stringValue2(event.data.model)
7957
8310
  }
7958
8311
  };
7959
8312
  }
7960
8313
  function applyLlmThinking(state, event) {
7961
- const reasoningText = stringValue(event.data.reasoning) || stringValue(event.data.content);
8314
+ const reasoningText = stringValue2(event.data.reasoning) || stringValue2(event.data.content);
7962
8315
  const messages = [...state.messages];
7963
8316
  const idx = findLastAssistantIndex(messages);
7964
8317
  const prev = idx >= 0 ? messages[idx] : null;
@@ -8006,11 +8359,11 @@ function applyToolCall(state, event) {
8006
8359
  messages[assistantIdx] = assistant;
8007
8360
  }
8008
8361
  const existingCalls = assistant.toolCalls ?? [];
8009
- const toolCallId = stringValue(event.data.tool_call_id) || `tc-${event.eventId}`;
8362
+ const toolCallId = stringValue2(event.data.tool_call_id) || `tc-${event.eventId}`;
8010
8363
  if (!existingCalls.some((tc) => tc.id === toolCallId)) {
8011
8364
  const entry = {
8012
8365
  id: toolCallId,
8013
- toolName: stringValue(event.data.name) || stringValue(event.data.tool_name) || "unknown",
8366
+ toolName: stringValue2(event.data.name) || stringValue2(event.data.tool_name) || "unknown",
8014
8367
  args: typeof event.data.arguments === "string" ? event.data.arguments : JSON.stringify(event.data.arguments ?? {}),
8015
8368
  status: "running"
8016
8369
  };
@@ -8026,7 +8379,7 @@ function applyToolCall(state, event) {
8026
8379
  };
8027
8380
  }
8028
8381
  function applyToolResult(messages, data) {
8029
- const targetId = stringValue(data.tool_call_id);
8382
+ const targetId = stringValue2(data.tool_call_id);
8030
8383
  let matchIdx = -1;
8031
8384
  for (let i = messages.length - 1; i >= 0; i--) {
8032
8385
  if (messages[i]?.role === "assistant" && messages[i]?.toolCalls?.some((tc) => tc.id === targetId)) {
@@ -8095,8 +8448,8 @@ function buildErrorInfo(event) {
8095
8448
  }
8096
8449
  return {
8097
8450
  category: event.data.error_category,
8098
- title: stringValue(event.data.error_title) || "The session failed due to an internal error.",
8099
- detail: stringValue(event.data.error_detail) || stringValue(event.data.error),
8451
+ title: stringValue2(event.data.error_title) || "The session failed due to an internal error.",
8452
+ detail: stringValue2(event.data.error_detail) || stringValue2(event.data.error),
8100
8453
  retryable: Boolean(event.data.retryable)
8101
8454
  };
8102
8455
  }
@@ -8140,7 +8493,7 @@ function applyExpertFeedback(messages, type, data) {
8140
8493
  return messages;
8141
8494
  }
8142
8495
  function applyClarifyResponse(messages, data) {
8143
- const targetToolId = stringValue(data.tool_call_id);
8496
+ const targetToolId = stringValue2(data.tool_call_id);
8144
8497
  const responses = Array.isArray(data.responses) ? data.responses : void 0;
8145
8498
  if (!targetToolId || !responses) return messages;
8146
8499
  const next = [...messages];
@@ -8155,8 +8508,8 @@ function applyClarifyResponse(messages, data) {
8155
8508
  clarifyAnswers: responses.map((response) => {
8156
8509
  const row = objectValue(response) ?? {};
8157
8510
  return {
8158
- question: stringValue(row.question),
8159
- answer: stringValue(row.answer),
8511
+ question: stringValue2(row.question),
8512
+ answer: stringValue2(row.answer),
8160
8513
  is_other: Boolean(row.is_other)
8161
8514
  };
8162
8515
  })
@@ -8176,7 +8529,7 @@ function applyPolicyDenied(state, data) {
8176
8529
  ...policyMsg,
8177
8530
  content: `${policyMsg.content}
8178
8531
 
8179
- **Policy denied**: ${stringValue(data.reason) || "Action blocked by governance policy."}`,
8532
+ **Policy denied**: ${stringValue2(data.reason) || "Action blocked by governance policy."}`,
8180
8533
  status: "error"
8181
8534
  };
8182
8535
  }
@@ -8228,7 +8581,7 @@ function objectValue(value) {
8228
8581
  }
8229
8582
  return void 0;
8230
8583
  }
8231
- function stringValue(value) {
8584
+ function stringValue2(value) {
8232
8585
  return typeof value === "string" ? value : "";
8233
8586
  }
8234
8587
  function numberValue(value) {
@@ -8245,6 +8598,7 @@ function useAgentChatRuntime({
8245
8598
  const [state, setState] = react.useState(
8246
8599
  () => createInitialAgentChatState({ isLoadingHistory: Boolean(sessionId) })
8247
8600
  );
8601
+ const [session, setSession] = react.useState(null);
8248
8602
  const stateRef = react.useRef(state);
8249
8603
  const streamRef = react.useRef(null);
8250
8604
  const reconnectTimerRef = react.useRef(null);
@@ -8269,6 +8623,7 @@ function useAgentChatRuntime({
8269
8623
  clearReconnectTimer();
8270
8624
  closeStream();
8271
8625
  if (!sessionId) {
8626
+ setSession(null);
8272
8627
  setState(createInitialAgentChatState());
8273
8628
  return;
8274
8629
  }
@@ -8316,17 +8671,19 @@ function useAgentChatRuntime({
8316
8671
  isLoadingHistory: true
8317
8672
  });
8318
8673
  stateRef.current = initialState;
8674
+ setSession(null);
8319
8675
  setState(initialState);
8320
8676
  connect(0);
8321
- adapter.getSession({ sessionId }).then((session) => {
8677
+ adapter.getSession({ sessionId }).then((loadedSession) => {
8322
8678
  if (cancelled) return;
8323
- if (session.messageCount === 0) {
8679
+ setSession(loadedSession);
8680
+ if (loadedSession.messageCount === 0) {
8324
8681
  setState((prev) => ({
8325
8682
  ...prev,
8326
8683
  isLoadingHistory: false
8327
8684
  }));
8328
8685
  }
8329
- if (isTerminalStatus(session.status)) {
8686
+ if (isTerminalStatus(loadedSession.status)) {
8330
8687
  setState((prev) => ({
8331
8688
  ...prev,
8332
8689
  terminal: true,
@@ -8415,9 +8772,9 @@ function useAgentChatRuntime({
8415
8772
  markSending(content, images);
8416
8773
  if (!sessionId) {
8417
8774
  try {
8418
- const session = await adapter.createSession({ agentId });
8419
- onSessionChange?.(session.id);
8420
- await adapter.sendMessage({ sessionId: session.id, content, images });
8775
+ const session2 = await adapter.createSession({ agentId });
8776
+ onSessionChange?.(session2.id);
8777
+ await adapter.sendMessage({ sessionId: session2.id, content, images });
8421
8778
  } catch (error) {
8422
8779
  markSendError(error instanceof Error ? error.message : "send failed");
8423
8780
  throw error;
@@ -8458,6 +8815,8 @@ function useAgentChatRuntime({
8458
8815
  }
8459
8816
  }, [adapter, sessionId]);
8460
8817
  return {
8818
+ state,
8819
+ session,
8461
8820
  messages: state.messages,
8462
8821
  isRunning: state.isRunning,
8463
8822
  isLoadingHistory: state.isLoadingHistory,
@@ -8505,6 +8864,10 @@ function AgentChat({
8505
8864
  sessionId,
8506
8865
  onSessionChange
8507
8866
  });
8867
+ const isReadOnlySession = isScheduledRunSession(runtime.session);
8868
+ const effectiveDisabled = disabled || isReadOnlySession;
8869
+ const disabledReason = isReadOnlySession ? "Scheduled run is read-only" : void 0;
8870
+ const browserState = runtime.state.browser;
8508
8871
  react.useEffect(() => {
8509
8872
  onMessagesChange?.(runtime.messages);
8510
8873
  }, [onMessagesChange, runtime.messages]);
@@ -8535,29 +8898,341 @@ function AgentChat({
8535
8898
  onStop: () => void runtime.stop(),
8536
8899
  onRetry: runtime.retry,
8537
8900
  onFileSelect: handleFileSelect,
8538
- disabled,
8901
+ disabled: effectiveDisabled,
8902
+ disabledReason,
8539
8903
  tokenUsage: runtime.tokenUsage,
8540
8904
  retryIndicator: runtime.retryIndicator
8541
8905
  }
8542
8906
  ) }),
8543
- /* @__PURE__ */ jsxRuntime.jsx(
8544
- WorkspacePanel,
8545
- {
8546
- adapter,
8547
- sessionId,
8548
- selectedPath: workspacePath,
8549
- onSelectedPathChange: setWorkspacePath,
8550
- collapsed: workspaceCollapsed,
8551
- onCollapsedChange: setWorkspaceCollapsed,
8552
- refreshSignal: runtime.workspaceRefreshKey,
8553
- disabled
8554
- }
8555
- )
8907
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "right-stack", className: "flex min-h-0 shrink-0 flex-col", children: [
8908
+ browserState !== null && sessionId && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 border-b border-line", children: /* @__PURE__ */ jsxRuntime.jsx(
8909
+ BrowserPane,
8910
+ {
8911
+ sessionId,
8912
+ state: browserState,
8913
+ adapter
8914
+ }
8915
+ ) }),
8916
+ /* @__PURE__ */ jsxRuntime.jsx(
8917
+ WorkspacePanel,
8918
+ {
8919
+ adapter,
8920
+ sessionId,
8921
+ selectedPath: workspacePath,
8922
+ onSelectedPathChange: setWorkspacePath,
8923
+ collapsed: workspaceCollapsed,
8924
+ onCollapsedChange: setWorkspaceCollapsed,
8925
+ refreshSignal: runtime.workspaceRefreshKey,
8926
+ disabled: effectiveDisabled
8927
+ }
8928
+ )
8929
+ ] })
8556
8930
  ] }) })
8557
8931
  }
8558
8932
  );
8559
8933
  }
8560
8934
  init_utils();
8935
+ var DEFAULT_LIMIT = 50;
8936
+ var DEFAULT_POLL_INTERVAL_MS = 3e4;
8937
+ function fingerprint(items) {
8938
+ return items.map(
8939
+ (item) => `${item.id}:${item.status}:${item.kind ?? ""}:${item.name ?? ""}:${item.prompt}:${item.scheduleDisplay}:${item.runCount}:${item.nextRunAt ?? ""}:${item.lastRunAt ?? ""}:${item.lastSessionId ?? ""}:${item.lastError ?? ""}:${item.updatedAt}`
8940
+ ).join("|");
8941
+ }
8942
+ function formatKind(value) {
8943
+ if (value === "dynamic_loop") return "Dynamic loop";
8944
+ if (value === "cron") return "Cron";
8945
+ if (value === "one_shot") return "One-shot";
8946
+ if (value === "scheduled") return "Scheduled";
8947
+ if (!value) return "Scheduled";
8948
+ return value.split(/[_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
8949
+ }
8950
+ function formatRelative(value, label) {
8951
+ if (!value) return null;
8952
+ const date = new Date(value);
8953
+ if (Number.isNaN(date.getTime())) return null;
8954
+ return `${label} ${dateFns.formatDistanceToNow(date, { addSuffix: true })}`;
8955
+ }
8956
+ function formatRuns(count) {
8957
+ return `${count} ${count === 1 ? "run" : "runs"}`;
8958
+ }
8959
+ function scheduleTitle(item) {
8960
+ const title = item.name?.trim();
8961
+ if (title) return title;
8962
+ return item.prompt;
8963
+ }
8964
+ function canRunScheduleNow(item) {
8965
+ return item.repeatLimit === 1;
8966
+ }
8967
+ function scheduleDisplay(item) {
8968
+ if (item.kind === "dynamic_loop") return null;
8969
+ return item.scheduleDisplay;
8970
+ }
8971
+ function sortScheduledWork(items) {
8972
+ return [...items].sort((a, b) => {
8973
+ if (a.status === "active" && b.status !== "active") return -1;
8974
+ if (a.status !== "active" && b.status === "active") return 1;
8975
+ const aNext = a.nextRunAt ?? "";
8976
+ const bNext = b.nextRunAt ?? "";
8977
+ if (aNext && bNext && aNext !== bNext) return aNext < bNext ? -1 : 1;
8978
+ if (aNext && !bNext) return -1;
8979
+ if (!aNext && bNext) return 1;
8980
+ return (a.updatedAt ?? "") > (b.updatedAt ?? "") ? -1 : 1;
8981
+ });
8982
+ }
8983
+ function ScheduledWorkRow({
8984
+ item,
8985
+ canRunNow,
8986
+ canCancel,
8987
+ actionScheduleId,
8988
+ onOpenLastRun,
8989
+ onRunNow,
8990
+ onCancel
8991
+ }) {
8992
+ const isActive = item.status === "active";
8993
+ const disabled = actionScheduleId === item.id;
8994
+ const title = scheduleTitle(item);
8995
+ const statusText = item.status === "active" ? null : item.status;
8996
+ const showRunNow = canRunNow && isActive && canRunScheduleNow(item);
8997
+ const lastSessionId = item.lastSessionId ?? null;
8998
+ const meta = [
8999
+ scheduleDisplay(item),
9000
+ formatRelative(item.nextRunAt, "Next"),
9001
+ formatRelative(item.lastRunAt, "Last"),
9002
+ formatRuns(item.runCount),
9003
+ item.expiresAt ? formatRelative(item.expiresAt, "Expires") : null
9004
+ ].filter(Boolean);
9005
+ const openLastRun = () => {
9006
+ if (lastSessionId) onOpenLastRun(lastSessionId);
9007
+ };
9008
+ const handleRowKeyDown = (e) => {
9009
+ if (!lastSessionId) return;
9010
+ if (e.key !== "Enter" && e.key !== " ") return;
9011
+ e.preventDefault();
9012
+ openLastRun();
9013
+ };
9014
+ const stopActionClick = (e) => {
9015
+ e.stopPropagation();
9016
+ };
9017
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9018
+ "div",
9019
+ {
9020
+ className: cn(
9021
+ "group flex min-w-0 items-start gap-2 border-l-2 border-l-transparent px-3 py-2 text-sm transition-colors hover:border-l-primary hover:bg-input",
9022
+ lastSessionId && "cursor-pointer"
9023
+ ),
9024
+ onClick: openLastRun,
9025
+ onKeyDown: handleRowKeyDown,
9026
+ role: lastSessionId ? "button" : void 0,
9027
+ tabIndex: lastSessionId ? 0 : void 0,
9028
+ "aria-label": lastSessionId ? `Open last run for ${title}` : void 0,
9029
+ title: lastSessionId ? "Open last run" : void 0,
9030
+ children: [
9031
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-0.5 flex size-5 shrink-0 items-center justify-center text-faint", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CalendarClockIcon, { className: "size-3.5" }) }),
9032
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
9033
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [
9034
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
9035
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-foreground", children: title }),
9036
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-0.5 flex min-w-0 items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "secondary", className: "shrink-0", children: formatKind(item.kind) }) }),
9037
+ meta.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-0.5 space-y-0.5 text-xs text-faint", children: meta.map((line) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate", children: line }, line)) })
9038
+ ] }),
9039
+ statusText && /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "destructive", className: "shrink-0", children: statusText })
9040
+ ] }),
9041
+ item.lastError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 line-clamp-2 text-xs text-destructive", children: item.lastError })
9042
+ ] }),
9043
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [
9044
+ showRunNow && /* @__PURE__ */ jsxRuntime.jsx(
9045
+ "button",
9046
+ {
9047
+ type: "button",
9048
+ className: "rounded p-1 text-faint opacity-70 transition-all hover:bg-line hover:text-foreground disabled:pointer-events-none disabled:opacity-40 group-hover:opacity-100",
9049
+ onClick: (e) => {
9050
+ stopActionClick(e);
9051
+ onRunNow(item.id);
9052
+ },
9053
+ "aria-label": "Run schedule now",
9054
+ title: "Run schedule now",
9055
+ disabled,
9056
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CalendarClockIcon, { className: "size-3.5" })
9057
+ }
9058
+ ),
9059
+ canCancel && isActive && /* @__PURE__ */ jsxRuntime.jsx(
9060
+ "button",
9061
+ {
9062
+ type: "button",
9063
+ className: "rounded p-1 text-faint opacity-70 transition-all hover:bg-destructive/10 hover:text-destructive disabled:pointer-events-none disabled:opacity-40 group-hover:opacity-100",
9064
+ onClick: (e) => {
9065
+ stopActionClick(e);
9066
+ onCancel(item.id);
9067
+ },
9068
+ "aria-label": "Cancel schedule",
9069
+ title: "Cancel schedule",
9070
+ disabled,
9071
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2Icon, { className: "size-3.5" })
9072
+ }
9073
+ )
9074
+ ] })
9075
+ ]
9076
+ }
9077
+ );
9078
+ }
9079
+ function ScheduledWorkPanel({
9080
+ adapter,
9081
+ agentId,
9082
+ title = "Scheduled Work",
9083
+ limit = DEFAULT_LIMIT,
9084
+ status = "active",
9085
+ hideHeader = false,
9086
+ pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
9087
+ onSessionSelect,
9088
+ onScheduleCancel,
9089
+ onScheduleRunNow
9090
+ }) {
9091
+ const [items, setItems] = react.useState([]);
9092
+ const [error, setError] = react.useState(null);
9093
+ const [hasEverLoaded, setHasEverLoaded] = react.useState(false);
9094
+ const [actionScheduleId, setActionScheduleId] = react.useState(null);
9095
+ const [collapsed, setCollapsed] = react.useState(false);
9096
+ const mounted = react.useRef(true);
9097
+ const requestId = react.useRef(0);
9098
+ const lastFingerprint = react.useRef("");
9099
+ const refetch = react.useCallback(
9100
+ async (opts) => {
9101
+ if (!adapter.listScheduledWork) {
9102
+ setItems([]);
9103
+ setHasEverLoaded(true);
9104
+ return;
9105
+ }
9106
+ const currentRequestId = ++requestId.current;
9107
+ try {
9108
+ const list = await adapter.listScheduledWork({
9109
+ agentId,
9110
+ status,
9111
+ limit
9112
+ });
9113
+ if (!mounted.current || currentRequestId !== requestId.current) return;
9114
+ const nextItems = sortScheduledWork(list.items);
9115
+ const fp = fingerprint(nextItems);
9116
+ if (fp !== lastFingerprint.current) {
9117
+ lastFingerprint.current = fp;
9118
+ setItems(nextItems);
9119
+ }
9120
+ setError(null);
9121
+ setHasEverLoaded(true);
9122
+ } catch (e) {
9123
+ if (!mounted.current || currentRequestId !== requestId.current) return;
9124
+ if (!opts?.silent) {
9125
+ setError(
9126
+ e instanceof Error ? e.message : "Failed to load scheduled work"
9127
+ );
9128
+ setHasEverLoaded(true);
9129
+ }
9130
+ }
9131
+ },
9132
+ [adapter, agentId, limit, status]
9133
+ );
9134
+ react.useEffect(() => {
9135
+ mounted.current = true;
9136
+ return () => {
9137
+ mounted.current = false;
9138
+ };
9139
+ }, []);
9140
+ react.useEffect(() => {
9141
+ setError(null);
9142
+ lastFingerprint.current = "";
9143
+ void refetch();
9144
+ }, [refetch]);
9145
+ react.useEffect(() => {
9146
+ if (!adapter.listScheduledWork || pollIntervalMs <= 0) return;
9147
+ const id = setInterval(() => {
9148
+ void refetch({ silent: true });
9149
+ }, pollIntervalMs);
9150
+ return () => clearInterval(id);
9151
+ }, [adapter.listScheduledWork, pollIntervalMs, refetch]);
9152
+ const activeCount = react.useMemo(
9153
+ () => items.filter((item) => item.status === "active").length,
9154
+ [items]
9155
+ );
9156
+ const handleOpenLastRun = react.useCallback(
9157
+ (sessionId) => {
9158
+ onSessionSelect?.(sessionId);
9159
+ },
9160
+ [onSessionSelect]
9161
+ );
9162
+ const handleRunNow = react.useCallback(
9163
+ async (scheduleId) => {
9164
+ if (!adapter.runScheduledWorkNow || actionScheduleId) return;
9165
+ setActionScheduleId(scheduleId);
9166
+ try {
9167
+ const result = await adapter.runScheduledWorkNow({ scheduleId });
9168
+ onScheduleRunNow?.(scheduleId, result?.sessionId);
9169
+ await refetch({ silent: true });
9170
+ } catch (e) {
9171
+ setError(e instanceof Error ? e.message : "Failed to run schedule");
9172
+ } finally {
9173
+ if (mounted.current) setActionScheduleId(null);
9174
+ }
9175
+ },
9176
+ [actionScheduleId, adapter, onScheduleRunNow, refetch]
9177
+ );
9178
+ const handleCancel = react.useCallback(
9179
+ async (scheduleId) => {
9180
+ if (!adapter.cancelScheduledWork || actionScheduleId) return;
9181
+ setActionScheduleId(scheduleId);
9182
+ try {
9183
+ await adapter.cancelScheduledWork({ scheduleId });
9184
+ setItems((current) => {
9185
+ const next = current.filter((item) => item.id !== scheduleId);
9186
+ lastFingerprint.current = fingerprint(next);
9187
+ return next;
9188
+ });
9189
+ onScheduleCancel?.(scheduleId);
9190
+ await refetch({ silent: true });
9191
+ } catch (e) {
9192
+ setError(e instanceof Error ? e.message : "Failed to cancel schedule");
9193
+ } finally {
9194
+ if (mounted.current) setActionScheduleId(null);
9195
+ }
9196
+ },
9197
+ [actionScheduleId, adapter, onScheduleCancel, refetch]
9198
+ );
9199
+ if (!hasEverLoaded) return null;
9200
+ if (!adapter.listScheduledWork) return null;
9201
+ const showBody = hideHeader || !collapsed;
9202
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "min-w-0 border-t border-line", children: [
9203
+ !hideHeader && /* @__PURE__ */ jsxRuntime.jsxs(
9204
+ "button",
9205
+ {
9206
+ type: "button",
9207
+ className: "flex w-full items-center gap-1.5 px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-foreground transition-colors hover:bg-input",
9208
+ onClick: () => setCollapsed((current) => !current),
9209
+ "aria-expanded": !collapsed,
9210
+ children: [
9211
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CalendarClockIcon, { className: "size-3.5" }),
9212
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: title }),
9213
+ activeCount > 0 && /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "default", className: "ml-auto", children: activeCount }),
9214
+ collapsed ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRightIcon, { className: "size-3.5 shrink-0 text-faint" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDownIcon, { className: "size-3.5 shrink-0 text-faint" })
9215
+ ]
9216
+ }
9217
+ ),
9218
+ showBody && error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-2 text-xs text-destructive", children: error }),
9219
+ showBody && !error && items.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-2 text-xs text-faint", children: "No scheduled work" }),
9220
+ showBody && !error && items.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("pb-2", hideHeader && "pt-1"), children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
9221
+ ScheduledWorkRow,
9222
+ {
9223
+ item,
9224
+ canRunNow: Boolean(adapter.runScheduledWorkNow),
9225
+ canCancel: Boolean(adapter.cancelScheduledWork),
9226
+ actionScheduleId,
9227
+ onOpenLastRun: handleOpenLastRun,
9228
+ onRunNow: handleRunNow,
9229
+ onCancel: handleCancel
9230
+ },
9231
+ item.id
9232
+ )) })
9233
+ ] });
9234
+ }
9235
+ init_utils();
8561
9236
  var POLL_INTERVAL_ACTIVE_MS = 4e3;
8562
9237
  var POLL_INTERVAL_IDLE_MS = 3e4;
8563
9238
  var DEFAULT_SESSION_LIST_LIMIT = 50;
@@ -8582,7 +9257,7 @@ function buildTree(nodes) {
8582
9257
  }
8583
9258
  function treeFingerprint(nodes) {
8584
9259
  return nodes.map(
8585
- (n) => `${n.id}:${n.parentId ?? ""}:${n.status}:${n.agentType ?? ""}:${n.messageCount ?? 0}:${n.toolCallCount ?? 0}:${n.updatedAt}`
9260
+ (n) => `${n.id}:${n.parentId ?? ""}:${n.status}:${n.agentType ?? ""}:${n.runKind ?? ""}:${n.title ?? ""}:${n.messageCount ?? 0}:${n.toolCallCount ?? 0}:${n.updatedAt}`
8586
9261
  ).join("|");
8587
9262
  }
8588
9263
  function sessionToTreeNode(session) {
@@ -8592,6 +9267,7 @@ function sessionToTreeNode(session) {
8592
9267
  parentId: session.parentId ?? null,
8593
9268
  agentId: session.agentId,
8594
9269
  channel: session.channel,
9270
+ runKind: session.runKind ?? deriveRunKind(session.channel, session.config),
8595
9271
  status: session.status,
8596
9272
  title: session.title,
8597
9273
  model: session.model,
@@ -8640,6 +9316,23 @@ function formatSessionTime(value) {
8640
9316
  if (Number.isNaN(date.getTime())) return "";
8641
9317
  return dateFns.formatDistanceToNow(date, { addSuffix: true });
8642
9318
  }
9319
+ function deriveRunKind(channel, config) {
9320
+ if (channel === "scheduled" && config?.scheduled_dynamic_loop === true) {
9321
+ return "dynamic_loop";
9322
+ }
9323
+ if (channel === "scheduled") return "scheduled";
9324
+ return null;
9325
+ }
9326
+ function formatRunKind(value) {
9327
+ if (value === "dynamic_loop") return "Loop";
9328
+ if (value === "scheduled") return "Scheduled";
9329
+ return null;
9330
+ }
9331
+ function fallbackSessionTitle(entry) {
9332
+ if (entry.runKind === "dynamic_loop") return "Loop run";
9333
+ if (entry.runKind === "scheduled") return "Scheduled run";
9334
+ return "New session";
9335
+ }
8643
9336
  function TreeNodeRow({
8644
9337
  entry,
8645
9338
  depth,
@@ -8655,9 +9348,11 @@ function TreeNodeRow({
8655
9348
  const hasChildren = entry.children.length > 0;
8656
9349
  const isActive = entry.id === activeSessionId;
8657
9350
  const isRunning = entry.status === "active";
8658
- const isSubAgent = entry.parentId != null;
8659
- const title = entry.title ?? "New session";
9351
+ const isChildSession = entry.parentId != null;
9352
+ const title = entry.title ?? fallbackSessionTitle(entry);
8660
9353
  const subtitle = [
9354
+ formatRunKind(entry.runKind),
9355
+ entry.agentType,
8661
9356
  entry.model ?? "default",
8662
9357
  formatSessionTime(entry.updatedAt)
8663
9358
  ].filter(Boolean).join(" \xB7 ");
@@ -8695,7 +9390,7 @@ function TreeNodeRow({
8695
9390
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm truncate", children: title }),
8696
9391
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-faint truncate", children: subtitle })
8697
9392
  ] }),
8698
- isRunning && canStop && isSubAgent && /* @__PURE__ */ jsxRuntime.jsx(
9393
+ isRunning && canStop && isChildSession && /* @__PURE__ */ jsxRuntime.jsx(
8699
9394
  "button",
8700
9395
  {
8701
9396
  type: "button",
@@ -8704,8 +9399,8 @@ function TreeNodeRow({
8704
9399
  e.stopPropagation();
8705
9400
  onStop(entry.id);
8706
9401
  },
8707
- "aria-label": "Stop sub-agent",
8708
- title: "Stop sub-agent",
9402
+ "aria-label": "Stop child session",
9403
+ title: "Stop child session",
8709
9404
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.SquareIcon, { className: "w-3 h-3", fill: "currentColor" })
8710
9405
  }
8711
9406
  ),
@@ -8909,6 +9604,7 @@ function SessionTreePanel({
8909
9604
  exports.AgentChat = AgentChat;
8910
9605
  exports.AgentChatAdapterProvider = AgentChatAdapterProvider;
8911
9606
  exports.MessageResponse = MessageResponse;
9607
+ exports.ScheduledWorkPanel = ScheduledWorkPanel;
8912
9608
  exports.SessionTreePanel = SessionTreePanel;
8913
9609
  exports.useAgentChatAdapterContext = useAgentChatAdapterContext;
8914
9610
  exports.useAgentChatRuntime = useAgentChatRuntime;