@stigmer/react 3.5.2 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/agent/AgentPicker.d.ts +10 -1
  2. package/agent/AgentPicker.d.ts.map +1 -1
  3. package/agent/AgentPicker.js +2 -2
  4. package/agent/AgentPicker.js.map +1 -1
  5. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  6. package/channel/AgentChannelsPanel.js +22 -5
  7. package/channel/AgentChannelsPanel.js.map +1 -1
  8. package/channel/ChannelTemplatesDialog.d.ts +53 -0
  9. package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
  10. package/channel/ChannelTemplatesDialog.js +150 -0
  11. package/channel/ChannelTemplatesDialog.js.map +1 -0
  12. package/channel/index.d.ts +8 -0
  13. package/channel/index.d.ts.map +1 -1
  14. package/channel/index.js +4 -0
  15. package/channel/index.js.map +1 -1
  16. package/channel/providers.d.ts +12 -0
  17. package/channel/providers.d.ts.map +1 -1
  18. package/channel/providers.js +14 -2
  19. package/channel/providers.js.map +1 -1
  20. package/channel/templatePresentation.d.ts +37 -0
  21. package/channel/templatePresentation.d.ts.map +1 -0
  22. package/channel/templatePresentation.js +69 -0
  23. package/channel/templatePresentation.js.map +1 -0
  24. package/channel/useChannelTemplateList.d.ts +64 -0
  25. package/channel/useChannelTemplateList.d.ts.map +1 -0
  26. package/channel/useChannelTemplateList.js +69 -0
  27. package/channel/useChannelTemplateList.js.map +1 -0
  28. package/channel/useChannelTemplateReadiness.d.ts +53 -0
  29. package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
  30. package/channel/useChannelTemplateReadiness.js +42 -0
  31. package/channel/useChannelTemplateReadiness.js.map +1 -0
  32. package/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
  33. package/dashboard/useDashboardFailedRuns.js +2 -0
  34. package/dashboard/useDashboardFailedRuns.js.map +1 -1
  35. package/index.d.ts +4 -2
  36. package/index.d.ts.map +1 -1
  37. package/index.js +9 -1
  38. package/index.js.map +1 -1
  39. package/library/useExportResource.d.ts +4 -3
  40. package/library/useExportResource.d.ts.map +1 -1
  41. package/library/useExportResource.js +1 -1
  42. package/library/useExportResource.js.map +1 -1
  43. package/organization/OrgProvider.d.ts +11 -4
  44. package/organization/OrgProvider.d.ts.map +1 -1
  45. package/organization/OrgProvider.js +28 -4
  46. package/organization/OrgProvider.js.map +1 -1
  47. package/package.json +4 -4
  48. package/resource-detail/useDeleteResource.d.ts +1 -1
  49. package/resource-detail/useDeleteResource.d.ts.map +1 -1
  50. package/resource-detail/useDeleteResource.js +5 -0
  51. package/resource-detail/useDeleteResource.js.map +1 -1
  52. package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
  53. package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
  54. package/resource-workbench/components/ResourceWorkbench.js +6 -2
  55. package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
  56. package/resource-workbench/hooks/useResourceCollection.d.ts +8 -0
  57. package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
  58. package/resource-workbench/hooks/useResourceCollection.js +2 -2
  59. package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
  60. package/schedule/CadenceField.d.ts +31 -0
  61. package/schedule/CadenceField.d.ts.map +1 -0
  62. package/schedule/CadenceField.js +116 -0
  63. package/schedule/CadenceField.js.map +1 -0
  64. package/schedule/ScheduleDetailView.d.ts +58 -0
  65. package/schedule/ScheduleDetailView.d.ts.map +1 -0
  66. package/schedule/ScheduleDetailView.js +236 -0
  67. package/schedule/ScheduleDetailView.js.map +1 -0
  68. package/schedule/ScheduleForm.d.ts +43 -0
  69. package/schedule/ScheduleForm.d.ts.map +1 -0
  70. package/schedule/ScheduleForm.js +129 -0
  71. package/schedule/ScheduleForm.js.map +1 -0
  72. package/schedule/ScheduleRowActions.d.ts +20 -0
  73. package/schedule/ScheduleRowActions.d.ts.map +1 -0
  74. package/schedule/ScheduleRowActions.js +54 -0
  75. package/schedule/ScheduleRowActions.js.map +1 -0
  76. package/schedule/TimeZoneField.d.ts +32 -0
  77. package/schedule/TimeZoneField.d.ts.map +1 -0
  78. package/schedule/TimeZoneField.js +104 -0
  79. package/schedule/TimeZoneField.js.map +1 -0
  80. package/schedule/cadence.d.ts +95 -0
  81. package/schedule/cadence.d.ts.map +1 -0
  82. package/schedule/cadence.js +242 -0
  83. package/schedule/cadence.js.map +1 -0
  84. package/schedule/index.d.ts +31 -0
  85. package/schedule/index.d.ts.map +1 -0
  86. package/schedule/index.js +16 -0
  87. package/schedule/index.js.map +1 -0
  88. package/schedule/scheduleColumns.d.ts +26 -0
  89. package/schedule/scheduleColumns.d.ts.map +1 -0
  90. package/schedule/scheduleColumns.js +77 -0
  91. package/schedule/scheduleColumns.js.map +1 -0
  92. package/schedule/scheduleListFn.d.ts +50 -0
  93. package/schedule/scheduleListFn.d.ts.map +1 -0
  94. package/schedule/scheduleListFn.js +65 -0
  95. package/schedule/scheduleListFn.js.map +1 -0
  96. package/schedule/scheduleState.d.ts +43 -0
  97. package/schedule/scheduleState.d.ts.map +1 -0
  98. package/schedule/scheduleState.js +67 -0
  99. package/schedule/scheduleState.js.map +1 -0
  100. package/schedule/useCreateSchedule.d.ts +57 -0
  101. package/schedule/useCreateSchedule.d.ts.map +1 -0
  102. package/schedule/useCreateSchedule.js +61 -0
  103. package/schedule/useCreateSchedule.js.map +1 -0
  104. package/schedule/useResumeSchedule.d.ts +30 -0
  105. package/schedule/useResumeSchedule.d.ts.map +1 -0
  106. package/schedule/useResumeSchedule.js +45 -0
  107. package/schedule/useResumeSchedule.js.map +1 -0
  108. package/schedule/useSchedule.d.ts +39 -0
  109. package/schedule/useSchedule.d.ts.map +1 -0
  110. package/schedule/useSchedule.js +44 -0
  111. package/schedule/useSchedule.js.map +1 -0
  112. package/schedule/useScheduleCount.d.ts +38 -0
  113. package/schedule/useScheduleCount.d.ts.map +1 -0
  114. package/schedule/useScheduleCount.js +30 -0
  115. package/schedule/useScheduleCount.js.map +1 -0
  116. package/schedule/useScheduleList.d.ts +48 -0
  117. package/schedule/useScheduleList.d.ts.map +1 -0
  118. package/schedule/useScheduleList.js +43 -0
  119. package/schedule/useScheduleList.js.map +1 -0
  120. package/schedule/useSetScheduleEnabled.d.ts +31 -0
  121. package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
  122. package/schedule/useSetScheduleEnabled.js +53 -0
  123. package/schedule/useSetScheduleEnabled.js.map +1 -0
  124. package/schedule/useTriggerSchedule.d.ts +28 -0
  125. package/schedule/useTriggerSchedule.d.ts.map +1 -0
  126. package/schedule/useTriggerSchedule.js +43 -0
  127. package/schedule/useTriggerSchedule.js.map +1 -0
  128. package/search/useResourceCount.d.ts +2 -1
  129. package/search/useResourceCount.d.ts.map +1 -1
  130. package/search/useResourceCount.js +6 -1
  131. package/search/useResourceCount.js.map +1 -1
  132. package/src/agent/AgentPicker.tsx +13 -1
  133. package/src/channel/AgentChannelsPanel.tsx +43 -1
  134. package/src/channel/ChannelTemplatesDialog.tsx +433 -0
  135. package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
  136. package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
  137. package/src/channel/__tests__/providers.test.ts +9 -0
  138. package/src/channel/__tests__/templatePresentation.test.ts +96 -0
  139. package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
  140. package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
  141. package/src/channel/index.ts +15 -0
  142. package/src/channel/providers.ts +26 -2
  143. package/src/channel/templatePresentation.ts +82 -0
  144. package/src/channel/useChannelTemplateList.ts +123 -0
  145. package/src/channel/useChannelTemplateReadiness.ts +69 -0
  146. package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
  147. package/src/dashboard/useDashboardFailedRuns.ts +2 -0
  148. package/src/execution/__tests__/PlanArtifactCard.test.tsx +23 -2
  149. package/src/index.ts +65 -0
  150. package/src/library/useExportResource.ts +4 -3
  151. package/src/organization/OrgProvider.tsx +29 -4
  152. package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
  153. package/src/resource-detail/useDeleteResource.ts +11 -1
  154. package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
  155. package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
  156. package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
  157. package/src/schedule/CadenceField.tsx +365 -0
  158. package/src/schedule/ScheduleDetailView.tsx +583 -0
  159. package/src/schedule/ScheduleForm.tsx +405 -0
  160. package/src/schedule/ScheduleRowActions.tsx +124 -0
  161. package/src/schedule/TimeZoneField.tsx +199 -0
  162. package/src/schedule/__tests__/ScheduleDetailView.test.tsx +260 -0
  163. package/src/schedule/__tests__/cadence.test.ts +278 -0
  164. package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
  165. package/src/schedule/__tests__/scheduleCreation.test.tsx +340 -0
  166. package/src/schedule/__tests__/scheduleHooks.test.tsx +312 -0
  167. package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
  168. package/src/schedule/__tests__/scheduleState.test.ts +133 -0
  169. package/src/schedule/cadence.ts +298 -0
  170. package/src/schedule/index.ts +57 -0
  171. package/src/schedule/scheduleColumns.tsx +118 -0
  172. package/src/schedule/scheduleListFn.ts +108 -0
  173. package/src/schedule/scheduleState.ts +96 -0
  174. package/src/schedule/useCreateSchedule.ts +88 -0
  175. package/src/schedule/useResumeSchedule.ts +67 -0
  176. package/src/schedule/useSchedule.ts +67 -0
  177. package/src/schedule/useScheduleCount.ts +57 -0
  178. package/src/schedule/useScheduleList.ts +83 -0
  179. package/src/schedule/useSetScheduleEnabled.ts +84 -0
  180. package/src/schedule/useTriggerSchedule.ts +65 -0
  181. package/src/search/__tests__/useResourceCount.test.tsx +89 -0
  182. package/src/search/useResourceCount.ts +8 -2
  183. package/src/workflow/WorkflowDashboard.tsx +1 -0
  184. package/src/workflow/useWorkflowExecutionList.ts +13 -2
  185. package/styles.css +1 -1
  186. package/workflow/CanvasTransitionEdge.d.ts +1 -1
  187. package/workflow/WorkflowDashboard.d.ts.map +1 -1
  188. package/workflow/WorkflowDashboard.js +1 -0
  189. package/workflow/WorkflowDashboard.js.map +1 -1
  190. package/workflow/useWorkflowExecutionList.d.ts +10 -0
  191. package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
  192. package/workflow/useWorkflowExecutionList.js +3 -2
  193. package/workflow/useWorkflowExecutionList.js.map +1 -1
