@stigmer/react 3.5.3 → 3.7.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 (283) 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/billing/CreditLedgerTable.d.ts.map +1 -1
  6. package/billing/CreditLedgerTable.js +2 -7
  7. package/billing/CreditLedgerTable.js.map +1 -1
  8. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  9. package/channel/AgentChannelsPanel.js +22 -5
  10. package/channel/AgentChannelsPanel.js.map +1 -1
  11. package/channel/ChannelTemplatesDialog.d.ts +53 -0
  12. package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
  13. package/channel/ChannelTemplatesDialog.js +150 -0
  14. package/channel/ChannelTemplatesDialog.js.map +1 -0
  15. package/channel/index.d.ts +8 -0
  16. package/channel/index.d.ts.map +1 -1
  17. package/channel/index.js +4 -0
  18. package/channel/index.js.map +1 -1
  19. package/channel/providers.d.ts +12 -0
  20. package/channel/providers.d.ts.map +1 -1
  21. package/channel/providers.js +14 -2
  22. package/channel/providers.js.map +1 -1
  23. package/channel/templatePresentation.d.ts +37 -0
  24. package/channel/templatePresentation.d.ts.map +1 -0
  25. package/channel/templatePresentation.js +69 -0
  26. package/channel/templatePresentation.js.map +1 -0
  27. package/channel/useChannelTemplateList.d.ts +64 -0
  28. package/channel/useChannelTemplateList.d.ts.map +1 -0
  29. package/channel/useChannelTemplateList.js +69 -0
  30. package/channel/useChannelTemplateList.js.map +1 -0
  31. package/channel/useChannelTemplateReadiness.d.ts +53 -0
  32. package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
  33. package/channel/useChannelTemplateReadiness.js +42 -0
  34. package/channel/useChannelTemplateReadiness.js.map +1 -0
  35. package/composer/ComposerToolbar.d.ts +6 -1
  36. package/composer/ComposerToolbar.d.ts.map +1 -1
  37. package/composer/ComposerToolbar.js +2 -2
  38. package/composer/ComposerToolbar.js.map +1 -1
  39. package/composer/SessionComposer.d.ts +9 -0
  40. package/composer/SessionComposer.d.ts.map +1 -1
  41. package/composer/SessionComposer.js +13 -2
  42. package/composer/SessionComposer.js.map +1 -1
  43. package/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
  44. package/dashboard/useDashboardFailedRuns.js +2 -0
  45. package/dashboard/useDashboardFailedRuns.js.map +1 -1
  46. package/execution/useCreateAgentExecution.d.ts +14 -0
  47. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  48. package/execution/useCreateAgentExecution.js +5 -0
  49. package/execution/useCreateAgentExecution.js.map +1 -1
  50. package/index.d.ts +4 -2
  51. package/index.d.ts.map +1 -1
  52. package/index.js +10 -1
  53. package/index.js.map +1 -1
  54. package/internal/Pagination.d.ts +15 -0
  55. package/internal/Pagination.d.ts.map +1 -0
  56. package/internal/Pagination.js +20 -0
  57. package/internal/Pagination.js.map +1 -0
  58. package/library/useExportResource.d.ts +4 -3
  59. package/library/useExportResource.d.ts.map +1 -1
  60. package/library/useExportResource.js +1 -1
  61. package/library/useExportResource.js.map +1 -1
  62. package/models/ModelSelector.d.ts +26 -1
  63. package/models/ModelSelector.d.ts.map +1 -1
  64. package/models/ModelSelector.js +27 -6
  65. package/models/ModelSelector.js.map +1 -1
  66. package/models/index.d.ts +2 -0
  67. package/models/index.d.ts.map +1 -1
  68. package/models/index.js +1 -0
  69. package/models/index.js.map +1 -1
  70. package/models/registry.d.ts +10 -0
  71. package/models/registry.d.ts.map +1 -1
  72. package/models/registry.js +3 -0
  73. package/models/registry.js.map +1 -1
  74. package/models/service-tier.d.ts +27 -0
  75. package/models/service-tier.d.ts.map +1 -0
  76. package/models/service-tier.js +34 -0
  77. package/models/service-tier.js.map +1 -0
  78. package/organization/OrgProvider.d.ts +11 -4
  79. package/organization/OrgProvider.d.ts.map +1 -1
  80. package/organization/OrgProvider.js +28 -4
  81. package/organization/OrgProvider.js.map +1 -1
  82. package/package.json +4 -4
  83. package/resource-detail/useDeleteResource.d.ts +1 -1
  84. package/resource-detail/useDeleteResource.d.ts.map +1 -1
  85. package/resource-detail/useDeleteResource.js +5 -0
  86. package/resource-detail/useDeleteResource.js.map +1 -1
  87. package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
  88. package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
  89. package/resource-workbench/components/ResourceWorkbench.js +6 -2
  90. package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
  91. package/resource-workbench/hooks/useResourceCollection.d.ts +8 -0
  92. package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
  93. package/resource-workbench/hooks/useResourceCollection.js +2 -2
  94. package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
  95. package/schedule/CadenceField.d.ts +31 -0
  96. package/schedule/CadenceField.d.ts.map +1 -0
  97. package/schedule/CadenceField.js +116 -0
  98. package/schedule/CadenceField.js.map +1 -0
  99. package/schedule/ScheduleDetailView.d.ts +81 -0
  100. package/schedule/ScheduleDetailView.d.ts.map +1 -0
  101. package/schedule/ScheduleDetailView.js +648 -0
  102. package/schedule/ScheduleDetailView.js.map +1 -0
  103. package/schedule/ScheduleForm.d.ts +46 -0
  104. package/schedule/ScheduleForm.d.ts.map +1 -0
  105. package/schedule/ScheduleForm.js +214 -0
  106. package/schedule/ScheduleForm.js.map +1 -0
  107. package/schedule/ScheduleRowActions.d.ts +20 -0
  108. package/schedule/ScheduleRowActions.d.ts.map +1 -0
  109. package/schedule/ScheduleRowActions.js +54 -0
  110. package/schedule/ScheduleRowActions.js.map +1 -0
  111. package/schedule/ScheduleRunsTable.d.ts +72 -0
  112. package/schedule/ScheduleRunsTable.d.ts.map +1 -0
  113. package/schedule/ScheduleRunsTable.js +161 -0
  114. package/schedule/ScheduleRunsTable.js.map +1 -0
  115. package/schedule/TimeZoneField.d.ts +32 -0
  116. package/schedule/TimeZoneField.d.ts.map +1 -0
  117. package/schedule/TimeZoneField.js +104 -0
  118. package/schedule/TimeZoneField.js.map +1 -0
  119. package/schedule/cadence.d.ts +95 -0
  120. package/schedule/cadence.d.ts.map +1 -0
  121. package/schedule/cadence.js +242 -0
  122. package/schedule/cadence.js.map +1 -0
  123. package/schedule/index.d.ts +37 -0
  124. package/schedule/index.d.ts.map +1 -0
  125. package/schedule/index.js +19 -0
  126. package/schedule/index.js.map +1 -0
  127. package/schedule/scheduleColumns.d.ts +26 -0
  128. package/schedule/scheduleColumns.d.ts.map +1 -0
  129. package/schedule/scheduleColumns.js +77 -0
  130. package/schedule/scheduleColumns.js.map +1 -0
  131. package/schedule/scheduleListFn.d.ts +50 -0
  132. package/schedule/scheduleListFn.d.ts.map +1 -0
  133. package/schedule/scheduleListFn.js +65 -0
  134. package/schedule/scheduleListFn.js.map +1 -0
  135. package/schedule/scheduleState.d.ts +43 -0
  136. package/schedule/scheduleState.d.ts.map +1 -0
  137. package/schedule/scheduleState.js +67 -0
  138. package/schedule/scheduleState.js.map +1 -0
  139. package/schedule/useCreateSchedule.d.ts +57 -0
  140. package/schedule/useCreateSchedule.d.ts.map +1 -0
  141. package/schedule/useCreateSchedule.js +61 -0
  142. package/schedule/useCreateSchedule.js.map +1 -0
  143. package/schedule/useResumeSchedule.d.ts +30 -0
  144. package/schedule/useResumeSchedule.d.ts.map +1 -0
  145. package/schedule/useResumeSchedule.js +45 -0
  146. package/schedule/useResumeSchedule.js.map +1 -0
  147. package/schedule/useSchedule.d.ts +39 -0
  148. package/schedule/useSchedule.d.ts.map +1 -0
  149. package/schedule/useSchedule.js +44 -0
  150. package/schedule/useSchedule.js.map +1 -0
  151. package/schedule/useScheduleCount.d.ts +38 -0
  152. package/schedule/useScheduleCount.d.ts.map +1 -0
  153. package/schedule/useScheduleCount.js +30 -0
  154. package/schedule/useScheduleCount.js.map +1 -0
  155. package/schedule/useScheduleList.d.ts +48 -0
  156. package/schedule/useScheduleList.d.ts.map +1 -0
  157. package/schedule/useScheduleList.js +43 -0
  158. package/schedule/useScheduleList.js.map +1 -0
  159. package/schedule/useScheduleRuns.d.ts +44 -0
  160. package/schedule/useScheduleRuns.d.ts.map +1 -0
  161. package/schedule/useScheduleRuns.js +48 -0
  162. package/schedule/useScheduleRuns.js.map +1 -0
  163. package/schedule/useSetScheduleEnabled.d.ts +31 -0
  164. package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
  165. package/schedule/useSetScheduleEnabled.js +53 -0
  166. package/schedule/useSetScheduleEnabled.js.map +1 -0
  167. package/schedule/useTriggerSchedule.d.ts +41 -0
  168. package/schedule/useTriggerSchedule.d.ts.map +1 -0
  169. package/schedule/useTriggerSchedule.js +62 -0
  170. package/schedule/useTriggerSchedule.js.map +1 -0
  171. package/schedule/useUpdateScheduleSpec.d.ts +49 -0
  172. package/schedule/useUpdateScheduleSpec.d.ts.map +1 -0
  173. package/schedule/useUpdateScheduleSpec.js +70 -0
  174. package/schedule/useUpdateScheduleSpec.js.map +1 -0
  175. package/search/useResourceCount.d.ts +2 -1
  176. package/search/useResourceCount.d.ts.map +1 -1
  177. package/search/useResourceCount.js +6 -1
  178. package/search/useResourceCount.js.map +1 -1
  179. package/session/useNewSessionFlow.d.ts.map +1 -1
  180. package/session/useNewSessionFlow.js +1 -0
  181. package/session/useNewSessionFlow.js.map +1 -1
  182. package/session/useSessionConversation.d.ts +11 -0
  183. package/session/useSessionConversation.d.ts.map +1 -1
  184. package/session/useSessionConversation.js +1 -0
  185. package/session/useSessionConversation.js.map +1 -1
  186. package/session/useSessionPageFlow.d.ts.map +1 -1
  187. package/session/useSessionPageFlow.js +1 -0
  188. package/session/useSessionPageFlow.js.map +1 -1
  189. package/src/agent/AgentPicker.tsx +13 -1
  190. package/src/billing/CreditLedgerTable.tsx +3 -42
  191. package/src/channel/AgentChannelsPanel.tsx +43 -1
  192. package/src/channel/ChannelTemplatesDialog.tsx +433 -0
  193. package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
  194. package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
  195. package/src/channel/__tests__/providers.test.ts +9 -0
  196. package/src/channel/__tests__/templatePresentation.test.ts +96 -0
  197. package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
  198. package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
  199. package/src/channel/index.ts +15 -0
  200. package/src/channel/providers.ts +26 -2
  201. package/src/channel/templatePresentation.ts +82 -0
  202. package/src/channel/useChannelTemplateList.ts +123 -0
  203. package/src/channel/useChannelTemplateReadiness.ts +69 -0
  204. package/src/composer/ComposerToolbar.tsx +9 -0
  205. package/src/composer/SessionComposer.tsx +24 -1
  206. package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +156 -0
  207. package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
  208. package/src/dashboard/useDashboardFailedRuns.ts +2 -0
  209. package/src/execution/useCreateAgentExecution.ts +18 -0
  210. package/src/index.ts +70 -0
  211. package/src/internal/Pagination.tsx +72 -0
  212. package/src/library/useExportResource.ts +4 -3
  213. package/src/models/ModelSelector.tsx +100 -3
  214. package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +172 -0
  215. package/src/models/__tests__/useModelRegistry.test.tsx +15 -0
  216. package/src/models/index.ts +2 -0
  217. package/src/models/registry.ts +16 -0
  218. package/src/models/service-tier.ts +49 -0
  219. package/src/organization/OrgProvider.tsx +29 -4
  220. package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
  221. package/src/resource-detail/useDeleteResource.ts +11 -1
  222. package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
  223. package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
  224. package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
  225. package/src/schedule/CadenceField.tsx +365 -0
  226. package/src/schedule/ScheduleDetailView.tsx +1651 -0
  227. package/src/schedule/ScheduleForm.tsx +615 -0
  228. package/src/schedule/ScheduleRowActions.tsx +124 -0
  229. package/src/schedule/ScheduleRunsTable.tsx +424 -0
  230. package/src/schedule/TimeZoneField.tsx +199 -0
  231. package/src/schedule/__tests__/ScheduleDetailView.test.tsx +683 -0
  232. package/src/schedule/__tests__/cadence.test.ts +278 -0
  233. package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
  234. package/src/schedule/__tests__/scheduleCreation.test.tsx +537 -0
  235. package/src/schedule/__tests__/scheduleHooks.test.tsx +399 -0
  236. package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
  237. package/src/schedule/__tests__/scheduleState.test.ts +133 -0
  238. package/src/schedule/cadence.ts +298 -0
  239. package/src/schedule/index.ts +69 -0
  240. package/src/schedule/scheduleColumns.tsx +118 -0
  241. package/src/schedule/scheduleListFn.ts +108 -0
  242. package/src/schedule/scheduleState.ts +96 -0
  243. package/src/schedule/useCreateSchedule.ts +88 -0
  244. package/src/schedule/useResumeSchedule.ts +67 -0
  245. package/src/schedule/useSchedule.ts +67 -0
  246. package/src/schedule/useScheduleCount.ts +57 -0
  247. package/src/schedule/useScheduleList.ts +83 -0
  248. package/src/schedule/useScheduleRuns.ts +93 -0
  249. package/src/schedule/useSetScheduleEnabled.ts +84 -0
  250. package/src/schedule/useTriggerSchedule.ts +90 -0
  251. package/src/schedule/useUpdateScheduleSpec.ts +108 -0
  252. package/src/search/__tests__/useResourceCount.test.tsx +89 -0
  253. package/src/search/useResourceCount.ts +8 -2
  254. package/src/session/useNewSessionFlow.ts +1 -0
  255. package/src/session/useSessionConversation.ts +12 -0
  256. package/src/session/useSessionPageFlow.ts +1 -0
  257. package/src/workflow/WorkflowDashboard.tsx +1 -0
  258. package/src/workflow/__tests__/inspector-forms.test.tsx +119 -2
  259. package/src/workflow/inspector/forms/AgentCallForm.tsx +260 -37
  260. package/src/workflow/inspector/tabs/RuntimeTab.tsx +72 -33
  261. package/src/workflow/starter-workflow-yaml.ts +2 -1
  262. package/src/workflow/useWorkflowExecutionList.ts +13 -2
  263. package/styles.css +1 -1
  264. package/workflow/CanvasTransitionEdge.d.ts +1 -1
  265. package/workflow/WorkflowDashboard.d.ts.map +1 -1
  266. package/workflow/WorkflowDashboard.js +1 -0
  267. package/workflow/WorkflowDashboard.js.map +1 -1
  268. package/workflow/inspector/forms/AgentCallForm.d.ts +18 -2
  269. package/workflow/inspector/forms/AgentCallForm.d.ts.map +1 -1
  270. package/workflow/inspector/forms/AgentCallForm.js +87 -9
  271. package/workflow/inspector/forms/AgentCallForm.js.map +1 -1
  272. package/workflow/inspector/tabs/RuntimeTab.d.ts +2 -1
  273. package/workflow/inspector/tabs/RuntimeTab.d.ts.map +1 -1
  274. package/workflow/inspector/tabs/RuntimeTab.js +31 -5
  275. package/workflow/inspector/tabs/RuntimeTab.js.map +1 -1
  276. package/workflow/starter-workflow-yaml.d.ts +1 -1
  277. package/workflow/starter-workflow-yaml.d.ts.map +1 -1
  278. package/workflow/starter-workflow-yaml.js +2 -1
  279. package/workflow/starter-workflow-yaml.js.map +1 -1
  280. package/workflow/useWorkflowExecutionList.d.ts +10 -0
  281. package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
  282. package/workflow/useWorkflowExecutionList.js +3 -2
  283. package/workflow/useWorkflowExecutionList.js.map +1 -1
