@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.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { cn } from './chunk-QSC4UIVT.js';
2
2
  import { createContext, memo, useRef, useState, useEffect, useCallback, useMemo, lazy, useContext, Suspense, Children } from 'react';
3
+ import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, ChevronRightIcon, GitBranchIcon, XIcon, ThumbsUpIcon, ThumbsDownIcon, ActivityIcon, Loader2Icon, GlobeIcon, SearchIcon, ListTodoIcon, XCircleIcon, CheckCircle2Icon, Code, CheckIcon, CopyIcon, TerminalIcon, Trash2Icon, CircleDotIcon, CircleCheckIcon, CircleXIcon, SkullIcon, ClockIcon, CheckCircleIcon, CircleIcon, CalendarClockIcon, UsersIcon, MessageSquareIcon, ZapIcon, FolderOpenIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, SquareIcon, ArrowDownIcon, RotateCcwIcon, MousePointer2Icon, DownloadIcon, TrashIcon, ImageIcon, FileTextIcon, Maximize2Icon, FolderIcon, FileIcon, ChevronLeftIcon, MinusIcon, PlusIcon, CornerDownLeftIcon } from 'lucide-react';
3
4
  import { cva } from 'class-variance-authority';
4
5
  import { Collapsible as Collapsible$1, Slot, Dialog as Dialog$1, ScrollArea as ScrollArea$1, Tooltip as Tooltip$1, Tabs as Tabs$1, Popover as Popover$1, DropdownMenu as DropdownMenu$1, HoverCard as HoverCard$1, Progress as Progress$1 } from 'radix-ui';
5
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
- import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, ChevronRightIcon, GitBranchIcon, XIcon, ThumbsUpIcon, ThumbsDownIcon, ActivityIcon, Loader2Icon, GlobeIcon, SearchIcon, ListTodoIcon, XCircleIcon, CheckCircle2Icon, Code, CheckIcon, CopyIcon, TerminalIcon, Trash2Icon, CircleDotIcon, CircleCheckIcon, CircleXIcon, SkullIcon, ClockIcon, CheckCircleIcon, CircleIcon, UsersIcon, MessageSquareIcon, FolderOpenIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, SquareIcon, ArrowDownIcon, DownloadIcon, TrashIcon, ImageIcon, FileTextIcon, Maximize2Icon, FolderIcon, FileIcon, ChevronLeftIcon, MinusIcon, PlusIcon, CornerDownLeftIcon } from 'lucide-react';
7
7
  import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';
8
8
  import { cjk } from '@streamdown/cjk';
9
9
  import { code } from '@streamdown/code';
@@ -79,6 +79,114 @@ function Button({
79
79
  }
80
80
  );
81
81
  }
82
+ function BrowserControlBar({
83
+ sessionId,
84
+ hasControl,
85
+ adapter
86
+ }) {
87
+ const [pending, setPending] = useState(false);
88
+ const [error, setError] = useState(null);
89
+ async function toggleControl() {
90
+ if (!sessionId || pending) return;
91
+ setPending(true);
92
+ setError(null);
93
+ try {
94
+ if (hasControl) {
95
+ await adapter.releaseBrowserControl(sessionId);
96
+ } else {
97
+ await adapter.acquireBrowserControl(sessionId);
98
+ }
99
+ } catch (nextError) {
100
+ setError(nextError.message);
101
+ } finally {
102
+ setPending(false);
103
+ }
104
+ }
105
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-h-11 items-center gap-2 border-t border-line bg-card px-3 py-2", children: [
106
+ /* @__PURE__ */ jsxs(
107
+ Button,
108
+ {
109
+ type: "button",
110
+ size: "xs",
111
+ variant: hasControl ? "secondary" : "default",
112
+ disabled: !sessionId || pending,
113
+ onClick: () => void toggleControl(),
114
+ children: [
115
+ hasControl ? /* @__PURE__ */ jsx(RotateCcwIcon, { className: "size-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(MousePointer2Icon, { className: "size-3", "aria-hidden": "true" }),
116
+ hasControl ? "Return control" : "Take control"
117
+ ]
118
+ }
119
+ ),
120
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [
121
+ /* @__PURE__ */ jsx("span", { className: "size-1.5 rounded-full bg-red-500", "aria-hidden": "true" }),
122
+ "Live"
123
+ ] }),
124
+ error && /* @__PURE__ */ jsx("span", { className: "max-w-40 truncate text-[11px] text-destructive", children: error })
125
+ ] });
126
+ }
127
+ function BrowserLiveView({ src }) {
128
+ return /* @__PURE__ */ jsx(
129
+ "iframe",
130
+ {
131
+ "data-testid": "browser-iframe",
132
+ title: "Browser live view",
133
+ src,
134
+ className: "h-full w-full border-0 bg-black",
135
+ allow: "clipboard-read; clipboard-write"
136
+ }
137
+ );
138
+ }
139
+ function BrowserStatusDot({
140
+ status
141
+ }) {
142
+ return /* @__PURE__ */ jsx(
143
+ "span",
144
+ {
145
+ "aria-hidden": "true",
146
+ className: cn(
147
+ "inline-block size-2 shrink-0 rounded-full",
148
+ status === "live" && "bg-emerald-500",
149
+ status === "user-control" && "bg-amber-500",
150
+ status === "provisioning" && "animate-pulse bg-sky-500",
151
+ status === "closed" && "bg-muted-foreground"
152
+ )
153
+ }
154
+ );
155
+ }
156
+ function BrowserPane({ sessionId, state, adapter }) {
157
+ const liveViewUrl = useMemo(
158
+ () => adapter.browserLiveViewUrl(sessionId),
159
+ [adapter, sessionId]
160
+ );
161
+ const hasLiveView = state.status !== "provisioning" && state.status !== "closed";
162
+ return /* @__PURE__ */ jsxs(
163
+ "div",
164
+ {
165
+ "data-testid": "browser-pane",
166
+ className: "flex h-full min-h-0 flex-col bg-background",
167
+ children: [
168
+ /* @__PURE__ */ jsxs("header", { className: "flex min-h-10 items-center gap-2 border-b border-line bg-card px-3 text-xs text-foreground", children: [
169
+ /* @__PURE__ */ jsx(ZapIcon, { className: "size-3.5 text-muted-foreground", "aria-hidden": "true" }),
170
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Browser" }),
171
+ /* @__PURE__ */ jsx(BrowserStatusDot, { status: state.status }),
172
+ state.controlOwner && /* @__PURE__ */ jsxs("span", { className: "min-w-0 truncate text-amber-500", children: [
173
+ state.controlOwner,
174
+ " has control"
175
+ ] })
176
+ ] }),
177
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-black", children: state.status === "provisioning" ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Starting browser..." }) : state.status === "closed" ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center bg-background text-sm text-muted-foreground", children: "Browser closed." }) : /* @__PURE__ */ jsx(BrowserLiveView, { src: liveViewUrl }) }),
178
+ hasLiveView && /* @__PURE__ */ jsx(
179
+ BrowserControlBar,
180
+ {
181
+ sessionId,
182
+ hasControl: state.status === "user-control",
183
+ adapter
184
+ }
185
+ )
186
+ ]
187
+ }
188
+ );
189
+ }
82
190
  var Conversation = ({ className, ...props }) => /* @__PURE__ */ jsx(
83
191
  StickToBottom,
84
192
  {
@@ -565,6 +673,80 @@ function TimelineSeparator({
565
673
  }
566
674
  );
567
675
  }
676
+ function BrowserActivityGroup({ calls }) {
677
+ const [open, setOpen] = useState(false);
678
+ const latest = calls[calls.length - 1];
679
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-none border border-line bg-card text-xs", children: [
680
+ /* @__PURE__ */ jsxs(
681
+ "button",
682
+ {
683
+ type: "button",
684
+ onClick: () => setOpen((current) => !current),
685
+ className: "flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted/40",
686
+ children: [
687
+ open ? /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3 shrink-0", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-3 shrink-0", "aria-hidden": "true" }),
688
+ /* @__PURE__ */ jsx(ZapIcon, { className: "size-3 shrink-0 text-amber-500", "aria-hidden": "true" }),
689
+ /* @__PURE__ */ jsx("span", { className: "font-semibold text-foreground", children: "browser" }),
690
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 truncate text-muted-foreground", children: [
691
+ "(",
692
+ calls.length,
693
+ " ",
694
+ calls.length === 1 ? "action" : "actions",
695
+ " - latest:",
696
+ " ",
697
+ latest ? summarise(latest) : "none",
698
+ ")"
699
+ ] })
700
+ ]
701
+ }
702
+ ),
703
+ open && /* @__PURE__ */ jsx("ul", { className: "border-t border-line py-1", children: calls.map((call) => {
704
+ const error = stringValue(parseJson(call.result).error);
705
+ return /* @__PURE__ */ jsxs(
706
+ "li",
707
+ {
708
+ className: "flex items-center gap-2 px-6 py-1 font-mono text-[11px]",
709
+ children: [
710
+ error && /* @__PURE__ */ jsx(
711
+ AlertCircleIcon,
712
+ {
713
+ "data-testid": `activity-error-${call.id}`,
714
+ className: "size-3 shrink-0 text-destructive",
715
+ "aria-hidden": "true"
716
+ }
717
+ ),
718
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: summarise(call) }),
719
+ error && /* @__PURE__ */ jsx("span", { className: "ml-auto max-w-40 truncate text-destructive", children: error })
720
+ ]
721
+ },
722
+ call.id
723
+ );
724
+ }) })
725
+ ] });
726
+ }
727
+ function summarise(call) {
728
+ const verb = call.toolName.replace(/^browser_/, "");
729
+ const args = parseJson(call.args);
730
+ const ref = stringValue(args.ref);
731
+ const url = stringValue(args.url);
732
+ const text = stringValue(args.text);
733
+ if (url) return `${verb} ${url}`;
734
+ if (ref) return `${verb} ${ref}`;
735
+ if (text) return `${verb} "${text.slice(0, 24)}"`;
736
+ return verb;
737
+ }
738
+ function parseJson(raw) {
739
+ if (!raw) return {};
740
+ try {
741
+ const parsed = JSON.parse(raw);
742
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
743
+ } catch {
744
+ return {};
745
+ }
746
+ }
747
+ function stringValue(value) {
748
+ return typeof value === "string" ? value : "";
749
+ }
568
750
 
