@stigmer/react 3.5.3 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/agent/AgentPicker.d.ts +10 -1
  2. package/agent/AgentPicker.d.ts.map +1 -1
  3. package/agent/AgentPicker.js +2 -2
  4. package/agent/AgentPicker.js.map +1 -1
  5. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  6. package/channel/AgentChannelsPanel.js +22 -5
  7. package/channel/AgentChannelsPanel.js.map +1 -1
  8. package/channel/ChannelTemplatesDialog.d.ts +53 -0
  9. package/channel/ChannelTemplatesDialog.d.ts.map +1 -0
  10. package/channel/ChannelTemplatesDialog.js +150 -0
  11. package/channel/ChannelTemplatesDialog.js.map +1 -0
  12. package/channel/index.d.ts +8 -0
  13. package/channel/index.d.ts.map +1 -1
  14. package/channel/index.js +4 -0
  15. package/channel/index.js.map +1 -1
  16. package/channel/providers.d.ts +12 -0
  17. package/channel/providers.d.ts.map +1 -1
  18. package/channel/providers.js +14 -2
  19. package/channel/providers.js.map +1 -1
  20. package/channel/templatePresentation.d.ts +37 -0
  21. package/channel/templatePresentation.d.ts.map +1 -0
  22. package/channel/templatePresentation.js +69 -0
  23. package/channel/templatePresentation.js.map +1 -0
  24. package/channel/useChannelTemplateList.d.ts +64 -0
  25. package/channel/useChannelTemplateList.d.ts.map +1 -0
  26. package/channel/useChannelTemplateList.js +69 -0
  27. package/channel/useChannelTemplateList.js.map +1 -0
  28. package/channel/useChannelTemplateReadiness.d.ts +53 -0
  29. package/channel/useChannelTemplateReadiness.d.ts.map +1 -0
  30. package/channel/useChannelTemplateReadiness.js +42 -0
  31. package/channel/useChannelTemplateReadiness.js.map +1 -0
  32. package/dashboard/useDashboardFailedRuns.d.ts.map +1 -1
  33. package/dashboard/useDashboardFailedRuns.js +2 -0
  34. package/dashboard/useDashboardFailedRuns.js.map +1 -1
  35. package/index.d.ts +4 -2
  36. package/index.d.ts.map +1 -1
  37. package/index.js +9 -1
  38. package/index.js.map +1 -1
  39. package/library/useExportResource.d.ts +4 -3
  40. package/library/useExportResource.d.ts.map +1 -1
  41. package/library/useExportResource.js +1 -1
  42. package/library/useExportResource.js.map +1 -1
  43. package/organization/OrgProvider.d.ts +11 -4
  44. package/organization/OrgProvider.d.ts.map +1 -1
  45. package/organization/OrgProvider.js +28 -4
  46. package/organization/OrgProvider.js.map +1 -1
  47. package/package.json +4 -4
  48. package/resource-detail/useDeleteResource.d.ts +1 -1
  49. package/resource-detail/useDeleteResource.d.ts.map +1 -1
  50. package/resource-detail/useDeleteResource.js +5 -0
  51. package/resource-detail/useDeleteResource.js.map +1 -1
  52. package/resource-workbench/components/ResourceWorkbench.d.ts +10 -1
  53. package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
  54. package/resource-workbench/components/ResourceWorkbench.js +6 -2
  55. package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
  56. package/resource-workbench/hooks/useResourceCollection.d.ts +8 -0
  57. package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
  58. package/resource-workbench/hooks/useResourceCollection.js +2 -2
  59. package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
  60. package/schedule/CadenceField.d.ts +31 -0
  61. package/schedule/CadenceField.d.ts.map +1 -0
  62. package/schedule/CadenceField.js +116 -0
  63. package/schedule/CadenceField.js.map +1 -0
  64. package/schedule/ScheduleDetailView.d.ts +58 -0
  65. package/schedule/ScheduleDetailView.d.ts.map +1 -0
  66. package/schedule/ScheduleDetailView.js +236 -0
  67. package/schedule/ScheduleDetailView.js.map +1 -0
  68. package/schedule/ScheduleForm.d.ts +43 -0
  69. package/schedule/ScheduleForm.d.ts.map +1 -0
  70. package/schedule/ScheduleForm.js +129 -0
  71. package/schedule/ScheduleForm.js.map +1 -0
  72. package/schedule/ScheduleRowActions.d.ts +20 -0
  73. package/schedule/ScheduleRowActions.d.ts.map +1 -0
  74. package/schedule/ScheduleRowActions.js +54 -0
  75. package/schedule/ScheduleRowActions.js.map +1 -0
  76. package/schedule/TimeZoneField.d.ts +32 -0
  77. package/schedule/TimeZoneField.d.ts.map +1 -0
  78. package/schedule/TimeZoneField.js +104 -0
  79. package/schedule/TimeZoneField.js.map +1 -0
  80. package/schedule/cadence.d.ts +95 -0
  81. package/schedule/cadence.d.ts.map +1 -0
  82. package/schedule/cadence.js +242 -0
  83. package/schedule/cadence.js.map +1 -0
  84. package/schedule/index.d.ts +31 -0
  85. package/schedule/index.d.ts.map +1 -0
  86. package/schedule/index.js +16 -0
  87. package/schedule/index.js.map +1 -0
  88. package/schedule/scheduleColumns.d.ts +26 -0
  89. package/schedule/scheduleColumns.d.ts.map +1 -0
  90. package/schedule/scheduleColumns.js +77 -0
  91. package/schedule/scheduleColumns.js.map +1 -0
  92. package/schedule/scheduleListFn.d.ts +50 -0
  93. package/schedule/scheduleListFn.d.ts.map +1 -0
  94. package/schedule/scheduleListFn.js +65 -0
  95. package/schedule/scheduleListFn.js.map +1 -0
  96. package/schedule/scheduleState.d.ts +43 -0
  97. package/schedule/scheduleState.d.ts.map +1 -0
  98. package/schedule/scheduleState.js +67 -0
  99. package/schedule/scheduleState.js.map +1 -0
  100. package/schedule/useCreateSchedule.d.ts +57 -0
  101. package/schedule/useCreateSchedule.d.ts.map +1 -0
  102. package/schedule/useCreateSchedule.js +61 -0
  103. package/schedule/useCreateSchedule.js.map +1 -0
  104. package/schedule/useResumeSchedule.d.ts +30 -0
  105. package/schedule/useResumeSchedule.d.ts.map +1 -0
  106. package/schedule/useResumeSchedule.js +45 -0
  107. package/schedule/useResumeSchedule.js.map +1 -0
  108. package/schedule/useSchedule.d.ts +39 -0
  109. package/schedule/useSchedule.d.ts.map +1 -0
  110. package/schedule/useSchedule.js +44 -0
  111. package/schedule/useSchedule.js.map +1 -0
  112. package/schedule/useScheduleCount.d.ts +38 -0
  113. package/schedule/useScheduleCount.d.ts.map +1 -0
  114. package/schedule/useScheduleCount.js +30 -0
  115. package/schedule/useScheduleCount.js.map +1 -0
  116. package/schedule/useScheduleList.d.ts +48 -0
  117. package/schedule/useScheduleList.d.ts.map +1 -0
  118. package/schedule/useScheduleList.js +43 -0
  119. package/schedule/useScheduleList.js.map +1 -0
  120. package/schedule/useSetScheduleEnabled.d.ts +31 -0
  121. package/schedule/useSetScheduleEnabled.d.ts.map +1 -0
  122. package/schedule/useSetScheduleEnabled.js +53 -0
  123. package/schedule/useSetScheduleEnabled.js.map +1 -0
  124. package/schedule/useTriggerSchedule.d.ts +28 -0
  125. package/schedule/useTriggerSchedule.d.ts.map +1 -0
  126. package/schedule/useTriggerSchedule.js +43 -0
  127. package/schedule/useTriggerSchedule.js.map +1 -0
  128. package/search/useResourceCount.d.ts +2 -1
  129. package/search/useResourceCount.d.ts.map +1 -1
  130. package/search/useResourceCount.js +6 -1
  131. package/search/useResourceCount.js.map +1 -1
  132. package/src/agent/AgentPicker.tsx +13 -1
  133. package/src/channel/AgentChannelsPanel.tsx +43 -1
  134. package/src/channel/ChannelTemplatesDialog.tsx +433 -0
  135. package/src/channel/__tests__/AgentChannelsPanel.test.tsx +73 -0
  136. package/src/channel/__tests__/ChannelTemplatesDialog.test.tsx +292 -0
  137. package/src/channel/__tests__/providers.test.ts +9 -0
  138. package/src/channel/__tests__/templatePresentation.test.ts +96 -0
  139. package/src/channel/__tests__/useChannelTemplateList.test.tsx +216 -0
  140. package/src/channel/__tests__/useChannelTemplateReadiness.test.tsx +124 -0
  141. package/src/channel/index.ts +15 -0
  142. package/src/channel/providers.ts +26 -2
  143. package/src/channel/templatePresentation.ts +82 -0
  144. package/src/channel/useChannelTemplateList.ts +123 -0
  145. package/src/channel/useChannelTemplateReadiness.ts +69 -0
  146. package/src/dashboard/__tests__/useDashboardFailedRuns.test.tsx +106 -0
  147. package/src/dashboard/useDashboardFailedRuns.ts +2 -0
  148. package/src/index.ts +65 -0
  149. package/src/library/useExportResource.ts +4 -3
  150. package/src/organization/OrgProvider.tsx +29 -4
  151. package/src/organization/__tests__/OrgProvider.orgSwitch.test.tsx +89 -0
  152. package/src/resource-detail/useDeleteResource.ts +11 -1
  153. package/src/resource-workbench/__tests__/ResourceWorkbench.test.tsx +112 -0
  154. package/src/resource-workbench/components/ResourceWorkbench.tsx +32 -15
  155. package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
  156. package/src/schedule/CadenceField.tsx +365 -0
  157. package/src/schedule/ScheduleDetailView.tsx +583 -0
  158. package/src/schedule/ScheduleForm.tsx +405 -0
  159. package/src/schedule/ScheduleRowActions.tsx +124 -0
  160. package/src/schedule/TimeZoneField.tsx +199 -0
  161. package/src/schedule/__tests__/ScheduleDetailView.test.tsx +260 -0
  162. package/src/schedule/__tests__/cadence.test.ts +278 -0
  163. package/src/schedule/__tests__/scheduleColumnsRowActions.test.tsx +176 -0
  164. package/src/schedule/__tests__/scheduleCreation.test.tsx +340 -0
  165. package/src/schedule/__tests__/scheduleHooks.test.tsx +312 -0
  166. package/src/schedule/__tests__/scheduleListFn.test.ts +128 -0
  167. package/src/schedule/__tests__/scheduleState.test.ts +133 -0
  168. package/src/schedule/cadence.ts +298 -0
  169. package/src/schedule/index.ts +57 -0
  170. package/src/schedule/scheduleColumns.tsx +118 -0
  171. package/src/schedule/scheduleListFn.ts +108 -0
  172. package/src/schedule/scheduleState.ts +96 -0
  173. package/src/schedule/useCreateSchedule.ts +88 -0
  174. package/src/schedule/useResumeSchedule.ts +67 -0
  175. package/src/schedule/useSchedule.ts +67 -0
  176. package/src/schedule/useScheduleCount.ts +57 -0
  177. package/src/schedule/useScheduleList.ts +83 -0
  178. package/src/schedule/useSetScheduleEnabled.ts +84 -0
  179. package/src/schedule/useTriggerSchedule.ts +65 -0
  180. package/src/search/__tests__/useResourceCount.test.tsx +89 -0
  181. package/src/search/useResourceCount.ts +8 -2
  182. package/src/workflow/WorkflowDashboard.tsx +1 -0
  183. package/src/workflow/useWorkflowExecutionList.ts +13 -2
  184. package/styles.css +1 -1
  185. package/workflow/CanvasTransitionEdge.d.ts +1 -1
  186. package/workflow/WorkflowDashboard.d.ts.map +1 -1
  187. package/workflow/WorkflowDashboard.js +1 -0
  188. package/workflow/WorkflowDashboard.js.map +1 -1
  189. package/workflow/useWorkflowExecutionList.d.ts +10 -0
  190. package/workflow/useWorkflowExecutionList.d.ts.map +1 -1
  191. package/workflow/useWorkflowExecutionList.js +3 -2
  192. package/workflow/useWorkflowExecutionList.js.map +1 -1
