@stigmer/react 3.5.3 → 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 (192) 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/index.ts +65 -0
  149. package/src/library/useExportResource.ts +4 -3
  150. package/src/organization/OrgProvider.tsx +29 -4
  151. package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
  152. package/src/resource-detail/useDeleteResource.ts +11 -1
  153. package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
  154. package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
  155. package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
  156. package/src/schedule/CadenceField.tsx +365 -0
  157. package/src/schedule/ScheduleDetailView.tsx +583 -0
  158. package/src/schedule/ScheduleForm.tsx +405 -0
  159. package/src/schedule/ScheduleRowActions.tsx +124 -0
  160. package/src/schedule/TimeZoneField.tsx +199 -0
  161. package/src/schedule/__tests__/ScheduleDetailView.test.tsx +260 -0
  162. package/src/schedule/__tests__/cadence.test.ts +278 -0
  163. package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
  164. package/src/schedule/__tests__/scheduleCreation.test.tsx +340 -0
  165. package/src/schedule/__tests__/scheduleHooks.test.tsx +312 -0
  166. package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
  167. package/src/schedule/__tests__/scheduleState.test.ts +133 -0
  168. package/src/schedule/cadence.ts +298 -0
  169. package/src/schedule/index.ts +57 -0
  170. package/src/schedule/scheduleColumns.tsx +118 -0
  171. package/src/schedule/scheduleListFn.ts +108 -0
  172. package/src/schedule/scheduleState.ts +96 -0
  173. package/src/schedule/useCreateSchedule.ts +88 -0
  174. package/src/schedule/useResumeSchedule.ts +67 -0
  175. package/src/schedule/useSchedule.ts +67 -0
  176. package/src/schedule/useScheduleCount.ts +57 -0
  177. package/src/schedule/useScheduleList.ts +83 -0
  178. package/src/schedule/useSetScheduleEnabled.ts +84 -0
  179. package/src/schedule/useTriggerSchedule.ts +65 -0
  180. package/src/search/__tests__/useResourceCount.test.tsx +89 -0
  181. package/src/search/useResourceCount.ts +8 -2
  182. package/src/workflow/WorkflowDashboard.tsx +1 -0
  183. package/src/workflow/useWorkflowExecutionList.ts +13 -2
  184. package/styles.css +1 -1
  185. package/workflow/CanvasTransitionEdge.d.ts +1 -1
  186. package/workflow/WorkflowDashboard.d.ts.map +1 -1
  187. package/workflow/WorkflowDashboard.js +1 -0
  188. package/workflow/WorkflowDashboard.js.map +1 -1
  189. package/workflow/useWorkflowExecutionList.d.ts +10 -0
  190. package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
  191. package/workflow/useWorkflowExecutionList.js +3 -2
  192. package/workflow/useWorkflowExecutionList.js.map +1 -1