569
751
  // src/components/chat/tools/shared.ts
570
752
  function statusColorClass(status) {
@@ -3304,13 +3486,16 @@ function ClarifyLocked({
3304
3486
  }) })
3305
3487
  ] });
3306
3488
  }
3307
- function ArtifactToolBlock({ tc }) {
3308
- const args = parseArgs(tc.args) ?? {};
3489
+ function ArtifactToolBlock({
3490
+ tc,
3491
+ resolvedName
3492
+ }) {
3309
3493
  const status = effectiveStatus(tc);
3310
3494
  const label = status === "running" ? "Creating artifact\u2026" : status === "error" ? "Creating artifact\u2026" : "Created";
3495
+ const name = resolvedName ?? parseArgs(tc.args)?.name;
3311
3496
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-sm ", children: [
3312
3497
  /* @__PURE__ */ jsx("span", { className: "font-semibold text-foreground", children: label }),
3313
- args.name && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground truncate", children: args.name })
3498
+ name && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground truncate", children: name })
3314
3499
  ] });
3315
3500
  }
3316
3501
  function firstLine(s) {
@@ -3636,7 +3821,11 @@ function DefaultToolBlock({ tc }) {
3636
3821
  ] })
3637
3822
  ] });
3638
3823
  }
3639
- function ToolCallBlock({ tc, onFileSelect }) {
3824
+ function ToolCallBlock({
3825
+ tc,
3826
+ resolvedArtifactName,
3827
+ onFileSelect
3828
+ }) {
3640
3829
  switch (tc.toolName) {
3641
3830
  case "terminal":
3642
3831
  return /* @__PURE__ */ jsx(TerminalToolBlock, { tc });
@@ -3673,7 +3862,7 @@ function ToolCallBlock({ tc, onFileSelect }) {
3673
3862
  case "clarify":
3674
3863
  return /* @__PURE__ */ jsx(ClarifyToolBlock, { tc });
3675
3864
  case "create_artifact":
3676
- return /* @__PURE__ */ jsx(ArtifactToolBlock, { tc });
3865
+ return /* @__PURE__ */ jsx(ArtifactToolBlock, { tc, resolvedName: resolvedArtifactName });
3677
3866
  case "delegate_task":
3678
3867
  return /* @__PURE__ */ jsx(DelegateToolBlock, { tc });
3679
3868
  case "memory":
@@ -5055,6 +5244,7 @@ function ChatComposerInner({
5055
5244
  onStop,
5056
5245
  isRunning,
5057
5246
  disabled = false,
5247
+ disabledReason,
5058
5248
  tokenUsage
5059
5249
  }) {
5060
5250
  const { adapter } = useAgentChatAdapterContext();
@@ -5083,7 +5273,10 @@ function ChatComposerInner({
5083
5273
  const slashCommands = useMemo(() => {
5084
5274
  const builtin = [
5085
5275
  { value: "/clear", label: "/clear", description: "Clear conversation" },
5086
- { value: "/compress", label: "/compress", description: "Compress context" }
5276
+ { value: "/compress", label: "/compress", description: "Compress context" },
5277
+ { value: "/loop", label: "/loop", description: "Schedule recurring prompt" },
5278
+ { value: "/loop list", label: "/loop list", description: "List active loops" },
5279
+ { value: "/loop cancel", label: "/loop cancel", description: "Cancel a loop by ID" }
5087
5280
  ];
5088
5281
  return [...adapterCommands, ...builtin];
5089
5282
  }, [adapterCommands]);
@@ -5162,7 +5355,7 @@ function ChatComposerInner({
5162
5355
  /* @__PURE__ */ jsx(PromptInputBody, { children: /* @__PURE__ */ jsx(
5163
5356
  PromptInputTextarea,
5164
5357
  {
5165
- placeholder: disabled ? "Session disabled" : "Send a message...",
5358
+ placeholder: disabled ? disabledReason ?? "Session disabled" : "Send a message...",
5166
5359
  disabled,
5167
5360
  onKeyDown: handleKeyDown
5168
5361
  }
@@ -5807,7 +6000,7 @@ var ErrorMessage = memo(function ErrorMessage2({
5807
6000
  }
5808
6001
  );
5809
6002
  });
5810
- function messageToEntries(msg, isLast) {
6003
+ function messageToEntries(msg, isLast, artifactFallbacks) {
5811
6004
  if (msg.role === "system") {
5812
6005
  if (msg.systemKind === "skill_invoked") {
5813
6006
  return [{
@@ -5849,6 +6042,18 @@ function messageToEntries(msg, isLast) {
5849
6042
  }
5850
6043
  if (hasToolCalls) {
5851
6044
  for (const tc of msg.toolCalls) {
6045
+ if (tc.toolName === "create_artifact") {
6046
+ const resolvedName = parseArgs(tc.args)?.name ?? artifactFallbacks[tc.id];
6047
+ const status = effectiveStatus(tc);
6048
+ if (!resolvedName && status !== "error") continue;
6049
+ entries.push({
6050
+ kind: "tool",
6051
+ key: tc.id,
6052
+ tc,
6053
+ resolvedArtifactName: resolvedName
6054
+ });
6055
+ continue;
6056
+ }
5852
6057
  entries.push({ kind: "tool", key: tc.id, tc });
5853
6058
  }
5854
6059
  }
@@ -5863,6 +6068,32 @@ function messageToEntries(msg, isLast) {
5863
6068
  }
5864
6069
  return entries;
5865
6070
  }
6071
+ function groupBrowserActivityEntries(entries) {
6072
+ const grouped = [];
6073
+ let buffer = [];
6074
+ const flush = () => {
6075
+ if (buffer.length === 0) return;
6076
+ grouped.push({
6077
+ kind: "browser_activity",
6078
+ key: `browser-activity-${buffer[0]?.id}-${buffer[buffer.length - 1]?.id}`,
6079
+ calls: buffer
6080
+ });
6081
+ buffer = [];
6082
+ };
6083
+ for (const entry of entries) {
6084
+ if (entry.kind === "tool" && isBrowserToolCall(entry.tc)) {
6085
+ buffer.push(entry.tc);
6086
+ continue;
6087
+ }
6088
+ flush();
6089
+ grouped.push(entry);
6090
+ }
6091
+ flush();
6092
+ return grouped;
6093
+ }
6094
+ function isBrowserToolCall(call) {
6095
+ return call.toolName.startsWith("browser_");
6096
+ }
5866
6097
  function groupMessages(messages) {
5867
6098
  const groups = [];
5868
6099
  let pendingSystem = [];
@@ -6007,11 +6238,27 @@ function TimelineEntryItem({
6007
6238
  )
6008
6239
  ] }),
6009
6240
  /* @__PURE__ */ jsx(TimelineContent, { children: entry.tc.cancelled ? /* @__PURE__ */ jsx(CancelledToolRow, { tc: entry.tc }) : /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
6010
- /* @__PURE__ */ jsx(ToolCallBlock, { tc: entry.tc, onFileSelect }),
6241
+ /* @__PURE__ */ jsx(
6242
+ ToolCallBlock,
6243
+ {
6244
+ tc: entry.tc,
6245
+ resolvedArtifactName: entry.resolvedArtifactName,
6246
+ onFileSelect
6247
+ }
6248
+ ),
6011
6249
  failureSummary ? /* @__PURE__ */ jsx("div", { className: "max-w-full truncate text-xs text-destructive", title: failureSummary, children: failureSummary }) : null
6012
6250
  ] }) })
6013
6251
  ] });
6014
6252
  }
6253
+ if (entry.kind === "browser_activity") {
6254
+ return /* @__PURE__ */ jsxs(TimelineItem, { step, children: [
6255
+ /* @__PURE__ */ jsxs(TimelineHeader, { children: [
6256
+ /* @__PURE__ */ jsx(TimelineSeparator, { style: { backgroundColor: "var(--color-border)" } }),
6257
+ /* @__PURE__ */ jsx(TimelineIndicator, { className: "size-2 border-none bg-amber-500" })
6258
+ ] }),
6259
+ /* @__PURE__ */ jsx(TimelineContent, { children: /* @__PURE__ */ jsx(BrowserActivityGroup, { calls: entry.calls }) })
6260
+ ] });
6261
+ }
6015
6262
  if (entry.kind === "text") {
6016
6263
  return /* @__PURE__ */ jsxs(TimelineItem, { step, children: [
6017
6264
  /* @__PURE__ */ jsxs(TimelineHeader, { children: [
@@ -6069,14 +6316,16 @@ function AssistantGroup({
6069
6316
  totalMessages,
6070
6317
  isRunning,
6071
6318
  sessionId,
6319
+ artifactFallbacks,
6072
6320
  onFileSelect,
6073
6321
  onRetry
6074
6322
  }) {
6075
- const entries = [];
6323
+ let entries = [];
6076
6324
  for (let i = 0; i < messages.length; i++) {
6077
6325
  const isLast = i === messages.length - 1 && lastGlobalIndex === totalMessages - 1;
6078
- entries.push(...messageToEntries(messages[i], isLast));
6326
+ entries.push(...messageToEntries(messages[i], isLast, artifactFallbacks));
6079
6327
  }
6328
+ entries = groupBrowserActivityEntries(entries);
6080
6329
  const isTailGroup = lastGlobalIndex === totalMessages - 1;
6081
6330
  const lastEntry = entries[entries.length - 1];
6082
6331
  const hasRunningTool = entries.some(
@@ -6142,11 +6391,34 @@ function ChatThread({
6142
6391
  onStop,
6143
6392
  onFileSelect,
6144
6393
  disabled = false,
6394
+ disabledReason,
6145
6395
  tokenUsage,
6146
6396
  retryIndicator,
6147
6397
  onRetry
6148
6398
  }) {
6149
6399
  const groups = useMemo(() => groupMessages(messages), [messages]);
6400
+ const artifactFallbacks = useMemo(() => {
6401
+ const fallbacks = {};
6402
+ const createArtifactToolCallIds = [];
6403
+ let artifactIdx = 0;
6404
+ for (const msg of messages) {
6405
+ if (msg.role === "assistant" && msg.toolCalls) {
6406
+ for (const tc of msg.toolCalls) {
6407
+ if (tc.toolName === "create_artifact" && !tc.cancelled && tc.status !== "error") {
6408
+ createArtifactToolCallIds.push(tc.id);
6409
+ }
6410
+ }
6411
+ } else if (msg.role === "system" && msg.systemKind === "artifact") {
6412
+ const tcId = createArtifactToolCallIds[artifactIdx];
6413
+ if (tcId) {
6414
+ const { name } = unpackArtifactMeta(msg.systemMeta, msg.content);
6415
+ if (name) fallbacks[tcId] = name;
6416
+ }
6417
+ artifactIdx += 1;
6418
+ }
6419
+ }
6420
+ return fallbacks;
6421
+ }, [messages]);
6150
6422
  const activeFailureId = useMemo(() => {
6151
6423
  if (messages.length === 0) return null;
6152
6424
  const tail = messages[messages.length - 1];
@@ -6211,6 +6483,7 @@ function ChatThread({
6211
6483
  totalMessages: messages.length,
6212
6484
  isRunning,
6213
6485
  sessionId,
6486
+ artifactFallbacks,
6214
6487
  onFileSelect,
6215
6488
  onRetry: groupRetry
6216
6489
  },
@@ -6223,13 +6496,21 @@ function ChatThread({
6223
6496
  ] }),
6224
6497
  /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-4xl px-6 pb-5 pt-3", children: [
6225
6498
  retryIndicator && /* @__PURE__ */ jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsx(RetryBanner, { indicator: retryIndicator }) }),
6226
- /* @__PURE__ */ jsx(
6499
+ disabled && disabledReason ? /* @__PURE__ */ jsx(
6500
+ "div",
6501
+ {
6502
+ className: "rounded border border-line bg-muted/40 px-3 py-2 text-sm text-muted-foreground",
6503
+ role: "status",
6504
+ children: disabledReason
6505
+ }
6506
+ ) : /* @__PURE__ */ jsx(
6227
6507
  ChatComposer,
6228
6508
  {
6229
6509
  onSend,
6230
6510
  onStop,
6231
6511
  isRunning,
6232
6512
  disabled,
6513
+ disabledReason,
6233
6514
  tokenUsage
6234
6515
  }
6235
6516
  )
@@ -7058,7 +7339,7 @@ function WorkspacePanel({
7058
7339
  );
7059
7340
  const handleDelete = useCallback(
7060
7341
  async (path) => {
7061
- if (!sessionId) return;
7342
+ if (disabled || !sessionId) return;
7062
7343
  try {
7063
7344
  await adapter.deleteWorkspaceFile({ sessionId, path });
7064
7345
  if (selectedPath === path) {
@@ -7074,7 +7355,7 @@ function WorkspacePanel({
7074
7355
  setDeleteTarget(null);
7075
7356
  }
7076
7357
  },
7077
- [adapter, fetchTree, onSelectedPathChange, selectedPath, sessionId]
7358
+ [adapter, disabled, fetchTree, onSelectedPathChange, selectedPath, sessionId]
7078
7359
  );
7079
7360
  const onResizeStart = useCallback(
7080
7361
  (event) => {
@@ -7112,6 +7393,7 @@ function WorkspacePanel({
7112
7393
  return /* @__PURE__ */ jsx(
7113
7394
  "aside",
7114
7395
  {
7396
+ "data-testid": "workspace-panel",
7115
7397
  role: "button",
7116
7398
  tabIndex: 0,
7117
7399
  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",
@@ -7130,6 +7412,7 @@ function WorkspacePanel({
7130
7412
  return /* @__PURE__ */ jsxs(
7131
7413
  "aside",
7132
7414
  {
7415
+ "data-testid": "workspace-panel",
7133
7416
  className: "relative z-10 flex min-h-0 flex-col overflow-hidden border-l border-muted-foreground/20 bg-card",
7134
7417
  style: { width, minWidth: MIN_WIDTH, maxWidth: MAX_WIDTH },
7135
7418
  children: [
@@ -7157,7 +7440,7 @@ function WorkspacePanel({
7157
7440
  /* @__PURE__ */ jsx(FolderOpenIcon, { className: "size-4 shrink-0 text-amber-500" }),
7158
7441
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
7159
7442
  /* @__PURE__ */ jsx("div", { className: "truncate text-sm font-medium text-foreground", children: rootName }),
7160
- /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-faint", children: "Workspace" })
7443
+ /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-faint", children: disabled ? "Workspace - Read-only" : "Workspace" })
7161
7444
  ] }),
7162
7445
  /* @__PURE__ */ jsxs(Tooltip, { children: [
7163
7446
  /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
@@ -7268,7 +7551,7 @@ function WorkspacePanel({
7268
7551
  sessionId,
7269
7552
  path: file.path
7270
7553
  }) : null,
7271
- onDelete: file ? () => setDeleteTarget(file.path) : null,
7554
+ onDelete: file && !disabled ? () => setDeleteTarget(file.path) : null,
7272
7555
  onClose: () => {
7273
7556
  onSelectedPathChange(null);
7274
7557
  setFile(null);
@@ -7332,6 +7615,13 @@ function ConfirmDialog({
7332
7615
  );
7333
7616
  }
7334
7617
 
7618
+ // src/lib/sessions.ts
7619
+ function isScheduledRunSession(session) {
7620
+ return Boolean(
7621
+ session?.channel === "scheduled" || session?.config?.scheduled_session_id
7622
+ );
7623
+ }
7624
+
7335
7625
  // src/runtime/events.ts
7336
7626
  var WORKSPACE_MUTATING_TOOLS = /* @__PURE__ */ new Set([
7337
7627
  "terminal",
@@ -7364,6 +7654,10 @@ var AGENT_CHAT_LISTENED_EVENTS = [
7364
7654
  "expert.override",
7365
7655
  "artifact.created",
7366
7656
  "artifact.updated",
7657
+ "browser.provisioned",
7658
+ "browser.destroyed",
7659
+ "browser.control_granted",
7660
+ "browser.control_returned",
7367
7661
  "clarify.response"
7368
7662
  ];
7369
7663
 
@@ -7388,7 +7682,8 @@ function createInitialAgentChatState(options = {}) {
7388
7682
  sessionDone: false,
7389
7683
  hadDeltas: false,
7390
7684
  terminal: false,
7391
- workspaceRefreshKey: 0
7685
+ workspaceRefreshKey: 0,
7686
+ browser: null
7392
7687
  };
7393
7688
  }
7394
7689
  function applyAgentChatEvent(state, event) {
@@ -7412,7 +7707,7 @@ function applyAgentChatEvent(state, event) {
7412
7707
  {
7413
7708
  id: `evt-${event.eventId}`,
7414
7709
  role: "system",
7415
- content: stringValue(event.data.skill),
7710
+ content: stringValue2(event.data.skill),
7416
7711
  createdAt: /* @__PURE__ */ new Date(),
7417
7712
  status: "complete",
7418
7713
  systemKind: "skill_invoked",
@@ -7429,7 +7724,7 @@ function applyAgentChatEvent(state, event) {
7429
7724
  {
7430
7725
  id: `evt-${event.eventId}`,
7431
7726
  role: "system",
7432
- content: stringValue(event.data.name),
7727
+ content: stringValue2(event.data.name),
7433
7728
  createdAt: /* @__PURE__ */ new Date(),
7434
7729
  status: "complete",
7435
7730
  systemKind: "artifact",
@@ -7442,6 +7737,23 @@ function applyAgentChatEvent(state, event) {
7442
7737
  }
7443
7738
  }
7444
7739
  ]);
7740
+ case "browser.provisioned":
7741
+ return applyBrowserEvent(nextState, event, {
7742
+ status: "live",
7743
+ controlOwner: null
7744
+ });
7745
+ case "browser.control_granted":
7746
+ return applyBrowserEvent(nextState, event, {
7747
+ status: "user-control",
7748
+ controlOwner: stringValue2(event.data.owner_user_id) || null
7749
+ });
7750
+ case "browser.control_returned":
7751
+ return applyBrowserEvent(nextState, event, {
7752
+ status: "live",
7753
+ controlOwner: null
7754
+ });
7755
+ case "browser.destroyed":
7756
+ return applyBrowserEvent(nextState, event, null);
7445
7757
  case "llm.delta":
7446
7758
  return applyLlmDelta(nextState, event);
7447
7759
  case "llm.response":
@@ -7451,7 +7763,7 @@ function applyAgentChatEvent(state, event) {
7451
7763
  case "tool.call":
7452
7764
  return applyToolCall(nextState, event);
7453
7765
  case "tool.result": {
7454
- const toolCallId = stringValue(event.data.tool_call_id);
7766
+ const toolCallId = stringValue2(event.data.tool_call_id);
7455
7767
  const toolName = findToolNameById(nextState.messages, toolCallId);
7456
7768
  const messages = applyToolResult(nextState.messages, event.data);
7457
7769
  const mutatesWorkspace = toolName !== null && WORKSPACE_MUTATING_TOOLS.has(toolName);
@@ -7521,8 +7833,8 @@ function applyRetryIndicator(state, event) {
7521
7833
  case "session.done":
7522
7834
  return { ...state, retryIndicator: null };
7523
7835
  case "harness.crash": {
7524
- const title = stringValue(event.data.error_title) || "A transient error occurred, retrying...";
7525
- const detail = stringValue(event.data.error_detail) || stringValue(event.data.error);
7836
+ const title = stringValue2(event.data.error_title) || "A transient error occurred, retrying...";
7837
+ const detail = stringValue2(event.data.error_detail) || stringValue2(event.data.error);
7526
7838
  return {
7527
7839
  ...state,
7528
7840
  retryIndicator: {
@@ -7537,7 +7849,7 @@ function applyRetryIndicator(state, event) {
7537
7849
  }
7538
7850
  }
7539
7851
  function applyUserMessage(messages, eventId, data) {
7540
- const content = stringValue(data.content);
7852
+ const content = stringValue2(data.content);
7541
7853
  const next = [...messages];
7542
7854
  const localIdx = next.findIndex(
7543
7855
  (m) => m.role === "user" && m.id.startsWith("local-") && m.content === content
@@ -7555,9 +7867,47 @@ function applyUserMessage(messages, eventId, data) {
7555
7867
  });
7556
7868
  return next;
7557
7869
  }
7870
+ function applyBrowserEvent(state, event, browser) {
7871
+ return withMessages(
7872
+ { ...state, browser },
7873
+ [...state.messages, browserMarker(event)]
7874
+ );
7875
+ }
7876
+ function browserMarker(event) {
7877
+ const labels = {
7878
+ "browser.provisioned": {
7879
+ content: "Browser ready.",
7880
+ warning: false
7881
+ },
7882
+ "browser.control_granted": {
7883
+ content: "A user took control of the browser.",
7884
+ warning: true
7885
+ },
7886
+ "browser.control_returned": {
7887
+ content: "Browser control returned to the agent.",
7888
+ warning: false
7889
+ },
7890
+ "browser.destroyed": {
7891
+ content: "Browser closed.",
7892
+ warning: false
7893
+ }
7894
+ };
7895
+ const label = labels[event.type] ?? {
7896
+ content: "Browser updated.",
7897
+ warning: false
7898
+ };
7899
+ return {
7900
+ id: `browser-marker-${event.eventId}`,
7901
+ role: "system",
7902
+ content: label.content,
7903
+ createdAt: /* @__PURE__ */ new Date(),
7904
+ status: "complete",
7905
+ systemKind: label.warning ? "browser_marker_warning" : "browser_marker"
7906
+ };
7907
+ }
7558
7908
  function applyLlmDelta(state, event) {
7559
- const deltaContent = stringValue(event.data.content);
7560
- const deltaReasoning = stringValue(event.data.reasoning);
7909
+ const deltaContent = stringValue2(event.data.content);
7910
+ const deltaReasoning = stringValue2(event.data.reasoning);
7561
7911
  const messages = [...state.messages];
7562
7912
  const lastIdx = findLastAssistantIndex(messages);
7563
7913
  const lastMsg = lastIdx >= 0 ? messages[lastIdx] : null;
@@ -7594,7 +7944,7 @@ function applyLlmDelta(state, event) {
7594
7944
  function applyLlmResponse(state, event) {
7595
7945
  const messages = [...state.messages];
7596
7946
  const message = objectValue(event.data.message);
7597
- const responseContent = stringValue(message?.content) || stringValue(event.data.content);
7947
+ const responseContent = stringValue2(message?.content) || stringValue2(event.data.content);
7598
7948
  const toolCalls = Array.isArray(message?.tool_calls) ? message?.tool_calls : [];
7599
7949
  const hasToolCalls = toolCalls.length > 0;
7600
7950
  const idx = findLastAssistantIndex(messages);
@@ -7652,12 +8002,12 @@ function applyLlmResponse(state, event) {
7652
8002
  cachedInputTokens: numberValue(event.data.cache_read_tokens),
7653
8003
  totalTokens: inputTokens + outputTokens,
7654
8004
  contextWindow: numberValue(event.data.context_window),
7655
- model: stringValue(event.data.model)
8005
+ model: stringValue2(event.data.model)
7656
8006
  }
7657
8007
  };
7658
8008
  }
7659
8009
  function applyLlmThinking(state, event) {
7660
- const reasoningText = stringValue(event.data.reasoning) || stringValue(event.data.content);
8010
+ const reasoningText = stringValue2(event.data.reasoning) || stringValue2(event.data.content);
7661
8011
  const messages = [...state.messages];
7662
8012
  const idx = findLastAssistantIndex(messages);
7663
8013
  const prev = idx >= 0 ? messages[idx] : null;
@@ -7705,11 +8055,11 @@ function applyToolCall(state, event) {
7705
8055
  messages[assistantIdx] = assistant;
7706
8056
  }
7707
8057
  const existingCalls = assistant.toolCalls ?? [];
7708
- const toolCallId = stringValue(event.data.tool_call_id) || `tc-${event.eventId}`;
8058
+ const toolCallId = stringValue2(event.data.tool_call_id) || `tc-${event.eventId}`;
7709
8059
  if (!existingCalls.some((tc) => tc.id === toolCallId)) {
7710
8060
  const entry = {
7711
8061
  id: toolCallId,
7712
- toolName: stringValue(event.data.name) || stringValue(event.data.tool_name) || "unknown",
8062
+ toolName: stringValue2(event.data.name) || stringValue2(event.data.tool_name) || "unknown",
7713
8063
  args: typeof event.data.arguments === "string" ? event.data.arguments : JSON.stringify(event.data.arguments ?? {}),
7714
8064
  status: "running"
7715
8065
  };
@@ -7725,7 +8075,7 @@ function applyToolCall(state, event) {
7725
8075
  };
7726
8076
  }
7727
8077
  function applyToolResult(messages, data) {
7728
- const targetId = stringValue(data.tool_call_id);
8078
+ const targetId = stringValue2(data.tool_call_id);
7729
8079
  let matchIdx = -1;
7730
8080
  for (let i = messages.length - 1; i >= 0; i--) {
7731
8081
  if (messages[i]?.role === "assistant" && messages[i]?.toolCalls?.some((tc) => tc.id === targetId)) {
@@ -7794,8 +8144,8 @@ function buildErrorInfo(event) {
7794
8144
  }
7795
8145
  return {
7796
8146
  category: event.data.error_category,
7797
- title: stringValue(event.data.error_title) || "The session failed due to an internal error.",
7798
- detail: stringValue(event.data.error_detail) || stringValue(event.data.error),
8147
+ title: stringValue2(event.data.error_title) || "The session failed due to an internal error.",
8148
+ detail: stringValue2(event.data.error_detail) || stringValue2(event.data.error),
7799
8149
  retryable: Boolean(event.data.retryable)
7800
8150
  };
7801
8151
  }
@@ -7839,7 +8189,7 @@ function applyExpertFeedback(messages, type, data) {
7839
8189
  return messages;
7840
8190
  }
7841
8191
  function applyClarifyResponse(messages, data) {
7842
- const targetToolId = stringValue(data.tool_call_id);
8192
+ const targetToolId = stringValue2(data.tool_call_id);
7843
8193
  const responses = Array.isArray(data.responses) ? data.responses : void 0;
7844
8194
  if (!targetToolId || !responses) return messages;
7845
8195
  const next = [...messages];
@@ -7854,8 +8204,8 @@ function applyClarifyResponse(messages, data) {
7854
8204
  clarifyAnswers: responses.map((response) => {
7855
8205
  const row = objectValue(response) ?? {};
7856
8206
  return {
7857
- question: stringValue(row.question),
7858
- answer: stringValue(row.answer),
8207
+ question: stringValue2(row.question),
8208
+ answer: stringValue2(row.answer),
7859
8209
  is_other: Boolean(row.is_other)
7860
8210
  };
7861
8211
  })
@@ -7875,7 +8225,7 @@ function applyPolicyDenied(state, data) {
7875
8225
  ...policyMsg,
7876
8226
  content: `${policyMsg.content}
7877
8227
 
7878
- **Policy denied**: ${stringValue(data.reason) || "Action blocked by governance policy."}`,
8228
+ **Policy denied**: ${stringValue2(data.reason) || "Action blocked by governance policy."}`,
7879
8229
  status: "error"
7880
8230
  };
7881
8231
  }
@@ -7927,7 +8277,7 @@ function objectValue(value) {
7927
8277
  }
7928
8278
  return void 0;
7929
8279
  }
7930
- function stringValue(value) {
8280
+ function stringValue2(value) {
7931
8281
  return typeof value === "string" ? value : "";
7932
8282
  }
7933
8283
  function numberValue(value) {
@@ -7944,6 +8294,7 @@ function useAgentChatRuntime({
7944
8294
  const [state, setState] = useState(
7945
8295
  () => createInitialAgentChatState({ isLoadingHistory: Boolean(sessionId) })
7946
8296
  );
8297
+ const [session, setSession] = useState(null);
7947
8298
  const stateRef = useRef(state);
7948
8299
  const streamRef = useRef(null);
7949
8300
  const reconnectTimerRef = useRef(null);
@@ -7968,6 +8319,7 @@ function useAgentChatRuntime({
7968
8319
  clearReconnectTimer();
7969
8320
  closeStream();
7970
8321
  if (!sessionId) {
8322
+ setSession(null);
7971
8323
  setState(createInitialAgentChatState());
7972
8324
  return;
7973
8325
  }
@@ -8015,17 +8367,19 @@ function useAgentChatRuntime({
8015
8367
  isLoadingHistory: true
8016
8368
  });
8017
8369
  stateRef.current = initialState;
8370
+ setSession(null);
8018
8371
  setState(initialState);
8019
8372
  connect(0);
8020
- adapter.getSession({ sessionId }).then((session) => {
8373
+ adapter.getSession({ sessionId }).then((loadedSession) => {
8021
8374
  if (cancelled) return;
8022
- if (session.messageCount === 0) {
8375
+ setSession(loadedSession);
8376
+ if (loadedSession.messageCount === 0) {
8023
8377
  setState((prev) => ({
8024
8378
  ...prev,
8025
8379
  isLoadingHistory: false
8026
8380
  }));
8027
8381
  }
8028
- if (isTerminalStatus(session.status)) {
8382
+ if (isTerminalStatus(loadedSession.status)) {
8029
8383
  setState((prev) => ({
8030
8384
  ...prev,
8031
8385
  terminal: true,
@@ -8114,9 +8468,9 @@ function useAgentChatRuntime({
8114
8468
  markSending(content, images);
8115
8469
  if (!sessionId) {
8116
8470
  try {
8117
- const session = await adapter.createSession({ agentId });
8118
- onSessionChange?.(session.id);
8119
- await adapter.sendMessage({ sessionId: session.id, content, images });
8471
+ const session2 = await adapter.createSession({ agentId });
8472
+ onSessionChange?.(session2.id);
8473
+ await adapter.sendMessage({ sessionId: session2.id, content, images });
8120
8474
  } catch (error) {
8121
8475
  markSendError(error instanceof Error ? error.message : "send failed");
8122
8476
  throw error;
@@ -8157,6 +8511,8 @@ function useAgentChatRuntime({
8157
8511
  }
8158
8512
  }, [adapter, sessionId]);
8159
8513
  return {
8514
+ state,
8515
+ session,
8160
8516
  messages: state.messages,
8161
8517
  isRunning: state.isRunning,
8162
8518
  isLoadingHistory: state.isLoadingHistory,
@@ -8204,6 +8560,10 @@ function AgentChat({
8204
8560
  sessionId,
8205
8561
  onSessionChange
8206
8562
  });
8563
+ const isReadOnlySession = isScheduledRunSession(runtime.session);
8564
+ const effectiveDisabled = disabled || isReadOnlySession;
8565
+ const disabledReason = isReadOnlySession ? "Scheduled run is read-only" : void 0;
8566
+ const browserState = runtime.state.browser;
8207
8567
  useEffect(() => {
8208
8568
  onMessagesChange?.(runtime.messages);
8209
8569
  }, [onMessagesChange, runtime.messages]);
@@ -8234,28 +8594,339 @@ function AgentChat({
8234
8594
  onStop: () => void runtime.stop(),
8235
8595
  onRetry: runtime.retry,
8236
8596
  onFileSelect: handleFileSelect,
8237
- disabled,
8597
+ disabled: effectiveDisabled,
8598
+ disabledReason,
8238
8599
  tokenUsage: runtime.tokenUsage,
8239
8600
  retryIndicator: runtime.retryIndicator
8240
8601
  }
8241
8602
  ) }),
8242
- /* @__PURE__ */ jsx(
8243
- WorkspacePanel,
8244
- {
8245
- adapter,
8246
- sessionId,
8247
- selectedPath: workspacePath,
8248
- onSelectedPathChange: setWorkspacePath,
8249
- collapsed: workspaceCollapsed,
8250
- onCollapsedChange: setWorkspaceCollapsed,
8251
- refreshSignal: runtime.workspaceRefreshKey,
8252
- disabled
8253
- }
8254
- )
8603
+ /* @__PURE__ */ jsxs("div", { "data-testid": "right-stack", className: "flex min-h-0 shrink-0 flex-col", children: [
8604
+ browserState !== null && sessionId && /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 border-b border-line", children: /* @__PURE__ */ jsx(
8605
+ BrowserPane,
8606
+ {
8607
+ sessionId,
8608
+ state: browserState,
8609
+ adapter
8610
+ }
8611
+ ) }),
8612
+ /* @__PURE__ */ jsx(
8613
+ WorkspacePanel,
8614
+ {
8615
+ adapter,
8616
+ sessionId,
8617
+ selectedPath: workspacePath,
8618
+ onSelectedPathChange: setWorkspacePath,
8619
+ collapsed: workspaceCollapsed,
8620
+ onCollapsedChange: setWorkspaceCollapsed,
8621
+ refreshSignal: runtime.workspaceRefreshKey,
8622
+ disabled: effectiveDisabled
8623
+ }
8624
+ )
8625
+ ] })
8255
8626
  ] }) })
8256
8627
  }
8257
8628
  );
8258
8629
  }
8630
+ var DEFAULT_LIMIT = 50;
8631
+ var DEFAULT_POLL_INTERVAL_MS = 3e4;
8632
+ function fingerprint(items) {
8633
+ return items.map(
8634
+ (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}`
8635
+ ).join("|");
8636
+ }
8637
+ function formatKind(value) {
8638
+ if (value === "dynamic_loop") return "Dynamic loop";
8639
+ if (value === "cron") return "Cron";
8640
+ if (value === "one_shot") return "One-shot";
8641
+ if (value === "scheduled") return "Scheduled";
8642
+ if (!value) return "Scheduled";
8643
+ return value.split(/[_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
8644
+ }
8645
+ function formatRelative(value, label) {
8646
+ if (!value) return null;
8647
+ const date = new Date(value);
8648
+ if (Number.isNaN(date.getTime())) return null;
8649
+ return `${label} ${formatDistanceToNow(date, { addSuffix: true })}`;
8650
+ }
8651
+ function formatRuns(count) {
8652
+ return `${count} ${count === 1 ? "run" : "runs"}`;
8653
+ }
8654
+ function scheduleTitle(item) {
8655
+ const title = item.name?.trim();
8656
+ if (title) return title;
8657
+ return item.prompt;
8658
+ }
8659
+ function canRunScheduleNow(item) {
8660
+ return item.repeatLimit === 1;
8661
+ }
8662
+ function scheduleDisplay(item) {
8663
+ if (item.kind === "dynamic_loop") return null;
8664
+ return item.scheduleDisplay;
8665
+ }
8666
+ function sortScheduledWork(items) {
8667
+ return [...items].sort((a, b) => {
8668
+ if (a.status === "active" && b.status !== "active") return -1;
8669
+ if (a.status !== "active" && b.status === "active") return 1;
8670
+ const aNext = a.nextRunAt ?? "";
8671
+ const bNext = b.nextRunAt ?? "";
8672
+ if (aNext && bNext && aNext !== bNext) return aNext < bNext ? -1 : 1;
8673
+ if (aNext && !bNext) return -1;
8674
+ if (!aNext && bNext) return 1;
8675
+ return (a.updatedAt ?? "") > (b.updatedAt ?? "") ? -1 : 1;
8676
+ });
8677
+ }
8678
+ function ScheduledWorkRow({
8679
+ item,
8680
+ canRunNow,
8681
+ canCancel,
8682
+ actionScheduleId,
8683
+ onOpenLastRun,
8684
+ onRunNow,
8685
+ onCancel
8686
+ }) {
8687
+ const isActive = item.status === "active";
8688
+ const disabled = actionScheduleId === item.id;
8689
+ const title = scheduleTitle(item);
8690
+ const statusText = item.status === "active" ? null : item.status;
8691
+ const showRunNow = canRunNow && isActive && canRunScheduleNow(item);
8692
+ const lastSessionId = item.lastSessionId ?? null;
8693
+ const meta = [
8694
+ scheduleDisplay(item),
8695
+ formatRelative(item.nextRunAt, "Next"),
8696
+ formatRelative(item.lastRunAt, "Last"),
8697
+ formatRuns(item.runCount),
8698
+ item.expiresAt ? formatRelative(item.expiresAt, "Expires") : null
8699
+ ].filter(Boolean);
8700
+ const openLastRun = () => {
8701
+ if (lastSessionId) onOpenLastRun(lastSessionId);
8702
+ };
8703
+ const handleRowKeyDown = (e) => {
8704
+ if (!lastSessionId) return;
8705
+ if (e.key !== "Enter" && e.key !== " ") return;
8706
+ e.preventDefault();
8707
+ openLastRun();
8708
+ };
8709
+ const stopActionClick = (e) => {
8710
+ e.stopPropagation();
8711
+ };
8712
+ return /* @__PURE__ */ jsxs(
8713
+ "div",
8714
+ {
8715
+ className: cn(
8716
+ "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",
8717
+ lastSessionId && "cursor-pointer"
8718
+ ),
8719
+ onClick: openLastRun,
8720
+ onKeyDown: handleRowKeyDown,
8721
+ role: lastSessionId ? "button" : void 0,
8722
+ tabIndex: lastSessionId ? 0 : void 0,
8723
+ "aria-label": lastSessionId ? `Open last run for ${title}` : void 0,
8724
+ title: lastSessionId ? "Open last run" : void 0,
8725
+ children: [
8726
+ /* @__PURE__ */ jsx("div", { className: "mt-0.5 flex size-5 shrink-0 items-center justify-center text-faint", children: /* @__PURE__ */ jsx(CalendarClockIcon, { className: "size-3.5" }) }),
8727
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
8728
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [
8729
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
8730
+ /* @__PURE__ */ jsx("div", { className: "truncate text-foreground", children: title }),
8731
+ /* @__PURE__ */ jsx("div", { className: "mt-0.5 flex min-w-0 items-center gap-2", children: /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "shrink-0", children: formatKind(item.kind) }) }),
8732
+ meta.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-0.5 space-y-0.5 text-xs text-faint", children: meta.map((line) => /* @__PURE__ */ jsx("div", { className: "truncate", children: line }, line)) })
8733
+ ] }),
8734
+ statusText && /* @__PURE__ */ jsx(Badge, { variant: "destructive", className: "shrink-0", children: statusText })
8735
+ ] }),
8736
+ item.lastError && /* @__PURE__ */ jsx("div", { className: "mt-1 line-clamp-2 text-xs text-destructive", children: item.lastError })
8737
+ ] }),
8738
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [
8739
+ showRunNow && /* @__PURE__ */ jsx(
8740
+ "button",
8741
+ {
8742
+ type: "button",
8743
+ 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",
8744
+ onClick: (e) => {
8745
+ stopActionClick(e);
8746
+ onRunNow(item.id);
8747
+ },
8748
+ "aria-label": "Run schedule now",
8749
+ title: "Run schedule now",
8750
+ disabled,
8751
+ children: /* @__PURE__ */ jsx(CalendarClockIcon, { className: "size-3.5" })
8752
+ }
8753
+ ),
8754
+ canCancel && isActive && /* @__PURE__ */ jsx(
8755
+ "button",
8756
+ {
8757
+ type: "button",
8758
+ 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",
8759
+ onClick: (e) => {
8760
+ stopActionClick(e);
8761
+ onCancel(item.id);
8762
+ },
8763
+ "aria-label": "Cancel schedule",
8764
+ title: "Cancel schedule",
8765
+ disabled,
8766
+ children: /* @__PURE__ */ jsx(Trash2Icon, { className: "size-3.5" })
8767
+ }
8768
+ )
8769
+ ] })
8770
+ ]
8771
+ }
8772
+ );
8773
+ }
8774
+ function ScheduledWorkPanel({
8775
+ adapter,
8776
+ agentId,
8777
+ title = "Scheduled Work",
8778
+ limit = DEFAULT_LIMIT,
8779
+ status = "active",
8780
+ hideHeader = false,
8781
+ pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
8782
+ onSessionSelect,
8783
+ onScheduleCancel,
8784
+ onScheduleRunNow
8785
+ }) {
8786
+ const [items, setItems] = useState([]);
8787
+ const [error, setError] = useState(null);
8788
+ const [hasEverLoaded, setHasEverLoaded] = useState(false);
8789
+ const [actionScheduleId, setActionScheduleId] = useState(null);
8790
+ const [collapsed, setCollapsed] = useState(false);
8791
+ const mounted = useRef(true);
8792
+ const requestId = useRef(0);
8793
+ const lastFingerprint = useRef("");
8794
+ const refetch = useCallback(
8795
+ async (opts) => {
8796
+ if (!adapter.listScheduledWork) {
8797
+ setItems([]);
8798
+ setHasEverLoaded(true);
8799
+ return;
8800
+ }
8801
+ const currentRequestId = ++requestId.current;
8802
+ try {
8803
+ const list = await adapter.listScheduledWork({
8804
+ agentId,
8805
+ status,
8806
+ limit
8807
+ });
8808
+ if (!mounted.current || currentRequestId !== requestId.current) return;
8809
+ const nextItems = sortScheduledWork(list.items);
8810
+ const fp = fingerprint(nextItems);
8811
+ if (fp !== lastFingerprint.current) {
8812
+ lastFingerprint.current = fp;
8813
+ setItems(nextItems);
8814
+ }
8815
+ setError(null);
8816
+ setHasEverLoaded(true);
8817
+ } catch (e) {
8818
+ if (!mounted.current || currentRequestId !== requestId.current) return;
8819
+ if (!opts?.silent) {
8820
+ setError(
8821
+ e instanceof Error ? e.message : "Failed to load scheduled work"
8822
+ );
8823
+ setHasEverLoaded(true);
8824
+ }
8825
+ }
8826
+ },
8827
+ [adapter, agentId, limit, status]
8828
+ );
8829
+ useEffect(() => {
8830
+ mounted.current = true;
8831
+ return () => {
8832
+ mounted.current = false;
8833
+ };
8834
+ }, []);
8835
+ useEffect(() => {
8836
+ setError(null);
8837
+ lastFingerprint.current = "";
8838
+ void refetch();
8839
+ }, [refetch]);
8840
+ useEffect(() => {
8841
+ if (!adapter.listScheduledWork || pollIntervalMs <= 0) return;
8842
+ const id = setInterval(() => {
8843
+ void refetch({ silent: true });
8844
+ }, pollIntervalMs);
8845
+ return () => clearInterval(id);
8846
+ }, [adapter.listScheduledWork, pollIntervalMs, refetch]);
8847
+ const activeCount = useMemo(
8848
+ () => items.filter((item) => item.status === "active").length,
8849
+ [items]
8850
+ );
8851
+ const handleOpenLastRun = useCallback(
8852
+ (sessionId) => {
8853
+ onSessionSelect?.(sessionId);
8854
+ },
8855
+ [onSessionSelect]
8856
+ );
8857
+ const handleRunNow = useCallback(
8858
+ async (scheduleId) => {
8859
+ if (!adapter.runScheduledWorkNow || actionScheduleId) return;
8860
+ setActionScheduleId(scheduleId);
8861
+ try {
8862
+ const result = await adapter.runScheduledWorkNow({ scheduleId });
8863
+ onScheduleRunNow?.(scheduleId, result?.sessionId);
8864
+ await refetch({ silent: true });
8865
+ } catch (e) {
8866
+ setError(e instanceof Error ? e.message : "Failed to run schedule");
8867
+ } finally {
8868
+ if (mounted.current) setActionScheduleId(null);
8869
+ }
8870
+ },
8871
+ [actionScheduleId, adapter, onScheduleRunNow, refetch]
8872
+ );
8873
+ const handleCancel = useCallback(
8874
+ async (scheduleId) => {
8875
+ if (!adapter.cancelScheduledWork || actionScheduleId) return;
8876
+ setActionScheduleId(scheduleId);
8877
+ try {
8878
+ await adapter.cancelScheduledWork({ scheduleId });
8879
+ setItems((current) => {
8880
+ const next = current.filter((item) => item.id !== scheduleId);
8881
+ lastFingerprint.current = fingerprint(next);
8882
+ return next;
8883
+ });
8884
+ onScheduleCancel?.(scheduleId);
8885
+ await refetch({ silent: true });
8886
+ } catch (e) {
8887
+ setError(e instanceof Error ? e.message : "Failed to cancel schedule");
8888
+ } finally {
8889
+ if (mounted.current) setActionScheduleId(null);
8890
+ }
8891
+ },
8892
+ [actionScheduleId, adapter, onScheduleCancel, refetch]
8893
+ );
8894
+ if (!hasEverLoaded) return null;
8895
+ if (!adapter.listScheduledWork) return null;
8896
+ const showBody = hideHeader || !collapsed;
8897
+ return /* @__PURE__ */ jsxs("section", { className: "min-w-0 border-t border-line", children: [
8898
+ !hideHeader && /* @__PURE__ */ jsxs(
8899
+ "button",
8900
+ {
8901
+ type: "button",
8902
+ 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",
8903
+ onClick: () => setCollapsed((current) => !current),
8904
+ "aria-expanded": !collapsed,
8905
+ children: [
8906
+ /* @__PURE__ */ jsx(CalendarClockIcon, { className: "size-3.5" }),
8907
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: title }),
8908
+ activeCount > 0 && /* @__PURE__ */ jsx(Badge, { variant: "default", className: "ml-auto", children: activeCount }),
8909
+ collapsed ? /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-3.5 shrink-0 text-faint" }) : /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3.5 shrink-0 text-faint" })
8910
+ ]
8911
+ }
8912
+ ),
8913
+ showBody && error && /* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-xs text-destructive", children: error }),
8914
+ showBody && !error && items.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-xs text-faint", children: "No scheduled work" }),
8915
+ showBody && !error && items.length > 0 && /* @__PURE__ */ jsx("div", { className: cn("pb-2", hideHeader && "pt-1"), children: items.map((item) => /* @__PURE__ */ jsx(
8916
+ ScheduledWorkRow,
8917
+ {
8918
+ item,
8919
+ canRunNow: Boolean(adapter.runScheduledWorkNow),
8920
+ canCancel: Boolean(adapter.cancelScheduledWork),
8921
+ actionScheduleId,
8922
+ onOpenLastRun: handleOpenLastRun,
8923
+ onRunNow: handleRunNow,
8924
+ onCancel: handleCancel
8925
+ },
8926
+ item.id
8927
+ )) })
8928
+ ] });
8929
+ }
8259
8930
  var POLL_INTERVAL_ACTIVE_MS = 4e3;
8260
8931
  var POLL_INTERVAL_IDLE_MS = 3e4;
8261
8932
  var DEFAULT_SESSION_LIST_LIMIT = 50;
@@ -8280,7 +8951,7 @@ function buildTree(nodes) {
8280
8951
  }
8281
8952
  function treeFingerprint(nodes) {
8282
8953
  return nodes.map(
8283
- (n) => `${n.id}:${n.parentId ?? ""}:${n.status}:${n.agentType ?? ""}:${n.messageCount ?? 0}:${n.toolCallCount ?? 0}:${n.updatedAt}`
8954
+ (n) => `${n.id}:${n.parentId ?? ""}:${n.status}:${n.agentType ?? ""}:${n.runKind ?? ""}:${n.title ?? ""}:${n.messageCount ?? 0}:${n.toolCallCount ?? 0}:${n.updatedAt}`
8284
8955
  ).join("|");
8285
8956
  }
8286
8957
  function sessionToTreeNode(session) {
@@ -8290,6 +8961,7 @@ function sessionToTreeNode(session) {
8290
8961
  parentId: session.parentId ?? null,
8291
8962
  agentId: session.agentId,
8292
8963
  channel: session.channel,
8964
+ runKind: session.runKind ?? deriveRunKind(session.channel, session.config),
8293
8965
  status: session.status,
8294
8966
  title: session.title,
8295
8967
  model: session.model,
@@ -8338,6 +9010,23 @@ function formatSessionTime(value) {
8338
9010
  if (Number.isNaN(date.getTime())) return "";
8339
9011
  return formatDistanceToNow(date, { addSuffix: true });
8340
9012
  }
9013
+ function deriveRunKind(channel, config) {
9014
+ if (channel === "scheduled" && config?.scheduled_dynamic_loop === true) {
9015
+ return "dynamic_loop";
9016
+ }
9017
+ if (channel === "scheduled") return "scheduled";
9018
+ return null;
9019
+ }
9020
+ function formatRunKind(value) {
9021
+ if (value === "dynamic_loop") return "Loop";
9022
+ if (value === "scheduled") return "Scheduled";
9023
+ return null;
9024
+ }
9025
+ function fallbackSessionTitle(entry) {
9026
+ if (entry.runKind === "dynamic_loop") return "Loop run";
9027
+ if (entry.runKind === "scheduled") return "Scheduled run";
9028
+ return "New session";
9029
+ }
8341
9030
  function TreeNodeRow({
8342
9031
  entry,
8343
9032
  depth,
@@ -8353,9 +9042,11 @@ function TreeNodeRow({
8353
9042
  const hasChildren = entry.children.length > 0;
8354
9043
  const isActive = entry.id === activeSessionId;
8355
9044
  const isRunning = entry.status === "active";
8356
- const isSubAgent = entry.parentId != null;
8357
- const title = entry.title ?? "New session";
9045
+ const isChildSession = entry.parentId != null;
9046
+ const title = entry.title ?? fallbackSessionTitle(entry);
8358
9047
  const subtitle = [
9048
+ formatRunKind(entry.runKind),
9049
+ entry.agentType,
8359
9050
  entry.model ?? "default",
8360
9051
  formatSessionTime(entry.updatedAt)
8361
9052
  ].filter(Boolean).join(" \xB7 ");
@@ -8393,7 +9084,7 @@ function TreeNodeRow({
8393
9084
  /* @__PURE__ */ jsx("div", { className: "text-sm truncate", children: title }),
8394
9085
  /* @__PURE__ */ jsx("div", { className: "text-xs text-faint truncate", children: subtitle })
8395
9086
  ] }),
8396
- isRunning && canStop && isSubAgent && /* @__PURE__ */ jsx(
9087
+ isRunning && canStop && isChildSession && /* @__PURE__ */ jsx(
8397
9088
  "button",
8398
9089
  {
8399
9090
  type: "button",
@@ -8402,8 +9093,8 @@ function TreeNodeRow({
8402
9093
  e.stopPropagation();
8403
9094
  onStop(entry.id);
8404
9095
  },
8405
- "aria-label": "Stop sub-agent",
8406
- title: "Stop sub-agent",
9096
+ "aria-label": "Stop child session",
9097
+ title: "Stop child session",
8407
9098
  children: /* @__PURE__ */ jsx(SquareIcon, { className: "w-3 h-3", fill: "currentColor" })
8408
9099
  }
8409
9100
  ),
@@ -8604,6 +9295,6 @@ function SessionTreePanel({
8604
9295
  ] });
8605
9296
  }
8606
9297
 
8607
- export { AgentChat, AgentChatAdapterProvider, MessageResponse, SessionTreePanel, useAgentChatAdapterContext, useAgentChatRuntime };
9298
+ export { AgentChat, AgentChatAdapterProvider, MessageResponse, ScheduledWorkPanel, SessionTreePanel, useAgentChatAdapterContext, useAgentChatRuntime };
8608
9299
  //# sourceMappingURL=index.js.map
8609
9300
  //# sourceMappingURL=index.js.map