@invergent/agent-chat-react 1.5.9 → 1.5.10

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
@@ -3552,13 +3552,16 @@ function ClarifyLocked({
3552
3552
  }) })
3553
3553
  ] });
3554
3554
  }
3555
- function ArtifactToolBlock({ tc }) {
3556
- const args = parseArgs(tc.args) ?? {};
3555
+ function ArtifactToolBlock({
3556
+ tc,
3557
+ resolvedName
3558
+ }) {
3557
3559
  const status = effectiveStatus(tc);
3558
3560
  const label = status === "running" ? "Creating artifact\u2026" : status === "error" ? "Creating artifact\u2026" : "Created";
3561
+ const name = resolvedName ?? parseArgs(tc.args)?.name;
3559
3562
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-sm ", children: [
3560
3563
  /* @__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 })
3564
+ name && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground truncate", children: name })
3562
3565
  ] });
3563
3566
  }
3564
3567
 
@@ -3891,7 +3894,11 @@ function DefaultToolBlock({ tc }) {
3891
3894
  ] })
3892
3895
  ] });
3893
3896
  }
3894
- function ToolCallBlock({ tc, onFileSelect }) {
3897
+ function ToolCallBlock({
3898
+ tc,
3899
+ resolvedArtifactName,
3900
+ onFileSelect
3901
+ }) {
3895
3902
  switch (tc.toolName) {
3896
3903
  case "terminal":
3897
3904
  return /* @__PURE__ */ jsxRuntime.jsx(TerminalToolBlock, { tc });
@@ -3928,7 +3935,7 @@ function ToolCallBlock({ tc, onFileSelect }) {
3928
3935
  case "clarify":
3929
3936
  return /* @__PURE__ */ jsxRuntime.jsx(ClarifyToolBlock, { tc });
3930
3937
  case "create_artifact":
3931
- return /* @__PURE__ */ jsxRuntime.jsx(ArtifactToolBlock, { tc });
3938
+ return /* @__PURE__ */ jsxRuntime.jsx(ArtifactToolBlock, { tc, resolvedName: resolvedArtifactName });
3932
3939
  case "delegate_task":
3933
3940
  return /* @__PURE__ */ jsxRuntime.jsx(DelegateToolBlock, { tc });
3934
3941
  case "memory":
@@ -5337,6 +5344,7 @@ function ChatComposerInner({
5337
5344
  onStop,
5338
5345
  isRunning,
5339
5346
  disabled = false,
5347
+ disabledReason,
5340
5348
  tokenUsage
5341
5349
  }) {
5342
5350
  const { adapter } = useAgentChatAdapterContext();
@@ -5365,7 +5373,10 @@ function ChatComposerInner({
5365
5373
  const slashCommands = react.useMemo(() => {
5366
5374
  const builtin = [
5367
5375
  { value: "/clear", label: "/clear", description: "Clear conversation" },
5368
- { value: "/compress", label: "/compress", description: "Compress context" }
5376
+ { value: "/compress", label: "/compress", description: "Compress context" },
5377
+ { value: "/loop", label: "/loop", description: "Schedule recurring prompt" },
5378
+ { value: "/loop list", label: "/loop list", description: "List active loops" },
5379
+ { value: "/loop cancel", label: "/loop cancel", description: "Cancel a loop by ID" }
5369
5380
  ];
5370
5381
  return [...adapterCommands, ...builtin];
5371
5382
  }, [adapterCommands]);
@@ -5444,7 +5455,7 @@ function ChatComposerInner({
5444
5455
  /* @__PURE__ */ jsxRuntime.jsx(PromptInputBody, { children: /* @__PURE__ */ jsxRuntime.jsx(
5445
5456
  PromptInputTextarea,
5446
5457
  {
5447
- placeholder: disabled ? "Session disabled" : "Send a message...",
5458
+ placeholder: disabled ? disabledReason ?? "Session disabled" : "Send a message...",
5448
5459
  disabled,
5449
5460
  onKeyDown: handleKeyDown
5450
5461
  }
@@ -6099,7 +6110,7 @@ var ErrorMessage = react.memo(function ErrorMessage2({
6099
6110
 
6100
6111
  // src/components/chat/chat-thread.tsx
6101
6112
  init_utils();
6102
- function messageToEntries(msg, isLast) {
6113
+ function messageToEntries(msg, isLast, artifactFallbacks) {
6103
6114
  if (msg.role === "system") {
6104
6115
  if (msg.systemKind === "skill_invoked") {
6105
6116
  return [{
@@ -6141,6 +6152,18 @@ function messageToEntries(msg, isLast) {
6141
6152
  }
6142
6153
  if (hasToolCalls) {
6143
6154
  for (const tc of msg.toolCalls) {
6155
+ if (tc.toolName === "create_artifact") {
6156
+ const resolvedName = parseArgs(tc.args)?.name ?? artifactFallbacks[tc.id];
6157
+ const status = effectiveStatus(tc);
6158
+ if (!resolvedName && status !== "error") continue;
6159
+ entries.push({
6160
+ kind: "tool",
6161
+ key: tc.id,
6162
+ tc,
6163
+ resolvedArtifactName: resolvedName
6164
+ });
6165
+ continue;
6166
+ }
6144
6167
  entries.push({ kind: "tool", key: tc.id, tc });
6145
6168
  }
6146
6169
  }
@@ -6299,7 +6322,14 @@ function TimelineEntryItem({
6299
6322
  )
6300
6323
  ] }),
6301
6324
  /* @__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 }),
6325
+ /* @__PURE__ */ jsxRuntime.jsx(
6326
+ ToolCallBlock,
6327
+ {
6328
+ tc: entry.tc,
6329
+ resolvedArtifactName: entry.resolvedArtifactName,
6330
+ onFileSelect
6331
+ }
6332
+ ),
6303
6333
  failureSummary ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-w-full truncate text-xs text-destructive", title: failureSummary, children: failureSummary }) : null
6304
6334
  ] }) })
6305
6335
  ] });
@@ -6361,13 +6391,14 @@ function AssistantGroup({
6361
6391
  totalMessages,
6362
6392
  isRunning,
6363
6393
  sessionId,
6394
+ artifactFallbacks,
6364
6395
  onFileSelect,
6365
6396
  onRetry
6366
6397
  }) {
6367
6398
  const entries = [];
6368
6399
  for (let i = 0; i < messages.length; i++) {
6369
6400
  const isLast = i === messages.length - 1 && lastGlobalIndex === totalMessages - 1;
6370
- entries.push(...messageToEntries(messages[i], isLast));
6401
+ entries.push(...messageToEntries(messages[i], isLast, artifactFallbacks));
6371
6402
  }
6372
6403
  const isTailGroup = lastGlobalIndex === totalMessages - 1;
6373
6404
  const lastEntry = entries[entries.length - 1];
@@ -6434,11 +6465,34 @@ function ChatThread({
6434
6465
  onStop,
6435
6466
  onFileSelect,
6436
6467
  disabled = false,
6468
+ disabledReason,
6437
6469
  tokenUsage,
6438
6470
  retryIndicator,
6439
6471
  onRetry
6440
6472
  }) {
6441
6473
  const groups = react.useMemo(() => groupMessages(messages), [messages]);
6474
+ const artifactFallbacks = react.useMemo(() => {
6475
+ const fallbacks = {};
6476
+ const createArtifactToolCallIds = [];
6477
+ let artifactIdx = 0;
6478
+ for (const msg of messages) {
6479
+ if (msg.role === "assistant" && msg.toolCalls) {
6480
+ for (const tc of msg.toolCalls) {
6481
+ if (tc.toolName === "create_artifact" && !tc.cancelled && tc.status !== "error") {
6482
+ createArtifactToolCallIds.push(tc.id);
6483
+ }
6484
+ }
6485
+ } else if (msg.role === "system" && msg.systemKind === "artifact") {
6486
+ const tcId = createArtifactToolCallIds[artifactIdx];
6487
+ if (tcId) {
6488
+ const { name } = unpackArtifactMeta(msg.systemMeta, msg.content);
6489
+ if (name) fallbacks[tcId] = name;
6490
+ }
6491
+ artifactIdx += 1;
6492
+ }
6493
+ }
6494
+ return fallbacks;
6495
+ }, [messages]);
6442
6496
  const activeFailureId = react.useMemo(() => {
6443
6497
  if (messages.length === 0) return null;
6444
6498
  const tail = messages[messages.length - 1];
@@ -6503,6 +6557,7 @@ function ChatThread({
6503
6557
  totalMessages: messages.length,
6504
6558
  isRunning,
6505
6559
  sessionId,
6560
+ artifactFallbacks,
6506
6561
  onFileSelect,
6507
6562
  onRetry: groupRetry
6508
6563
  },
@@ -6515,13 +6570,21 @@ function ChatThread({
6515
6570
  ] }),
6516
6571
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto w-full max-w-4xl px-6 pb-5 pt-3", children: [
6517
6572
  retryIndicator && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsxRuntime.jsx(RetryBanner, { indicator: retryIndicator }) }),
6518
- /* @__PURE__ */ jsxRuntime.jsx(
6573
+ disabled && disabledReason ? /* @__PURE__ */ jsxRuntime.jsx(
6574
+ "div",
6575
+ {
6576
+ className: "rounded border border-line bg-muted/40 px-3 py-2 text-sm text-muted-foreground",
6577
+ role: "status",
6578
+ children: disabledReason
6579
+ }
6580
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
6519
6581
  ChatComposer,
6520
6582
  {
6521
6583
  onSend,
6522
6584
  onStop,
6523
6585
  isRunning,
6524
6586
  disabled,
6587
+ disabledReason,
6525
6588
  tokenUsage
6526
6589
  }
6527
6590
  )
@@ -7359,7 +7422,7 @@ function WorkspacePanel({
7359
7422
  );
7360
7423
  const handleDelete = react.useCallback(
7361
7424
  async (path) => {
7362
- if (!sessionId) return;
7425
+ if (disabled || !sessionId) return;
7363
7426
  try {
7364
7427
  await adapter.deleteWorkspaceFile({ sessionId, path });
7365
7428
  if (selectedPath === path) {
@@ -7375,7 +7438,7 @@ function WorkspacePanel({
7375
7438
  setDeleteTarget(null);
7376
7439
  }
7377
7440
  },
7378
- [adapter, fetchTree, onSelectedPathChange, selectedPath, sessionId]
7441
+ [adapter, disabled, fetchTree, onSelectedPathChange, selectedPath, sessionId]
7379
7442
  );
7380
7443
  const onResizeStart = react.useCallback(
7381
7444
  (event) => {
@@ -7458,7 +7521,7 @@ function WorkspacePanel({
7458
7521
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FolderOpenIcon, { className: "size-4 shrink-0 text-amber-500" }),
7459
7522
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
7460
7523
  /* @__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" })
7524
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-xs text-faint", children: disabled ? "Workspace - Read-only" : "Workspace" })
7462
7525
  ] }),
7463
7526
  /* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
7464
7527
  /* @__PURE__ */ jsxRuntime.jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -7569,7 +7632,7 @@ function WorkspacePanel({
7569
7632
  sessionId,
7570
7633
  path: file.path
7571
7634
  }) : null,
7572
- onDelete: file ? () => setDeleteTarget(file.path) : null,
7635
+ onDelete: file && !disabled ? () => setDeleteTarget(file.path) : null,
7573
7636
  onClose: () => {
7574
7637
  onSelectedPathChange(null);
7575
7638
  setFile(null);
@@ -7633,6 +7696,13 @@ function ConfirmDialog({
7633
7696
  );
7634
7697
  }
7635
7698
 
7699
+ // src/lib/sessions.ts
7700
+ function isScheduledRunSession(session) {
7701
+ return Boolean(
7702
+ session?.channel === "scheduled" || session?.config?.scheduled_session_id
7703
+ );
7704
+ }
7705
+
7636
7706
  // src/runtime/events.ts
7637
7707
  var WORKSPACE_MUTATING_TOOLS = /* @__PURE__ */ new Set([
7638
7708
  "terminal",
@@ -8245,6 +8315,7 @@ function useAgentChatRuntime({
8245
8315
  const [state, setState] = react.useState(
8246
8316
  () => createInitialAgentChatState({ isLoadingHistory: Boolean(sessionId) })
8247
8317
  );
8318
+ const [session, setSession] = react.useState(null);
8248
8319
  const stateRef = react.useRef(state);
8249
8320
  const streamRef = react.useRef(null);
8250
8321
  const reconnectTimerRef = react.useRef(null);
@@ -8269,6 +8340,7 @@ function useAgentChatRuntime({
8269
8340
  clearReconnectTimer();
8270
8341
  closeStream();
8271
8342
  if (!sessionId) {
8343
+ setSession(null);
8272
8344
  setState(createInitialAgentChatState());
8273
8345
  return;
8274
8346
  }
@@ -8316,17 +8388,19 @@ function useAgentChatRuntime({
8316
8388
  isLoadingHistory: true
8317
8389
  });
8318
8390
  stateRef.current = initialState;
8391
+ setSession(null);
8319
8392
  setState(initialState);
8320
8393
  connect(0);
8321
- adapter.getSession({ sessionId }).then((session) => {
8394
+ adapter.getSession({ sessionId }).then((loadedSession) => {
8322
8395
  if (cancelled) return;
8323
- if (session.messageCount === 0) {
8396
+ setSession(loadedSession);
8397
+ if (loadedSession.messageCount === 0) {
8324
8398
  setState((prev) => ({
8325
8399
  ...prev,
8326
8400
  isLoadingHistory: false
8327
8401
  }));
8328
8402
  }
8329
- if (isTerminalStatus(session.status)) {
8403
+ if (isTerminalStatus(loadedSession.status)) {
8330
8404
  setState((prev) => ({
8331
8405
  ...prev,
8332
8406
  terminal: true,
@@ -8415,9 +8489,9 @@ function useAgentChatRuntime({
8415
8489
  markSending(content, images);
8416
8490
  if (!sessionId) {
8417
8491
  try {
8418
- const session = await adapter.createSession({ agentId });
8419
- onSessionChange?.(session.id);
8420
- await adapter.sendMessage({ sessionId: session.id, content, images });
8492
+ const session2 = await adapter.createSession({ agentId });
8493
+ onSessionChange?.(session2.id);
8494
+ await adapter.sendMessage({ sessionId: session2.id, content, images });
8421
8495
  } catch (error) {
8422
8496
  markSendError(error instanceof Error ? error.message : "send failed");
8423
8497
  throw error;
@@ -8458,6 +8532,7 @@ function useAgentChatRuntime({
8458
8532
  }
8459
8533
  }, [adapter, sessionId]);
8460
8534
  return {
8535
+ session,
8461
8536
  messages: state.messages,
8462
8537
  isRunning: state.isRunning,
8463
8538
  isLoadingHistory: state.isLoadingHistory,
@@ -8505,6 +8580,9 @@ function AgentChat({
8505
8580
  sessionId,
8506
8581
  onSessionChange
8507
8582
  });
8583
+ const isReadOnlySession = isScheduledRunSession(runtime.session);
8584
+ const effectiveDisabled = disabled || isReadOnlySession;
8585
+ const disabledReason = isReadOnlySession ? "Scheduled run is read-only" : void 0;
8508
8586
  react.useEffect(() => {
8509
8587
  onMessagesChange?.(runtime.messages);
8510
8588
  }, [onMessagesChange, runtime.messages]);
@@ -8535,7 +8613,8 @@ function AgentChat({
8535
8613
  onStop: () => void runtime.stop(),
8536
8614
  onRetry: runtime.retry,
8537
8615
  onFileSelect: handleFileSelect,
8538
- disabled,
8616
+ disabled: effectiveDisabled,
8617
+ disabledReason,
8539
8618
  tokenUsage: runtime.tokenUsage,
8540
8619
  retryIndicator: runtime.retryIndicator
8541
8620
  }
@@ -8550,7 +8629,7 @@ function AgentChat({
8550
8629
  collapsed: workspaceCollapsed,
8551
8630
  onCollapsedChange: setWorkspaceCollapsed,
8552
8631
  refreshSignal: runtime.workspaceRefreshKey,
8553
- disabled
8632
+ disabled: effectiveDisabled
8554
8633
  }
8555
8634
  )
8556
8635
  ] }) })
@@ -8558,6 +8637,307 @@ function AgentChat({
8558
8637
  );
8559
8638
  }
8560
8639
  init_utils();
8640
+ var DEFAULT_LIMIT = 50;
8641
+ var DEFAULT_POLL_INTERVAL_MS = 3e4;
8642
+ function fingerprint(items) {
8643
+ return items.map(
8644
+ (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}`
8645
+ ).join("|");
8646
+ }
8647
+ function formatKind(value) {
8648
+ if (value === "dynamic_loop") return "Dynamic loop";
8649
+ if (value === "cron") return "Cron";
8650
+ if (value === "one_shot") return "One-shot";
8651
+ if (value === "scheduled") return "Scheduled";
8652
+ if (!value) return "Scheduled";
8653
+ return value.split(/[_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
8654
+ }
8655
+ function formatRelative(value, label) {
8656
+ if (!value) return null;
8657
+ const date = new Date(value);
8658
+ if (Number.isNaN(date.getTime())) return null;
8659
+ return `${label} ${dateFns.formatDistanceToNow(date, { addSuffix: true })}`;
8660
+ }
8661
+ function formatRuns(count) {
8662
+ return `${count} ${count === 1 ? "run" : "runs"}`;
8663
+ }
8664
+ function scheduleTitle(item) {
8665
+ const title = item.name?.trim();
8666
+ if (title) return title;
8667
+ return item.prompt;
8668
+ }
8669
+ function canRunScheduleNow(item) {
8670
+ return item.repeatLimit === 1;
8671
+ }
8672
+ function scheduleDisplay(item) {
8673
+ if (item.kind === "dynamic_loop") return null;
8674
+ return item.scheduleDisplay;
8675
+ }
8676
+ function sortScheduledWork(items) {
8677
+ return [...items].sort((a, b) => {
8678
+ if (a.status === "active" && b.status !== "active") return -1;
8679
+ if (a.status !== "active" && b.status === "active") return 1;
8680
+ const aNext = a.nextRunAt ?? "";
8681
+ const bNext = b.nextRunAt ?? "";
8682
+ if (aNext && bNext && aNext !== bNext) return aNext < bNext ? -1 : 1;
8683
+ if (aNext && !bNext) return -1;
8684
+ if (!aNext && bNext) return 1;
8685
+ return (a.updatedAt ?? "") > (b.updatedAt ?? "") ? -1 : 1;
8686
+ });
8687
+ }
8688
+ function ScheduledWorkRow({
8689
+ item,
8690
+ canRunNow,
8691
+ canCancel,
8692
+ actionScheduleId,
8693
+ onOpenLastRun,
8694
+ onRunNow,
8695
+ onCancel
8696
+ }) {
8697
+ const isActive = item.status === "active";
8698
+ const disabled = actionScheduleId === item.id;
8699
+ const title = scheduleTitle(item);
8700
+ const statusText = item.status === "active" ? null : item.status;
8701
+ const showRunNow = canRunNow && isActive && canRunScheduleNow(item);
8702
+ const lastSessionId = item.lastSessionId ?? null;
8703
+ const meta = [
8704
+ scheduleDisplay(item),
8705
+ formatRelative(item.nextRunAt, "Next"),
8706
+ formatRelative(item.lastRunAt, "Last"),
8707
+ formatRuns(item.runCount),
8708
+ item.expiresAt ? formatRelative(item.expiresAt, "Expires") : null
8709
+ ].filter(Boolean);
8710
+ const openLastRun = () => {
8711
+ if (lastSessionId) onOpenLastRun(lastSessionId);
8712
+ };
8713
+ const handleRowKeyDown = (e) => {
8714
+ if (!lastSessionId) return;
8715
+ if (e.key !== "Enter" && e.key !== " ") return;
8716
+ e.preventDefault();
8717
+ openLastRun();
8718
+ };
8719
+ const stopActionClick = (e) => {
8720
+ e.stopPropagation();
8721
+ };
8722
+ return /* @__PURE__ */ jsxRuntime.jsxs(
8723
+ "div",
8724
+ {
8725
+ className: cn(
8726
+ "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",
8727
+ lastSessionId && "cursor-pointer"
8728
+ ),
8729
+ onClick: openLastRun,
8730
+ onKeyDown: handleRowKeyDown,
8731
+ role: lastSessionId ? "button" : void 0,
8732
+ tabIndex: lastSessionId ? 0 : void 0,
8733
+ "aria-label": lastSessionId ? `Open last run for ${title}` : void 0,
8734
+ title: lastSessionId ? "Open last run" : void 0,
8735
+ children: [
8736
+ /* @__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" }) }),
8737
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
8738
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [
8739
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
8740
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-foreground", children: title }),
8741
+ /* @__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) }) }),
8742
+ 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)) })
8743
+ ] }),
8744
+ statusText && /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "destructive", className: "shrink-0", children: statusText })
8745
+ ] }),
8746
+ item.lastError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 line-clamp-2 text-xs text-destructive", children: item.lastError })
8747
+ ] }),
8748
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [
8749
+ showRunNow && /* @__PURE__ */ jsxRuntime.jsx(
8750
+ "button",
8751
+ {
8752
+ type: "button",
8753
+ 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",
8754
+ onClick: (e) => {
8755
+ stopActionClick(e);
8756
+ onRunNow(item.id);
8757
+ },
8758
+ "aria-label": "Run schedule now",
8759
+ title: "Run schedule now",
8760
+ disabled,
8761
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CalendarClockIcon, { className: "size-3.5" })
8762
+ }
8763
+ ),
8764
+ canCancel && isActive && /* @__PURE__ */ jsxRuntime.jsx(
8765
+ "button",
8766
+ {
8767
+ type: "button",
8768
+ 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",
8769
+ onClick: (e) => {
8770
+ stopActionClick(e);
8771
+ onCancel(item.id);
8772
+ },
8773
+ "aria-label": "Cancel schedule",
8774
+ title: "Cancel schedule",
8775
+ disabled,
8776
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2Icon, { className: "size-3.5" })
8777
+ }
8778
+ )
8779
+ ] })
8780
+ ]
8781
+ }
8782
+ );
8783
+ }
8784
+ function ScheduledWorkPanel({
8785
+ adapter,
8786
+ agentId,
8787
+ title = "Scheduled Work",
8788
+ limit = DEFAULT_LIMIT,
8789
+ status = "active",
8790
+ hideHeader = false,
8791
+ pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
8792
+ onSessionSelect,
8793
+ onScheduleCancel,
8794
+ onScheduleRunNow
8795
+ }) {
8796
+ const [items, setItems] = react.useState([]);
8797
+ const [error, setError] = react.useState(null);
8798
+ const [hasEverLoaded, setHasEverLoaded] = react.useState(false);
8799
+ const [actionScheduleId, setActionScheduleId] = react.useState(null);
8800
+ const [collapsed, setCollapsed] = react.useState(false);
8801
+ const mounted = react.useRef(true);
8802
+ const requestId = react.useRef(0);
8803
+ const lastFingerprint = react.useRef("");
8804
+ const refetch = react.useCallback(
8805
+ async (opts) => {
8806
+ if (!adapter.listScheduledWork) {
8807
+ setItems([]);
8808
+ setHasEverLoaded(true);
8809
+ return;
8810
+ }
8811
+ const currentRequestId = ++requestId.current;
8812
+ try {
8813
+ const list = await adapter.listScheduledWork({
8814
+ agentId,
8815
+ status,
8816
+ limit
8817
+ });
8818
+ if (!mounted.current || currentRequestId !== requestId.current) return;
8819
+ const nextItems = sortScheduledWork(list.items);
8820
+ const fp = fingerprint(nextItems);
8821
+ if (fp !== lastFingerprint.current) {
8822
+ lastFingerprint.current = fp;
8823
+ setItems(nextItems);
8824
+ }
8825
+ setError(null);
8826
+ setHasEverLoaded(true);
8827
+ } catch (e) {
8828
+ if (!mounted.current || currentRequestId !== requestId.current) return;
8829
+ if (!opts?.silent) {
8830
+ setError(
8831
+ e instanceof Error ? e.message : "Failed to load scheduled work"
8832
+ );
8833
+ setHasEverLoaded(true);
8834
+ }
8835
+ }
8836
+ },
8837
+ [adapter, agentId, limit, status]
8838
+ );
8839
+ react.useEffect(() => {
8840
+ mounted.current = true;
8841
+ return () => {
8842
+ mounted.current = false;
8843
+ };
8844
+ }, []);
8845
+ react.useEffect(() => {
8846
+ setError(null);
8847
+ lastFingerprint.current = "";
8848
+ void refetch();
8849
+ }, [refetch]);
8850
+ react.useEffect(() => {
8851
+ if (!adapter.listScheduledWork || pollIntervalMs <= 0) return;
8852
+ const id = setInterval(() => {
8853
+ void refetch({ silent: true });
8854
+ }, pollIntervalMs);
8855
+ return () => clearInterval(id);
8856
+ }, [adapter.listScheduledWork, pollIntervalMs, refetch]);
8857
+ const activeCount = react.useMemo(
8858
+ () => items.filter((item) => item.status === "active").length,
8859
+ [items]
8860
+ );
8861
+ const handleOpenLastRun = react.useCallback(
8862
+ (sessionId) => {
8863
+ onSessionSelect?.(sessionId);
8864
+ },
8865
+ [onSessionSelect]
8866
+ );
8867
+ const handleRunNow = react.useCallback(
8868
+ async (scheduleId) => {
8869
+ if (!adapter.runScheduledWorkNow || actionScheduleId) return;
8870
+ setActionScheduleId(scheduleId);
8871
+ try {
8872
+ const result = await adapter.runScheduledWorkNow({ scheduleId });
8873
+ onScheduleRunNow?.(scheduleId, result?.sessionId);
8874
+ await refetch({ silent: true });
8875
+ } catch (e) {
8876
+ setError(e instanceof Error ? e.message : "Failed to run schedule");
8877
+ } finally {
8878
+ if (mounted.current) setActionScheduleId(null);
8879
+ }
8880
+ },
8881
+ [actionScheduleId, adapter, onScheduleRunNow, refetch]
8882
+ );
8883
+ const handleCancel = react.useCallback(
8884
+ async (scheduleId) => {
8885
+ if (!adapter.cancelScheduledWork || actionScheduleId) return;
8886
+ setActionScheduleId(scheduleId);
8887
+ try {
8888
+ await adapter.cancelScheduledWork({ scheduleId });
8889
+ setItems((current) => {
8890
+ const next = current.filter((item) => item.id !== scheduleId);
8891
+ lastFingerprint.current = fingerprint(next);
8892
+ return next;
8893
+ });
8894
+ onScheduleCancel?.(scheduleId);
8895
+ await refetch({ silent: true });
8896
+ } catch (e) {
8897
+ setError(e instanceof Error ? e.message : "Failed to cancel schedule");
8898
+ } finally {
8899
+ if (mounted.current) setActionScheduleId(null);
8900
+ }
8901
+ },
8902
+ [actionScheduleId, adapter, onScheduleCancel, refetch]
8903
+ );
8904
+ if (!hasEverLoaded) return null;
8905
+ if (!adapter.listScheduledWork) return null;
8906
+ const showBody = hideHeader || !collapsed;
8907
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "min-w-0 border-t border-line", children: [
8908
+ !hideHeader && /* @__PURE__ */ jsxRuntime.jsxs(
8909
+ "button",
8910
+ {
8911
+ type: "button",
8912
+ 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",
8913
+ onClick: () => setCollapsed((current) => !current),
8914
+ "aria-expanded": !collapsed,
8915
+ children: [
8916
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CalendarClockIcon, { className: "size-3.5" }),
8917
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: title }),
8918
+ activeCount > 0 && /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "default", className: "ml-auto", children: activeCount }),
8919
+ 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" })
8920
+ ]
8921
+ }
8922
+ ),
8923
+ showBody && error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-2 text-xs text-destructive", children: error }),
8924
+ showBody && !error && items.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-2 text-xs text-faint", children: "No scheduled work" }),
8925
+ showBody && !error && items.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("pb-2", hideHeader && "pt-1"), children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
8926
+ ScheduledWorkRow,
8927
+ {
8928
+ item,
8929
+ canRunNow: Boolean(adapter.runScheduledWorkNow),
8930
+ canCancel: Boolean(adapter.cancelScheduledWork),
8931
+ actionScheduleId,
8932
+ onOpenLastRun: handleOpenLastRun,
8933
+ onRunNow: handleRunNow,
8934
+ onCancel: handleCancel
8935
+ },
8936
+ item.id
8937
+ )) })
8938
+ ] });
8939
+ }
8940
+ init_utils();
8561
8941
  var POLL_INTERVAL_ACTIVE_MS = 4e3;
8562
8942
  var POLL_INTERVAL_IDLE_MS = 3e4;
8563
8943
  var DEFAULT_SESSION_LIST_LIMIT = 50;
@@ -8582,7 +8962,7 @@ function buildTree(nodes) {
8582
8962
  }
8583
8963
  function treeFingerprint(nodes) {
8584
8964
  return nodes.map(
8585
- (n) => `${n.id}:${n.parentId ?? ""}:${n.status}:${n.agentType ?? ""}:${n.messageCount ?? 0}:${n.toolCallCount ?? 0}:${n.updatedAt}`
8965
+ (n) => `${n.id}:${n.parentId ?? ""}:${n.status}:${n.agentType ?? ""}:${n.runKind ?? ""}:${n.title ?? ""}:${n.messageCount ?? 0}:${n.toolCallCount ?? 0}:${n.updatedAt}`
8586
8966
  ).join("|");
8587
8967
  }
8588
8968
  function sessionToTreeNode(session) {
@@ -8592,6 +8972,7 @@ function sessionToTreeNode(session) {
8592
8972
  parentId: session.parentId ?? null,
8593
8973
  agentId: session.agentId,
8594
8974
  channel: session.channel,
8975
+ runKind: session.runKind ?? deriveRunKind(session.channel, session.config),
8595
8976
  status: session.status,
8596
8977
  title: session.title,
8597
8978
  model: session.model,
@@ -8640,6 +9021,23 @@ function formatSessionTime(value) {
8640
9021
  if (Number.isNaN(date.getTime())) return "";
8641
9022
  return dateFns.formatDistanceToNow(date, { addSuffix: true });
8642
9023
  }
9024
+ function deriveRunKind(channel, config) {
9025
+ if (channel === "scheduled" && config?.scheduled_dynamic_loop === true) {
9026
+ return "dynamic_loop";
9027
+ }
9028
+ if (channel === "scheduled") return "scheduled";
9029
+ return null;
9030
+ }
9031
+ function formatRunKind(value) {
9032
+ if (value === "dynamic_loop") return "Loop";
9033
+ if (value === "scheduled") return "Scheduled";
9034
+ return null;
9035
+ }
9036
+ function fallbackSessionTitle(entry) {
9037
+ if (entry.runKind === "dynamic_loop") return "Loop run";
9038
+ if (entry.runKind === "scheduled") return "Scheduled run";
9039
+ return "New session";
9040
+ }
8643
9041
  function TreeNodeRow({
8644
9042
  entry,
8645
9043
  depth,
@@ -8655,9 +9053,11 @@ function TreeNodeRow({
8655
9053
  const hasChildren = entry.children.length > 0;
8656
9054
  const isActive = entry.id === activeSessionId;
8657
9055
  const isRunning = entry.status === "active";
8658
- const isSubAgent = entry.parentId != null;
8659
- const title = entry.title ?? "New session";
9056
+ const isChildSession = entry.parentId != null;
9057
+ const title = entry.title ?? fallbackSessionTitle(entry);
8660
9058
  const subtitle = [
9059
+ formatRunKind(entry.runKind),
9060
+ entry.agentType,
8661
9061
  entry.model ?? "default",
8662
9062
  formatSessionTime(entry.updatedAt)
8663
9063
  ].filter(Boolean).join(" \xB7 ");
@@ -8695,7 +9095,7 @@ function TreeNodeRow({
8695
9095
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm truncate", children: title }),
8696
9096
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-faint truncate", children: subtitle })
8697
9097
  ] }),
8698
- isRunning && canStop && isSubAgent && /* @__PURE__ */ jsxRuntime.jsx(
9098
+ isRunning && canStop && isChildSession && /* @__PURE__ */ jsxRuntime.jsx(
8699
9099
  "button",
8700
9100
  {
8701
9101
  type: "button",
@@ -8704,8 +9104,8 @@ function TreeNodeRow({
8704
9104
  e.stopPropagation();
8705
9105
  onStop(entry.id);
8706
9106
  },
8707
- "aria-label": "Stop sub-agent",
8708
- title: "Stop sub-agent",
9107
+ "aria-label": "Stop child session",
9108
+ title: "Stop child session",
8709
9109
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.SquareIcon, { className: "w-3 h-3", fill: "currentColor" })
8710
9110
  }
8711
9111
  ),
@@ -8909,6 +9309,7 @@ function SessionTreePanel({
8909
9309
  exports.AgentChat = AgentChat;
8910
9310
  exports.AgentChatAdapterProvider = AgentChatAdapterProvider;
8911
9311
  exports.MessageResponse = MessageResponse;
9312
+ exports.ScheduledWorkPanel = ScheduledWorkPanel;
8912
9313
  exports.SessionTreePanel = SessionTreePanel;
8913
9314
  exports.useAgentChatAdapterContext = useAgentChatAdapterContext;
8914
9315
  exports.useAgentChatRuntime = useAgentChatRuntime;