@@ -0,0 +1,615 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState, type FormEvent } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ import {
6
+ getUserMessage,
7
+ type ResourceRef,
8
+ type RunConfigInput,
9
+ } from "@stigmer/sdk";
10
+ import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
11
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
12
+ import { Popover } from "@base-ui/react/popover";
13
+ import { AgentPicker } from "../agent/AgentPicker.js";
14
+ import { EnvironmentPicker } from "../environment/EnvironmentPicker.js";
15
+ import { useGitHubConnection } from "../github/useGitHubConnection.js";
16
+ import { ModelSelector } from "../models/ModelSelector.js";
17
+ import { toProtoHarness, type HarnessOption } from "../models/harness.js";
18
+ import { toProtoServiceTier, type ServiceTierOption } from "../models/service-tier.js";
19
+ import { Switch } from "../switch/Switch.js";
20
+ import { useStigmerPortalContainer } from "../portal-container.js";
21
+ import { useWorkspaceEntries } from "../workspace/useWorkspaceEntries.js";
22
+ import { WorkspaceEditor } from "../workspace/WorkspaceEditor.js";
23
+ import { CadenceField } from "./CadenceField.js";
24
+ import { TimeZoneField, browserTimeZone } from "./TimeZoneField.js";
25
+ import { useCreateSchedule } from "./useCreateSchedule.js";
26
+ import { cadenceToCron, validateCron, type CadencePreset } from "./cadence.js";
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Public API
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /** Props for {@link ScheduleForm}. */
33
+ export interface ScheduleFormProps {
34
+ /**
35
+ * Organization slug. Schedules are org-local: the server requires the
36
+ * target agent to live in this same org, so the agent picker is
37
+ * locked to org scope.
38
+ */
39
+ readonly org: string;
40
+ /** Fired with the created schedule after a successful submit. */
41
+ readonly onComplete?: (schedule: Schedule) => void;
42
+ /** Fired when the user cancels. */
43
+ readonly onCancel?: () => void;
44
+ /** Additional CSS class names for the root form element. */
45
+ readonly className?: string;
46
+ }
47
+
48
+ /** invocation.proto pins AgentInvocation.message to 8192 characters. */
49
+ const MESSAGE_MAX_LEN = 8192;
50
+ /** Show the remaining-characters counter once within this margin. */
51
+ const MESSAGE_COUNTER_THRESHOLD = 500;
52
+
53
+ /**
54
+ * Single-page form for creating a {@link Schedule}.
55
+ *
56
+ * The form speaks the composer's vocabulary (DD-018 D-5): a schedule is
57
+ * a saved run — which agent, what message, what workspace, which
58
+ * engine and model, what budget — plus a cadence. It reuses the
59
+ * composer's own pickers ({@link ModelSelector}, {@link WorkspaceEditor})
60
+ * so the interactive and scheduled surfaces cannot drift, minus only
61
+ * what unattended makes impossible (no local folders, no attachments,
62
+ * no approval-mode choice).
63
+ *
64
+ * Enabled defaults to OFF, deliberately: it matches the proto/YAML
65
+ * default (an omitted `enabled` is false) and the platform's
66
+ * recommended workflow — create staged-disabled, validate with
67
+ * "Run now" on the detail page, then enable.
68
+ *
69
+ * Creation only. Editing an existing schedule must not flow through
70
+ * this form's curated input — see the note on {@link useCreateSchedule}.
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * <ScheduleForm
75
+ * org="acme"
76
+ * onComplete={(s) => navigate(`/library/schedules/${s.metadata?.org}/${s.metadata?.slug}`)}
77
+ * onCancel={() => navigate("/library/schedules")}
78
+ * />
79
+ * ```
80
+ */
81
+ export function ScheduleForm({
82
+ org,
83
+ onComplete,
84
+ onCancel,
85
+ className,
86
+ }: ScheduleFormProps) {
87
+ const { create, isCreating, error, clearError } = useCreateSchedule();
88
+
89
+ const [name, setName] = useState("");
90
+ const [agentRef, setAgentRef] = useState<ResourceRef | null>(null);
91
+ const [agentName, setAgentName] = useState<string | null>(null);
92
+ const [message, setMessage] = useState("");
93
+ const [cadence, setCadence] = useState<CadencePreset>({
94
+ kind: "daily",
95
+ hour: 9,
96
+ minute: 0,
97
+ });
98
+ const [timeZone, setTimeZone] = useState(browserTimeZone);
99
+ const [enabled, setEnabled] = useState(false);
100
+ const [pickerOpen, setPickerOpen] = useState(false);
101
+ const [environmentRefs, setEnvironmentRefs] = useState<ResourceRef[]>([]);
102
+
103
+ // Engine & model are one atomic choice: picking a model pins BOTH the
104
+ // harness and the model (the registry scopes models per harness, so a
105
+ // model without its harness is meaningless). Empty model = nothing
106
+ // pinned = the platform defaults apply to both. Browsing harnesses in
107
+ // the popover without picking a model pins nothing.
108
+ const [modelName, setModelName] = useState("");
109
+ const [modelHarness, setModelHarness] = useState<HarnessOption>("cursor");
110
+ // Service tier rides the model choice (#357): the ModelSelector renders
111
+ // the fast toggle only for models whose registry entry prices a fast
112
+ // variant, and resets it when the user switches to one that does not.
113
+ // No model pinned = no tier pinned (fast requires a model, and the form's
114
+ // reset button clears both).
115
+ const [serviceTier, setServiceTier] = useState<ServiceTierOption>("standard");
116
+
117
+ const [budgetUsd, setBudgetUsd] = useState("");
118
+
119
+ // Workspace the fresh per-fire session clones — git sources only
120
+ // (write-time validated server-side: no client is connected at fire
121
+ // time to serve a local folder). The GitHub repo picker lights up
122
+ // when the user already has a connection; otherwise the editor's
123
+ // manual URL+branch input applies, with no OAuth flow forced into
124
+ // this form.
125
+ const workspace = useWorkspaceEntries();
126
+ const gitHub = useGitHubConnection(org);
127
+
128
+ const trimmedName = name.trim();
129
+ const trimmedMessage = message.trim();
130
+ const cron = cadenceToCron(cadence).trim();
131
+ const cronValid = cron !== "" && validateCron(cron) === null;
132
+
133
+ const canSubmit =
134
+ trimmedName !== "" &&
135
+ agentRef !== null &&
136
+ trimmedMessage !== "" &&
137
+ cronValid &&
138
+ !isCreating;
139
+
140
+ const handleAgentChange = useCallback((ref: ResourceRef | null) => {
141
+ setAgentRef(ref);
142
+ if (ref === null) setAgentName(null);
143
+ setPickerOpen(false);
144
+ }, []);
145
+
146
+ const handleSubmit = useCallback(
147
+ async (e: FormEvent) => {
148
+ e.preventDefault();
149
+ if (!canSubmit || !agentRef) return;
150
+
151
+ clearError();
152
+ const runConfig = buildRunConfig(modelName, budgetUsd, serviceTier);
153
+ try {
154
+ const schedule = await create({
155
+ name: trimmedName,
156
+ org,
157
+ cron,
158
+ timeZone,
159
+ enabled,
160
+ agent: {
161
+ agentRef,
162
+ message: trimmedMessage,
163
+ // The harness travels with the model it was picked under;
164
+ // no model means no harness — the platform defaults apply.
165
+ ...(modelName !== ""
166
+ ? { harness: toProtoHarness(modelHarness) }
167
+ : {}),
168
+ ...(workspace.hasEntries
169
+ ? { workspaceEntries: workspace.toInput() }
170
+ : {}),
171
+ ...(environmentRefs.length > 0 ? { environmentRefs } : {}),
172
+ ...(runConfig ? { runConfig } : {}),
173
+ },
174
+ });
175
+ onComplete?.(schedule);
176
+ } catch {
177
+ // error state is managed by useCreateSchedule
178
+ }
179
+ },
180
+ [
181
+ canSubmit,
182
+ agentRef,
183
+ clearError,
184
+ create,
185
+ trimmedName,
186
+ org,
187
+ cron,
188
+ timeZone,
189
+ enabled,
190
+ trimmedMessage,
191
+ environmentRefs,
192
+ modelName,
193
+ modelHarness,
194
+ serviceTier,
195
+ budgetUsd,
196
+ workspace,
197
+ onComplete,
198
+ ],
199
+ );
200
+
201
+ const remaining = MESSAGE_MAX_LEN - message.length;
202
+
203
+ return (
204
+ <form
205
+ onSubmit={handleSubmit}
206
+ className={cn("max-w-2xl space-y-5", className)}
207
+ aria-label="New schedule"
208
+ >
209
+ {/* Name */}
210
+ <div className="space-y-1">
211
+ <label htmlFor="stgm-new-schedule-name" className={labelClasses}>
212
+ Name
213
+ </label>
214
+ <input
215
+ id="stgm-new-schedule-name"
216
+ type="text"
217
+ value={name}
218
+ onChange={(e) => setName(e.target.value)}
219
+ placeholder="e.g. daily-fee-reminders"
220
+ disabled={isCreating}
221
+ autoFocus
222
+ required
223
+ className={inputClasses}
224
+ />
225
+ </div>
226
+
227
+ {/* Target agent */}
228
+ <div className="space-y-1">
229
+ <span id="stgm-new-schedule-agent-label" className={labelClasses}>
230
+ Agent to run
231
+ </span>
232
+ <Popover.Root open={pickerOpen} onOpenChange={setPickerOpen}>
233
+ <Popover.Trigger
234
+ disabled={isCreating}
235
+ aria-labelledby="stgm-new-schedule-agent-label"
236
+ className={cn(
237
+ "flex w-full items-center justify-between rounded-md border border-input bg-background px-2.5 py-1.5 text-left text-xs",
238
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
239
+ "disabled:pointer-events-none disabled:opacity-50",
240
+ agentRef ? "text-foreground" : "text-muted-foreground",
241
+ )}
242
+ >
243
+ <span className="truncate">
244
+ {agentRef
245
+ ? (agentName ?? `${agentRef.org}/${agentRef.slug}`)
246
+ : "Choose an agent…"}
247
+ </span>
248
+ <ChevronIcon />
249
+ </Popover.Trigger>
250
+ <AgentPickerPopup
251
+ org={org}
252
+ value={agentRef}
253
+ onChange={handleAgentChange}
254
+ onDisplayNameResolved={(_key, resolvedName) =>
255
+ setAgentName(resolvedName)
256
+ }
257
+ disabled={isCreating}
258
+ />
259
+ </Popover.Root>
260
+ <p className={hintClasses}>
261
+ Each fire runs this agent unattended in a fresh session. Only
262
+ agents in <span className="font-medium">{org}</span> can be
263
+ scheduled.
264
+ </p>
265
+ </div>
266
+
267
+ {/* Message */}
268
+ <div className="space-y-1">
269
+ <label htmlFor="stgm-new-schedule-message" className={labelClasses}>
270
+ Message
271
+ </label>
272
+ <textarea
273
+ id="stgm-new-schedule-message"
274
+ value={message}
275
+ onChange={(e) => setMessage(e.target.value)}
276
+ placeholder="The instruction the agent receives on every fire — write it for a run with no human present."
277
+ disabled={isCreating}
278
+ required
279
+ rows={5}
280
+ maxLength={MESSAGE_MAX_LEN}
281
+ className={cn(inputClasses, "resize-y")}
282
+ />
283
+ {remaining <= MESSAGE_COUNTER_THRESHOLD && (
284
+ <p className={hintClasses}>{remaining} characters left</p>
285
+ )}
286
+ </div>
287
+
288
+ {/* Workspace — what each fire's fresh session operates on
289
+ (DD-018 D-4). Git sources only; the server refuses local
290
+ folders at write time because no client is connected when a
291
+ schedule fires. */}
292
+ <div className="space-y-1">
293
+ <span className={labelClasses}>
294
+ Workspace{" "}
295
+ <span className="font-normal text-muted-foreground">(optional)</span>
296
+ </span>
297
+ <WorkspaceEditor
298
+ workspace={workspace}
299
+ gitHubConnection={gitHub.isConnected ? gitHub : undefined}
300
+ enableGitHub
301
+ enableLocal={false}
302
+ disabled={isCreating}
303
+ />
304
+ <p className={hintClasses}>
305
+ Each run clones these repositories fresh. Private repositories need
306
+ a <code className="font-mono">GITHUB_TOKEN</code> in one of this
307
+ schedule&rsquo;s environments; public ones need nothing.
308
+ </p>
309
+ </div>
310
+
311
+ {/* Environments — how a tool-using agent becomes schedulable
312
+ (DD-017 D-2). Only org-shared environments resolve for a
313
+ schedule fire, so the picker is filtered to visibility_org —
314
+ the same credential surface a channel binding uses. */}
315
+ <div className="space-y-1">
316
+ <span id="stgm-new-schedule-env-label" className={labelClasses}>
317
+ Environments <span className="font-normal text-muted-foreground">(optional)</span>
318
+ </span>
319
+ <EnvironmentPicker
320
+ org={org}
321
+ value={environmentRefs}
322
+ onChange={setEnvironmentRefs}
323
+ disabled={isCreating}
324
+ filterEnvironment={(env) =>
325
+ env.metadata?.visibility === ApiResourceVisibility.visibility_org
326
+ }
327
+ />
328
+ <p className={hintClasses}>
329
+ Bind org-shared credentials (for example an MCP server&rsquo;s secret)
330
+ so the agent&rsquo;s tools work on an unattended fire. Without this, an
331
+ agent whose tools need credentials will be refused every run.
332
+ </p>
333
+ </div>
334
+
335
+ {/* Cadence */}
336
+ <div className="space-y-1">
337
+ <span className={labelClasses}>Runs</span>
338
+ <CadenceField
339
+ value={cadence}
340
+ onChange={setCadence}
341
+ timeZone={timeZone}
342
+ disabled={isCreating}
343
+ />
344
+ </div>
345
+
346
+ {/* Engine & model — the composer's own picker (DD-018 D-5),
347
+ replacing the free-text model box whose typos surfaced as
348
+ fire-time failures. Nothing is pinned until a model is picked. */}
349
+ <div className="space-y-1">
350
+ <span className={labelClasses}>
351
+ Engine &amp; model{" "}
352
+ <span className="font-normal text-muted-foreground">(optional)</span>
353
+ </span>
354
+ <div className="flex items-center gap-2">
355
+ <ModelSelector
356
+ value={modelName}
357
+ onValueChange={setModelName}
358
+ initialHarness={modelHarness}
359
+ onHarnessChange={setModelHarness}
360
+ serviceTier={serviceTier}
361
+ onServiceTierChange={setServiceTier}
362
+ placeholderLabel="Platform default"
363
+ disabled={isCreating}
364
+ />
365
+ {modelName !== "" && (
366
+ <button
367
+ type="button"
368
+ onClick={() => {
369
+ setModelName("");
370
+ setServiceTier("standard");
371
+ }}
372
+ disabled={isCreating}
373
+ className={cn(
374
+ "rounded-md px-2 py-1 text-[0.65rem] text-muted-foreground",
375
+ "hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
376
+ "disabled:pointer-events-none disabled:opacity-50",
377
+ )}
378
+ >
379
+ Reset to platform default
380
+ </button>
381
+ )}
382
+ </div>
383
+ <p className={hintClasses}>
384
+ Runs use the platform&rsquo;s default engine and model unless you
385
+ pick one here. Picking a model pins the engine it belongs to.
386
+ </p>
387
+ </div>
388
+
389
+ {/* Budget — the one run bound that matters for an unattended
390
+ surface: nobody is watching a 3 AM fire (DD-018 D-5). Clamped
391
+ by the platform profile; tool-round bounds stay API-only. */}
392
+ <div className="space-y-1">
393
+ <label htmlFor="stgm-new-schedule-budget" className={labelClasses}>
394
+ Budget per run (USD){" "}
395
+ <span className="font-normal text-muted-foreground">(optional)</span>
396
+ </label>
397
+ <input
398
+ id="stgm-new-schedule-budget"
399
+ type="number"
400
+ min="0"
401
+ step="any"
402
+ value={budgetUsd}
403
+ onChange={(e) => setBudgetUsd(e.target.value)}
404
+ placeholder="platform default"
405
+ disabled={isCreating}
406
+ className={cn(inputClasses, "sm:max-w-48")}
407
+ />
408
+ <p className={hintClasses}>
409
+ Each run stops when it reaches this spend. You can lower the
410
+ platform&rsquo;s per-run cap, never raise it past the
411
+ platform&rsquo;s ceiling.
412
+ </p>
413
+ </div>
414
+
415
+ {/* Time zone */}
416
+ <div className="space-y-1">
417
+ <label htmlFor="stgm-new-schedule-tz" className={labelClasses}>
418
+ Time zone
419
+ </label>
420
+ <TimeZoneField
421
+ id="stgm-new-schedule-tz"
422
+ value={timeZone}
423
+ onChange={setTimeZone}
424
+ disabled={isCreating}
425
+ />
426
+ </div>
427
+
428
+ {/* Enabled */}
429
+ <div className="flex items-start gap-3">
430
+ <Switch
431
+ id="stgm-new-schedule-enabled"
432
+ checked={enabled}
433
+ onCheckedChange={setEnabled}
434
+ disabled={isCreating}
435
+ aria-labelledby="stgm-new-schedule-enabled-label"
436
+ />
437
+ <div className="space-y-0.5">
438
+ <label
439
+ id="stgm-new-schedule-enabled-label"
440
+ htmlFor="stgm-new-schedule-enabled"
441
+ className={labelClasses}
442
+ >
443
+ Enabled
444
+ </label>
445
+ <p className={hintClasses}>
446
+ Leave off to create the schedule staged: validate it with
447
+ &ldquo;Run now&rdquo; on the schedule page, then enable it there.
448
+ </p>
449
+ </div>
450
+ </div>
451
+
452
+ {error && (
453
+ <p className="text-xs text-destructive" role="alert">
454
+ {getUserMessage(error)}
455
+ </p>
456
+ )}
457
+
458
+ <div className="flex items-center gap-2">
459
+ <button
460
+ type="submit"
461
+ disabled={!canSubmit}
462
+ className={cn(
463
+ "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium",
464
+ "bg-primary text-primary-foreground hover:bg-primary-hover",
465
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
466
+ "disabled:pointer-events-none disabled:opacity-40",
467
+ )}
468
+ >
469
+ {isCreating && <SpinnerIcon />}
470
+ Create schedule
471
+ </button>
472
+
473
+ {onCancel && (
474
+ <button
475
+ type="button"
476
+ onClick={onCancel}
477
+ disabled={isCreating}
478
+ className={cn(
479
+ "rounded-md px-3 py-1.5 text-sm",
480
+ "text-muted-foreground hover:bg-accent-hover hover:text-foreground",
481
+ "disabled:pointer-events-none disabled:opacity-50",
482
+ )}
483
+ >
484
+ Cancel
485
+ </button>
486
+ )}
487
+ </div>
488
+ </form>
489
+ );
490
+ }
491
+
492
+ // ---------------------------------------------------------------------------
493
+ // Private helpers
494
+ // ---------------------------------------------------------------------------
495
+
496
+ const labelClasses = "block text-xs font-medium text-foreground";
497
+ const hintClasses = "text-[0.65rem] text-muted-foreground";
498
+
499
+ /**
500
+ * Assemble a {@link RunConfigInput} from the model choice and budget, or
501
+ * `undefined` when both are unset — an all-empty run_config carries no
502
+ * meaning, and omitting it keeps the schedule on the platform defaults
503
+ * (the server's own "empty = inherit" contract, DD-017 D-3 as carried
504
+ * into DD-018 D-2). Non-numeric or negative budgets are dropped rather
505
+ * than sent; the proto's `gte = 0` constraint would reject them anyway,
506
+ * and a blank field must not become a zero override. `max_tool_rounds`
507
+ * is deliberately not collected here — an implementation knob, not a
508
+ * user concept; API-reachable for operators (DD-018 D-5).
509
+ */
510
+ function buildRunConfig(
511
+ modelName: string,
512
+ budgetUsd: string,
513
+ serviceTier: ServiceTierOption,
514
+ ): RunConfigInput | undefined {
515
+ const model = modelName.trim();
516
+ const cost = Number.parseFloat(budgetUsd);
517
+
518
+ const config: RunConfigInput = {};
519
+ if (model !== "") config.modelName = model;
520
+ if (Number.isFinite(cost) && cost > 0) config.maxCostUsd = cost;
521
+ // Carried only when the user actively chose fast AND pinned a model
522
+ // (fast is a per-model price; the server refuses it without one). An
523
+ // untouched toggle stays absent — unspecified-vs-explicit is a
524
+ // load-bearing ledger distinction (#357).
525
+ if (serviceTier === "fast" && model !== "") {
526
+ config.serviceTier = toProtoServiceTier(serviceTier);
527
+ }
528
+
529
+ return Object.keys(config).length > 0 ? config : undefined;
530
+ }
531
+ const inputClasses = cn(
532
+ "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
533
+ "placeholder:text-muted-foreground",
534
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
535
+ "disabled:pointer-events-none disabled:opacity-50",
536
+ );
537
+
538
+ function AgentPickerPopup({
539
+ org,
540
+ value,
541
+ onChange,
542
+ onDisplayNameResolved,
543
+ disabled,
544
+ }: {
545
+ readonly org: string;
546
+ readonly value: ResourceRef | null;
547
+ readonly onChange: (ref: ResourceRef | null) => void;
548
+ readonly onDisplayNameResolved: (key: string, name: string) => void;
549
+ readonly disabled?: boolean;
550
+ }) {
551
+ const portalContainer = useStigmerPortalContainer();
552
+ return (
553
+ <Popover.Portal container={portalContainer}>
554
+ <Popover.Positioner sideOffset={4} align="start">
555
+ <Popover.Popup
556
+ className={cn(
557
+ "z-popover overflow-x-hidden overflow-y-auto rounded-lg border border-border",
558
+ "bg-popover p-3 text-popover-foreground shadow-md",
559
+ "max-h-[60vh]",
560
+ )}
561
+ >
562
+ <AgentPicker
563
+ org={org}
564
+ value={value}
565
+ onChange={onChange}
566
+ onDisplayNameResolved={onDisplayNameResolved}
567
+ // Schedules are org-local: the server rejects a target
568
+ // agent outside the schedule's org, so cross-org browsing
569
+ // could only offer choices that fail at submit.
570
+ scope="org"
571
+ showScopeToggle={false}
572
+ disabled={disabled}
573
+ />
574
+ </Popover.Popup>
575
+ </Popover.Positioner>
576
+ </Popover.Portal>
577
+ );
578
+ }
579
+
580
+ function ChevronIcon() {
581
+ return (
582
+ <svg
583
+ width="12"
584
+ height="12"
585
+ viewBox="0 0 16 16"
586
+ fill="none"
587
+ stroke="currentColor"
588
+ strokeWidth="1.5"
589
+ strokeLinecap="round"
590
+ strokeLinejoin="round"
591
+ className="shrink-0 text-muted-foreground"
592
+ aria-hidden="true"
593
+ >
594
+ <path d="M4 6l4 4 4-4" />
595
+ </svg>
596
+ );
597
+ }
598
+
599
+ function SpinnerIcon() {
600
+ return (
601
+ <svg
602
+ width="14"
603
+ height="14"
604
+ viewBox="0 0 16 16"
605
+ fill="none"
606
+ stroke="currentColor"
607
+ strokeWidth="2"
608
+ strokeLinecap="round"
609
+ className="animate-spin"
610
+ aria-hidden="true"
611
+ >
612
+ <path d="M8 2a6 6 0 1 0 6 6" />
613
+ </svg>
614
+ );
615
+ }