@@ -0,0 +1,405 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState, type FormEvent } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ import { getUserMessage, type ResourceRef } from "@stigmer/sdk";
6
+ import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
7
+ import { Popover } from "@base-ui/react/popover";
8
+ import { AgentPicker } from "../agent/AgentPicker.js";
9
+ import { Switch } from "../switch/Switch.js";
10
+ import { useStigmerPortalContainer } from "../portal-container.js";
11
+ import { CadenceField } from "./CadenceField.js";
12
+ import { TimeZoneField, browserTimeZone } from "./TimeZoneField.js";
13
+ import { useCreateSchedule } from "./useCreateSchedule.js";
14
+ import { cadenceToCron, validateCron, type CadencePreset } from "./cadence.js";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Public API
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /** Props for {@link ScheduleForm}. */
21
+ export interface ScheduleFormProps {
22
+ /**
23
+ * Organization slug. Schedules are org-local: the server requires the
24
+ * target agent to live in this same org, so the agent picker is
25
+ * locked to org scope.
26
+ */
27
+ readonly org: string;
28
+ /** Fired with the created schedule after a successful submit. */
29
+ readonly onComplete?: (schedule: Schedule) => void;
30
+ /** Fired when the user cancels. */
31
+ readonly onCancel?: () => void;
32
+ /** Additional CSS class names for the root form element. */
33
+ readonly className?: string;
34
+ }
35
+
36
+ /** spec.proto pins AgentTarget.message to 8192 characters. */
37
+ const MESSAGE_MAX_LEN = 8192;
38
+ /** Show the remaining-characters counter once within this margin. */
39
+ const MESSAGE_COUNTER_THRESHOLD = 500;
40
+
41
+ /**
42
+ * Single-page form for creating a {@link Schedule}.
43
+ *
44
+ * Collects the four things a user decides about a schedule — which
45
+ * agent, what message, when ({@link CadenceField}, generating the cron
46
+ * expression from human-friendly presets), and whether it starts
47
+ * enabled — then creates it via {@link useCreateSchedule}.
48
+ *
49
+ * Enabled defaults to OFF, deliberately: it matches the proto/YAML
50
+ * default (an omitted `enabled` is false) and the platform's
51
+ * recommended workflow — create staged-disabled, validate with
52
+ * "Run now" on the detail page, then enable.
53
+ *
54
+ * Creation only. Editing an existing schedule must not flow through
55
+ * this form's curated input — see the note on {@link useCreateSchedule}.
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * <ScheduleForm
60
+ * org="acme"
61
+ * onComplete={(s) => navigate(`/library/schedules/${s.metadata?.org}/${s.metadata?.slug}`)}
62
+ * onCancel={() => navigate("/library/schedules")}
63
+ * />
64
+ * ```
65
+ */
66
+ export function ScheduleForm({
67
+ org,
68
+ onComplete,
69
+ onCancel,
70
+ className,
71
+ }: ScheduleFormProps) {
72
+ const { create, isCreating, error, clearError } = useCreateSchedule();
73
+
74
+ const [name, setName] = useState("");
75
+ const [agentRef, setAgentRef] = useState<ResourceRef | null>(null);
76
+ const [agentName, setAgentName] = useState<string | null>(null);
77
+ const [message, setMessage] = useState("");
78
+ const [cadence, setCadence] = useState<CadencePreset>({
79
+ kind: "daily",
80
+ hour: 9,
81
+ minute: 0,
82
+ });
83
+ const [timeZone, setTimeZone] = useState(browserTimeZone);
84
+ const [enabled, setEnabled] = useState(false);
85
+ const [pickerOpen, setPickerOpen] = useState(false);
86
+
87
+ const trimmedName = name.trim();
88
+ const trimmedMessage = message.trim();
89
+ const cron = cadenceToCron(cadence).trim();
90
+ const cronValid = cron !== "" && validateCron(cron) === null;
91
+
92
+ const canSubmit =
93
+ trimmedName !== "" &&
94
+ agentRef !== null &&
95
+ trimmedMessage !== "" &&
96
+ cronValid &&
97
+ !isCreating;
98
+
99
+ const handleAgentChange = useCallback((ref: ResourceRef | null) => {
100
+ setAgentRef(ref);
101
+ if (ref === null) setAgentName(null);
102
+ setPickerOpen(false);
103
+ }, []);
104
+
105
+ const handleSubmit = useCallback(
106
+ async (e: FormEvent) => {
107
+ e.preventDefault();
108
+ if (!canSubmit || !agentRef) return;
109
+
110
+ clearError();
111
+ try {
112
+ const schedule = await create({
113
+ name: trimmedName,
114
+ org,
115
+ cron,
116
+ timeZone,
117
+ enabled,
118
+ agent: { agentRef, message: trimmedMessage },
119
+ });
120
+ onComplete?.(schedule);
121
+ } catch {
122
+ // error state is managed by useCreateSchedule
123
+ }
124
+ },
125
+ [
126
+ canSubmit,
127
+ agentRef,
128
+ clearError,
129
+ create,
130
+ trimmedName,
131
+ org,
132
+ cron,
133
+ timeZone,
134
+ enabled,
135
+ trimmedMessage,
136
+ onComplete,
137
+ ],
138
+ );
139
+
140
+ const remaining = MESSAGE_MAX_LEN - message.length;
141
+
142
+ return (
143
+ <form
144
+ onSubmit={handleSubmit}
145
+ className={cn("max-w-2xl space-y-5", className)}
146
+ aria-label="New schedule"
147
+ >
148
+ {/* Name */}
149
+ <div className="space-y-1">
150
+ <label htmlFor="stgm-new-schedule-name" className={labelClasses}>
151
+ Name
152
+ </label>
153
+ <input
154
+ id="stgm-new-schedule-name"
155
+ type="text"
156
+ value={name}
157
+ onChange={(e) => setName(e.target.value)}
158
+ placeholder="e.g. daily-fee-reminders"
159
+ disabled={isCreating}
160
+ autoFocus
161
+ required
162
+ className={inputClasses}
163
+ />
164
+ </div>
165
+
166
+ {/* Target agent */}
167
+ <div className="space-y-1">
168
+ <span id="stgm-new-schedule-agent-label" className={labelClasses}>
169
+ Agent to run
170
+ </span>
171
+ <Popover.Root open={pickerOpen} onOpenChange={setPickerOpen}>
172
+ <Popover.Trigger
173
+ disabled={isCreating}
174
+ aria-labelledby="stgm-new-schedule-agent-label"
175
+ className={cn(
176
+ "flex w-full items-center justify-between rounded-md border border-input bg-background px-2.5 py-1.5 text-left text-xs",
177
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
178
+ "disabled:pointer-events-none disabled:opacity-50",
179
+ agentRef ? "text-foreground" : "text-muted-foreground",
180
+ )}
181
+ >
182
+ <span className="truncate">
183
+ {agentRef
184
+ ? (agentName ?? `${agentRef.org}/${agentRef.slug}`)
185
+ : "Choose an agent…"}
186
+ </span>
187
+ <ChevronIcon />
188
+ </Popover.Trigger>
189
+ <AgentPickerPopup
190
+ org={org}
191
+ value={agentRef}
192
+ onChange={handleAgentChange}
193
+ onDisplayNameResolved={(_key, resolvedName) =>
194
+ setAgentName(resolvedName)
195
+ }
196
+ disabled={isCreating}
197
+ />
198
+ </Popover.Root>
199
+ <p className={hintClasses}>
200
+ Each fire runs this agent unattended in a fresh session. Only
201
+ agents in <span className="font-medium">{org}</span> can be
202
+ scheduled.
203
+ </p>
204
+ </div>
205
+
206
+ {/* Message */}
207
+ <div className="space-y-1">
208
+ <label htmlFor="stgm-new-schedule-message" className={labelClasses}>
209
+ Message
210
+ </label>
211
+ <textarea
212
+ id="stgm-new-schedule-message"
213
+ value={message}
214
+ onChange={(e) => setMessage(e.target.value)}
215
+ placeholder="The instruction the agent receives on every fire — write it for a run with no human present."
216
+ disabled={isCreating}
217
+ required
218
+ rows={5}
219
+ maxLength={MESSAGE_MAX_LEN}
220
+ className={cn(inputClasses, "resize-y")}
221
+ />
222
+ {remaining <= MESSAGE_COUNTER_THRESHOLD && (
223
+ <p className={hintClasses}>{remaining} characters left</p>
224
+ )}
225
+ </div>
226
+
227
+ {/* Cadence */}
228
+ <div className="space-y-1">
229
+ <span className={labelClasses}>Runs</span>
230
+ <CadenceField
231
+ value={cadence}
232
+ onChange={setCadence}
233
+ timeZone={timeZone}
234
+ disabled={isCreating}
235
+ />
236
+ </div>
237
+
238
+ {/* Time zone */}
239
+ <div className="space-y-1">
240
+ <label htmlFor="stgm-new-schedule-tz" className={labelClasses}>
241
+ Time zone
242
+ </label>
243
+ <TimeZoneField
244
+ id="stgm-new-schedule-tz"
245
+ value={timeZone}
246
+ onChange={setTimeZone}
247
+ disabled={isCreating}
248
+ />
249
+ </div>
250
+
251
+ {/* Enabled */}
252
+ <div className="flex items-start gap-3">
253
+ <Switch
254
+ id="stgm-new-schedule-enabled"
255
+ checked={enabled}
256
+ onCheckedChange={setEnabled}
257
+ disabled={isCreating}
258
+ aria-labelledby="stgm-new-schedule-enabled-label"
259
+ />
260
+ <div className="space-y-0.5">
261
+ <label
262
+ id="stgm-new-schedule-enabled-label"
263
+ htmlFor="stgm-new-schedule-enabled"
264
+ className={labelClasses}
265
+ >
266
+ Enabled
267
+ </label>
268
+ <p className={hintClasses}>
269
+ Leave off to create the schedule staged: validate it with
270
+ &ldquo;Run now&rdquo; on the schedule page, then enable it there.
271
+ </p>
272
+ </div>
273
+ </div>
274
+
275
+ {error && (
276
+ <p className="text-xs text-destructive" role="alert">
277
+ {getUserMessage(error)}
278
+ </p>
279
+ )}
280
+
281
+ <div className="flex items-center gap-2">
282
+ <button
283
+ type="submit"
284
+ disabled={!canSubmit}
285
+ className={cn(
286
+ "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium",
287
+ "bg-primary text-primary-foreground hover:bg-primary-hover",
288
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
289
+ "disabled:pointer-events-none disabled:opacity-40",
290
+ )}
291
+ >
292
+ {isCreating && <SpinnerIcon />}
293
+ Create schedule
294
+ </button>
295
+
296
+ {onCancel && (
297
+ <button
298
+ type="button"
299
+ onClick={onCancel}
300
+ disabled={isCreating}
301
+ className={cn(
302
+ "rounded-md px-3 py-1.5 text-sm",
303
+ "text-muted-foreground hover:bg-accent-hover hover:text-foreground",
304
+ "disabled:pointer-events-none disabled:opacity-50",
305
+ )}
306
+ >
307
+ Cancel
308
+ </button>
309
+ )}
310
+ </div>
311
+ </form>
312
+ );
313
+ }
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // Private helpers
317
+ // ---------------------------------------------------------------------------
318
+
319
+ const labelClasses = "block text-xs font-medium text-foreground";
320
+ const hintClasses = "text-[0.65rem] text-muted-foreground";
321
+ const inputClasses = cn(
322
+ "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
323
+ "placeholder:text-muted-foreground",
324
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
325
+ "disabled:pointer-events-none disabled:opacity-50",
326
+ );
327
+
328
+ function AgentPickerPopup({
329
+ org,
330
+ value,
331
+ onChange,
332
+ onDisplayNameResolved,
333
+ disabled,
334
+ }: {
335
+ readonly org: string;
336
+ readonly value: ResourceRef | null;
337
+ readonly onChange: (ref: ResourceRef | null) => void;
338
+ readonly onDisplayNameResolved: (key: string, name: string) => void;
339
+ readonly disabled?: boolean;
340
+ }) {
341
+ const portalContainer = useStigmerPortalContainer();
342
+ return (
343
+ <Popover.Portal container={portalContainer}>
344
+ <Popover.Positioner sideOffset={4} align="start">
345
+ <Popover.Popup
346
+ className={cn(
347
+ "z-popover overflow-x-hidden overflow-y-auto rounded-lg border border-border",
348
+ "bg-popover p-3 text-popover-foreground shadow-md",
349
+ "max-h-[60vh]",
350
+ )}
351
+ >
352
+ <AgentPicker
353
+ org={org}
354
+ value={value}
355
+ onChange={onChange}
356
+ onDisplayNameResolved={onDisplayNameResolved}
357
+ // Schedules are org-local: the server rejects a target
358
+ // agent outside the schedule's org, so cross-org browsing
359
+ // could only offer choices that fail at submit.
360
+ scope="org"
361
+ showScopeToggle={false}
362
+ disabled={disabled}
363
+ />
364
+ </Popover.Popup>
365
+ </Popover.Positioner>
366
+ </Popover.Portal>
367
+ );
368
+ }
369
+
370
+ function ChevronIcon() {
371
+ return (
372
+ <svg
373
+ width="12"
374
+ height="12"
375
+ viewBox="0 0 16 16"
376
+ fill="none"
377
+ stroke="currentColor"
378
+ strokeWidth="1.5"
379
+ strokeLinecap="round"
380
+ strokeLinejoin="round"
381
+ className="shrink-0 text-muted-foreground"
382
+ aria-hidden="true"
383
+ >
384
+ <path d="M4 6l4 4 4-4" />
385
+ </svg>
386
+ );
387
+ }
388
+
389
+ function SpinnerIcon() {
390
+ return (
391
+ <svg
392
+ width="14"
393
+ height="14"
394
+ viewBox="0 0 16 16"
395
+ fill="none"
396
+ stroke="currentColor"
397
+ strokeWidth="2"
398
+ strokeLinecap="round"
399
+ className="animate-spin"
400
+ aria-hidden="true"
401
+ >
402
+ <path d="M8 2a6 6 0 1 0 6 6" />
403
+ </svg>
404
+ );
405
+ }
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+ import { cn } from "@stigmer/theme";
4
+ import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
5
+ import { ConfirmDialog } from "../resource-detail/ConfirmDialog.js";
6
+ import { useConfirmAction } from "../resource-detail/useConfirmAction.js";
7
+ import { deriveScheduleState } from "./scheduleState.js";
8
+ import { useResumeSchedule } from "./useResumeSchedule.js";
9
+ import { useTriggerSchedule } from "./useTriggerSchedule.js";
10
+
11
+ /** Props for {@link ScheduleRowActions}. */
12
+ export interface ScheduleRowActionsProps {
13
+ /** The schedule this row represents. */
14
+ readonly schedule: Schedule;
15
+ /** Called after a successful action so the list can refetch. */
16
+ readonly onChanged?: () => void;
17
+ }
18
+
19
+ /**
20
+ * Compact per-row actions for a schedule list: **Run now** on an active
21
+ * schedule (confirmation-gated — it starts a real, billable execution)
22
+ * and **Resume** on a platform-paused one. Exactly one action renders
23
+ * at a time — the one that is currently meaningful; an owner-disabled
24
+ * schedule renders neither (its remedy lives on the detail page).
25
+ *
26
+ * Shared by every console's schedule workbench via `renderItemAction`
27
+ * so row behavior never diverges between apps (DD-016).
28
+ */
29
+ export function ScheduleRowActions({
30
+ schedule,
31
+ onChanged,
32
+ }: ScheduleRowActionsProps) {
33
+ const { resumeSchedule, isResuming } = useResumeSchedule();
34
+ const { triggerSchedule, isTriggering } = useTriggerSchedule();
35
+ const { confirmState, confirm, handleConfirm, handleCancel } =
36
+ useConfirmAction();
37
+
38
+ const info = deriveScheduleState(schedule.spec, schedule.status);
39
+ const scheduleId = schedule.metadata?.id ?? "";
40
+ const name = schedule.metadata?.name || schedule.metadata?.slug || "schedule";
41
+
42
+ const handleTrigger = async () => {
43
+ const confirmed = await confirm({
44
+ title: "Run this schedule now?",
45
+ description:
46
+ `"${name}" starts a real agent execution immediately, outside ` +
47
+ "the cron cadence.",
48
+ confirmLabel: "Start run",
49
+ variant: "default",
50
+ });
51
+ if (!confirmed) return;
52
+ await triggerSchedule(scheduleId);
53
+ onChanged?.();
54
+ };
55
+
56
+ const handleResume = async () => {
57
+ await resumeSchedule(scheduleId);
58
+ onChanged?.();
59
+ };
60
+
61
+ if (info.state === "disabled") return null;
62
+
63
+ return (
64
+ // Row actions must never bubble into the row's navigate-to-detail
65
+ // click handler. The span is a non-interactive event fence — the
66
+ // real interactive element (with keyboard support) is the button.
67
+ <span onClick={(e) => e.stopPropagation()}>
68
+ {info.state === "active" ? (
69
+ <RowActionButton
70
+ label={`Run ${name} now`}
71
+ busy={isTriggering}
72
+ onClick={() => void handleTrigger()}
73
+ >
74
+ Run now
75
+ </RowActionButton>
76
+ ) : (
77
+ <RowActionButton
78
+ label={`Resume ${name}`}
79
+ busy={isResuming}
80
+ onClick={() => void handleResume()}
81
+ >
82
+ Resume
83
+ </RowActionButton>
84
+ )}
85
+
86
+ {confirmState && (
87
+ <ConfirmDialog
88
+ state={confirmState}
89
+ onConfirm={handleConfirm}
90
+ onCancel={handleCancel}
91
+ />
92
+ )}
93
+ </span>
94
+ );
95
+ }
96
+
97
+ function RowActionButton({
98
+ label,
99
+ busy,
100
+ onClick,
101
+ children,
102
+ }: {
103
+ readonly label: string;
104
+ readonly busy: boolean;
105
+ readonly onClick: () => void;
106
+ readonly children: React.ReactNode;
107
+ }) {
108
+ return (
109
+ <button
110
+ type="button"
111
+ aria-label={label}
112
+ onClick={onClick}
113
+ disabled={busy}
114
+ className={cn(
115
+ "rounded-md border border-input bg-background px-2 py-1 text-xs font-medium text-foreground",
116
+ "hover:bg-accent hover:text-accent-foreground",
117
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
118
+ "disabled:pointer-events-none disabled:opacity-50",
119
+ )}
120
+ >
121
+ {children}
122
+ </button>
123
+ );
124
+ }