@stigmer/react 3.5.2 → 3.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.
Files changed (193) hide show
  1. package/agent/AgentPicker.d.ts +10 -1
  2. package/agent/AgentPicker.d.ts.map +1 -1
  3. package/agent/AgentPicker.js +2 -2
  4. package/agent/AgentPicker.js.map +1 -1
  5. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  6. package/channel/AgentChannelsPanel.js +22 -5
  7. package/channel/AgentChannelsPanel.js.map +1 -1
  8. package/channel/ChannelTemplatesDialog.d.ts +53 -0
  9. package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
  10. package/channel/ChannelTemplatesDialog.js +150 -0
  11. package/channel/ChannelTemplatesDialog.js.map +1 -0
  12. package/channel/index.d.ts +8 -0
  13. package/channel/index.d.ts.map +1 -1
  14. package/channel/index.js +4 -0
  15. package/channel/index.js.map +1 -1
  16. package/channel/providers.d.ts +12 -0
  17. package/channel/providers.d.ts.map +1 -1
  18. package/channel/providers.js +14 -2
  19. package/channel/providers.js.map +1 -1
  20. package/channel/templatePresentation.d.ts +37 -0
  21. package/channel/templatePresentation.d.ts.map +1 -0
  22. package/channel/templatePresentation.js +69 -0
  23. package/channel/templatePresentation.js.map +1 -0
  24. package/channel/useChannelTemplateList.d.ts +64 -0
  25. package/channel/useChannelTemplateList.d.ts.map +1 -0
  26. package/channel/useChannelTemplateList.js +69 -0
  27. package/channel/useChannelTemplateList.js.map +1 -0
  28. package/channel/useChannelTemplateReadiness.d.ts +53 -0
  29. package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
  30. package/channel/useChannelTemplateReadiness.js +42 -0
  31. package/channel/useChannelTemplateReadiness.js.map +1 -0
  32. package/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
  33. package/dashboard/useDashboardFailedRuns.js +2 -0
  34. package/dashboard/useDashboardFailedRuns.js.map +1 -1
  35. package/index.d.ts +4 -2
  36. package/index.d.ts.map +1 -1
  37. package/index.js +9 -1
  38. package/index.js.map +1 -1
  39. package/library/useExportResource.d.ts +4 -3
  40. package/library/useExportResource.d.ts.map +1 -1
  41. package/library/useExportResource.js +1 -1
  42. package/library/useExportResource.js.map +1 -1
  43. package/organization/OrgProvider.d.ts +11 -4
  44. package/organization/OrgProvider.d.ts.map +1 -1
  45. package/organization/OrgProvider.js +28 -4
  46. package/organization/OrgProvider.js.map +1 -1
  47. package/package.json +4 -4
  48. package/resource-detail/useDeleteResource.d.ts +1 -1
  49. package/resource-detail/useDeleteResource.d.ts.map +1 -1
  50. package/resource-detail/useDeleteResource.js +5 -0
  51. package/resource-detail/useDeleteResource.js.map +1 -1
  52. package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
  53. package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
  54. package/resource-workbench/components/ResourceWorkbench.js +6 -2
  55. package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
  56. package/resource-workbench/hooks/useResourceCollection.d.ts +8 -0
  57. package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
  58. package/resource-workbench/hooks/useResourceCollection.js +2 -2
  59. package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
  60. package/schedule/CadenceField.d.ts +31 -0
  61. package/schedule/CadenceField.d.ts.map +1 -0
  62. package/schedule/CadenceField.js +116 -0
  63. package/schedule/CadenceField.js.map +1 -0
  64. package/schedule/ScheduleDetailView.d.ts +58 -0
  65. package/schedule/ScheduleDetailView.d.ts.map +1 -0
  66. package/schedule/ScheduleDetailView.js +236 -0
  67. package/schedule/ScheduleDetailView.js.map +1 -0
  68. package/schedule/ScheduleForm.d.ts +43 -0
  69. package/schedule/ScheduleForm.d.ts.map +1 -0
  70. package/schedule/ScheduleForm.js +129 -0
  71. package/schedule/ScheduleForm.js.map +1 -0
  72. package/schedule/ScheduleRowActions.d.ts +20 -0
  73. package/schedule/ScheduleRowActions.d.ts.map +1 -0
  74. package/schedule/ScheduleRowActions.js +54 -0
  75. package/schedule/ScheduleRowActions.js.map +1 -0
  76. package/schedule/TimeZoneField.d.ts +32 -0
  77. package/schedule/TimeZoneField.d.ts.map +1 -0
  78. package/schedule/TimeZoneField.js +104 -0
  79. package/schedule/TimeZoneField.js.map +1 -0
  80. package/schedule/cadence.d.ts +95 -0
  81. package/schedule/cadence.d.ts.map +1 -0
  82. package/schedule/cadence.js +242 -0
  83. package/schedule/cadence.js.map +1 -0
  84. package/schedule/index.d.ts +31 -0
  85. package/schedule/index.d.ts.map +1 -0
  86. package/schedule/index.js +16 -0
  87. package/schedule/index.js.map +1 -0
  88. package/schedule/scheduleColumns.d.ts +26 -0
  89. package/schedule/scheduleColumns.d.ts.map +1 -0
  90. package/schedule/scheduleColumns.js +77 -0
  91. package/schedule/scheduleColumns.js.map +1 -0
  92. package/schedule/scheduleListFn.d.ts +50 -0
  93. package/schedule/scheduleListFn.d.ts.map +1 -0
  94. package/schedule/scheduleListFn.js +65 -0
  95. package/schedule/scheduleListFn.js.map +1 -0
  96. package/schedule/scheduleState.d.ts +43 -0
  97. package/schedule/scheduleState.d.ts.map +1 -0
  98. package/schedule/scheduleState.js +67 -0
  99. package/schedule/scheduleState.js.map +1 -0
  100. package/schedule/useCreateSchedule.d.ts +57 -0
  101. package/schedule/useCreateSchedule.d.ts.map +1 -0
  102. package/schedule/useCreateSchedule.js +61 -0
  103. package/schedule/useCreateSchedule.js.map +1 -0
  104. package/schedule/useResumeSchedule.d.ts +30 -0
  105. package/schedule/useResumeSchedule.d.ts.map +1 -0
  106. package/schedule/useResumeSchedule.js +45 -0
  107. package/schedule/useResumeSchedule.js.map +1 -0
  108. package/schedule/useSchedule.d.ts +39 -0
  109. package/schedule/useSchedule.d.ts.map +1 -0
  110. package/schedule/useSchedule.js +44 -0
  111. package/schedule/useSchedule.js.map +1 -0
  112. package/schedule/useScheduleCount.d.ts +38 -0
  113. package/schedule/useScheduleCount.d.ts.map +1 -0
  114. package/schedule/useScheduleCount.js +30 -0
  115. package/schedule/useScheduleCount.js.map +1 -0
  116. package/schedule/useScheduleList.d.ts +48 -0
  117. package/schedule/useScheduleList.d.ts.map +1 -0
  118. package/schedule/useScheduleList.js +43 -0
  119. package/schedule/useScheduleList.js.map +1 -0
  120. package/schedule/useSetScheduleEnabled.d.ts +31 -0
  121. package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
  122. package/schedule/useSetScheduleEnabled.js +53 -0
  123. package/schedule/useSetScheduleEnabled.js.map +1 -0
  124. package/schedule/useTriggerSchedule.d.ts +28 -0
  125. package/schedule/useTriggerSchedule.d.ts.map +1 -0
  126. package/schedule/useTriggerSchedule.js +43 -0
  127. package/schedule/useTriggerSchedule.js.map +1 -0
  128. package/search/useResourceCount.d.ts +2 -1
  129. package/search/useResourceCount.d.ts.map +1 -1
  130. package/search/useResourceCount.js +6 -1
  131. package/search/useResourceCount.js.map +1 -1
  132. package/src/agent/AgentPicker.tsx +13 -1
  133. package/src/channel/AgentChannelsPanel.tsx +43 -1
  134. package/src/channel/ChannelTemplatesDialog.tsx +433 -0
  135. package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
  136. package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
  137. package/src/channel/__tests__/providers.test.ts +9 -0
  138. package/src/channel/__tests__/templatePresentation.test.ts +96 -0
  139. package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
  140. package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
  141. package/src/channel/index.ts +15 -0
  142. package/src/channel/providers.ts +26 -2
  143. package/src/channel/templatePresentation.ts +82 -0
  144. package/src/channel/useChannelTemplateList.ts +123 -0
  145. package/src/channel/useChannelTemplateReadiness.ts +69 -0
  146. package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
  147. package/src/dashboard/useDashboardFailedRuns.ts +2 -0
  148. package/src/execution/__tests__/PlanArtifactCard.test.tsx +23 -2
  149. package/src/index.ts +65 -0
  150. package/src/library/useExportResource.ts +4 -3
  151. package/src/organization/OrgProvider.tsx +29 -4
  152. package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
  153. package/src/resource-detail/useDeleteResource.ts +11 -1
  154. package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
  155. package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
  156. package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
  157. package/src/schedule/CadenceField.tsx +365 -0
  158. package/src/schedule/ScheduleDetailView.tsx +583 -0
  159. package/src/schedule/ScheduleForm.tsx +405 -0
  160. package/src/schedule/ScheduleRowActions.tsx +124 -0
  161. package/src/schedule/TimeZoneField.tsx +199 -0
  162. package/src/schedule/__tests__/ScheduleDetailView.test.tsx +260 -0
  163. package/src/schedule/__tests__/cadence.test.ts +278 -0
  164. package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
  165. package/src/schedule/__tests__/scheduleCreation.test.tsx +340 -0
  166. package/src/schedule/__tests__/scheduleHooks.test.tsx +312 -0
  167. package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
  168. package/src/schedule/__tests__/scheduleState.test.ts +133 -0
  169. package/src/schedule/cadence.ts +298 -0
  170. package/src/schedule/index.ts +57 -0
  171. package/src/schedule/scheduleColumns.tsx +118 -0
  172. package/src/schedule/scheduleListFn.ts +108 -0
  173. package/src/schedule/scheduleState.ts +96 -0
  174. package/src/schedule/useCreateSchedule.ts +88 -0
  175. package/src/schedule/useResumeSchedule.ts +67 -0
  176. package/src/schedule/useSchedule.ts +67 -0
  177. package/src/schedule/useScheduleCount.ts +57 -0
  178. package/src/schedule/useScheduleList.ts +83 -0
  179. package/src/schedule/useSetScheduleEnabled.ts +84 -0
  180. package/src/schedule/useTriggerSchedule.ts +65 -0
  181. package/src/search/__tests__/useResourceCount.test.tsx +89 -0
  182. package/src/search/useResourceCount.ts +8 -2
  183. package/src/workflow/WorkflowDashboard.tsx +1 -0
  184. package/src/workflow/useWorkflowExecutionList.ts +13 -2
  185. package/styles.css +1 -1
  186. package/workflow/CanvasTransitionEdge.d.ts +1 -1
  187. package/workflow/WorkflowDashboard.d.ts.map +1 -1
  188. package/workflow/WorkflowDashboard.js +1 -0
  189. package/workflow/WorkflowDashboard.js.map +1 -1
  190. package/workflow/useWorkflowExecutionList.d.ts +10 -0
  191. package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
  192. package/workflow/useWorkflowExecutionList.js +3 -2
  193. package/workflow/useWorkflowExecutionList.js.map +1 -1
