@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,1651 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ import { create } from "@bufbuild/protobuf";
6
+ import { timestampDate } from "@bufbuild/protobuf/wkt";
7
+ import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
8
+ import type { Environment } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
9
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
10
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
11
+ import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
12
+ import type { ResourceRef } from "@stigmer/sdk";
13
+ import { formatRelativeTime } from "../activity/format-relative-time.js";
14
+ import { ErrorMessage } from "../error/ErrorMessage.js";
15
+ import { InlineEditTextarea } from "../inline-edit/InlineEditTextarea.js";
16
+ import { EditResourceYamlDialog } from "../manifest/EditResourceYamlDialog.js";
17
+ import { ConfirmDialog } from "../resource-detail/ConfirmDialog.js";
18
+ import { ResourceDetailShell } from "../resource-detail/ResourceDetailShell.js";
19
+ import { Section } from "../resource-detail/Section.js";
20
+ import { useConfirmAction } from "../resource-detail/useConfirmAction.js";
21
+ import { useCopyResource } from "../resource-detail/useCopyResource.js";
22
+ import { useDeleteResource } from "../resource-detail/useDeleteResource.js";
23
+ import { useDetailTabs } from "../resource-detail/useDetailTabs.js";
24
+ import type {
25
+ AdditionalTab,
26
+ DetailAction,
27
+ ResourceHeaderMeta,
28
+ } from "../resource-detail/types.js";
29
+ import type { TabItem } from "../tabs/Tabs.js";
30
+ import { useExportResource } from "../library/useExportResource.js";
31
+ import { ScheduleRunOutcome } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/io_pb";
32
+ import type { ScheduleSpec } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/spec_pb";
33
+ import {
34
+ RunConfigSchema,
35
+ type AgentInvocation,
36
+ type RunConfig,
37
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/invocation_pb";
38
+ import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
39
+ import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
40
+ import type { WorkspaceEntry } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
41
+ import {
42
+ cadenceToCron,
43
+ cronToCadence,
44
+ describeCadence,
45
+ validateCron,
46
+ type CadencePreset,
47
+ } from "./cadence.js";
48
+ import { CadenceField } from "./CadenceField.js";
49
+ import { EnvironmentPicker } from "../environment/EnvironmentPicker.js";
50
+ import { ModelSelector } from "../models/ModelSelector.js";
51
+ import {
52
+ HARNESS_META,
53
+ fromProtoHarness,
54
+ toProtoHarness,
55
+ type HarnessOption,
56
+ } from "../models/harness.js";
57
+ import {
58
+ fromProtoServiceTier,
59
+ toProtoServiceTier,
60
+ type ServiceTierOption,
61
+ } from "../models/service-tier.js";
62
+ import { deriveScheduleState, formatNextFire } from "./scheduleState.js";
63
+ import {
64
+ ScheduleRunsCompactList,
65
+ ScheduleRunsTable,
66
+ } from "./ScheduleRunsTable.js";
67
+ import { TimeZoneField, browserTimeZone } from "./TimeZoneField.js";
68
+ import { useSchedule } from "./useSchedule.js";
69
+ import { useScheduleRuns } from "./useScheduleRuns.js";
70
+ import { useResumeSchedule } from "./useResumeSchedule.js";
71
+ import { useSetScheduleEnabled } from "./useSetScheduleEnabled.js";
72
+ import { useTriggerSchedule } from "./useTriggerSchedule.js";
73
+ import { useUpdateScheduleSpec } from "./useUpdateScheduleSpec.js";
74
+
75
+ /** Props for {@link ScheduleDetailView}. */
76
+ export interface ScheduleDetailViewProps {
77
+ /** Organization slug. */
78
+ readonly org: string;
79
+ /** Schedule slug. */
80
+ readonly slug: string;
81
+ /**
82
+ * Called when the user activates the target-agent reference.
83
+ * Navigation is the consumer's concern (DD-004) — the Console pushes
84
+ * a Library route; an embedding host does whatever fits.
85
+ */
86
+ readonly onNavigateToAgent?: (org: string, slug: string) => void;
87
+ /**
88
+ * Called when the user activates an execution reference (`aex_…`),
89
+ * from the status row or a run-history row. When omitted, ids render
90
+ * as plain text.
91
+ */
92
+ readonly onNavigateToExecution?: (executionId: string) => void;
93
+ /** Called after a successful delete (navigate back to the list). */
94
+ readonly onDeleted?: () => void;
95
+ /** Called when the schedule loads or reloads (e.g. breadcrumb label sync). */
96
+ readonly onResourceLoad?: (schedule: Schedule) => void;
97
+ /**
98
+ * Enable inline editing of the schedule's mutable spec fields
99
+ * (cadence, message, environments, engine & model, budget). Each
100
+ * field saves independently via a lossless full-resource re-apply
101
+ * ({@link useUpdateScheduleSpec}). The server-immutable fields —
102
+ * slug, target agent, target type — never gain an edit affordance;
103
+ * workspace edits go through Edit YAML for now.
104
+ * @default false
105
+ */
106
+ readonly editable?: boolean;
107
+ /** Consumer-provided extension tabs, appended after the built-ins. */
108
+ readonly additionalTabs?: readonly AdditionalTab[];
109
+ /** Controlled active tab (pair with `onTabChange`). */
110
+ readonly activeTab?: string;
111
+ /** Controlled tab change handler (pair with `activeTab`). */
112
+ readonly onTabChange?: (tabId: string) => void;
113
+ /** Initial tab in uncontrolled mode. @default "overview" */
114
+ readonly defaultTab?: string;
115
+ /**
116
+ * The instant "next fire" countdowns and run timestamps are computed
117
+ * against. Injectable for deterministic tests and Scenar fixtures.
118
+ * @default new Date() at render
119
+ */
120
+ readonly now?: Date;
121
+ /** Additional CSS classes for the root container. */
122
+ readonly className?: string;
123
+ }
124
+
125
+ const OVERVIEW_TAB_ID = "overview";
126
+ const RUNS_TAB_ID = "runs";
127
+
128
+ // The Overview strip shows the newest handful of fires; the Runs tab
129
+ // owns the full paginated history.
130
+ const RECENT_RUNS_OPTIONS = { pageSize: 5 } as const;
131
+
132
+ /** invocation.proto pins AgentInvocation.message to 8192 characters. */
133
+ const MESSAGE_MAX_LEN = 8192;
134
+
135
+ /**
136
+ * Self-contained detail view for a Schedule (stigmer/stigmer#352),
137
+ * split into an Overview tab (definition + status + recent runs) and a
138
+ * Runs tab (the full paginated fire ledger).
139
+ *
140
+ * The view's one non-negotiable is rendering the two stop-levers
141
+ * distinctly, each with its inline remedy:
142
+ *
143
+ * - **Disabled** (owner's `spec.enabled` switch) — banner with an
144
+ * Enable action (a lossless full-proto re-apply).
145
+ * - **Paused** (platform's failure-streak latch,
146
+ * `status.paused_reason`) — banner showing the reason with a Resume
147
+ * action (the one path that clears the pause).
148
+ *
149
+ * Trigger ("Run now") starts a real, billable execution and is gated
150
+ * behind a confirmation; it is disabled while the schedule cannot fire
151
+ * (the banner names the remedy). With `editable`, the mutable spec
152
+ * fields edit inline; Edit YAML, Export, and Delete round out the
153
+ * action set.
154
+ *
155
+ * Handles loading, error, and not-found states automatically.
156
+ * Zero Console dependencies — safe for platform builder embedding.
157
+ * All visual properties flow through `--stgm-*` design tokens.
158
+ */
159
+ export function ScheduleDetailView({
160
+ org,
161
+ slug,
162
+ onNavigateToAgent,
163
+ onNavigateToExecution,
164
+ onDeleted,
165
+ onResourceLoad,
166
+ editable = false,
167
+ additionalTabs,
168
+ activeTab,
169
+ onTabChange,
170
+ defaultTab,
171
+ now,
172
+ className,
173
+ }: ScheduleDetailViewProps) {
174
+ const { schedule, isLoading, error, refetch } = useSchedule(org, slug);
175
+
176
+ // The recent-runs fetch stays mounted regardless of the active tab:
177
+ // it feeds the Runs tab badge (total count), the Overview strip, and
178
+ // post-trigger freshness. The Runs tab's full table owns its own
179
+ // paginated fetch inside ScheduleRunsTable.
180
+ const {
181
+ runs: recentRuns,
182
+ totalCount: totalRunCount,
183
+ isLoading: recentRunsLoading,
184
+ refetch: refetchRecentRuns,
185
+ } = useScheduleRuns(schedule?.metadata?.id ?? null, RECENT_RUNS_OPTIONS);
186
+
187
+ const { resumeSchedule, isResuming } = useResumeSchedule();
188
+ const { triggerSchedule, isTriggering } = useTriggerSchedule();
189
+ const { setEnabled, isPending: isToggling } = useSetScheduleEnabled();
190
+ const { updateSpec, isUpdating } = useUpdateScheduleSpec();
191
+ const { deleteResource, isDeleting } = useDeleteResource(
192
+ "schedule",
193
+ schedule?.metadata?.id ?? null,
194
+ schedule?.metadata?.name,
195
+ );
196
+ const { confirmState, confirm, handleConfirm, handleCancel } =
197
+ useConfirmAction();
198
+ const { copyId, copyQualifiedSlug } = useCopyResource();
199
+ const [editOpen, setEditOpen] = useState(false);
200
+
201
+ // Remount key for the Runs tab's table: a manual trigger bumps it so
202
+ // the table refetches and returns to page 1, where the new fire
203
+ // appears (the key-remount reset idiom, DD-014).
204
+ const [runsVersion, setRunsVersion] = useState(0);
205
+
206
+ // Last failed inline save, attributed to the field that was edited so
207
+ // only that editor shows the message. The backend's message is the
208
+ // UX (DD-006) — e.g. the cron validator's copy surfaces verbatim.
209
+ const [saveError, setSaveError] = useState<{
210
+ field: string;
211
+ message: string;
212
+ } | null>(null);
213
+
214
+ const saveSpecField = useCallback(
215
+ async (
216
+ field: string,
217
+ mutate: (spec: ScheduleSpec) => void,
218
+ ): Promise<boolean> => {
219
+ if (!schedule) return false;
220
+ setSaveError(null);
221
+ try {
222
+ await updateSpec(schedule, mutate);
223
+ refetch();
224
+ return true;
225
+ } catch (err) {
226
+ setSaveError({
227
+ field,
228
+ message: err instanceof Error ? err.message : String(err),
229
+ });
230
+ return false;
231
+ }
232
+ },
233
+ [schedule, updateSpec, refetch],
234
+ );
235
+
236
+ const builtInTabs = useMemo<readonly TabItem[]>(
237
+ () => [
238
+ { id: OVERVIEW_TAB_ID, label: "Overview" },
239
+ {
240
+ id: RUNS_TAB_ID,
241
+ label: "Runs",
242
+ ...(totalRunCount > 0 ? { badge: totalRunCount } : {}),
243
+ },
244
+ ],
245
+ [totalRunCount],
246
+ );
247
+
248
+ const {
249
+ effectiveTabs,
250
+ effectiveActiveTab,
251
+ effectiveOnTabChange,
252
+ activeAdditionalTab,
253
+ } = useDetailTabs({
254
+ builtInTabs,
255
+ additionalTabs,
256
+ activeTab,
257
+ onTabChange,
258
+ defaultTab,
259
+ });
260
+
261
+ const { copyYaml, downloadYaml } = useExportResource({
262
+ kind: "Schedule",
263
+ resource: schedule,
264
+ });
265
+
266
+ useEffect(() => {
267
+ if (schedule) onResourceLoad?.(schedule);
268
+ }, [schedule, onResourceLoad]);
269
+
270
+ if (isLoading) return <LoadingSkeleton className={className} />;
271
+ if (error)
272
+ return <ErrorMessage error={error} retry={refetch} className={className} />;
273
+ if (!schedule) return <NotFoundState className={className} />;
274
+
275
+ const meta = schedule.metadata;
276
+ const spec = schedule.spec;
277
+ const status = schedule.status;
278
+ const specAudit = status?.audit?.specAudit;
279
+ const stateInfo = deriveScheduleState(spec, status);
280
+ const renderNow = now ?? new Date();
281
+
282
+ const scheduleId = meta?.id ?? "";
283
+ const scheduleOrg = meta?.org || org;
284
+ const target = spec?.target?.case === "agent" ? spec.target.value : undefined;
285
+
286
+ const handleResume = async () => {
287
+ await resumeSchedule(scheduleId);
288
+ refetch();
289
+ };
290
+
291
+ const handleToggleEnabled = async () => {
292
+ await setEnabled(schedule, !spec?.enabled);
293
+ refetch();
294
+ };
295
+
296
+ // One fire: trigger, refresh the schedule and its run history, and — on
297
+ // a started run — hand the execution to the host so it can navigate
298
+ // straight to it (the whole point of the synchronous trigger, DD-017
299
+ // D-6). A refused run resolves too; its reason is toasted by the hook
300
+ // and lands in the run history below.
301
+ const fireNow = async () => {
302
+ const result = await triggerSchedule(scheduleId);
303
+ refetch();
304
+ refetchRecentRuns();
305
+ setRunsVersion((v) => v + 1);
306
+ if (
307
+ result.outcome === ScheduleRunOutcome.STARTED &&
308
+ result.executionId &&
309
+ onNavigateToExecution
310
+ ) {
311
+ onNavigateToExecution(result.executionId);
312
+ }
313
+ };
314
+
315
+ const handleTrigger = async () => {
316
+ const confirmed = await confirm({
317
+ title: "Run this schedule now?",
318
+ description:
319
+ "This starts a real agent execution immediately, outside the cron " +
320
+ "cadence. The run is recorded in this schedule's run history.",
321
+ confirmLabel: "Start run",
322
+ variant: "default",
323
+ });
324
+ if (!confirmed) return;
325
+ await fireNow();
326
+ };
327
+
328
+ // A disabled schedule refuses to fire at the server — ScheduleBlueprintAccess
329
+ // requires spec.enabled at the create gate AND the mid-run read (DD-017
330
+ // D-5), so a disabled run would die mid-execution after billing. The
331
+ // staged-disabled test flow the creation form promises is therefore
332
+ // "enable, then fire", and this makes it one click.
333
+ const handleEnableAndRun = async () => {
334
+ const confirmed = await confirm({
335
+ title: "Enable and run this schedule now?",
336
+ description:
337
+ "This schedule is staged disabled. Enabling it lets it fire on its " +
338
+ "cron cadence going forward, and starts one real run immediately so " +
339
+ "you can see the result. You can disable it again afterwards.",
340
+ confirmLabel: "Enable & run",
341
+ variant: "default",
342
+ });
343
+ if (!confirmed) return;
344
+ await setEnabled(schedule, true);
345
+ await fireNow();
346
+ };
347
+
348
+ const handleDelete = async () => {
349
+ const confirmed = await confirm({
350
+ title: "Delete this schedule?",
351
+ description:
352
+ `"${meta?.name ?? slug}" will stop firing and be permanently ` +
353
+ "removed. Past executions are not affected. This cannot be undone.",
354
+ confirmLabel: "Delete",
355
+ variant: "destructive",
356
+ });
357
+ if (!confirmed) return;
358
+ await deleteResource();
359
+ onDeleted?.();
360
+ };
361
+
362
+ // Disabled → "Enable & run now" (the one-click staged-test flow);
363
+ // active and paused → "Run now" (a paused schedule's owner needs a
364
+ // test fire to verify a fix before resuming — DD-017 D-5).
365
+ const primaryAction: DetailAction =
366
+ stateInfo.state === "disabled"
367
+ ? {
368
+ id: "enable-and-run",
369
+ label: "Enable & run now",
370
+ onAction: () => void handleEnableAndRun(),
371
+ disabled: isTriggering || isToggling,
372
+ }
373
+ : {
374
+ id: "trigger",
375
+ label: "Run now",
376
+ onAction: () => void handleTrigger(),
377
+ disabled: isTriggering,
378
+ };
379
+
380
+ const actions: DetailAction[] = [
381
+ ...(stateInfo.isPaused
382
+ ? [
383
+ {
384
+ id: "resume",
385
+ label: "Resume schedule",
386
+ onAction: () => void handleResume(),
387
+ disabled: isResuming,
388
+ group: "state",
389
+ },
390
+ ]
391
+ : []),
392
+ {
393
+ id: "toggle-enabled",
394
+ label: spec?.enabled ? "Disable schedule" : "Enable schedule",
395
+ onAction: () => void handleToggleEnabled(),
396
+ disabled: isToggling,
397
+ group: "state",
398
+ },
399
+ {
400
+ id: "copy-id",
401
+ label: "Copy ID",
402
+ onAction: () => void copyId(scheduleId),
403
+ group: "clipboard",
404
+ },
405
+ {
406
+ id: "copy-slug",
407
+ label: "Copy slug",
408
+ onAction: () => void copyQualifiedSlug(org, slug),
409
+ group: "clipboard",
410
+ },
411
+ {
412
+ id: "edit-yaml",
413
+ label: "Edit YAML",
414
+ onAction: () => setEditOpen(true),
415
+ group: "edit",
416
+ },
417
+ {
418
+ id: "copy-yaml",
419
+ label: "Copy YAML",
420
+ onAction: () => void copyYaml(),
421
+ group: "export",
422
+ },
423
+ {
424
+ id: "download-yaml",
425
+ label: "Download YAML",
426
+ onAction: downloadYaml,
427
+ group: "export",
428
+ },
429
+ {
430
+ id: "delete",
431
+ label: "Delete",
432
+ variant: "destructive",
433
+ onAction: () => void handleDelete(),
434
+ disabled: isDeleting,
435
+ group: "danger",
436
+ },
437
+ ];
438
+
439
+ const headerMeta: ResourceHeaderMeta = {
440
+ name: meta?.name || meta?.slug || "Untitled",
441
+ id: scheduleId,
442
+ org: meta?.org,
443
+ slug: meta?.slug,
444
+ icon: <ScheduleIcon className="size-6 text-muted-foreground" />,
445
+ createdAt: specAudit?.createdAt ? timestampDate(specAudit.createdAt) : null,
446
+ updatedAt: specAudit?.updatedAt ? timestampDate(specAudit.updatedAt) : null,
447
+ status: stateInfo.phase,
448
+ statusLabel: stateInfo.label,
449
+ };
450
+
451
+ const headerBanner =
452
+ stateInfo.state === "disabled" ? (
453
+ <StateBanner
454
+ title="Schedule is disabled"
455
+ onAction={() => void handleToggleEnabled()}
456
+ actionLabel="Enable schedule"
457
+ actionBusy={isToggling}
458
+ >
459
+ This schedule is staged disabled (
460
+ <code className="font-mono">spec.enabled</code> is off) — it will not
461
+ fire on its cron cadence. Use{" "}
462
+ <span className="font-medium text-foreground">Enable &amp; run now</span>{" "}
463
+ to enable it and start one test run, or Enable it here to hand it to
464
+ the cadence.
465
+ {stateInfo.isPaused && (
466
+ <>
467
+ {" "}
468
+ It is also paused by the platform ({status?.pausedReason}); after
469
+ enabling, use Resume to clear the pause.
470
+ </>
471
+ )}
472
+ </StateBanner>
473
+ ) : stateInfo.state === "paused" ? (
474
+ <StateBanner
475
+ title="Paused by the platform"
476
+ onAction={() => void handleResume()}
477
+ actionLabel="Resume schedule"
478
+ actionBusy={isResuming}
479
+ >
480
+ {status?.pausedReason} — resuming clears the pause and the failure
481
+ streak. Re-applying the manifest does not.
482
+ </StateBanner>
483
+ ) : undefined;
484
+
485
+ const overviewContent = (
486
+ <div className="flex flex-col gap-6">
487
+ <Section title="Definition">
488
+ <dl className="divide-y divide-border">
489
+ <DetailRow label="Cadence">
490
+ {editable ? (
491
+ <CadenceInlineEditor
492
+ cron={spec?.cron ?? ""}
493
+ timeZone={spec?.timeZone ?? ""}
494
+ onSave={(cron, timeZone) =>
495
+ saveSpecField("cadence", (s) => {
496
+ s.cron = cron;
497
+ s.timeZone = timeZone;
498
+ })
499
+ }
500
+ isSaving={isUpdating}
501
+ error={
502
+ saveError?.field === "cadence" ? saveError.message : undefined
503
+ }
504
+ />
505
+ ) : (
506
+ <CadenceSummary
507
+ cron={spec?.cron ?? ""}
508
+ timeZone={spec?.timeZone ?? ""}
509
+ />
510
+ )}
511
+ </DetailRow>
512
+ {/* Target agent and target type are server-immutable on update,
513
+ so this row never gains an edit affordance — error
514
+ prevention over error handling. */}
515
+ <DetailRow label="Target agent">
516
+ {target?.agentRef ? (
517
+ <ReferenceLink
518
+ label={`${target.agentRef.org}/${target.agentRef.slug}`}
519
+ onNavigate={
520
+ onNavigateToAgent
521
+ ? () =>
522
+ onNavigateToAgent(
523
+ target.agentRef!.org,
524
+ target.agentRef!.slug,
525
+ )
526
+ : undefined
527
+ }
528
+ />
529
+ ) : (
530
+ <span className="text-sm text-muted-foreground">—</span>
531
+ )}
532
+ </DetailRow>
533
+ <DetailRow label="Message">
534
+ {editable && target ? (
535
+ <InlineEditTextarea
536
+ value={target.message ?? ""}
537
+ placeholder="The instruction the agent receives on every fire — write it for a run with no human present."
538
+ onSave={(v) =>
539
+ saveSpecField("message", (s) => {
540
+ if (s.target.case === "agent") {
541
+ s.target.value.message = v.trim();
542
+ }
543
+ })
544
+ }
545
+ isSaving={isUpdating}
546
+ error={
547
+ saveError?.field === "message" ? saveError.message : undefined
548
+ }
549
+ validate={validateMessage}
550
+ />
551
+ ) : (
552
+ <p className="whitespace-pre-wrap break-words text-sm text-foreground">
553
+ {target?.message || "—"}
554
+ </p>
555
+ )}
556
+ </DetailRow>
557
+ <DetailRow label="Environments">
558
+ {editable && target ? (
559
+ <EnvironmentsInlineEditor
560
+ org={scheduleOrg}
561
+ refs={target.environmentRefs ?? []}
562
+ onSave={(refs) =>
563
+ saveSpecField("environments", (s) => {
564
+ if (s.target.case === "agent") {
565
+ s.target.value.environmentRefs = refs.map((r) =>
566
+ create(ApiResourceReferenceSchema, {
567
+ org: r.org,
568
+ slug: r.slug,
569
+ kind: ApiResourceKind.environment,
570
+ }),
571
+ );
572
+ }
573
+ })
574
+ }
575
+ isSaving={isUpdating}
576
+ error={
577
+ saveError?.field === "environments"
578
+ ? saveError.message
579
+ : undefined
580
+ }
581
+ />
582
+ ) : (
583
+ <EnvironmentRefList
584
+ refs={target?.environmentRefs ?? []}
585
+ />
586
+ )}
587
+ </DetailRow>
588
+ {/* Workspace is authored in the creation form (or YAML); an
589
+ inline workspace editor is deliberately deferred — the
590
+ read view keeps every stored entry visible. */}
591
+ <DetailRow label="Workspace">
592
+ <WorkspaceSummary
593
+ entries={target?.workspaceEntries ?? []}
594
+ />
595
+ </DetailRow>
596
+ <DetailRow label="Engine & model">
597
+ {editable && target ? (
598
+ <EngineModelInlineEditor
599
+ invocation={target}
600
+ onSave={(harness, modelName, serviceTier) =>
601
+ saveSpecField("engine-model", (s) => {
602
+ if (s.target.case === "agent") {
603
+ applyEngineModel(s.target.value, harness, modelName, serviceTier);
604
+ }
605
+ })
606
+ }
607
+ isSaving={isUpdating}
608
+ error={
609
+ saveError?.field === "engine-model"
610
+ ? saveError.message
611
+ : undefined
612
+ }
613
+ />
614
+ ) : (
615
+ <EngineModelSummary invocation={target} />
616
+ )}
617
+ </DetailRow>
618
+ <DetailRow label="Budget per run">
619
+ {editable && target ? (
620
+ <BudgetInlineEditor
621
+ config={target.runConfig}
622
+ onSave={(maxCostUsd) =>
623
+ saveSpecField("budget", (s) => {
624
+ if (s.target.case === "agent") {
625
+ applyBudget(s.target.value, maxCostUsd);
626
+ }
627
+ })
628
+ }
629
+ isSaving={isUpdating}
630
+ error={
631
+ saveError?.field === "budget" ? saveError.message : undefined
632
+ }
633
+ />
634
+ ) : (
635
+ <BudgetSummary config={target?.runConfig} />
636
+ )}
637
+ </DetailRow>
638
+ </dl>
639
+ </Section>
640
+
641
+ <Section title="Status">
642
+ <dl className="divide-y divide-border">
643
+ <DetailRow label="Next fire">
644
+ <span className="text-sm text-foreground">
645
+ {stateInfo.state === "active" && status?.nextFireAt
646
+ ? formatNextFire(timestampDate(status.nextFireAt), renderNow)
647
+ : "—"}
648
+ </span>
649
+ </DetailRow>
650
+ <DetailRow label="Last fired">
651
+ <span className="text-sm text-foreground">
652
+ {status?.lastFireAt
653
+ ? formatRelativeTime(
654
+ timestampDate(status.lastFireAt),
655
+ renderNow,
656
+ )
657
+ : "Never"}
658
+ </span>
659
+ </DetailRow>
660
+ <DetailRow label="Last execution">
661
+ {status?.lastExecutionId ? (
662
+ <ReferenceLink
663
+ label={status.lastExecutionId}
664
+ mono
665
+ onNavigate={
666
+ onNavigateToExecution
667
+ ? () => onNavigateToExecution(status.lastExecutionId)
668
+ : undefined
669
+ }
670
+ />
671
+ ) : (
672
+ <span className="text-sm text-muted-foreground">—</span>
673
+ )}
674
+ </DetailRow>
675
+ <DetailRow label="Failure streak">
676
+ <FailureStreak count={status?.consecutiveFailures ?? 0} />
677
+ </DetailRow>
678
+ </dl>
679
+ </Section>
680
+
681
+ <Section
682
+ title="Recent runs"
683
+ headerActions={
684
+ totalRunCount > recentRuns.length ? (
685
+ <button
686
+ type="button"
687
+ onClick={() => effectiveOnTabChange(RUNS_TAB_ID)}
688
+ className={cn(
689
+ "text-xs font-medium text-primary underline-offset-2 hover:underline",
690
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm",
691
+ )}
692
+ >
693
+ View all {totalRunCount} runs
694
+ </button>
695
+ ) : undefined
696
+ }
697
+ >
698
+ <ScheduleRunsCompactList
699
+ runs={recentRuns}
700
+ isLoading={recentRunsLoading}
701
+ now={renderNow}
702
+ onNavigateToExecution={onNavigateToExecution}
703
+ />
704
+ </Section>
705
+ </div>
706
+ );
707
+
708
+ let tabContent: React.ReactNode;
709
+ if (activeAdditionalTab) {
710
+ tabContent = activeAdditionalTab.content;
711
+ } else if (effectiveActiveTab === RUNS_TAB_ID) {
712
+ tabContent = (
713
+ <ScheduleRunsTable
714
+ key={runsVersion}
715
+ scheduleId={scheduleId}
716
+ now={now}
717
+ onNavigateToExecution={onNavigateToExecution}
718
+ />
719
+ );
720
+ } else {
721
+ tabContent = overviewContent;
722
+ }
723
+
724
+ return (
725
+ <>
726
+ <ResourceDetailShell
727
+ header={headerMeta}
728
+ headerBanner={headerBanner}
729
+ primaryAction={primaryAction}
730
+ actions={actions}
731
+ tabs={effectiveTabs}
732
+ activeTab={effectiveTabs ? effectiveActiveTab : undefined}
733
+ onTabChange={effectiveTabs ? effectiveOnTabChange : undefined}
734
+ tabsAriaLabel="Schedule detail sections"
735
+ className={className}
736
+ >
737
+ {tabContent}
738
+ </ResourceDetailShell>
739
+
740
+ <ConfirmDialog
741
+ state={confirmState}
742
+ onConfirm={handleConfirm}
743
+ onCancel={handleCancel}
744
+ />
745
+
746
+ {/* Mounted on demand: the editor serializes the resource and pulls
747
+ the CodeMirror chunk, neither of which should cost anything
748
+ until the action is actually invoked (DD-013). The dialog
749
+ resets its edit state on open, so unmount-on-close loses
750
+ nothing. */}
751
+ {editOpen && (
752
+ <EditResourceYamlDialog
753
+ open={editOpen}
754
+ onOpenChange={setEditOpen}
755
+ resource={schedule}
756
+ onApplied={() => refetch()}
757
+ />
758
+ )}
759
+ </>
760
+ );
761
+ }
762
+
763
+ // ---------------------------------------------------------------------------
764
+ // State banner — one lever, one remedy, inline
765
+ // ---------------------------------------------------------------------------
766
+
767
+ function StateBanner({
768
+ title,
769
+ children,
770
+ actionLabel,
771
+ onAction,
772
+ actionBusy,
773
+ }: {
774
+ readonly title: string;
775
+ readonly children: React.ReactNode;
776
+ readonly actionLabel: string;
777
+ readonly onAction: () => void;
778
+ readonly actionBusy: boolean;
779
+ }) {
780
+ return (
781
+ <div
782
+ role="status"
783
+ className="flex items-start gap-2.5 rounded-lg border border-warning/30 bg-warning/5 px-4 py-3"
784
+ >
785
+ <WarningIcon className="mt-0.5 size-4 shrink-0 text-warning" />
786
+ <div className="min-w-0 flex-1">
787
+ <p className="text-sm font-medium text-foreground">{title}</p>
788
+ <p className="mt-0.5 text-xs text-muted-foreground">{children}</p>
789
+ </div>
790
+ <button
791
+ type="button"
792
+ onClick={onAction}
793
+ disabled={actionBusy}
794
+ className={cn(
795
+ "shrink-0 rounded-md border border-input bg-background px-3 py-1.5 text-xs font-medium text-foreground",
796
+ "hover:bg-accent hover:text-accent-foreground",
797
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
798
+ "disabled:pointer-events-none disabled:opacity-50",
799
+ )}
800
+ >
801
+ {actionLabel}
802
+ </button>
803
+ </div>
804
+ );
805
+ }
806
+
807
+ // ---------------------------------------------------------------------------
808
+ // Cadence summary — plain English first, raw cron as the precise record
809
+ // ---------------------------------------------------------------------------
810
+
811
+ /**
812
+ * Render a schedule's cadence for reading: the plain-English sentence
813
+ * first ("Every day at 09:00 (Asia/Kolkata)"), the raw cron beneath it
814
+ * in muted mono as the precise record.
815
+ *
816
+ * `cronToCadence` RECOGNIZES rather than parses (the platform owns no
817
+ * cron parser in either edition — see cadence.ts): expressions outside
818
+ * the builder's shapes come back as `custom`, for which the raw cron IS
819
+ * the primary display, with the time zone alongside so nothing the
820
+ * spec stores is hidden.
821
+ */
822
+ function CadenceSummary({
823
+ cron,
824
+ timeZone,
825
+ }: {
826
+ readonly cron: string;
827
+ readonly timeZone: string;
828
+ }) {
829
+ if (!cron) return <span className="text-sm text-muted-foreground">—</span>;
830
+
831
+ const preset = cronToCadence(cron);
832
+ if (preset.kind === "custom") {
833
+ return (
834
+ <div className="flex flex-col gap-0.5">
835
+ <code className="font-mono text-sm text-foreground">{cron}</code>
836
+ {timeZone && (
837
+ <span className="text-xs text-muted-foreground">{timeZone}</span>
838
+ )}
839
+ </div>
840
+ );
841
+ }
842
+ return (
843
+ <div className="flex flex-col gap-0.5">
844
+ <span className="text-sm text-foreground">
845
+ {describeCadence(preset, timeZone || undefined)}
846
+ </span>
847
+ <code className="font-mono text-xs text-muted-foreground">{cron}</code>
848
+ </div>
849
+ );
850
+ }
851
+
852
+ // ---------------------------------------------------------------------------
853
+ // Inline editors — per-field click-to-edit over the lossless write path
854
+ // ---------------------------------------------------------------------------
855
+ //
856
+ // Each editor follows the InlineEdit* family's contract: read mode is a
857
+ // click target with a hover pencil; edit mode holds a local draft with
858
+ // explicit Save/Cancel; a failed save keeps the editor open with the
859
+ // server's message rendered verbatim beneath it (DD-006). The editors
860
+ // stay in this file (the AgentDetailView single-organism precedent) and
861
+ // reuse the creation form's field components — CadenceField,
862
+ // TimeZoneField, EnvironmentPicker — so creating and editing a schedule
863
+ // are the same experience.
864
+
865
+ /** Cadence + time zone edit in one panel — they form one sentence. */
866
+ function CadenceInlineEditor({
867
+ cron,
868
+ timeZone,
869
+ onSave,
870
+ isSaving,
871
+ error,
872
+ }: {
873
+ readonly cron: string;
874
+ readonly timeZone: string;
875
+ readonly onSave: (cron: string, timeZone: string) => Promise<boolean>;
876
+ readonly isSaving: boolean;
877
+ readonly error?: string;
878
+ }) {
879
+ const [isEditing, setIsEditing] = useState(false);
880
+ const [draftCadence, setDraftCadence] = useState<CadencePreset>(() =>
881
+ cronToCadence(cron),
882
+ );
883
+ const [draftZone, setDraftZone] = useState(timeZone);
884
+
885
+ const startEdit = () => {
886
+ // cronToCadence round-trips the stored cron into the preset picker;
887
+ // unrecognized shapes land on the Custom escape hatch with the raw
888
+ // string intact.
889
+ setDraftCadence(cronToCadence(cron));
890
+ setDraftZone(timeZone || browserTimeZone());
891
+ setIsEditing(true);
892
+ };
893
+
894
+ if (!isEditing) {
895
+ return (
896
+ <InlineReadButton onEdit={startEdit} ariaLabel="Edit cadence">
897
+ <CadenceSummary cron={cron} timeZone={timeZone} />
898
+ </InlineReadButton>
899
+ );
900
+ }
901
+
902
+ const draftCron = cadenceToCron(draftCadence).trim();
903
+ const canSave = draftCron !== "" && validateCron(draftCron) === null;
904
+
905
+ return (
906
+ <div className="flex flex-col gap-2">
907
+ <CadenceField
908
+ value={draftCadence}
909
+ onChange={setDraftCadence}
910
+ timeZone={draftZone}
911
+ disabled={isSaving}
912
+ />
913
+ <div className="space-y-1">
914
+ <span className={editorLabelClasses}>Time zone</span>
915
+ <TimeZoneField
916
+ value={draftZone}
917
+ onChange={setDraftZone}
918
+ disabled={isSaving}
919
+ />
920
+ </div>
921
+ <InlineEditActions
922
+ onCancel={() => setIsEditing(false)}
923
+ onSave={async () => {
924
+ const ok = await onSave(draftCron, draftZone);
925
+ if (ok) setIsEditing(false);
926
+ }}
927
+ isSaving={isSaving}
928
+ canSave={canSave}
929
+ error={error}
930
+ />
931
+ </div>
932
+ );
933
+ }
934
+
935
+ /** Environment bindings edit — org-shared credentials only (DD-017 D-2). */
936
+ function EnvironmentsInlineEditor({
937
+ org,
938
+ refs,
939
+ onSave,
940
+ isSaving,
941
+ error,
942
+ }: {
943
+ readonly org: string;
944
+ readonly refs: readonly { org: string; slug: string }[];
945
+ readonly onSave: (refs: readonly ResourceRef[]) => Promise<boolean>;
946
+ readonly isSaving: boolean;
947
+ readonly error?: string;
948
+ }) {
949
+ const [isEditing, setIsEditing] = useState(false);
950
+ const [draft, setDraft] = useState<ResourceRef[]>([]);
951
+
952
+ const startEdit = () => {
953
+ setDraft(refs.map((r) => ({ org: r.org, slug: r.slug })));
954
+ setIsEditing(true);
955
+ };
956
+
957
+ if (!isEditing) {
958
+ return (
959
+ <InlineReadButton onEdit={startEdit} ariaLabel="Edit environments">
960
+ <EnvironmentRefList refs={refs} />
961
+ </InlineReadButton>
962
+ );
963
+ }
964
+
965
+ return (
966
+ <div className="flex flex-col gap-2">
967
+ <EnvironmentPicker
968
+ org={org}
969
+ value={draft}
970
+ onChange={setDraft}
971
+ disabled={isSaving}
972
+ // Only org-shared environments resolve for a schedule fire (the
973
+ // same credential surface a channel binding uses), so offering
974
+ // anything else could only produce refused runs.
975
+ filterEnvironment={isOrgSharedEnvironment}
976
+ />
977
+ <p className="text-[0.65rem] text-muted-foreground">
978
+ Bind org-shared credentials so the agent&rsquo;s tools work on an
979
+ unattended fire. Without this, an agent whose tools need credentials
980
+ will be refused every run.
981
+ </p>
982
+ <InlineEditActions
983
+ onCancel={() => setIsEditing(false)}
984
+ onSave={async () => {
985
+ const ok = await onSave(draft);
986
+ if (ok) setIsEditing(false);
987
+ }}
988
+ isSaving={isSaving}
989
+ canSave
990
+ error={error}
991
+ />
992
+ </div>
993
+ );
994
+ }
995
+
996
+ function isOrgSharedEnvironment(env: Environment): boolean {
997
+ return env.metadata?.visibility === ApiResourceVisibility.visibility_org;
998
+ }
999
+
1000
+ /**
1001
+ * Engine & model edit — the composer's own picker, with the creation
1002
+ * form's atomic semantics (DD-018 D-5): picking a model pins BOTH the
1003
+ * harness and the model (the registry scopes models per harness);
1004
+ * clearing the model unpins both, and the platform defaults apply.
1005
+ */
1006
+ function EngineModelInlineEditor({
1007
+ invocation,
1008
+ onSave,
1009
+ isSaving,
1010
+ error,
1011
+ }: {
1012
+ readonly invocation: AgentInvocation;
1013
+ readonly onSave: (
1014
+ harness: Harness,
1015
+ modelName: string,
1016
+ serviceTier: ServiceTierOption,
1017
+ ) => Promise<boolean>;
1018
+ readonly isSaving: boolean;
1019
+ readonly error?: string;
1020
+ }) {
1021
+ const [isEditing, setIsEditing] = useState(false);
1022
+ const [modelName, setModelName] = useState("");
1023
+ const [harness, setHarness] = useState<HarnessOption>("cursor");
1024
+ const [serviceTier, setServiceTier] = useState<ServiceTierOption>("standard");
1025
+
1026
+ const startEdit = () => {
1027
+ setModelName(invocation.runConfig?.modelName ?? "");
1028
+ setHarness(
1029
+ invocation.harness !== Harness.UNSPECIFIED
1030
+ ? fromProtoHarness(invocation.harness)
1031
+ : "cursor",
1032
+ );
1033
+ setServiceTier(
1034
+ fromProtoServiceTier(invocation.runConfig?.serviceTier) ?? "standard",
1035
+ );
1036
+ setIsEditing(true);
1037
+ };
1038
+
1039
+ if (!isEditing) {
1040
+ return (
1041
+ <InlineReadButton onEdit={startEdit} ariaLabel="Edit engine and model">
1042
+ <EngineModelSummary invocation={invocation} />
1043
+ </InlineReadButton>
1044
+ );
1045
+ }
1046
+
1047
+ return (
1048
+ <div className="flex flex-col gap-2">
1049
+ <div className="flex items-center gap-2">
1050
+ <ModelSelector
1051
+ value={modelName}
1052
+ onValueChange={setModelName}
1053
+ initialHarness={harness}
1054
+ onHarnessChange={setHarness}
1055
+ serviceTier={serviceTier}
1056
+ onServiceTierChange={setServiceTier}
1057
+ placeholderLabel="Platform default"
1058
+ disabled={isSaving}
1059
+ />
1060
+ {modelName !== "" && (
1061
+ <button
1062
+ type="button"
1063
+ onClick={() => {
1064
+ setModelName("");
1065
+ setServiceTier("standard");
1066
+ }}
1067
+ disabled={isSaving}
1068
+ className="rounded-md px-2 py-1 text-[0.65rem] text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
1069
+ >
1070
+ Reset to platform default
1071
+ </button>
1072
+ )}
1073
+ </div>
1074
+ <p className="text-[0.65rem] text-muted-foreground">
1075
+ Runs use the platform&rsquo;s default engine and model unless you pick
1076
+ one here. Picking a model pins the engine it belongs to.
1077
+ </p>
1078
+ <InlineEditActions
1079
+ onCancel={() => setIsEditing(false)}
1080
+ onSave={async () => {
1081
+ const ok = await onSave(
1082
+ modelName !== "" ? toProtoHarness(harness) : Harness.UNSPECIFIED,
1083
+ modelName,
1084
+ serviceTier,
1085
+ );
1086
+ if (ok) setIsEditing(false);
1087
+ }}
1088
+ isSaving={isSaving}
1089
+ canSave
1090
+ error={error}
1091
+ />
1092
+ </div>
1093
+ );
1094
+ }
1095
+
1096
+ /** Budget edit — blank inherits the platform default (DD-018 D-2). */
1097
+ function BudgetInlineEditor({
1098
+ config,
1099
+ onSave,
1100
+ isSaving,
1101
+ error,
1102
+ }: {
1103
+ readonly config: RunConfig | undefined;
1104
+ readonly onSave: (maxCostUsd: number | undefined) => Promise<boolean>;
1105
+ readonly isSaving: boolean;
1106
+ readonly error?: string;
1107
+ }) {
1108
+ const [isEditing, setIsEditing] = useState(false);
1109
+ const [budgetUsd, setBudgetUsd] = useState("");
1110
+
1111
+ const startEdit = () => {
1112
+ setBudgetUsd(config && config.maxCostUsd > 0 ? String(config.maxCostUsd) : "");
1113
+ setIsEditing(true);
1114
+ };
1115
+
1116
+ if (!isEditing) {
1117
+ return (
1118
+ <InlineReadButton onEdit={startEdit} ariaLabel="Edit budget">
1119
+ <BudgetSummary config={config} />
1120
+ </InlineReadButton>
1121
+ );
1122
+ }
1123
+
1124
+ const cost = Number.parseFloat(budgetUsd);
1125
+ const parsedBudget = Number.isFinite(cost) && cost > 0 ? cost : undefined;
1126
+
1127
+ return (
1128
+ <div className="flex flex-col gap-2">
1129
+ <input
1130
+ type="number"
1131
+ min="0"
1132
+ step="any"
1133
+ aria-label="Budget per run (USD)"
1134
+ value={budgetUsd}
1135
+ onChange={(e) => setBudgetUsd(e.target.value)}
1136
+ placeholder="platform default"
1137
+ disabled={isSaving}
1138
+ className={cn(editorInputClasses, "sm:max-w-48")}
1139
+ />
1140
+ <p className="text-[0.65rem] text-muted-foreground">
1141
+ Each run stops when it reaches this spend. You can lower the
1142
+ platform&rsquo;s per-run cap, never raise it past the
1143
+ platform&rsquo;s ceiling. Blank inherits the platform default.
1144
+ </p>
1145
+ <InlineEditActions
1146
+ onCancel={() => setIsEditing(false)}
1147
+ onSave={async () => {
1148
+ const ok = await onSave(parsedBudget);
1149
+ if (ok) setIsEditing(false);
1150
+ }}
1151
+ isSaving={isSaving}
1152
+ canSave
1153
+ error={error}
1154
+ />
1155
+ </div>
1156
+ );
1157
+ }
1158
+
1159
+ /**
1160
+ * Write the engine+model choice onto the invocation, preserving the
1161
+ * run-config fields the editor does not own (budget; the API-only tool
1162
+ * rounds), and dropping an all-empty run_config — the proto's "empty =
1163
+ * inherit" contract (DD-017 D-3 as carried into DD-018 D-2).
1164
+ */
1165
+ function applyEngineModel(
1166
+ invocation: AgentInvocation,
1167
+ harness: Harness,
1168
+ modelName: string,
1169
+ serviceTier: ServiceTierOption,
1170
+ ): void {
1171
+ invocation.harness = harness;
1172
+ invocation.runConfig = normalizeRunConfig(
1173
+ modelName,
1174
+ invocation.runConfig?.maxCostUsd ?? 0,
1175
+ invocation.runConfig?.maxToolRounds ?? 0,
1176
+ // The tier rides the model choice: no model, no tier (#357).
1177
+ modelName.trim() !== "" ? serviceTier : "standard",
1178
+ );
1179
+ }
1180
+
1181
+ /** Budget twin of {@link applyEngineModel} — writes only the cost cap. */
1182
+ function applyBudget(
1183
+ invocation: AgentInvocation,
1184
+ maxCostUsd: number | undefined,
1185
+ ): void {
1186
+ invocation.runConfig = normalizeRunConfig(
1187
+ invocation.runConfig?.modelName ?? "",
1188
+ maxCostUsd ?? 0,
1189
+ invocation.runConfig?.maxToolRounds ?? 0,
1190
+ fromProtoServiceTier(invocation.runConfig?.serviceTier) ?? "standard",
1191
+ );
1192
+ }
1193
+
1194
+ function normalizeRunConfig(
1195
+ modelName: string,
1196
+ maxCostUsd: number,
1197
+ maxToolRounds: number,
1198
+ serviceTier: ServiceTierOption,
1199
+ ): RunConfig | undefined {
1200
+ const fields: {
1201
+ modelName?: string;
1202
+ maxCostUsd?: number;
1203
+ maxToolRounds?: number;
1204
+ serviceTier?: ServiceTier;
1205
+ } = {};
1206
+ if (modelName.trim() !== "") fields.modelName = modelName.trim();
1207
+ if (maxCostUsd > 0) fields.maxCostUsd = maxCostUsd;
1208
+ if (maxToolRounds > 0) fields.maxToolRounds = maxToolRounds;
1209
+ // Only an active fast choice is carried — an untouched toggle stays
1210
+ // absent, preserving the unspecified-vs-explicit ledger distinction (#357).
1211
+ if (serviceTier === "fast") fields.serviceTier = toProtoServiceTier(serviceTier);
1212
+
1213
+ return Object.keys(fields).length > 0
1214
+ ? create(RunConfigSchema, fields)
1215
+ : undefined;
1216
+ }
1217
+
1218
+ /** Mirrors the server's constraint so the editor rejects bad input instantly. */
1219
+ function validateMessage(value: string): string | null {
1220
+ if (!value.trim()) {
1221
+ return "Message is required — the agent receives it on every fire.";
1222
+ }
1223
+ if (value.length > MESSAGE_MAX_LEN) {
1224
+ return `Message must be at most ${MESSAGE_MAX_LEN} characters.`;
1225
+ }
1226
+ return null;
1227
+ }
1228
+
1229
+ // ---------------------------------------------------------------------------
1230
+ // Inline-edit chrome shared by the bespoke editors above
1231
+ // ---------------------------------------------------------------------------
1232
+
1233
+ const editorLabelClasses = "block text-xs font-medium text-foreground";
1234
+
1235
+ const editorInputClasses = cn(
1236
+ "w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
1237
+ "placeholder:text-muted-foreground",
1238
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
1239
+ "disabled:pointer-events-none disabled:opacity-50",
1240
+ );
1241
+
1242
+ /** Read-mode click target with the family's hover pencil. */
1243
+ function InlineReadButton({
1244
+ onEdit,
1245
+ ariaLabel,
1246
+ children,
1247
+ }: {
1248
+ readonly onEdit: () => void;
1249
+ readonly ariaLabel: string;
1250
+ readonly children: React.ReactNode;
1251
+ }) {
1252
+ return (
1253
+ <div className="group/inline-edit">
1254
+ <button
1255
+ type="button"
1256
+ onClick={onEdit}
1257
+ aria-label={ariaLabel}
1258
+ className={cn(
1259
+ "-mx-2 w-full rounded-md px-2 py-1.5 text-left transition-colors",
1260
+ "hover:bg-accent-hover cursor-pointer",
1261
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
1262
+ )}
1263
+ >
1264
+ <div className="flex items-start justify-between gap-2">
1265
+ <div className="min-w-0 flex-1">{children}</div>
1266
+ <PencilIcon className="mt-0.5 size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover/inline-edit:opacity-100" />
1267
+ </div>
1268
+ </button>
1269
+ </div>
1270
+ );
1271
+ }
1272
+
1273
+ /** Save/Cancel footer with the field-attributed error line. */
1274
+ function InlineEditActions({
1275
+ onCancel,
1276
+ onSave,
1277
+ isSaving,
1278
+ canSave,
1279
+ error,
1280
+ }: {
1281
+ readonly onCancel: () => void;
1282
+ readonly onSave: () => void;
1283
+ readonly isSaving: boolean;
1284
+ readonly canSave: boolean;
1285
+ readonly error?: string;
1286
+ }) {
1287
+ return (
1288
+ <div className="flex flex-col gap-1.5">
1289
+ <div className="flex items-center justify-end gap-1.5">
1290
+ <button
1291
+ type="button"
1292
+ onClick={onCancel}
1293
+ disabled={isSaving}
1294
+ className={cn(
1295
+ "rounded-md px-2.5 py-1 text-xs font-medium",
1296
+ "border border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
1297
+ "disabled:opacity-50",
1298
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1299
+ )}
1300
+ >
1301
+ Cancel
1302
+ </button>
1303
+ <button
1304
+ type="button"
1305
+ onClick={onSave}
1306
+ disabled={!canSave || isSaving}
1307
+ className={cn(
1308
+ "inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium",
1309
+ "bg-primary text-primary-foreground hover:bg-primary-hover",
1310
+ "disabled:pointer-events-none disabled:opacity-50",
1311
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1312
+ )}
1313
+ >
1314
+ {isSaving && <SpinnerIcon />}
1315
+ Save
1316
+ </button>
1317
+ </div>
1318
+ {error && (
1319
+ <p className="text-xs text-destructive" role="alert">
1320
+ {error}
1321
+ </p>
1322
+ )}
1323
+ </div>
1324
+ );
1325
+ }
1326
+
1327
+ // ---------------------------------------------------------------------------
1328
+ // Detail rows and references
1329
+ // ---------------------------------------------------------------------------
1330
+
1331
+ function DetailRow({
1332
+ label,
1333
+ children,
1334
+ }: {
1335
+ readonly label: string;
1336
+ readonly children: React.ReactNode;
1337
+ }) {
1338
+ return (
1339
+ <div className="flex items-start gap-4 px-4 py-2.5">
1340
+ <dt className="w-40 shrink-0 pt-0.5 text-xs font-medium text-muted-foreground">
1341
+ {label}
1342
+ </dt>
1343
+ <dd className="min-w-0 flex-1">{children}</dd>
1344
+ </div>
1345
+ );
1346
+ }
1347
+
1348
+ /** The bound environment references, or the em-dash when there are none. */
1349
+ function EnvironmentRefList({
1350
+ refs,
1351
+ }: {
1352
+ readonly refs: readonly { org: string; slug: string }[];
1353
+ }) {
1354
+ if (refs.length === 0) {
1355
+ return <span className="text-sm text-muted-foreground">—</span>;
1356
+ }
1357
+ return (
1358
+ <ul className="flex flex-col gap-0.5">
1359
+ {refs.map((ref, i) => (
1360
+ <li
1361
+ key={`${ref.org}/${ref.slug}-${i}`}
1362
+ className="font-mono text-xs text-foreground"
1363
+ >
1364
+ {ref.org ? `${ref.org}/${ref.slug}` : ref.slug}
1365
+ </li>
1366
+ ))}
1367
+ </ul>
1368
+ );
1369
+ }
1370
+
1371
+ // ---------------------------------------------------------------------------
1372
+ // Failure streak — status.consecutive_failures, explained
1373
+ // ---------------------------------------------------------------------------
1374
+
1375
+ /**
1376
+ * The platform's failure streak: how many SCHEDULED runs in a row ended
1377
+ * badly. The server increments it per failed cron fire, resets it on a
1378
+ * successful run (or Resume), and auto-pauses the schedule when the
1379
+ * streak crosses its threshold. Manual "Run now" fires never count.
1380
+ *
1381
+ * The threshold itself is server configuration not exposed through the
1382
+ * API, so the copy stays qualitative — a hardcoded "of 5" here could
1383
+ * silently drift from what the platform actually enforces.
1384
+ */
1385
+ function FailureStreak({ count }: { readonly count: number }) {
1386
+ if (count === 0) {
1387
+ return <span className="text-sm text-foreground">0</span>;
1388
+ }
1389
+ return (
1390
+ <div className="flex flex-col gap-0.5">
1391
+ <span className="text-sm font-medium text-warning">
1392
+ {count} consecutive failed {count === 1 ? "run" : "runs"}
1393
+ </span>
1394
+ <p className="text-xs text-muted-foreground">
1395
+ Failed scheduled runs raise this streak; too many in a row and the
1396
+ platform pauses the schedule automatically. One successful run
1397
+ resets it to 0. Manual runs never count.
1398
+ </p>
1399
+ </div>
1400
+ );
1401
+ }
1402
+
1403
+ // ---------------------------------------------------------------------------
1404
+ // The invocation's run shape — engine+model, budget, workspace summaries
1405
+ // ---------------------------------------------------------------------------
1406
+
1407
+ /** "Cursor · composer-2.5", either half falling back to the platform default. */
1408
+ function EngineModelSummary({
1409
+ invocation,
1410
+ }: {
1411
+ readonly invocation: AgentInvocation | undefined;
1412
+ }) {
1413
+ const harness = invocation?.harness ?? Harness.UNSPECIFIED;
1414
+ const modelName = invocation?.runConfig?.modelName ?? "";
1415
+
1416
+ if (harness === Harness.UNSPECIFIED && modelName === "") {
1417
+ return (
1418
+ <span className="text-sm text-muted-foreground">Platform default</span>
1419
+ );
1420
+ }
1421
+
1422
+ const parts: string[] = [];
1423
+ if (harness !== Harness.UNSPECIFIED) {
1424
+ parts.push(HARNESS_META[fromProtoHarness(harness)].label);
1425
+ }
1426
+ parts.push(modelName !== "" ? modelName : "platform-default model");
1427
+ if (invocation?.runConfig?.serviceTier === ServiceTier.FAST) {
1428
+ parts.push("Fast tier");
1429
+ }
1430
+ return <span className="text-sm text-foreground">{parts.join(" · ")}</span>;
1431
+ }
1432
+
1433
+ /** The per-run cost cap, plus the API-only tool-round bound when set. */
1434
+ function BudgetSummary({
1435
+ config,
1436
+ }: {
1437
+ readonly config: RunConfig | undefined;
1438
+ }) {
1439
+ const maxCostUsd = config?.maxCostUsd ?? 0;
1440
+ const maxToolRounds = config?.maxToolRounds ?? 0;
1441
+
1442
+ return (
1443
+ <div className="flex flex-col gap-0.5">
1444
+ {maxCostUsd > 0 ? (
1445
+ <span className="text-sm text-foreground">
1446
+ ≤ ${maxCostUsd.toFixed(2)} per run
1447
+ </span>
1448
+ ) : (
1449
+ <span className="text-sm text-muted-foreground">Platform default</span>
1450
+ )}
1451
+ {/* Reachable through the API only (DD-018 D-5) — rendered when
1452
+ set so nothing the spec stores is hidden. */}
1453
+ {maxToolRounds > 0 && (
1454
+ <span className="text-xs text-muted-foreground">
1455
+ ≤ {maxToolRounds} tool rounds
1456
+ </span>
1457
+ )}
1458
+ </div>
1459
+ );
1460
+ }
1461
+
1462
+ /** The git workspace each fire clones, or the em-dash when none. */
1463
+ function WorkspaceSummary({
1464
+ entries,
1465
+ }: {
1466
+ readonly entries: readonly WorkspaceEntry[];
1467
+ }) {
1468
+ if (entries.length === 0) {
1469
+ return <span className="text-sm text-muted-foreground">—</span>;
1470
+ }
1471
+ return (
1472
+ <ul className="flex flex-col gap-0.5">
1473
+ {entries.map((entry, i) => {
1474
+ const git =
1475
+ entry.source?.source?.case === "gitRepo"
1476
+ ? entry.source.source.value
1477
+ : undefined;
1478
+ return (
1479
+ <li key={`${entry.name}-${i}`} className="text-xs text-foreground">
1480
+ <span className="font-medium">{entry.name}</span>
1481
+ {git?.url && (
1482
+ <span className="ml-1.5 font-mono text-muted-foreground">
1483
+ {git.url}
1484
+ {git.branch ? `@${git.branch}` : ""}
1485
+ </span>
1486
+ )}
1487
+ </li>
1488
+ );
1489
+ })}
1490
+ </ul>
1491
+ );
1492
+ }
1493
+
1494
+ function ReferenceLink({
1495
+ label,
1496
+ onNavigate,
1497
+ mono,
1498
+ }: {
1499
+ readonly label: string;
1500
+ readonly onNavigate?: () => void;
1501
+ readonly mono?: boolean;
1502
+ }) {
1503
+ const textClass = cn("text-sm", mono && "font-mono text-xs");
1504
+ if (!onNavigate) {
1505
+ return <span className={cn(textClass, "text-foreground")}>{label}</span>;
1506
+ }
1507
+ return (
1508
+ <button
1509
+ type="button"
1510
+ onClick={onNavigate}
1511
+ className={cn(
1512
+ textClass,
1513
+ "text-primary underline-offset-2 hover:underline",
1514
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm",
1515
+ )}
1516
+ >
1517
+ {label}
1518
+ </button>
1519
+ );
1520
+ }
1521
+
1522
+ // ---------------------------------------------------------------------------
1523
+ // Non-happy states
1524
+ // ---------------------------------------------------------------------------
1525
+
1526
+ function LoadingSkeleton({ className }: { readonly className?: string }) {
1527
+ return (
1528
+ <div
1529
+ className={cn("flex flex-col gap-6", className)}
1530
+ aria-busy="true"
1531
+ aria-label="Loading schedule details"
1532
+ >
1533
+ <div className="flex items-start gap-3">
1534
+ <div className="mt-1 size-6 shrink-0 animate-pulse rounded bg-muted" />
1535
+ <div className="flex-1 space-y-2">
1536
+ <div className="h-5 w-48 animate-pulse rounded bg-muted" />
1537
+ <div className="h-3 w-64 animate-pulse rounded bg-muted" />
1538
+ </div>
1539
+ </div>
1540
+ <div className="space-y-2">
1541
+ <div className="h-3 w-28 animate-pulse rounded bg-muted" />
1542
+ <div
1543
+ className="animate-pulse rounded-lg border border-border bg-muted-faint"
1544
+ style={{ height: "160px" }}
1545
+ />
1546
+ </div>
1547
+ <div className="space-y-2">
1548
+ <div className="h-3 w-20 animate-pulse rounded bg-muted" />
1549
+ <div
1550
+ className="animate-pulse rounded-lg border border-border bg-muted-faint"
1551
+ style={{ height: "160px" }}
1552
+ />
1553
+ </div>
1554
+ </div>
1555
+ );
1556
+ }
1557
+
1558
+ function NotFoundState({ className }: { readonly className?: string }) {
1559
+ return (
1560
+ <div
1561
+ role="status"
1562
+ className={cn("flex flex-col items-center gap-2 py-12 text-center", className)}
1563
+ >
1564
+ <ScheduleIcon className="size-10 text-muted-foreground-faint" />
1565
+ <p className="text-sm font-medium text-muted-foreground">
1566
+ Schedule not found
1567
+ </p>
1568
+ <p className="text-xs text-muted-foreground-subtle">
1569
+ This schedule doesn&apos;t exist or you don&apos;t have access to it.
1570
+ </p>
1571
+ </div>
1572
+ );
1573
+ }
1574
+
1575
+ // ---------------------------------------------------------------------------
1576
+ // Icons (inline SVG — no icon-library dependency)
1577
+ // ---------------------------------------------------------------------------
1578
+
1579
+ /** Clock-with-arrow icon shared by the schedule views. */
1580
+ export function ScheduleIcon({ className }: { readonly className?: string }) {
1581
+ return (
1582
+ <svg
1583
+ className={className}
1584
+ viewBox="0 0 24 24"
1585
+ fill="none"
1586
+ stroke="currentColor"
1587
+ strokeWidth="1.5"
1588
+ strokeLinecap="round"
1589
+ strokeLinejoin="round"
1590
+ aria-hidden="true"
1591
+ >
1592
+ <path d="M21 12a9 9 0 1 1-2.64-6.36" />
1593
+ <path d="M21 3v5h-5" />
1594
+ <path d="M12 7v5l3 3" />
1595
+ </svg>
1596
+ );
1597
+ }
1598
+
1599
+ function WarningIcon({ className }: { readonly className?: string }) {
1600
+ return (
1601
+ <svg
1602
+ className={className}
1603
+ viewBox="0 0 16 16"
1604
+ fill="none"
1605
+ stroke="currentColor"
1606
+ strokeWidth="1.5"
1607
+ strokeLinecap="round"
1608
+ strokeLinejoin="round"
1609
+ aria-hidden="true"
1610
+ >
1611
+ <path d="M8 1.5 15 14H1L8 1.5Z" />
1612
+ <path d="M8 6v4" />
1613
+ <path d="M8 12.2v.05" />
1614
+ </svg>
1615
+ );
1616
+ }
1617
+
1618
+ function PencilIcon({ className }: { readonly className?: string }) {
1619
+ return (
1620
+ <svg
1621
+ className={className}
1622
+ viewBox="0 0 16 16"
1623
+ fill="none"
1624
+ stroke="currentColor"
1625
+ strokeWidth="1.5"
1626
+ strokeLinecap="round"
1627
+ strokeLinejoin="round"
1628
+ aria-hidden="true"
1629
+ >
1630
+ <path d="M11.5 1.5a2.121 2.121 0 0 1 3 3L5 14l-4 1 1-4Z" />
1631
+ </svg>
1632
+ );
1633
+ }
1634
+
1635
+ function SpinnerIcon() {
1636
+ return (
1637
+ <svg
1638
+ width="14"
1639
+ height="14"
1640
+ viewBox="0 0 16 16"
1641
+ fill="none"
1642
+ stroke="currentColor"
1643
+ strokeWidth="2"
1644
+ strokeLinecap="round"
1645
+ className="animate-spin"
1646
+ aria-hidden="true"
1647
+ >
1648
+ <path d="M8 2a6 6 0 1 0 6 6" />
1649
+ </svg>
1650
+ );
1651
+ }