@springbrand/message-panel 0.3.0-alpha.1 → 0.3.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import type { ScheduleCardControls } from "./rich-blocks";
1
2
  import type { UIMessage } from "ai";
2
3
  import { AnimatePresence, motion, useReducedMotion } from "motion/react";
3
4
  import {
@@ -117,6 +118,7 @@ export interface CloudOsChatMessagesProps {
117
118
  */
118
119
  onRespond?: (toolCallId: string, response: unknown) => Promise<boolean>;
119
120
  /** 点击定时任务卡片:宿主决定跳到哪(缺省 → 卡片不可点)。 */
121
+ scheduleControls?: ScheduleCardControls;
120
122
  onOpenSchedule?: (scheduleId: string) => void;
121
123
  onCopy?: (text: string) => void;
122
124
  }
@@ -199,7 +201,7 @@ function UserBubble({
199
201
  {entry.text && (
200
202
  <div
201
203
  data-user-message-bubble=""
202
- className="themed-user-bubble-shadow cos-markdown cos-user-markdown w-fit max-w-[min(680px,78%)] rounded-[12px] border border-transparent bg-kumo-bubble-user px-4 py-2.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default animate-in fade-in slide-in-from-bottom-1 duration-300 motion-reduce:animate-none"
204
+ className="themed-user-bubble-shadow cos-markdown cos-user-markdown w-fit max-w-[min(680px,78%)] rounded-[12px] border border-transparent bg-kumo-bubble-user px-4 py-2.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default"
203
205
  >
204
206
  <MarkdownMessage
205
207
  message={entry.text}
@@ -393,6 +395,7 @@ export function CloudOsChatMessages({
393
395
  onSuggestion,
394
396
  onRespond,
395
397
  onOpenSchedule,
398
+ scheduleControls,
396
399
  onOpenArtifact,
397
400
  onCopy,
398
401
  }: CloudOsChatMessagesProps) {
@@ -840,6 +843,7 @@ export function CloudOsChatMessages({
840
843
  key={block.key}
841
844
  call={block.call}
842
845
  onOpen={onOpenSchedule}
846
+ controls={scheduleControls}
843
847
  />
844
848
  );
845
849
  case "approval": {
@@ -708,6 +708,14 @@ function scheduleCadence(input: Record<string, unknown>): string {
708
708
  } else if (trigger.kind === "cron" && typeof trigger.cron === "string") {
709
709
  cadence = `Cron ${trigger.cron}`;
710
710
  }
711
+ if (trigger.kind === "local" && typeof trigger.rule === "object" && trigger.rule) {
712
+ const rule = trigger.rule as Record<string, unknown>;
713
+ cadence = rule.kind === "once" ? `Once ${rule.date} at ${rule.time}`
714
+ : rule.kind === "weekly" ? `Weekly (${Array.isArray(rule.weekdays) ? rule.weekdays.map(day => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][Number(day)]).join(", ") : ""}) at ${rule.time}`
715
+ : `Daily at ${rule.time}`;
716
+ if (trigger.startDate) cadence += ` from ${trigger.startDate}`;
717
+ if (trigger.endDate) cadence += ` through ${trigger.endDate}`;
718
+ }
711
719
  if (!cadence) cadence = String(input.cadence ?? input.schedule ?? "Scheduled");
712
720
  const timezone =
713
721
  typeof input.timezone === "string"
@@ -736,9 +744,11 @@ export function ScheduleConfirmation({
736
744
  onCancel: () => void;
737
745
  onConfirm: () => void;
738
746
  }) {
747
+ const authorization = input.authorization as { scope?: string; missing?: string[] } | undefined;
748
+ const missing = authorization?.missing ?? [];
739
749
  const name = String(input.label ?? input.title ?? "Scheduled task");
740
750
  const prompt = typeof input.prompt === "string" && input.prompt.trim()
741
- ? input.prompt.trim()
751
+ ? input.prompt
742
752
  : "Run the scheduled task";
743
753
 
744
754
  return (
@@ -755,10 +765,10 @@ export function ScheduleConfirmation({
755
765
  <Clock size={14} />
756
766
  </span>
757
767
  <div className="flex min-w-0 flex-1 flex-col">
758
- <span className="truncate text-[13.5px] leading-[18px] font-medium">
768
+ <span className="break-words text-[13.5px] leading-[18px] font-medium">
759
769
  {name}
760
770
  </span>
761
- <span className="truncate font-mono text-[11px] leading-4 tracking-tight text-kumo-inactive">
771
+ <span className="break-words font-mono text-[11px] leading-4 tracking-tight text-kumo-inactive">
762
772
  {scheduleCadence(input)}
763
773
  </span>
764
774
  </div>
@@ -768,16 +778,20 @@ export function ScheduleConfirmation({
768
778
  <span className="shrink-0 font-mono text-[11px] leading-[18px] tracking-tight text-kumo-inactive">
769
779
  task
770
780
  </span>
771
- <span className="min-w-0 flex-1 text-[13px] leading-[18px] text-kumo-subtle">
781
+ <span className="min-w-0 flex-1 whitespace-pre-wrap text-[13px] leading-[18px] text-kumo-subtle">
772
782
  {prompt}
773
783
  </span>
774
784
  </div>
775
785
 
786
+ {input.id != null && <p className="m-0 text-xs">Task ID: {String(input.id)}</p>}
787
+ {typeof input.nextRunAt === "number" && <p className="m-0 text-xs">Next run: {new Intl.DateTimeFormat("en", { dateStyle: "medium", timeStyle: "short", timeZone: String(input.timezone) }).format(input.nextRunAt)}</p>}
788
+ {authorization?.scope && <div className="text-sm"><strong>Ongoing execution scope</strong><p className="m-0 whitespace-pre-wrap">{authorization.scope}</p></div>}
789
+ {missing.length > 0 && <div role="alert" className="text-sm text-kumo-danger">{missing.map((item, i) => <p key={i}>{item}</p>)}</div>}
776
790
  <div className="flex items-center justify-end gap-2">
777
791
  <WorkshopButton disabled={disabled} onClick={onCancel}>
778
792
  Cancel
779
793
  </WorkshopButton>
780
- <WorkshopButton tone="primary" disabled={disabled} onClick={onConfirm}>
794
+ <WorkshopButton tone="primary" disabled={disabled || missing.length > 0} onClick={onConfirm}>
781
795
  {disabled ? "Confirming…" : "Confirm schedule"}
782
796
  </WorkshopButton>
783
797
  </div>
@@ -785,81 +799,76 @@ export function ScheduleConfirmation({
785
799
  );
786
800
  }
787
801
 
788
- export function ScheduleBlock({
789
- call,
790
- onOpen,
791
- }: {
802
+ /** Current task facts are supplied by the Host, never inferred from a past Tool result. */
803
+ export interface ScheduleCardState {
804
+ id: string;
805
+ enabled: boolean;
806
+ status: string;
807
+ deleted: boolean;
808
+ canToggle?: boolean;
809
+ }
810
+ export interface ScheduleCardControls {
811
+ load(id: string): Promise<ScheduleCardState>;
812
+ setEnabled(id: string, enabled: boolean): Promise<ScheduleCardState>;
813
+ open(id: string): void;
814
+ }
815
+
816
+ export function ScheduleBlock({ call, onOpen, controls }: {
792
817
  call: CloudOsToolCall;
793
- /** 有 id 且宿主给了回调时,整张卡变成跳到「已安排」详情的按钮。 */
794
818
  onOpen?: (scheduleId: string) => void;
819
+ controls?: ScheduleCardControls;
795
820
  }) {
796
- // 待确认时由宿主把持久化审批渲染在输入框上方;这里不重复画第二张卡。
821
+ const scheduleId = !call.failed && !call.running ? scheduleIdOf(call.output) : "";
822
+ const [current, setCurrent] = useState<ScheduleCardState>();
823
+ const [busy, setBusy] = useState(false);
824
+ const [error, setError] = useState("");
825
+ const generation = useRef(0);
826
+ useEffect(() => {
827
+ const token = ++generation.current;
828
+ setCurrent(undefined); setError(""); setBusy(false);
829
+ if (scheduleId && controls) {
830
+ controls.load(scheduleId).then(value => {
831
+ if (token !== generation.current) return;
832
+ if (value.id !== scheduleId) throw new Error("Host returned a different task");
833
+ setCurrent(value);
834
+ }).catch(cause => { if (token === generation.current) setError(String(cause instanceof Error ? cause.message : cause)); });
835
+ }
836
+ return () => { generation.current++; };
837
+ }, [scheduleId, controls]);
797
838
  if (call.awaitingApproval) return null;
798
- const state = call.failed ? "failed" : call.running ? "running" : "scheduled";
799
- const scheduleId = state === "scheduled" ? scheduleIdOf(call.output) : "";
800
- const clickable = Boolean(scheduleId && onOpen);
801
- const Card = clickable ? "button" : "div";
802
- return (
803
- <div className="max-w-[860px]">
804
- <Card
805
- {...(clickable
806
- ? {
807
- type: "button" as const,
808
- onClick: () => onOpen?.(scheduleId),
809
- "aria-label": `Open scheduled task: ${String(call.input.label ?? call.input.title ?? "Scheduled task")}`,
810
- }
811
- : {})}
812
- className={`w-full rounded-2xl border border-kumo-line bg-kumo-base px-4 py-3 text-left${
813
- clickable
814
- ? " cursor-pointer transition-colors duration-150 ease-out hover:bg-kumo-tint focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/50"
815
- : ""
816
- }`}
817
- >
818
- <div className="flex flex-wrap items-start gap-3">
819
- <span
820
- className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-kumo-tint text-kumo-subtle"
821
- aria-hidden="true"
822
- >
823
- <Bell size={18} />
824
- </span>
825
- <div className="min-w-[10rem] flex-1">
826
- <span className="block truncate text-[14px] leading-5 font-medium tracking-[-0.25px] text-kumo-default">
827
- {String(call.input.label ?? call.input.title ?? "Scheduled task")}
828
- </span>
829
- <p className="mt-1 mb-0 text-[12px] leading-4 text-kumo-inactive">
830
- {scheduleCadence(call.input)}
831
- </p>
832
- </div>
833
- <span
834
- className={`ml-auto flex flex-shrink-0 items-center gap-1 self-center text-[12px] leading-4 font-medium ${
835
- state === "failed"
836
- ? "text-kumo-danger"
837
- : state === "running"
838
- ? "text-kumo-brand"
839
- : "text-kumo-success"
840
- }`}
841
- >
842
- {state === "failed" ? (
843
- <WarningCircle size={13} weight="fill" />
844
- ) : state === "running" ? (
845
- <CircleNotch
846
- size={13}
847
- weight="bold"
848
- className="animate-spin motion-reduce:animate-none"
849
- />
850
- ) : (
851
- <Check size={13} weight="bold" />
852
- )}
853
- {state === "failed"
854
- ? "Failed"
855
- : state === "running"
856
- ? "Scheduling…"
857
- : "Scheduled"}
858
- </span>
859
- </div>
860
- </Card>
861
- </div>
862
- );
839
+ const saved = (call.output as { task?: Record<string, unknown> } | undefined)?.task;
840
+ const task = saved ?? call.input;
841
+ const name = String(task.label ?? task.title ?? "Scheduled task");
842
+ const open = controls ? (current && !current.deleted ? controls.open : undefined) : onOpen;
843
+ const clickable = Boolean(scheduleId && open);
844
+ const Card = clickable && !controls ? "button" : "div";
845
+ const toggle = async () => {
846
+ if (!controls || !current || current.deleted || current.canToggle === false || busy) return;
847
+ const token = generation.current;
848
+ setBusy(true); setError("");
849
+ try {
850
+ const value = await controls.setEnabled(scheduleId, !current.enabled);
851
+ if (value.id !== scheduleId) throw new Error("Host returned a different task");
852
+ if (token === generation.current) setCurrent(value);
853
+ } catch (cause) {
854
+ if (token === generation.current) setError(cause instanceof Error ? cause.message : String(cause));
855
+ } finally { if (token === generation.current) setBusy(false); }
856
+ };
857
+ return <div className="max-w-[860px]">
858
+ <Card className="w-full rounded-2xl border border-kumo-line bg-kumo-base px-4 py-3 text-left"
859
+ {...(clickable && !controls ? { type: "button" as const, onClick: () => open?.(scheduleId), "aria-label": `Open scheduled task: ${name}` } : {})}>
860
+ <div className="flex items-center gap-3"><Bell size={18} /><span className="min-w-0 flex-1 break-words text-sm font-medium">{name}</span>
861
+ <span className="text-xs">{call.failed ? "Failed" : call.running ? "Scheduling…" : "Scheduled"}</span>
862
+ </div>
863
+ <p className="my-1 text-xs text-kumo-inactive">{scheduleCadence(task)}</p>
864
+ {controls && scheduleId && <div className="flex items-center gap-3 text-sm">
865
+ <span>{current?.status ?? (error ? "Status unavailable" : "Loading…")}</span>
866
+ {current && !current.deleted && <button type="button" role="switch" aria-label="Enable scheduled task" aria-checked={current.enabled} disabled={busy || current.canToggle === false} onClick={() => void toggle()}>{current.enabled ? "On" : "Off"}</button>}
867
+ {clickable && <button type="button" aria-label={`Open scheduled task: ${name}`} onClick={() => open?.(scheduleId)}>Scheduled</button>}
868
+ </div>}
869
+ {error && <p role="alert" className="text-xs text-kumo-danger">{error}</p>}
870
+ </Card>
871
+ </div>;
863
872
  }
864
873
 
865
874
  // ── 原位授权 ──────────────────────────────────────────────
@@ -241,7 +241,7 @@ export function canonicalToolKind(name: string): CloudOsToolKind {
241
241
  return "ask-user";
242
242
  }
243
243
  if (["suggest_followups", "suggestions"].includes(normalized)) return "suggestions";
244
- if (["schedule", "cancel_schedule"].includes(normalized)) return "schedule";
244
+ if (["schedule", "update_schedule", "pause_schedule", "resume_schedule", "cancel_schedule", "list_schedules", "change_schedule_agent"].includes(normalized)) return "schedule";
245
245
  if (normalized === "create_agent") return "create-agent";
246
246
  if (normalized === "publish_extension") return "publish-extension";
247
247
  return "generic";
@@ -353,7 +353,7 @@ export function getToolCallSummary(call: CloudOsToolCall): {
353
353
  case "schedule": {
354
354
  const label = stringField(input, "label", "title");
355
355
  return {
356
- verb: toolName === "cancel_schedule" ? "Cancelled schedule" : "Scheduled",
356
+ verb: toolName === "cancel_schedule" ? "Cancelled schedule" : toolName === "update_schedule" ? "Updated schedule" : toolName === "pause_schedule" ? "Paused schedule" : toolName === "resume_schedule" ? "Resumed schedule" : toolName === "list_schedules" ? "Listed schedules" : toolName === "change_schedule_agent" ? "Changed schedule Agent" : "Scheduled",
357
357
  target: label || undefined,
358
358
  };
359
359
  }
@@ -594,7 +594,7 @@ function assistantBlocks(
594
594
  }
595
595
  }
596
596
 
597
- if (call.kind === "schedule" && call.toolName === "schedule") {
597
+ if (call.kind === "schedule" && (call.toolName === "schedule" || call.toolName === "update_schedule")) {
598
598
  flush();
599
599
  blocks.push({ kind: "schedule", key, call });
600
600
  return;
package/cloud-os/index.ts CHANGED
@@ -149,3 +149,5 @@ export {
149
149
  WorkshopInput,
150
150
  } from "./primitives/workshop-controls";
151
151
  export type { CloudOsMode } from "./internal/theme-context";
152
+
153
+ export type { ScheduleCardState, ScheduleCardControls } from "./chat/rich-blocks";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/message-panel",
3
- "version": "0.3.0-alpha.1",
3
+ "version": "0.3.0-alpha.3",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -544,6 +544,9 @@ function summaryFor(
544
544
  ? `Could not publish ${extension}`
545
545
  : `Published ${extension}`;
546
546
  }
547
+ if (name === "schedule" || name === "update_schedule") {
548
+ return running ? "Preparing schedule..." : failed ? "Schedule was not saved" : name === "schedule" ? "Scheduled task" : "Updated schedule";
549
+ }
547
550
  if (name === "cancel_schedule") {
548
551
  return running
549
552
  ? "Cancelling schedule..."