package/src/index.ts CHANGED
@@ -770,6 +770,61 @@ export type {
770
770
  DatastoreDetailViewProps,
771
771
  } from "./datastore/index.js";
772
772
 
773
+ // Schedule — the disabled-vs-paused state derivation, the direct-query
774
+ // list core + workbench adapter (schedules are not search-backed), data
775
+ // hooks (get by reference, paginated list, count), behavior hooks
776
+ // (create via apply, resume, trigger once, owner enable/disable via
777
+ // lossless manifest apply), the cadence model (preset ⇄ cron, no cron
778
+ // parsing — recognition of builder-emitted shapes only), the cadence
779
+ // builder, the creation form, and the detail view
780
+ export {
781
+ deriveScheduleState,
782
+ formatNextFire,
783
+ createScheduleListFn,
784
+ listSchedulesPage,
785
+ useSchedule,
786
+ useScheduleList,
787
+ useScheduleCount,
788
+ useCreateSchedule,
789
+ useResumeSchedule,
790
+ useTriggerSchedule,
791
+ useSetScheduleEnabled,
792
+ createScheduleColumns,
793
+ cadenceToCron,
794
+ cronToCadence,
795
+ describeCadence,
796
+ validateCron,
797
+ validateTimeZone,
798
+ WEEKDAY_LABELS,
799
+ CadenceField,
800
+ ScheduleForm,
801
+ ScheduleRowActions,
802
+ ScheduleDetailView,
803
+ ScheduleIcon,
804
+ } from "./schedule/index.js";
805
+ export type {
806
+ ScheduleState,
807
+ ScheduleStateInfo,
808
+ ScheduleListClient,
809
+ SchedulePage,
810
+ UseScheduleReturn,
811
+ UseScheduleListOptions,
812
+ UseScheduleListReturn,
813
+ UseScheduleCountOptions,
814
+ UseScheduleCountReturn,
815
+ UseCreateScheduleReturn,
816
+ UseResumeScheduleReturn,
817
+ UseTriggerScheduleReturn,
818
+ UseSetScheduleEnabledReturn,
819
+ CadencePreset,
820
+ CadenceKind,
821
+ CadenceFieldProps,
822
+ ScheduleFormProps,
823
+ ScheduleColumnsOptions,
824
+ ScheduleRowActionsProps,
825
+ ScheduleDetailViewProps,
826
+ } from "./schedule/index.js";
827
+
773
828
  // Environment — data hooks, list hook, personal convenience hook, secret reveal, variable management, env var form, system env vars, and styled components