@@ -0,0 +1,118 @@
1
+ "use client";
2
+
3
+ import { timestampDate } from "@bufbuild/protobuf/wkt";
4
+ import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
5
+ import { formatRelativeTime } from "../activity/format-relative-time.js";
6
+ import { StatusBadge } from "../resource-workbench/components/StatusBadge.js";
7
+ import type { WorkbenchColumnDef } from "../resource-workbench/types.js";
8
+ import { deriveScheduleState, formatNextFire } from "./scheduleState.js";
9
+
10
+ /** Options for {@link createScheduleColumns}. */
11
+ export interface ScheduleColumnsOptions {
12
+ /**
13
+ * Clock provider for the "next fire" and "last run" columns.
14
+ * Injectable for deterministic tests and Scenar fixtures.
15
+ * @default () => new Date()
16
+ */
17
+ readonly now?: () => Date;
18
+ }
19
+
20
+ /**
21
+ * Column set for a schedule workbench — shared by every console so the
22
+ * list renders identically everywhere (DD-016).
23
+ *
24
+ * The columns exist because schedules are listed via the direct query:
25
+ * each row is a full `Schedule` proto, so the operational fields the
26
+ * issue calls for (next fire, last run, enabled/paused state) render
27
+ * straight from the row — no generic `SearchResult` limitations.
28
+ *
29
+ * Columns are not sortable: the direct query has one fixed server-side
30
+ * order (newest first), and a sort affordance that silently does
31
+ * nothing would lie.
32
+ */
33
+ export function createScheduleColumns(
34
+ options?: ScheduleColumnsOptions,
35
+ ): WorkbenchColumnDef<Schedule>[] {
36
+ const now = options?.now ?? (() => new Date());
37
+
38
+ return [
39
+ {
40
+ id: "name",
41
+ header: "Name",
42
+ cell: (item) => (
43
+ <span className="font-medium text-foreground">
44
+ {item.metadata?.name || item.metadata?.slug}
45
+ </span>
46
+ ),
47
+ flex: 2,
48
+ },
49
+ {
50
+ id: "state",
51
+ header: "State",
52
+ cell: (item) => {
53
+ const info = deriveScheduleState(item.spec, item.status);
54
+ return <StatusBadge phase={info.phase} label={info.label} />;
55
+ },
56
+ flex: 1,
57
+ },
58
+ {
59
+ id: "cron",
60
+ header: "Cadence",
61
+ cell: (item) => (
62
+ <span className="inline-flex items-baseline gap-1.5">
63
+ <code className="font-mono text-xs text-foreground">
64
+ {item.spec?.cron || "—"}
65
+ </code>
66
+ <span className="text-xs text-muted-foreground">
67
+ {item.spec?.timeZone}
68
+ </span>
69
+ </span>
70
+ ),
71
+ flex: 1.5,
72
+ },
73
+ {
74
+ id: "next-fire",
75
+ header: "Next fire",
76
+ cell: (item) => {
77
+ const info = deriveScheduleState(item.spec, item.status);
78
+ const nextFireAt = item.status?.nextFireAt;
79
+ return (
80
+ <span className="text-muted-foreground">
81
+ {info.state === "active" && nextFireAt
82
+ ? formatNextFire(timestampDate(nextFireAt), now())
83
+ : "—"}
84
+ </span>
85
+ );
86
+ },
87
+ flex: 1,
88
+ },
89
+ {
90
+ id: "last-run",
91
+ header: "Last run",
92
+ cell: (item) => (
93
+ <span className="text-muted-foreground">
94
+ {item.status?.lastFireAt
95
+ ? formatRelativeTime(timestampDate(item.status.lastFireAt), now())
96
+ : "Never"}
97
+ </span>
98
+ ),
99
+ flex: 1,
100
+ },
101
+ {
102
+ id: "target",
103
+ header: "Target agent",
104
+ cell: (item) => {
105
+ const target =
106
+ item.spec?.target?.case === "agent" ? item.spec.target.value : undefined;
107
+ return (
108
+ <span className="text-muted-foreground">
109
+ {target?.agentRef
110
+ ? `${target.agentRef.org}/${target.agentRef.slug}`
111
+ : "—"}
112
+ </span>
113
+ );
114
+ },
115
+ flex: 1.5,
116
+ },
117
+ ];
118
+ }
@@ -0,0 +1,108 @@
1
+ // Schedule list fetching — the direct-query core plus the workbench adapter.
2
+ //
3
+ // Schedules are the one Library kind NOT backed by the search service: the
4
+ // generic `SearchResult` summary cannot carry the operational fields a
5
+ // schedule list exists to show (`next_fire_at`, enabled/paused state, last
6
+ // run), while the direct `listSchedules` query returns full `Schedule`
7
+ // protos and was explicitly shaped for "the org-context view a console tab
8
+ // needs". Both consoles consume this single implementation — inlining it
9
+ // per-app would duplicate correctness-bearing pagination logic (DD-016).
10
+
11
+ import { create } from "@bufbuild/protobuf";
12
+ import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
13
+ import {
14
+ ListSchedulesRequestSchema,
15
+ type ListSchedulesRequest,
16
+ type ScheduleList,
17
+ } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/io_pb";
18
+ import type { SearchResult } from "@stigmer/protos/ai/stigmer/search/v1/io_pb";
19
+ import type { ListParams, ListResult } from "@stigmer/sdk";
20
+
21
+ /** The slice of the Stigmer client this module needs (mockable in tests). */
22
+ export interface ScheduleListClient {
23
+ readonly schedule: {
24
+ list(input: ListSchedulesRequest): Promise<ScheduleList>;
25
+ };
26
+ }
27
+
28
+ /** One page of schedules, with pagination totals. */
29
+ export interface SchedulePage {
30
+ /** Full `Schedule` protos (spec + status) for the requested page. */
31
+ readonly items: readonly Schedule[];
32
+ /** Total schedules in the org (across all pages). */
33
+ readonly totalCount: number;
34
+ /** Total pages at the requested page size. */
35
+ readonly totalPages: number;
36
+ }
37
+
38
+ const DEFAULT_PAGE_SIZE = 20;
39
+
40
+ /**
41
+ * Fetch one page of an organization's schedules via the direct query.
42
+ *
43
+ * Handles a verified split between the two server editions: cloud slices
44
+ * pages server-side, while the OSS server documents that it ignores
45
+ * `page_info` and returns every matching schedule. A server that
46
+ * paginated can never return more rows than the requested size, so an
47
+ * overflowing response is the tell that the server ignored pagination —
48
+ * only then is the page sliced locally (absolute indices into the full
49
+ * list). A cloud page passes through untouched.
50
+ */
51
+ export async function listSchedulesPage(
52
+ client: ScheduleListClient,
53
+ org: string,
54
+ page: { readonly num?: number; readonly size?: number } = {},
55
+ ): Promise<SchedulePage> {
56
+ const size = page.size && page.size > 0 ? page.size : DEFAULT_PAGE_SIZE;
57
+ const num = page.num && page.num > 0 ? page.num : 1;
58
+
59
+ const result = await client.schedule.list(
60
+ create(ListSchedulesRequestSchema, {
61
+ org,
62
+ pageInfo: { num, size },
63
+ }),
64
+ );
65
+
66
+ const items =
67
+ result.items.length > size
68
+ ? result.items.slice((num - 1) * size, num * size)
69
+ : result.items;
70
+
71
+ return {
72
+ items,
73
+ totalCount: result.totalCount,
74
+ totalPages: Math.ceil(result.totalCount / size),
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Build a `ResourceWorkbench`-compatible `listFn` over the direct
80
+ * schedule query.
81
+ *
82
+ * The returned entries are full `Schedule` protos occupying the
83
+ * `SearchResult` slot of `ListResult` — `ResourceWorkbench` is generic
84
+ * over its row type (`useResourceCollection` casts entries through to
85
+ * `TData`), and this is the one documented place where that cast
86
+ * happens. Consumers type their workbench as
87
+ * `ResourceWorkbench<Schedule>` and pass `getItemId={(s) => s.metadata?.id}`.
88
+ *
89
+ * `params.query` and `params.crossOrgPublic` are ignored: schedules have
90
+ * no server-side text search (render the workbench with
91
+ * `searchable={false}`) and are never public/cross-org.
92
+ */
93
+ export function createScheduleListFn(
94
+ client: ScheduleListClient,
95
+ ): (params: ListParams) => Promise<ListResult> {
96
+ return async (params: ListParams): Promise<ListResult> => {
97
+ const { items, totalCount, totalPages } = await listSchedulesPage(
98
+ client,
99
+ params.org,
100
+ params.page ?? {},
101
+ );
102
+ return {
103
+ entries: items as unknown as SearchResult[],
104
+ totalCount,
105
+ totalPages,
106
+ };
107
+ };
108
+ }
@@ -0,0 +1,96 @@
1
+ // Pure schedule-state derivation — the disabled-vs-paused vocabulary,
2
+ // computed on read.
3
+ //
4
+ // A schedule that is not firing is stopped by one of two levers with two
5
+ // different writers and two different remedies (the backend pins this
6
+ // vocabulary in its controller docs):
7
+ //
8
+ // - "disabled" — the OWNER's switch (`spec.enabled = false`), cleared
9
+ // by re-applying the manifest or the console's Enable action.
10
+ // - "paused" — the PLATFORM's failure-streak latch
11
+ // (`status.paused_reason` non-empty), cleared only by Resume.
12
+ //
13
+ // The console must never collapse these into one badge (stigmer/stigmer#352):
14
+ // each state names its own remedy. When both levers are set, "disabled"
15
+ // wins for display — the owner's intent dominates — but callers should
16
+ // keep the paused reason visible alongside (the banner does).
17
+
18
+ import type { ScheduleSpec } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/spec_pb";
19
+ import type { ScheduleStatus } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/status_pb";
20
+ import type { StatusPhase } from "../resource-workbench/types.js";
21
+
22
+ /** The three mutually-exclusive display states of a schedule. */
23
+ export type ScheduleState = "active" | "disabled" | "paused";
24
+
25
+ /** Derived display state: the tri-state plus its shared-badge rendering. */
26
+ export interface ScheduleStateInfo {
27
+ /** The display state ("disabled" wins when both levers are set). */
28
+ readonly state: ScheduleState;
29
+ /** `StatusPhase` for the shared `StatusBadge` (`--stgm-status-*` tokens). */
30
+ readonly phase: StatusPhase;
31
+ /** Human-readable badge label. */
32
+ readonly label: string;
33
+ /**
34
+ * `true` when the platform pause latch is set — independent of the
35
+ * display state, so a disabled-AND-paused schedule can still surface
36
+ * its `status.pausedReason`.
37
+ */
38
+ readonly isPaused: boolean;
39
+ }
40
+
41
+ /**
42
+ * Derive a schedule's display state from its spec and status.
43
+ *
44
+ * A missing spec derives as "disabled" — the same fail-closed reading the
45
+ * server applies to a manifest that omits `enabled`.
46
+ */
47
+ export function deriveScheduleState(
48
+ spec: ScheduleSpec | undefined,
49
+ status: ScheduleStatus | undefined,
50
+ ): ScheduleStateInfo {
51
+ const isPaused = (status?.pausedReason ?? "") !== "";
52
+
53
+ if (!spec?.enabled) {
54
+ return { state: "disabled", phase: "disabled", label: "Disabled", isPaused };
55
+ }
56
+ if (isPaused) {
57
+ return { state: "paused", phase: "degraded", label: "Paused", isPaused };
58
+ }
59
+ return { state: "active", phase: "ready", label: "Active", isPaused };
60
+ }
61
+
62
+ /**
63
+ * Compact future-instant formatter for a schedule's next fire time:
64
+ * `now`, `in 5m`, `in 3h`, `in 6d`, then a short date once "days from
65
+ * now" stops being how people think about it.
66
+ *
67
+ * The schedule-local sibling of the activity module's past-only
68
+ * `formatRelativeTime` (same compact units, same date tail) — kept
69
+ * separate because that util's contract is deliberately past-only.
70
+ * A fire time slightly in the past (a tick mid-flight, clock skew)
71
+ * renders as `now` rather than a nonsense negative countdown.
72
+ *
73
+ * `now` is injected, never read from the live clock — required for
74
+ * deterministic tests and Scenar fixtures.
75
+ */
76
+ export function formatNextFire(date: Date, now: Date): string {
77
+ const deltaMs = date.getTime() - now.getTime();
78
+
79
+ if (deltaMs < 60_000) return "now";
80
+
81
+ const minutes = Math.floor(deltaMs / 60_000);
82
+ if (minutes < 60) return `in ${minutes}m`;
83
+
84
+ const hours = Math.floor(minutes / 60);
85
+ if (hours < 24) return `in ${hours}h`;
86
+
87
+ const days = Math.floor(hours / 24);
88
+ if (days < 7) return `in ${days}d`;
89
+
90
+ const sameYear = date.getFullYear() === now.getFullYear();
91
+ return date.toLocaleDateString(undefined, {
92
+ month: "short",
93
+ day: "numeric",
94
+ ...(sameYear ? {} : { year: "numeric" }),
95
+ });
96
+ }
@@ -0,0 +1,88 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import type { ScheduleInput } from "@stigmer/sdk";
5
+ import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
6
+ import { useStigmer } from "../hooks.js";
7
+ import { toError } from "../internal/toError.js";
8
+
9
+ /** Return value of {@link useCreateSchedule}. */
10
+ export interface UseCreateScheduleReturn {
11
+ /**
12
+ * Submit a {@link ScheduleInput} to create (or upsert) a schedule.
13
+ *
14
+ * Uses `stigmer.schedule.apply()` — the idempotent upsert operation.
15
+ * Resolves with the server-created `Schedule` proto including
16
+ * populated metadata (id, slug, audit timestamps) and the
17
+ * platform-computed `status.next_fire_at`.
18
+ */
19
+ readonly create: (input: ScheduleInput) => Promise<Schedule>;
20
+ /** `true` while the apply request is in flight. */
21
+ readonly isCreating: boolean;
22
+ /** Error from the last failed create, or `null` when healthy. */
23
+ readonly error: Error | null;
24
+ /** Reset `error` to `null`. */
25
+ readonly clearError: () => void;
26
+ }
27
+
28
+ /**
29
+ * Behavior hook that wraps `schedule.apply()` with loading and error
30
+ * state.
31
+ *
32
+ * Creates a schedule from a {@link ScheduleInput}. Uses `apply()`
33
+ * (upsert) rather than `create()` so re-submissions are idempotent —
34
+ * matching the CLI's `stigmer apply` semantics and the established SDK
35
+ * mutation hook pattern (`isCreating` flag, `error` state, `clearError`
36
+ * reset, result returned from the promise, not stored in hook state).
37
+ *
38
+ * NOTE: this hook is for CREATION. Editing an existing schedule must
39
+ * NOT go through `ScheduleInput` — the input type cannot express every
40
+ * metadata field (tags, description), and the server replaces spec and
41
+ * mutable metadata wholesale on update, so a down-converting edit would
42
+ * silently wipe them. Edit flows use the full-proto path instead (see
43
+ * `useSetScheduleEnabled` for the pattern).
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * const { create, isCreating, error, clearError } = useCreateSchedule();
48
+ *
49
+ * const schedule = await create({
50
+ * name: "daily-fee-reminders",
51
+ * org: "acme",
52
+ * cron: "0 9 * * *",
53
+ * timeZone: "Asia/Kolkata",
54
+ * enabled: false,
55
+ * agent: {
56
+ * agentRef: { org: "acme", slug: "billing-assistant" },
57
+ * message: "Send today's fee reminders.",
58
+ * },
59
+ * });
60
+ * // schedule.metadata?.slug → "daily-fee-reminders"
61
+ * ```
62
+ */
63
+ export function useCreateSchedule(): UseCreateScheduleReturn {
64
+ const stigmer = useStigmer();
65
+ const [isCreating, setIsCreating] = useState(false);
66
+ const [error, setError] = useState<Error | null>(null);
67
+
68
+ const clearError = useCallback(() => setError(null), []);
69
+
70
+ const create = useCallback(
71
+ async (input: ScheduleInput): Promise<Schedule> => {
72
+ setIsCreating(true);
73
+ setError(null);
74
+
75
+ try {
76
+ return await stigmer.schedule.apply(input);
77
+ } catch (err) {
78
+ setError(toError(err));
79
+ throw err;
80
+ } finally {
81
+ setIsCreating(false);
82
+ }
83
+ },
84
+ [stigmer],
85
+ );
86
+
87
+ return { create, isCreating, error, clearError };
88
+ }
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
5
+ import { getUserMessage } from "@stigmer/sdk";
6
+ import { useStigmer } from "../hooks.js";
7
+ import { toast } from "../feedback/toast.js";
8
+ import { toError } from "../internal/toError.js";
9
+
10
+ /** Return value of {@link useResumeSchedule}. */
11
+ export interface UseResumeScheduleReturn {
12
+ /**
13
+ * Clear the platform's failure-streak pause and let the schedule fire
14
+ * again. Resolves with the post-resume schedule; the caller refetches
15
+ * or replaces its local copy from it.
16
+ */
17
+ readonly resumeSchedule: (scheduleId: string) => Promise<Schedule>;
18
+ /** `true` while the resume request is in flight. */
19
+ readonly isResuming: boolean;
20
+ /** Error from the last failed resume, or `null` when healthy. */
21
+ readonly error: Error | null;
22
+ }
23
+
24
+ /**
25
+ * Behavior hook that resumes a platform-paused schedule.
26
+ *
27
+ * Resume is the ONE path that clears `status.paused_reason` (and the
28
+ * failure streak with it) — re-applying the manifest deliberately does
29
+ * not, so a routine GitOps apply can never silently un-pause a failing
30
+ * schedule. Resuming a schedule that is not paused is a server-side
31
+ * no-op; resuming a disabled schedule clears the pause but the schedule
32
+ * stays disabled (the owner's lever is separate — see
33
+ * {@link useSetScheduleEnabled}).
34
+ *
35
+ * Error toasts relay the server's message verbatim: the copy is
36
+ * identical across editions and states the remedy.
37
+ */
38
+ export function useResumeSchedule(): UseResumeScheduleReturn {
39
+ const stigmer = useStigmer();
40
+ const [isResuming, setIsResuming] = useState(false);
41
+ const [error, setError] = useState<Error | null>(null);
42
+
43
+ const resumeSchedule = useCallback(
44
+ async (scheduleId: string): Promise<Schedule> => {
45
+ setIsResuming(true);
46
+ setError(null);
47
+ try {
48
+ const resumed = await stigmer.schedule.resume(scheduleId);
49
+ toast.success("Schedule resumed");
50
+ return resumed;
51
+ } catch (err) {
52
+ const wrapped = toError(err);
53
+ setError(wrapped);
54
+ toast.error(getUserMessage(err));
55
+ throw wrapped;
56
+ } finally {
57
+ setIsResuming(false);
58
+ }
59
+ },
60
+ [stigmer],
61
+ );
62
+
63
+ return useMemo(
64
+ () => ({ resumeSchedule, isResuming, error }),
65
+ [resumeSchedule, isResuming, error],
66
+ );
67
+ }
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
4
+ import { isNotFound } from "@stigmer/sdk";
5
+ import { useStigmer } from "../hooks.js";
6
+ import { useFetch } from "../internal/useFetch.js";
7
+
8
+ /** Return value of {@link useSchedule}. */
9
+ export interface UseScheduleReturn {
10
+ /** The resolved schedule, or `null` while loading, on error, or when not found. */
11
+ readonly schedule: Schedule | null;
12
+ /** `true` while the initial fetch is in flight. */
13
+ readonly isLoading: boolean;
14
+ /** `true` while a background refetch is in flight. */
15
+ readonly isRefetching: boolean;
16
+ /** Error from the last failed request, or `null` when healthy. */
17
+ readonly error: Error | null;
18
+ /** Discard cached data and re-fetch the schedule from the server. */
19
+ readonly refetch: () => void;
20
+ }
21
+
22
+ /**
23
+ * Data hook that fetches a single Schedule by organization and slug.
24
+ *
25
+ * Wraps `stigmer.schedule.getByReference()` with loading, error, and
26
+ * not-found state management. The loaded resource carries the spec
27
+ * (cron, time zone, enabled, target) and the platform-written status
28
+ * (`next_fire_at`, `last_fire_at`, `last_execution_id`,
29
+ * `consecutive_failures`, `paused_reason`) — everything the detail view
30
+ * renders, including the disabled-vs-paused distinction (see
31
+ * {@link deriveScheduleState}).
32
+ *
33
+ * Pass `null` for either `org` or `slug` to skip fetching (stable
34
+ * no-op).
35
+ *
36
+ * **Not-found handling:** a NOT_FOUND response sets `schedule` to
37
+ * `null` without raising an error; `schedule === null && !isLoading
38
+ * && !error` means the resource does not exist.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * const { schedule, isLoading, error, refetch } = useSchedule("isc", "daily-fee-reminders");
43
+ * ```
44
+ */
45
+ export function useSchedule(
46
+ org: string | null,
47
+ slug: string | null,
48
+ ): UseScheduleReturn {
49
+ const stigmer = useStigmer();
50
+
51
+ const { data: schedule, isLoading, isRefetching, error, refetch } = useFetch(
52
+ org && slug
53
+ ? async () => {
54
+ try {
55
+ return await stigmer.schedule.getByReference({ org, slug });
56
+ } catch (err) {
57
+ if (isNotFound(err)) return null;
58
+ throw err;
59
+ }
60
+ }
61
+ : null,
62
+ [org, slug, stigmer],
63
+ null,
64
+ );
65
+
66
+ return { schedule, isLoading, isRefetching, error, refetch };
67
+ }
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { useStigmer } from "../hooks.js";
5
+ import { useResourceCount } from "../search/index.js";
6
+ import { createScheduleListFn } from "./scheduleListFn.js";
7
+
8
+ /** Options for {@link useScheduleCount}. */
9
+ export interface UseScheduleCountOptions {
10
+ /** Opaque token that forces a recount when its value changes. */
11
+ readonly refetchToken?: unknown;
12
+ }
13
+
14
+ /** Return value of {@link useScheduleCount}. */
15
+ export interface UseScheduleCountReturn {
16
+ /**
17
+ * Total number of schedules in the organization. `undefined` until
18
+ * the first successful fetch completes.
19
+ */
20
+ readonly count: number | undefined;
21
+ /** `true` while the count fetch is in flight. */
22
+ readonly isLoading: boolean;
23
+ /** Error from the last failed request, or `null` when healthy. */
24
+ readonly error: Error | null;
25
+ /** Discard cached data and re-fetch the count from the server. */
26
+ readonly refetch: () => void;
27
+ }
28
+
29
+ /**
30
+ * Data hook that fetches the total count of an organization's schedules.
31
+ *
32
+ * Issues a minimal direct `listSchedules` call to retrieve only the
33
+ * total count. Useful for the Library landing card and dashboard
34
+ * widgets. There is no scope option: schedules are org-only.
35
+ *
36
+ * For the full paginated schedule list, use {@link useScheduleList}
37
+ * instead.
38
+ *
39
+ * Pass `null` as `org` to skip fetching (stable no-op).
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * const { count, isLoading } = useScheduleCount("isc");
44
+ * ```
45
+ */
46
+ export function useScheduleCount(
47
+ org: string | null,
48
+ options?: UseScheduleCountOptions,
49
+ ): UseScheduleCountReturn {
50
+ const stigmer = useStigmer();
51
+
52
+ const listFn = useMemo(() => createScheduleListFn(stigmer), [stigmer]);
53
+
54
+ return useResourceCount(listFn, org, {
55
+ refetchToken: options?.refetchToken,
56
+ });
57
+ }
@@ -0,0 +1,83 @@
1
+ "use client";
2
+
3
+ import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
4
+ import { useStigmer } from "../hooks.js";
5
+ import { useFetch } from "../internal/useFetch.js";
6
+ import { listSchedulesPage, type SchedulePage } from "./scheduleListFn.js";
7
+
8
+ /** Options for {@link useScheduleList}. */
9
+ export interface UseScheduleListOptions {
10
+ /** Maximum schedules per page. @default 20 */
11
+ readonly pageSize?: number;
12
+ /** Page number (1-indexed). @default 1 */
13
+ readonly page?: number;
14
+ }
15
+
16
+ /** Return value of {@link useScheduleList}. */
17
+ export interface UseScheduleListReturn {
18
+ /** Full `Schedule` protos (spec + status) for the current page. */
19
+ readonly schedules: readonly Schedule[];
20
+ /** Total number of schedules in the organization. */
21
+ readonly totalCount: number;
22
+ /** Total pages available at the current page size. */
23
+ readonly totalPages: number;
24
+ /** The current page number (mirrors the `page` option). */
25
+ readonly currentPage: number;
26
+ /** `true` while the initial fetch is in flight. */
27
+ readonly isLoading: boolean;
28
+ /** `true` while a background refetch is in flight. */
29
+ readonly isRefetching: boolean;
30
+ /** Error from the last failed request, or `null` when healthy. */
31
+ readonly error: Error | null;
32
+ /** Discard cached data and re-fetch the current page from the server. */
33
+ readonly refetch: () => void;
34
+ }
35
+
36
+ const INITIAL_PAGE: SchedulePage = { items: [], totalCount: 0, totalPages: 0 };
37
+
38
+ /**
39
+ * Data hook that fetches a paginated list of an organization's schedules.
40
+ *
41
+ * Backed by the direct `listSchedules` query — not the search service —
42
+ * so each entry is a full `Schedule` proto with the operational status
43
+ * fields a schedule list exists to show (`next_fire_at`, enabled/paused
44
+ * state, `last_fire_at`). There is no text query or visibility scope:
45
+ * schedules are org-only and have no server-side search.
46
+ *
47
+ * Pass `null` as `org` to skip fetching (stable no-op).
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * const { schedules, totalCount, isLoading } = useScheduleList("isc", {
52
+ * page: 1,
53
+ * pageSize: 20,
54
+ * });
55
+ * ```
56
+ */
57
+ export function useScheduleList(
58
+ org: string | null,
59
+ options?: UseScheduleListOptions,
60
+ ): UseScheduleListReturn {
61
+ const stigmer = useStigmer();
62
+ const pageSize = options?.pageSize;
63
+ const page = options?.page ?? 1;
64
+
65
+ const { data, isLoading, isRefetching, error, refetch } = useFetch<SchedulePage>(
66
+ org
67
+ ? () => listSchedulesPage(stigmer, org, { num: page, size: pageSize })
68
+ : null,
69
+ [stigmer, org, page, pageSize],
70
+ INITIAL_PAGE,
71
+ );
72
+
73
+ return {
74
+ schedules: data.items,
75
+ totalCount: data.totalCount,
76
+ totalPages: data.totalPages,
77
+ currentPage: page,
78
+ isLoading,
79
+ isRefetching,
80
+ error,
81
+ refetch,
82
+ };
83
+ }