@@ -0,0 +1,583 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ import { timestampDate } from "@bufbuild/protobuf/wkt";
6
+ import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
7
+ import { formatRelativeTime } from "../activity/format-relative-time.js";
8
+ import { ErrorMessage } from "../error/ErrorMessage.js";
9
+ import { EditResourceYamlDialog } from "../manifest/EditResourceYamlDialog.js";
10
+ import { ConfirmDialog } from "../resource-detail/ConfirmDialog.js";
11
+ import { ResourceDetailShell } from "../resource-detail/ResourceDetailShell.js";
12
+ import { Section } from "../resource-detail/Section.js";
13
+ import { useConfirmAction } from "../resource-detail/useConfirmAction.js";
14
+ import { useCopyResource } from "../resource-detail/useCopyResource.js";
15
+ import { useDeleteResource } from "../resource-detail/useDeleteResource.js";
16
+ import type { DetailAction, ResourceHeaderMeta } from "../resource-detail/types.js";
17
+ import { useExportResource } from "../library/useExportResource.js";
18
+ import { deriveScheduleState, formatNextFire } from "./scheduleState.js";
19
+ import { useSchedule } from "./useSchedule.js";
20
+ import { useResumeSchedule } from "./useResumeSchedule.js";
21
+ import { useSetScheduleEnabled } from "./useSetScheduleEnabled.js";
22
+ import { useTriggerSchedule } from "./useTriggerSchedule.js";
23
+
24
+ /** Props for {@link ScheduleDetailView}. */
25
+ export interface ScheduleDetailViewProps {
26
+ /** Organization slug. */
27
+ readonly org: string;
28
+ /** Schedule slug. */
29
+ readonly slug: string;
30
+ /**
31
+ * Called when the user activates the target-agent reference.
32
+ * Navigation is the consumer's concern (DD-004) — the Console pushes
33
+ * a Library route; an embedding host does whatever fits.
34
+ */
35
+ readonly onNavigateToAgent?: (org: string, slug: string) => void;
36
+ /**
37
+ * Called when the user activates the last-execution reference
38
+ * (`aex_…`). When omitted, the id renders as plain text.
39
+ */
40
+ readonly onNavigateToExecution?: (executionId: string) => void;
41
+ /** Called after a successful delete (navigate back to the list). */
42
+ readonly onDeleted?: () => void;
43
+ /** Called when the schedule loads or reloads (e.g. breadcrumb label sync). */
44
+ readonly onResourceLoad?: (schedule: Schedule) => void;
45
+ /**
46
+ * The instant "next fire" countdowns are computed against.
47
+ * Injectable for deterministic tests and Scenar fixtures.
48
+ * @default new Date() at render
49
+ */
50
+ readonly now?: Date;
51
+ /** Additional CSS classes for the root container. */
52
+ readonly className?: string;
53
+ }
54
+
55
+ /**
56
+ * Self-contained detail view for a Schedule (stigmer/stigmer#352).
57
+ *
58
+ * The view's one non-negotiable is rendering the two stop-levers
59
+ * distinctly, each with its inline remedy:
60
+ *
61
+ * - **Disabled** (owner's `spec.enabled` switch) — banner with an
62
+ * Enable action (a lossless full-proto re-apply).
63
+ * - **Paused** (platform's failure-streak latch,
64
+ * `status.paused_reason`) — banner showing the reason with a Resume
65
+ * action (the one path that clears the pause).
66
+ *
67
+ * Trigger ("Run now") starts a real, billable execution and is gated
68
+ * behind a confirmation; it is disabled while the schedule cannot fire
69
+ * (the banner names the remedy). Edit YAML, Export, and Delete round
70
+ * out the action set.
71
+ *
72
+ * Handles loading, error, and not-found states automatically.
73
+ * Zero Console dependencies — safe for platform builder embedding.
74
+ * All visual properties flow through `--stgm-*` design tokens.
75
+ */
76
+ export function ScheduleDetailView({
77
+ org,
78
+ slug,
79
+ onNavigateToAgent,
80
+ onNavigateToExecution,
81
+ onDeleted,
82
+ onResourceLoad,
83
+ now,
84
+ className,
85
+ }: ScheduleDetailViewProps) {
86
+ const { schedule, isLoading, error, refetch } = useSchedule(org, slug);
87
+
88
+ const { resumeSchedule, isResuming } = useResumeSchedule();
89
+ const { triggerSchedule, isTriggering } = useTriggerSchedule();
90
+ const { setEnabled, isPending: isToggling } = useSetScheduleEnabled();
91
+ const { deleteResource, isDeleting } = useDeleteResource(
92
+ "schedule",
93
+ schedule?.metadata?.id ?? null,
94
+ schedule?.metadata?.name,
95
+ );
96
+ const { confirmState, confirm, handleConfirm, handleCancel } =
97
+ useConfirmAction();
98
+ const { copyId, copyQualifiedSlug } = useCopyResource();
99
+ const [editOpen, setEditOpen] = useState(false);
100
+
101
+ const { copyYaml, downloadYaml } = useExportResource({
102
+ kind: "Schedule",
103
+ resource: schedule,
104
+ });
105
+
106
+ useEffect(() => {
107
+ if (schedule) onResourceLoad?.(schedule);
108
+ }, [schedule, onResourceLoad]);
109
+
110
+ if (isLoading) return <LoadingSkeleton className={className} />;
111
+ if (error)
112
+ return <ErrorMessage error={error} retry={refetch} className={className} />;
113
+ if (!schedule) return <NotFoundState className={className} />;
114
+
115
+ const meta = schedule.metadata;
116
+ const spec = schedule.spec;
117
+ const status = schedule.status;
118
+ const specAudit = status?.audit?.specAudit;
119
+ const stateInfo = deriveScheduleState(spec, status);
120
+ const renderNow = now ?? new Date();
121
+
122
+ const scheduleId = meta?.id ?? "";
123
+ const target = spec?.target?.case === "agent" ? spec.target.value : undefined;
124
+
125
+ const handleResume = async () => {
126
+ await resumeSchedule(scheduleId);
127
+ refetch();
128
+ };
129
+
130
+ const handleToggleEnabled = async () => {
131
+ await setEnabled(schedule, !spec?.enabled);
132
+ refetch();
133
+ };
134
+
135
+ const handleTrigger = async () => {
136
+ const confirmed = await confirm({
137
+ title: "Run this schedule now?",
138
+ description:
139
+ "This starts a real agent execution immediately, outside the cron " +
140
+ "cadence. The run appears as the schedule's last execution.",
141
+ confirmLabel: "Start run",
142
+ variant: "default",
143
+ });
144
+ if (!confirmed) return;
145
+ await triggerSchedule(scheduleId);
146
+ refetch();
147
+ };
148
+
149
+ const handleDelete = async () => {
150
+ const confirmed = await confirm({
151
+ title: "Delete this schedule?",
152
+ description:
153
+ `"${meta?.name ?? slug}" will stop firing and be permanently ` +
154
+ "removed. Past executions are not affected. This cannot be undone.",
155
+ confirmLabel: "Delete",
156
+ variant: "destructive",
157
+ });
158
+ if (!confirmed) return;
159
+ await deleteResource();
160
+ onDeleted?.();
161
+ };
162
+
163
+ const primaryAction: DetailAction = {
164
+ id: "trigger",
165
+ label: "Run now",
166
+ onAction: () => void handleTrigger(),
167
+ // A disabled or paused schedule refuses to trigger server-side; the
168
+ // banner above names the remedy, so the button reflects it too
169
+ // (error prevention over error recovery).
170
+ disabled: stateInfo.state !== "active" || isTriggering,
171
+ };
172
+
173
+ const actions: DetailAction[] = [
174
+ ...(stateInfo.isPaused
175
+ ? [
176
+ {
177
+ id: "resume",
178
+ label: "Resume schedule",
179
+ onAction: () => void handleResume(),
180
+ disabled: isResuming,
181
+ group: "state",
182
+ },
183
+ ]
184
+ : []),
185
+ {
186
+ id: "toggle-enabled",
187
+ label: spec?.enabled ? "Disable schedule" : "Enable schedule",
188
+ onAction: () => void handleToggleEnabled(),
189
+ disabled: isToggling,
190
+ group: "state",
191
+ },
192
+ {
193
+ id: "copy-id",
194
+ label: "Copy ID",
195
+ onAction: () => void copyId(scheduleId),
196
+ group: "clipboard",
197
+ },
198
+ {
199
+ id: "copy-slug",
200
+ label: "Copy slug",
201
+ onAction: () => void copyQualifiedSlug(org, slug),
202
+ group: "clipboard",
203
+ },
204
+ {
205
+ id: "edit-yaml",
206
+ label: "Edit YAML",
207
+ onAction: () => setEditOpen(true),
208
+ group: "edit",
209
+ },
210
+ {
211
+ id: "copy-yaml",
212
+ label: "Copy YAML",
213
+ onAction: () => void copyYaml(),
214
+ group: "export",
215
+ },
216
+ {
217
+ id: "download-yaml",
218
+ label: "Download YAML",
219
+ onAction: downloadYaml,
220
+ group: "export",
221
+ },
222
+ {
223
+ id: "delete",
224
+ label: "Delete",
225
+ variant: "destructive",
226
+ onAction: () => void handleDelete(),
227
+ disabled: isDeleting,
228
+ group: "danger",
229
+ },
230
+ ];
231
+
232
+ const headerMeta: ResourceHeaderMeta = {
233
+ name: meta?.name || meta?.slug || "Untitled",
234
+ id: scheduleId,
235
+ org: meta?.org,
236
+ slug: meta?.slug,
237
+ icon: <ScheduleIcon className="size-6 text-muted-foreground" />,
238
+ createdAt: specAudit?.createdAt ? timestampDate(specAudit.createdAt) : null,
239
+ updatedAt: specAudit?.updatedAt ? timestampDate(specAudit.updatedAt) : null,
240
+ status: stateInfo.phase,
241
+ statusLabel: stateInfo.label,
242
+ };
243
+
244
+ const headerBanner =
245
+ stateInfo.state === "disabled" ? (
246
+ <StateBanner
247
+ title="Schedule is disabled"
248
+ onAction={() => void handleToggleEnabled()}
249
+ actionLabel="Enable schedule"
250
+ actionBusy={isToggling}
251
+ >
252
+ The owner switch (<code className="font-mono">spec.enabled</code>) is
253
+ off — this schedule will not fire.
254
+ {stateInfo.isPaused && (
255
+ <>
256
+ {" "}
257
+ It is also paused by the platform ({status?.pausedReason}); after
258
+ enabling, use Resume to clear the pause.
259
+ </>
260
+ )}
261
+ </StateBanner>
262
+ ) : stateInfo.state === "paused" ? (
263
+ <StateBanner
264
+ title="Paused by the platform"
265
+ onAction={() => void handleResume()}
266
+ actionLabel="Resume schedule"
267
+ actionBusy={isResuming}
268
+ >
269
+ {status?.pausedReason} — resuming clears the pause and the failure
270
+ streak. Re-applying the manifest does not.
271
+ </StateBanner>
272
+ ) : undefined;
273
+
274
+ return (
275
+ <>
276
+ <ResourceDetailShell
277
+ header={headerMeta}
278
+ headerBanner={headerBanner}
279
+ primaryAction={primaryAction}
280
+ actions={actions}
281
+ className={className}
282
+ >
283
+ <div className="flex flex-col gap-6">
284
+ <Section title="Definition">
285
+ <dl className="divide-y divide-border">
286
+ <DetailRow label="Cron">
287
+ <code className="font-mono text-sm text-foreground">
288
+ {spec?.cron || "—"}
289
+ </code>
290
+ </DetailRow>
291
+ <DetailRow label="Time zone">
292
+ <span className="text-sm text-foreground">
293
+ {spec?.timeZone || "—"}
294
+ </span>
295
+ </DetailRow>
296
+ <DetailRow label="Target agent">
297
+ {target?.agentRef ? (
298
+ <ReferenceLink
299
+ label={`${target.agentRef.org}/${target.agentRef.slug}`}
300
+ onNavigate={
301
+ onNavigateToAgent
302
+ ? () =>
303
+ onNavigateToAgent(
304
+ target.agentRef!.org,
305
+ target.agentRef!.slug,
306
+ )
307
+ : undefined
308
+ }
309
+ />
310
+ ) : (
311
+ <span className="text-sm text-muted-foreground">—</span>
312
+ )}
313
+ </DetailRow>
314
+ <DetailRow label="Message">
315
+ <p className="whitespace-pre-wrap break-words text-sm text-foreground">
316
+ {target?.message || "—"}
317
+ </p>
318
+ </DetailRow>
319
+ </dl>
320
+ </Section>
321
+
322
+ <Section title="Status">
323
+ <dl className="divide-y divide-border">
324
+ <DetailRow label="Next fire">
325
+ <span className="text-sm text-foreground">
326
+ {stateInfo.state === "active" && status?.nextFireAt
327
+ ? formatNextFire(timestampDate(status.nextFireAt), renderNow)
328
+ : "—"}
329
+ </span>
330
+ </DetailRow>
331
+ <DetailRow label="Last fired">
332
+ <span className="text-sm text-foreground">
333
+ {status?.lastFireAt
334
+ ? formatRelativeTime(
335
+ timestampDate(status.lastFireAt),
336
+ renderNow,
337
+ )
338
+ : "Never"}
339
+ </span>
340
+ </DetailRow>
341
+ <DetailRow label="Last execution">
342
+ {status?.lastExecutionId ? (
343
+ <ReferenceLink
344
+ label={status.lastExecutionId}
345
+ mono
346
+ onNavigate={
347
+ onNavigateToExecution
348
+ ? () => onNavigateToExecution(status.lastExecutionId)
349
+ : undefined
350
+ }
351
+ />
352
+ ) : (
353
+ <span className="text-sm text-muted-foreground">—</span>
354
+ )}
355
+ </DetailRow>
356
+ <DetailRow label="Consecutive failures">
357
+ <span
358
+ className={cn(
359
+ "text-sm",
360
+ (status?.consecutiveFailures ?? 0) > 0
361
+ ? "font-medium text-warning"
362
+ : "text-foreground",
363
+ )}
364
+ >
365
+ {status?.consecutiveFailures ?? 0}
366
+ </span>
367
+ </DetailRow>
368
+ </dl>
369
+ </Section>
370
+ </div>
371
+ </ResourceDetailShell>
372
+
373
+ <ConfirmDialog
374
+ state={confirmState}
375
+ onConfirm={handleConfirm}
376
+ onCancel={handleCancel}
377
+ />
378
+
379
+ {/* Mounted on demand: the editor serializes the resource and pulls
380
+ the CodeMirror chunk, neither of which should cost anything
381
+ until the action is actually invoked (DD-013). The dialog
382
+ resets its edit state on open, so unmount-on-close loses
383
+ nothing. */}
384
+ {editOpen && (
385
+ <EditResourceYamlDialog
386
+ open={editOpen}
387
+ onOpenChange={setEditOpen}
388
+ resource={schedule}
389
+ onApplied={() => refetch()}
390
+ />
391
+ )}
392
+ </>
393
+ );
394
+ }
395
+
396
+ // ---------------------------------------------------------------------------
397
+ // State banner — one lever, one remedy, inline
398
+ // ---------------------------------------------------------------------------
399
+
400
+ function StateBanner({
401
+ title,
402
+ children,
403
+ actionLabel,
404
+ onAction,
405
+ actionBusy,
406
+ }: {
407
+ readonly title: string;
408
+ readonly children: React.ReactNode;
409
+ readonly actionLabel: string;
410
+ readonly onAction: () => void;
411
+ readonly actionBusy: boolean;
412
+ }) {
413
+ return (
414
+ <div
415
+ role="status"
416
+ className="flex items-start gap-2.5 rounded-lg border border-warning/30 bg-warning/5 px-4 py-3"
417
+ >
418
+ <WarningIcon className="mt-0.5 size-4 shrink-0 text-warning" />
419
+ <div className="min-w-0 flex-1">
420
+ <p className="text-sm font-medium text-foreground">{title}</p>
421
+ <p className="mt-0.5 text-xs text-muted-foreground">{children}</p>
422
+ </div>
423
+ <button
424
+ type="button"
425
+ onClick={onAction}
426
+ disabled={actionBusy}
427
+ className={cn(
428
+ "shrink-0 rounded-md border border-input bg-background px-3 py-1.5 text-xs font-medium text-foreground",
429
+ "hover:bg-accent hover:text-accent-foreground",
430
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
431
+ "disabled:pointer-events-none disabled:opacity-50",
432
+ )}
433
+ >
434
+ {actionLabel}
435
+ </button>
436
+ </div>
437
+ );
438
+ }
439
+
440
+ // ---------------------------------------------------------------------------
441
+ // Detail rows and references
442
+ // ---------------------------------------------------------------------------
443
+
444
+ function DetailRow({
445
+ label,
446
+ children,
447
+ }: {
448
+ readonly label: string;
449
+ readonly children: React.ReactNode;
450
+ }) {
451
+ return (
452
+ <div className="flex items-start gap-4 px-4 py-2.5">
453
+ <dt className="w-40 shrink-0 pt-0.5 text-xs font-medium text-muted-foreground">
454
+ {label}
455
+ </dt>
456
+ <dd className="min-w-0 flex-1">{children}</dd>
457
+ </div>
458
+ );
459
+ }
460
+
461
+ function ReferenceLink({
462
+ label,
463
+ onNavigate,
464
+ mono,
465
+ }: {
466
+ readonly label: string;
467
+ readonly onNavigate?: () => void;
468
+ readonly mono?: boolean;
469
+ }) {
470
+ const textClass = cn("text-sm", mono && "font-mono text-xs");
471
+ if (!onNavigate) {
472
+ return <span className={cn(textClass, "text-foreground")}>{label}</span>;
473
+ }
474
+ return (
475
+ <button
476
+ type="button"
477
+ onClick={onNavigate}
478
+ className={cn(
479
+ textClass,
480
+ "text-primary underline-offset-2 hover:underline",
481
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm",
482
+ )}
483
+ >
484
+ {label}
485
+ </button>
486
+ );
487
+ }
488
+
489
+ // ---------------------------------------------------------------------------
490
+ // Non-happy states
491
+ // ---------------------------------------------------------------------------
492
+
493
+ function LoadingSkeleton({ className }: { readonly className?: string }) {
494
+ return (
495
+ <div
496
+ className={cn("flex flex-col gap-6", className)}
497
+ aria-busy="true"
498
+ aria-label="Loading schedule details"
499
+ >
500
+ <div className="flex items-start gap-3">
501
+ <div className="mt-1 size-6 shrink-0 animate-pulse rounded bg-muted" />
502
+ <div className="flex-1 space-y-2">
503
+ <div className="h-5 w-48 animate-pulse rounded bg-muted" />
504
+ <div className="h-3 w-64 animate-pulse rounded bg-muted" />
505
+ </div>
506
+ </div>
507
+ <div className="space-y-2">
508
+ <div className="h-3 w-28 animate-pulse rounded bg-muted" />
509
+ <div
510
+ className="animate-pulse rounded-lg border border-border bg-muted-faint"
511
+ style={{ height: "160px" }}
512
+ />
513
+ </div>
514
+ <div className="space-y-2">
515
+ <div className="h-3 w-20 animate-pulse rounded bg-muted" />
516
+ <div
517
+ className="animate-pulse rounded-lg border border-border bg-muted-faint"
518
+ style={{ height: "160px" }}
519
+ />
520
+ </div>
521
+ </div>
522
+ );
523
+ }
524
+
525
+ function NotFoundState({ className }: { readonly className?: string }) {
526
+ return (
527
+ <div
528
+ role="status"
529
+ className={cn("flex flex-col items-center gap-2 py-12 text-center", className)}
530
+ >
531
+ <ScheduleIcon className="size-10 text-muted-foreground-faint" />
532
+ <p className="text-sm font-medium text-muted-foreground">
533
+ Schedule not found
534
+ </p>
535
+ <p className="text-xs text-muted-foreground-subtle">
536
+ This schedule doesn&apos;t exist or you don&apos;t have access to it.
537
+ </p>
538
+ </div>
539
+ );
540
+ }
541
+
542
+ // ---------------------------------------------------------------------------
543
+ // Icons (inline SVG — no icon-library dependency)
544
+ // ---------------------------------------------------------------------------
545
+
546
+ /** Clock-with-arrow icon shared by the schedule views. */
547
+ export function ScheduleIcon({ className }: { readonly className?: string }) {
548
+ return (
549
+ <svg
550
+ className={className}
551
+ viewBox="0 0 24 24"
552
+ fill="none"
553
+ stroke="currentColor"
554
+ strokeWidth="1.5"
555
+ strokeLinecap="round"
556
+ strokeLinejoin="round"
557
+ aria-hidden="true"
558
+ >
559
+ <path d="M21 12a9 9 0 1 1-2.64-6.36" />
560
+ <path d="M21 3v5h-5" />
561
+ <path d="M12 7v5l3 3" />
562
+ </svg>
563
+ );
564
+ }
565
+
566
+ function WarningIcon({ className }: { readonly className?: string }) {
567
+ return (
568
+ <svg
569
+ className={className}
570
+ viewBox="0 0 16 16"
571
+ fill="none"
572
+ stroke="currentColor"
573
+ strokeWidth="1.5"
574
+ strokeLinecap="round"
575
+ strokeLinejoin="round"
576
+ aria-hidden="true"
577
+ >
578
+ <path d="M8 1.5 15 14H1L8 1.5Z" />
579
+ <path d="M8 6v4" />
580
+ <path d="M8 12.2v.05" />
581
+ </svg>
582
+ );
583
+ }