774
829
  export {
775
830
  useEnvironment,
@@ -1259,13 +1314,18 @@ export {
1259
1314
  useConnectSlackChannel,
1260
1315
  useInstallChannel,
1261
1316
  useChannelToolReadiness,
1317
+ useChannelTemplateReadiness,
1262
1318
  useChannelSessions,
1319
+ useChannelTemplateList,
1320
+ templateStatusPhase,
1321
+ splitTemplateBody,
1263
1322
  AgentChannelsPanel,
1264
1323
  ConnectSlackDialog,
1265
1324
  ConnectWhatsAppDialog,
1266
1325
  ChannelCredentialsDialog,
1267
1326
  ChannelToolCredentials,
1268
1327
  ChannelConversationsDialog,
1328
+ ChannelTemplatesDialog,
1269
1329
  } from "./channel/index.js";
1270
1330
  export type {
1271
1331
  UseAgentChannelListReturn,
@@ -1277,13 +1337,18 @@ export type {
1277
1337
  SlackConnectPhase,
1278
1338
  UseInstallChannelReturn,
1279
1339
  InstallChannelPhase,
1340
+ ChannelTemplateReadiness,
1280
1341
  UseChannelSessionsReturn,
1342
+ UseChannelTemplateListOptions,
1343
+ UseChannelTemplateListReturn,
1344
+ TemplateBodySegment,
1281
1345
  AgentChannelsPanelProps,
1282
1346
  ConnectSlackDialogProps,
1283
1347
  ConnectWhatsAppDialogProps,
1284
1348
  ChannelCredentialsDialogProps,
1285
1349
  ChannelToolCredentialsProps,
1286
1350
  ChannelConversationsDialogProps,
1351
+ ChannelTemplatesDialogProps,
1287
1352
  } from "./channel/index.js";
1288
1353
 
1289
1354
  // Error — structured error display with classification, retry, and contextual guidance
@@ -5,6 +5,7 @@ import { parse as parseYaml } from "yaml";
5
5
  import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
6
6
  import type { Datastore } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
7
7
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
8
+ import type { Schedule } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
8
9
  import type { Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
9
10
  import { serializeManifest } from "@stigmer/sdk";
10
11
  import { toast } from "../feedback/toast.js";
@@ -17,9 +18,9 @@ import { serializeWorkflowYaml } from "../workflow/serialize-workflow-yaml.js";
17
18
  /** Options for {@link useExportResource}. */
18
19
  export interface UseExportResourceOptions {
19
20
  /** The resource kind — determines which serializer is used. */
20
- readonly kind: "Agent" | "McpServer" | "Workflow" | "Datastore";
21
+ readonly kind: "Agent" | "McpServer" | "Workflow" | "Datastore" | "Schedule";
21
22
  /** The proto resource to export, or `null` when not yet loaded. */
22
- readonly resource: Agent | McpServer | Workflow | Datastore | null;
23
+ readonly resource: Agent | McpServer | Workflow | Datastore | Schedule | null;
23
24
  }
24
25
 
25
26
  /** Return value of {@link useExportResource}. */
@@ -44,7 +45,7 @@ export interface UseExportResourceReturn {
44
45
 
45
46
  /**
46
47
  * Headless export hook for Stigmer resources (Agent, McpServer,
47
- * Workflow, Datastore).
48
+ * Workflow, Datastore, Schedule).
48
49
  *
49
50
  * Serializes the resource into YAML and JSON formats and provides
50
51
  * stable callbacks for copying to clipboard or triggering a file
@@ -12,6 +12,7 @@ import {
12
12
  } from "react";
13
13
  import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
14
14
  import { useStigmer } from "../hooks.js";
15
+ import { useFetchCache } from "../internal/FetchCacheProvider.js";
15
16
 
16
17
  /** Value exposed by {@link OrgProvider} via {@link useOrg}. */
17
18
  export interface OrgContextValue {
@@ -63,19 +64,27 @@ function persistSlug(slug: string): void {
63
64
  * organization selection, and persists the choice to `localStorage`
64
65
  * under the key `stigmer:activeOrgSlug`.
65
66
  *
66
- * Must be rendered inside a {@link StigmerProvider}.
67
+ * Must be rendered inside a {@link StigmerProvider}. Mount it BELOW
68
+ * `FetchCacheProvider` (when one is used): switching the active org clears
69
+ * the nearest fetch cache, so view state cached under the previous org
70
+ * context — session and execution entries are keyed by id, not org — can
71
+ * never bleed into the new one. Navigation on switch stays the host's
72
+ * responsibility via `OrgSwitcher`'s `onOrgChanged` callback.
67
73
  *
68
74
  * @example
69
75
  * ```tsx
70
76
  * <StigmerProvider client={client}>
71
- * <OrgProvider>
72
- * <App />
73
- * </OrgProvider>
77
+ * <FetchCacheProvider>
78
+ * <OrgProvider>
79
+ * <App />
80
+ * </OrgProvider>
81
+ * </FetchCacheProvider>
74
82
  * </StigmerProvider>
75
83
  * ```
76
84
  */
77
85
  export function OrgProvider({ children }: { children: ReactNode }) {
78
86
  const stigmer = useStigmer();
87
+ const fetchCache = useFetchCache();
79
88
  const [orgs, setOrgs] = useState<Organization[]>([]);
80
89
  const [activeOrg, setActiveOrgState] = useState<Organization | null>(null);
81
90
  const [isLoading, setIsLoading] = useState(true);
@@ -140,6 +149,22 @@ export function OrgProvider({ children }: { children: ReactNode }) {
140
149
  }
141
150
  }, []);
142
151
 
152
+ // Org-switch invariant: view state cached under one org context must not
153
+ // survive into another. Covers every path that changes the active org
154
+ // (explicit switch, post-create refresh) while skipping the initial
155
+ // restore (previous slug is null). Cache keys for sessions/executions are
156
+ // id-scoped, not org-scoped, so a TTL'd entry would otherwise outlive the
157
+ // switch. No-op when no FetchCacheProvider is mounted.
158
+ const previousSlugRef = useRef<string | null>(null);
159
+ const activeSlug = activeOrg?.metadata?.slug ?? null;
160
+ useEffect(() => {
161
+ const previous = previousSlugRef.current;
162
+ previousSlugRef.current = activeSlug;
163
+ if (previous !== null && activeSlug !== null && previous !== activeSlug) {
164
+ fetchCache?.clear();
165
+ }
166
+ }, [activeSlug, fetchCache]);
167
+
143
168
  const value = useMemo<OrgContextValue>(
144
169
  () => ({
145
170
  orgs,
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { act, renderHook, waitFor } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
5
+ import { StigmerContext } from "../../context";
6
+ import { FetchCache } from "../../internal/fetch-cache";
7
+ import { FetchCacheContext } from "../../internal/FetchCacheProvider";
8
+ import { OrgProvider, useOrg } from "../OrgProvider";
9
+
10
+ const acme = { metadata: { id: "acme", slug: "acme", name: "Acme" } } as Organization;
11
+ const globex = { metadata: { id: "globex", slug: "globex", name: "Globex" } } as Organization;
12
+
13
+ function createMockStigmer(orgs: Organization[]) {
14
+ return {
15
+ organization: {
16
+ findMyOrganizations: vi.fn().mockResolvedValue({ entries: orgs }),
17
+ },
18
+ } as never;
19
+ }
20
+
21
+ function wrapper(client: unknown, cache: FetchCache | null) {
22
+ return function Wrapper({ children }: { children: ReactNode }) {
23
+ return (
24
+ <StigmerContext.Provider value={client as never}>
25
+ <FetchCacheContext.Provider value={cache}>
26
+ <OrgProvider>{children}</OrgProvider>
27
+ </FetchCacheContext.Provider>
28
+ </StigmerContext.Provider>
29
+ );
30
+ };
31
+ }
32
+
33
+ describe("OrgProvider — org-switch cache invalidation", () => {
34
+ beforeEach(() => {
35
+ localStorage.clear();
36
+ });
37
+
38
+ it("clears the fetch cache when the active org changes", async () => {
39
+ const cache = new FetchCache();
40
+ cache.set("session:ses_1", { stale: "from previous org context" });
41
+
42
+ const { result } = renderHook(() => useOrg(), {
43
+ wrapper: wrapper(createMockStigmer([acme, globex]), cache),
44
+ });
45
+
46
+ await waitFor(() => expect(result.current.activeOrg).not.toBeNull());
47
+
48
+ // Initial restore must NOT clear — only a genuine change of context does.
49
+ // Session/execution cache keys are id-scoped (not org-scoped), so without
50
+ // the clear a switched-away org's view state would survive its 5-min TTL.
51
+ expect(cache.has("session:ses_1")).toBe(true);
52
+
53
+ act(() => result.current.setActiveOrg(globex));
54
+
55
+ await waitFor(() => expect(cache.has("session:ses_1")).toBe(false));
56
+ expect(result.current.activeOrg?.metadata?.slug).toBe("globex");
57
+ });
58
+
59
+ it("does not clear when re-selecting the already-active org", async () => {
60
+ const cache = new FetchCache();
61
+
62
+ const { result } = renderHook(() => useOrg(), {
63
+ wrapper: wrapper(createMockStigmer([acme, globex]), cache),
64
+ });
65
+
66
+ await waitFor(() => expect(result.current.activeOrg).not.toBeNull());
67
+ const active = result.current.activeOrg!;
68
+
69
+ cache.set("session:ses_1", { fresh: true });
70
+ act(() => result.current.setActiveOrg(active));
71
+
72
+ await new Promise((resolve) => setTimeout(resolve, 0));
73
+ expect(cache.has("session:ses_1")).toBe(true);
74
+ });
75
+
76
+ it("works without a FetchCacheProvider (no-op, no crash)", async () => {
77
+ const { result } = renderHook(() => useOrg(), {
78
+ wrapper: wrapper(createMockStigmer([acme, globex]), null),
79
+ });
80
+
81
+ await waitFor(() => expect(result.current.activeOrg).not.toBeNull());
82
+
83
+ act(() => result.current.setActiveOrg(globex));
84
+
85
+ await waitFor(() =>
86
+ expect(result.current.activeOrg?.metadata?.slug).toBe("globex"),
87
+ );
88
+ });
89
+ });
@@ -6,7 +6,12 @@ import { toError } from "../internal/toError.js";
6
6
  import { toast } from "../feedback/toast.js";
7
7
 
8
8
  /** Resource types that support deletion from detail pages. */
9
- export type DeletableResourceKind = "agent" | "skill" | "mcpServer" | "workflow";
9
+ export type DeletableResourceKind =
10
+ | "agent"
11
+ | "skill"
12
+ | "mcpServer"
13
+ | "workflow"
14
+ | "schedule";
10
15
 
11
16
  export interface UseDeleteResourceReturn {
12
17
  /**
@@ -74,6 +79,9 @@ export function useDeleteResource(
74
79
  case "workflow":
75
80
  await stigmer.workflow.delete(resourceId);
76
81
  break;
82
+ case "schedule":
83
+ await stigmer.schedule.delete(resourceId);
84
+ break;
77
85
  }
78
86
  toast.success(
79
87
  resourceName
@@ -106,5 +114,7 @@ function kindLabel(kind: DeletableResourceKind): string {
106
114
  return "MCP server";
107
115
  case "workflow":
108
116
  return "workflow";
117
+ case "schedule":
118
+ return "schedule";
109
119
  }
110
120
  }
@@ -0,0 +1,112 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { render, renderHook, screen, waitFor, cleanup } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import type { ListResult } from "@stigmer/sdk";
5
+ import { FetchCacheContext } from "../../internal/FetchCacheProvider";
6
+ import { ResourceWorkbench } from "../components/ResourceWorkbench";
7
+ import { useResourceCollection } from "../hooks/useResourceCollection";
8
+
9
+ afterEach(cleanup);
10
+
11
+ function Wrapper({ children }: { children: ReactNode }) {
12
+ return (
13
+ <FetchCacheContext.Provider value={null}>
14
+ {children}
15
+ </FetchCacheContext.Provider>
16
+ );
17
+ }
18
+
19
+ /** A row shaped like a resource proto: the id lives under metadata. */
20
+ interface ProtoLikeRow {
21
+ readonly metadata: { readonly id: string; readonly slug: string };
22
+ }
23
+
24
+ function listFnReturning(entries: unknown[], totalCount = entries.length) {
25
+ return vi.fn(
26
+ async (): Promise<ListResult> => ({
27
+ entries: entries as ListResult["entries"],
28
+ totalCount,
29
+ totalPages: 1,
30
+ }),
31
+ );
32
+ }
33
+
34
+ describe("ResourceWorkbench searchable prop", () => {
35
+ it("renders the search input by default", async () => {
36
+ render(
37
+ <ResourceWorkbench listFn={listFnReturning([])} org="acme" />,
38
+ { wrapper: Wrapper },
39
+ );
40
+
41
+ await waitFor(() =>
42
+ expect(screen.getByRole("textbox", { name: "Search…" })).toBeTruthy(),
43
+ );
44
+ });
45
+
46
+ it("hides the search input when searchable is false", async () => {
47
+ render(
48
+ <ResourceWorkbench
49
+ listFn={listFnReturning([])}
50
+ org="acme"
51
+ searchable={false}
52
+ />,
53
+ { wrapper: Wrapper },
54
+ );
55
+
56
+ await waitFor(() => expect(screen.getByText("No resources found")).toBeTruthy());
57
+ expect(screen.queryByRole("textbox")).toBeNull();
58
+ });
59
+ });
60
+
61
+ describe("useResourceCollection row identity", () => {
62
+ const protoRows: ProtoLikeRow[] = [
63
+ { metadata: { id: "sch_a", slug: "a" } },
64
+ { metadata: { id: "sch_b", slug: "b" } },
65
+ ];
66
+
67
+ const columns = [
68
+ {
69
+ id: "slug",
70
+ header: "Slug",
71
+ cell: (row: ProtoLikeRow) => row.metadata.slug,
72
+ },
73
+ ];
74
+
75
+ it("uses getItemId for rows without a top-level id", async () => {
76
+ // Hoisted: a listFn re-created per render would retrigger useFetch.
77
+ const listFn = listFnReturning(protoRows);
78
+ const { result } = renderHook(
79
+ () =>
80
+ useResourceCollection<ProtoLikeRow>({
81
+ listFn,
82
+ org: "acme",
83
+ columns,
84
+ getItemId: (row) => row.metadata.id,
85
+ }),
86
+ { wrapper: Wrapper },
87
+ );
88
+
89
+ await waitFor(() => expect(result.current.items).toHaveLength(2));
90
+ const rowIds = result.current.table!.getRowModel().rows.map((r) => r.id);
91
+ expect(rowIds).toEqual(["sch_a", "sch_b"]);
92
+ });
93
+
94
+ it("falls back to SearchResult.id, then the row index", async () => {
95
+ const mixed = [{ id: "res_1" }, { name: "no-id" }];
96
+ const listFn = listFnReturning(mixed);
97
+ const fallbackColumns = [{ id: "c", header: "C", cell: () => null }];
98
+ const { result } = renderHook(
99
+ () =>
100
+ useResourceCollection({
101
+ listFn,
102
+ org: "acme",
103
+ columns: fallbackColumns,
104
+ }),
105
+ { wrapper: Wrapper },
106
+ );
107
+
108
+ await waitFor(() => expect(result.current.items).toHaveLength(2));
109
+ const rowIds = result.current.table!.getRowModel().rows.map((r) => r.id);
110
+ expect(rowIds).toEqual(["res_1", "1"]);
111
+ });
112
+ });
@@ -155,6 +155,15 @@ export interface ResourceWorkbenchProps<TData = SearchResult> {
155
155
 
156
156
  // --- Layout ------------------------------------------------------------
157
157
 
158
+ /**
159
+ * Whether the toolbar renders the text-search input. Set `false` for
160
+ * kinds whose list backend has no text search (e.g. schedules, which
161
+ * use the direct query instead of the search service) — a search box
162
+ * that silently matches nothing is worse than none (DD-006). Opt-in
163
+ * per DD-011: existing consumers keep the search box.
164
+ * @default true
165
+ */
166
+ readonly searchable?: boolean;
158
167
  /** Search input placeholder text. @default "Search\u2026" */
159
168
  readonly searchPlaceholder?: string;
160
169
  /** Accessible label for the workbench region. @default "Resource workbench" */
@@ -228,6 +237,7 @@ export function ResourceWorkbench<TData = SearchResult>({
228
237
  emptyAction,
229
238
  onRetry,
230
239
  headerAction,
240
+ searchable = true,
231
241
  searchPlaceholder = "Search\u2026",
232
242
  "aria-label": ariaLabel = "Resource workbench",
233
243
  className,
@@ -267,6 +277,7 @@ export function ResourceWorkbench<TData = SearchResult>({
267
277
  onSortChange: filtersHook.setSort,
268
278
  columns: columns as WorkbenchColumnDef<TData>[],
269
279
  enableSelection,
280
+ getItemId,
270
281
  refetchToken,
271
282
  });
272
283
 
@@ -305,21 +316,27 @@ export function ResourceWorkbench<TData = SearchResult>({
305
316
  >
306
317
  {/* --- Toolbar: search + filters + view switcher --- */}
307
318
  <div className="flex flex-wrap items-center gap-2">
308
- <div className="relative flex-1">
309
- <SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground-subtle" />
310
- <input
311
- type="text"
312
- value={filtersHook.query}
313
- onChange={(e) => filtersHook.setQuery(e.target.value)}
314
- placeholder={searchPlaceholder}
315
- aria-label={searchPlaceholder}
316
- className={cn(
317
- "w-full rounded-md border border-input bg-background py-1.5 pl-8 pr-3 text-sm",
318
- "placeholder:text-muted-foreground-subtle",
319
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
320
- )}
321
- />
322
- </div>
319
+ {searchable ? (
320
+ <div className="relative flex-1">
321
+ <SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground-subtle" />
322
+ <input
323
+ type="text"
324
+ value={filtersHook.query}
325
+ onChange={(e) => filtersHook.setQuery(e.target.value)}
326
+ placeholder={searchPlaceholder}
327
+ aria-label={searchPlaceholder}
328
+ className={cn(
329
+ "w-full rounded-md border border-input bg-background py-1.5 pl-8 pr-3 text-sm",
330
+ "placeholder:text-muted-foreground-subtle",
331
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
332
+ )}
333
+ />
334
+ </div>
335
+ ) : (
336
+ // Spacer keeps the scope toggle / view switcher / header action
337
+ // right-aligned, matching the searchable layout.
338
+ <div className="flex-1" aria-hidden="true" />
339
+ )}
323
340
  {onScopeChange && (
324
341
  <ScopeToggle value={controlledScope} onChange={onScopeChange} />
325
342
  )}
@@ -57,6 +57,14 @@ export interface UseResourceCollectionOptions<TData = SearchResult> {
57
57
  readonly columns?: readonly WorkbenchColumnDef<TData>[];
58
58
  /** Enable row selection state tracking. @default false */
59
59
  readonly enableSelection?: boolean;
60
+ /**
61
+ * Extracts a stable unique ID from an item, used as the TanStack row
62
+ * id. Provide it whenever the row type does not carry a top-level
63
+ * `id` (e.g. full resource protos, where the id lives at
64
+ * `metadata.id`). When omitted, falls back to `SearchResult.id`, then
65
+ * the row index.
66
+ */
67
+ readonly getItemId?: (item: TData) => string;
60
68
  /**
61
69
  * Opaque token that forces a background refetch whenever its value
62
70
  * changes. Use it to re-read the list after an out-of-band mutation
@@ -157,6 +165,7 @@ export function useResourceCollection<TData = SearchResult>(
157
165
  onSortChange,
158
166
  columns = [],
159
167
  enableSelection = false,
168
+ getItemId,
160
169
  refetchToken,
161
170
  } = options;
162
171
 
@@ -247,7 +256,8 @@ export function useResourceCollection<TData = SearchResult>(
247
256
  onRowSelectionChange: enableSelection ? setRowSelection : undefined,
248
257
  getCoreRowModel: getCoreRowModel(),
249
258
  getSortedRowModel: getSortedRowModel(),
250
- getRowId: (row) => (row as SearchResult).id ?? String(items.indexOf(row as TData)),
259
+ getRowId: (row, index) =>
260
+ getItemId?.(row) || (row as SearchResult).id || String(index),
251
261
  manualPagination: true,
252
262
  manualSorting: true,
253
263
  enableRowSelection: enableSelection,