@stigmer/react 3.4.0 → 3.5.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 (208) hide show
  1. package/agent/AgentDetailView.d.ts +14 -4
  2. package/agent/AgentDetailView.d.ts.map +1 -1
  3. package/agent/AgentDetailView.js +73 -11
  4. package/agent/AgentDetailView.js.map +1 -1
  5. package/agent/internal/agentToInput.d.ts +3 -0
  6. package/agent/internal/agentToInput.d.ts.map +1 -1
  7. package/agent/internal/agentToInput.js +21 -1
  8. package/agent/internal/agentToInput.js.map +1 -1
  9. package/agent/useAgentCount.d.ts +2 -0
  10. package/agent/useAgentCount.d.ts.map +1 -1
  11. package/agent/useAgentCount.js.map +1 -1
  12. package/api-key/ApiKeyListPanel.d.ts +8 -1
  13. package/api-key/ApiKeyListPanel.d.ts.map +1 -1
  14. package/api-key/ApiKeyListPanel.js +9 -7
  15. package/api-key/ApiKeyListPanel.js.map +1 -1
  16. package/api-key/CreateApiKeyForm.d.ts +7 -1
  17. package/api-key/CreateApiKeyForm.d.ts.map +1 -1
  18. package/api-key/CreateApiKeyForm.js +2 -2
  19. package/api-key/CreateApiKeyForm.js.map +1 -1
  20. package/datastore/useDatastoreCount.d.ts +2 -0
  21. package/datastore/useDatastoreCount.d.ts.map +1 -1
  22. package/datastore/useDatastoreCount.js.map +1 -1
  23. package/dependency-graph/DependencyTreeNode.d.ts.map +1 -1
  24. package/dependency-graph/DependencyTreeNode.js +8 -0
  25. package/dependency-graph/DependencyTreeNode.js.map +1 -1
  26. package/dependency-graph/types.d.ts +20 -8
  27. package/dependency-graph/types.d.ts.map +1 -1
  28. package/dependency-graph/useDependencyGraph.d.ts.map +1 -1
  29. package/dependency-graph/useDependencyGraph.js +21 -1
  30. package/dependency-graph/useDependencyGraph.js.map +1 -1
  31. package/index.d.ts +6 -4
  32. package/index.d.ts.map +1 -1
  33. package/index.js +6 -2
  34. package/index.js.map +1 -1
  35. package/internal/scroll-area.d.ts +4 -0
  36. package/internal/scroll-area.d.ts.map +1 -0
  37. package/internal/scroll-area.js +15 -0
  38. package/internal/scroll-area.js.map +1 -0
  39. package/internal/tooltip.d.ts +7 -0
  40. package/internal/tooltip.d.ts.map +1 -0
  41. package/internal/tooltip.js +31 -0
  42. package/internal/tooltip.js.map +1 -0
  43. package/manifest/ApplyManifestDialog.d.ts +10 -5
  44. package/manifest/ApplyManifestDialog.d.ts.map +1 -1
  45. package/manifest/ApplyManifestDialog.js +30 -13
  46. package/manifest/ApplyManifestDialog.js.map +1 -1
  47. package/manifest/useApplyManifest.d.ts +6 -2
  48. package/manifest/useApplyManifest.d.ts.map +1 -1
  49. package/manifest/useApplyManifest.js +5 -5
  50. package/manifest/useApplyManifest.js.map +1 -1
  51. package/mcp-server/McpServerDetailView.d.ts +38 -2
  52. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  53. package/mcp-server/McpServerDetailView.js +21 -5
  54. package/mcp-server/McpServerDetailView.js.map +1 -1
  55. package/mcp-server/StdioSandboxNotice.d.ts +12 -11
  56. package/mcp-server/StdioSandboxNotice.d.ts.map +1 -1
  57. package/mcp-server/StdioSandboxNotice.js +13 -12
  58. package/mcp-server/StdioSandboxNotice.js.map +1 -1
  59. package/mcp-server/index.d.ts +7 -0
  60. package/mcp-server/index.d.ts.map +1 -1
  61. package/mcp-server/index.js +9 -0
  62. package/mcp-server/index.js.map +1 -1
  63. package/mcp-server/steps/EnvironmentAuthStep.d.ts +4 -0
  64. package/mcp-server/steps/EnvironmentAuthStep.d.ts.map +1 -1
  65. package/mcp-server/steps/EnvironmentAuthStep.js +4 -0
  66. package/mcp-server/steps/EnvironmentAuthStep.js.map +1 -1
  67. package/mcp-server/steps/IdentityTransportStep.d.ts +6 -0
  68. package/mcp-server/steps/IdentityTransportStep.d.ts.map +1 -1
  69. package/mcp-server/steps/IdentityTransportStep.js +7 -1
  70. package/mcp-server/steps/IdentityTransportStep.js.map +1 -1
  71. package/mcp-server/steps/ReviewStep.d.ts +5 -0
  72. package/mcp-server/steps/ReviewStep.d.ts.map +1 -1
  73. package/mcp-server/steps/ReviewStep.js +16 -2
  74. package/mcp-server/steps/ReviewStep.js.map +1 -1
  75. package/mcp-server/useMcpServerCount.d.ts +2 -0
  76. package/mcp-server/useMcpServerCount.d.ts.map +1 -1
  77. package/mcp-server/useMcpServerCount.js.map +1 -1
  78. package/package.json +4 -4
  79. package/resource-creation/CreationPicker.d.ts.map +1 -1
  80. package/resource-creation/CreationPicker.js +3 -3
  81. package/resource-creation/CreationPicker.js.map +1 -1
  82. package/resource-creation/WizardNav.d.ts.map +1 -1
  83. package/resource-creation/WizardNav.js +1 -1
  84. package/resource-creation/WizardNav.js.map +1 -1
  85. package/resource-creation/templates/mcp-server-templates.d.ts.map +1 -1
  86. package/resource-creation/templates/mcp-server-templates.js +17 -14
  87. package/resource-creation/templates/mcp-server-templates.js.map +1 -1
  88. package/resource-detail/Section.d.ts +8 -1
  89. package/resource-detail/Section.d.ts.map +1 -1
  90. package/resource-detail/Section.js +2 -2
  91. package/resource-detail/Section.js.map +1 -1
  92. package/resource-workbench/components/ResourceWorkbench.d.ts +9 -1
  93. package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
  94. package/resource-workbench/components/ResourceWorkbench.js +2 -1
  95. package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
  96. package/resource-workbench/hooks/useResourceCollection.d.ts +9 -0
  97. package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
  98. package/resource-workbench/hooks/useResourceCollection.js +2 -2
  99. package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
  100. package/search/useResourceCount.d.ts +7 -0
  101. package/search/useResourceCount.d.ts.map +1 -1
  102. package/search/useResourceCount.js +2 -1
  103. package/search/useResourceCount.js.map +1 -1
  104. package/session/NewSessionViewer.d.ts.map +1 -1
  105. package/session/NewSessionViewer.js +17 -3
  106. package/session/NewSessionViewer.js.map +1 -1
  107. package/session/SessionViewer.d.ts +2 -1
  108. package/session/SessionViewer.d.ts.map +1 -1
  109. package/session/SessionViewer.js +19 -11
  110. package/session/SessionViewer.js.map +1 -1
  111. package/session/SessionViewerLayout.d.ts +90 -0
  112. package/session/SessionViewerLayout.d.ts.map +1 -0
  113. package/session/SessionViewerLayout.js +32 -0
  114. package/session/SessionViewerLayout.js.map +1 -0
  115. package/session/index.d.ts +4 -0
  116. package/session/index.d.ts.map +1 -1
  117. package/session/index.js +10 -0
  118. package/session/index.js.map +1 -1
  119. package/session/useSessionRailViews.d.ts +2 -2
  120. package/sidebar/SettingsSidebar.d.ts +62 -0
  121. package/sidebar/SettingsSidebar.d.ts.map +1 -0
  122. package/sidebar/SettingsSidebar.js +60 -0
  123. package/sidebar/SettingsSidebar.js.map +1 -0
  124. package/sidebar/WorkspaceSidebar.d.ts +87 -0
  125. package/sidebar/WorkspaceSidebar.d.ts.map +1 -0
  126. package/sidebar/WorkspaceSidebar.js +95 -0
  127. package/sidebar/WorkspaceSidebar.js.map +1 -0
  128. package/sidebar/chrome.d.ts +48 -0
  129. package/sidebar/chrome.d.ts.map +1 -0
  130. package/sidebar/chrome.js +32 -0
  131. package/sidebar/chrome.js.map +1 -0
  132. package/sidebar/index.d.ts +6 -0
  133. package/sidebar/index.d.ts.map +1 -0
  134. package/sidebar/index.js +9 -0
  135. package/sidebar/index.js.map +1 -0
  136. package/sidebar/types.d.ts +54 -0
  137. package/sidebar/types.d.ts.map +1 -0
  138. package/sidebar/types.js +2 -0
  139. package/sidebar/types.js.map +1 -0
  140. package/skill/useSkillCount.d.ts +2 -0
  141. package/skill/useSkillCount.d.ts.map +1 -1
  142. package/skill/useSkillCount.js.map +1 -1
  143. package/src/agent/AgentDetailView.tsx +198 -11
  144. package/src/agent/__tests__/AgentDetailView.datastores.test.tsx +166 -0
  145. package/src/agent/internal/__tests__/agentToInput.test.ts +154 -0
  146. package/src/agent/internal/agentToInput.ts +25 -7
  147. package/src/agent/useAgentCount.ts +2 -0
  148. package/src/api-key/ApiKeyListPanel.tsx +17 -4
  149. package/src/api-key/CreateApiKeyForm.tsx +8 -1
  150. package/src/api-key/__tests__/CreateApiKeyForm.test.tsx +58 -0
  151. package/src/datastore/useDatastoreCount.ts +2 -0
  152. package/src/dependency-graph/DependencyTreeNode.tsx +8 -0
  153. package/src/dependency-graph/__tests__/useDependencyGraph.test.ts +108 -0
  154. package/src/dependency-graph/types.ts +20 -8
  155. package/src/dependency-graph/useDependencyGraph.ts +23 -1
  156. package/src/index.ts +24 -0
  157. package/src/internal/scroll-area.tsx +37 -0
  158. package/src/internal/tooltip.tsx +66 -0
  159. package/src/manifest/ApplyManifestDialog.tsx +40 -16
  160. package/src/manifest/__tests__/ApplyManifestDialog.test.tsx +147 -0
  161. package/src/manifest/__tests__/useApplyManifest.test.tsx +6 -6
  162. package/src/manifest/useApplyManifest.ts +12 -8
  163. package/src/mcp-server/McpServerDetailView.tsx +52 -4
  164. package/src/mcp-server/StdioSandboxNotice.tsx +16 -14
  165. package/src/mcp-server/__tests__/McpServerDetailView.test.tsx +345 -0
  166. package/src/mcp-server/__tests__/StdioSandboxNotice.test.tsx +3 -2
  167. package/src/mcp-server/index.ts +13 -0
  168. package/src/mcp-server/steps/EnvironmentAuthStep.tsx +4 -0
  169. package/src/mcp-server/steps/IdentityTransportStep.tsx +16 -1
  170. package/src/mcp-server/steps/ReviewStep.tsx +18 -1
  171. package/src/mcp-server/useMcpServerCount.ts +2 -0
  172. package/src/resource-creation/CreationPicker.tsx +7 -0
  173. package/src/resource-creation/WizardNav.tsx +1 -0
  174. package/src/resource-creation/templates/mcp-server-templates.ts +17 -14
  175. package/src/resource-detail/Section.tsx +9 -1
  176. package/src/resource-workbench/components/ResourceWorkbench.tsx +13 -0
  177. package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
  178. package/src/search/useResourceCount.ts +9 -1
  179. package/src/session/NewSessionViewer.tsx +48 -61
  180. package/src/session/SessionViewer.tsx +71 -86
  181. package/src/session/SessionViewerLayout.tsx +131 -0
  182. package/src/session/__tests__/SessionViewerLayout.test.tsx +140 -0
  183. package/src/session/index.ts +14 -0
  184. package/src/session/useSessionRailViews.tsx +2 -2
  185. package/src/sidebar/SettingsSidebar.tsx +173 -0
  186. package/src/sidebar/WorkspaceSidebar.tsx +386 -0
  187. package/src/sidebar/__tests__/SettingsSidebar.test.tsx +107 -0
  188. package/src/sidebar/__tests__/WorkspaceSidebar.test.tsx +236 -0
  189. package/src/sidebar/chrome.tsx +129 -0
  190. package/src/sidebar/index.ts +16 -0
  191. package/src/sidebar/types.ts +55 -0
  192. package/src/skill/useSkillCount.ts +2 -0
  193. package/src/test/__tests__/samples.test.ts +161 -2
  194. package/src/test/index.ts +1 -1
  195. package/src/test/samples.ts +157 -14
  196. package/src/workflow/useWorkflowCount.ts +2 -0
  197. package/styles.css +1 -1
  198. package/test/index.d.ts +1 -1
  199. package/test/index.d.ts.map +1 -1
  200. package/test/index.js +1 -1
  201. package/test/index.js.map +1 -1
  202. package/test/samples.d.ts +100 -7
  203. package/test/samples.d.ts.map +1 -1
  204. package/test/samples.js +133 -14
  205. package/test/samples.js.map +1 -1
  206. package/workflow/useWorkflowCount.d.ts +2 -0
  207. package/workflow/useWorkflowCount.d.ts.map +1 -1
  208. package/workflow/useWorkflowCount.js.map +1 -1
@@ -0,0 +1,154 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { create, equals, isFieldSet, toJson } from "@bufbuild/protobuf";
3
+ import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
4
+ import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
5
+ import { AgentSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
6
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
7
+ import { buildAgentProto } from "@stigmer/sdk";
8
+ import { agentToInput } from "../agentToInput";
9
+
10
+ /**
11
+ * Regression guard for stigmer/stigmer#319 — the console agent editor
12
+ * silently deleting `datastore_usages` on save.
13
+ *
14
+ * `agentToInput()` reconstructs a full `AgentInput` from a fetched Agent
15
+ * so inline edits can re-submit the whole spec (the backend does full
16
+ * spec replacement). Any spec field the converter does not map is
17
+ * therefore *cleared* by the next console edit. Two tests enforce the
18
+ * converter's "must be kept exhaustive" contract:
19
+ *
20
+ * 1. The round-trip test proves no field of the fixture below survives
21
+ * conversion distorted or dropped.
22
+ * 2. The tripwire test proves the fixture itself covers every
23
+ * `AgentSpec` field, via schema reflection. When a new proto field
24
+ * lands, the tripwire fails until the fixture sets it — and the
25
+ * round-trip then fails until `agentToInput()` maps it.
26
+ *
27
+ * Nested messages are covered by population, not reflection: every
28
+ * sub-field of the fixture (ref `version` pins, approval overrides,
29
+ * sub-agent grants, env metadata) must carry a non-default value so the
30
+ * round-trip comparison can catch its loss.
31
+ */
32
+ function fullyPopulatedAgent(): Agent {
33
+ return create(AgentSchema, {
34
+ apiVersion: "agentic.stigmer.ai/v1",
35
+ kind: "Agent",
36
+ metadata: {
37
+ id: "agt_test",
38
+ name: "Clinic Assistant",
39
+ slug: "clinic-assistant",
40
+ org: "acme",
41
+ labels: { team: "care-ops" },
42
+ },
43
+ spec: {
44
+ description: "Front-desk assistant for the clinic pilot.",
45
+ iconUrl: "https://example.com/clinic.svg",
46
+ instructions: "You are a careful clinic assistant. Verify identity first.",
47
+ mcpServerUsages: [
48
+ {
49
+ mcpServerRef: {
50
+ org: "acme",
51
+ slug: "github",
52
+ kind: ApiResourceKind.mcp_server,
53
+ },
54
+ enabledTools: ["create_issue", "search_issues"],
55
+ toolApprovalOverrides: [
56
+ {
57
+ toolName: "create_issue",
58
+ requiresApproval: true,
59
+ message: "Create issue {{args.title}}?",
60
+ },
61
+ ],
62
+ },
63
+ ],
64
+ skillRefs: [
65
+ {
66
+ org: "acme",
67
+ slug: "triage-guide",
68
+ kind: ApiResourceKind.skill,
69
+ // The version pin (tag or hash) must survive conversion — a
70
+ // dropped pin silently resets the skill to "latest".
71
+ version: "stable",
72
+ },
73
+ ],
74
+ subAgents: [
75
+ {
76
+ name: "researcher",
77
+ description: "Looks up prior cases",
78
+ instructions: "Research thoroughly before answering anything.",
79
+ mcpAccess: [{ mcpServer: "github", enabledTools: ["search_issues"] }],
80
+ skillRefs: [
81
+ {
82
+ org: "acme",
83
+ slug: "research-methods",
84
+ kind: ApiResourceKind.skill,
85
+ version: "v1.2",
86
+ },
87
+ ],
88
+ modelOverride: "claude-sonnet",
89
+ },
90
+ ],
91
+ env: {
92
+ CLINIC_API_KEY: {
93
+ isSecret: true,
94
+ description: "Records bridge credential",
95
+ },
96
+ REGION: { optional: true },
97
+ },
98
+ datastoreUsages: [
99
+ {
100
+ datastoreRef: {
101
+ org: "acme",
102
+ slug: "clinic-records",
103
+ kind: ApiResourceKind.datastore,
104
+ },
105
+ },
106
+ ],
107
+ },
108
+ });
109
+ }
110
+
111
+ describe("agentToInput", () => {
112
+ it("round-trips a fully-populated spec through AgentInput without loss", () => {
113
+ const agent = fullyPopulatedAgent();
114
+
115
+ const rebuilt = buildAgentProto(agentToInput(agent));
116
+
117
+ // toJson gives a readable structural diff on failure; the protobuf
118
+ // equals() assertion below is the authoritative semantic check.
119
+ expect(toJson(AgentSpecSchema, rebuilt.spec!)).toEqual(
120
+ toJson(AgentSpecSchema, agent.spec!),
121
+ );
122
+ expect(equals(AgentSpecSchema, rebuilt.spec!, agent.spec!)).toBe(true);
123
+ });
124
+
125
+ it("round-trips the input-owned metadata fields", () => {
126
+ const agent = fullyPopulatedAgent();
127
+
128
+ const rebuilt = buildAgentProto(agentToInput(agent));
129
+
130
+ // Only the fields AgentInput owns: id is server-assigned, and
131
+ // visibility is deliberately unmapped (the backend preserves it on
132
+ // omit — see preserveImmutableFields in the update pipeline).
133
+ expect(rebuilt.metadata?.name).toBe(agent.metadata?.name);
134
+ expect(rebuilt.metadata?.org).toBe(agent.metadata?.org);
135
+ expect(rebuilt.metadata?.slug).toBe(agent.metadata?.slug);
136
+ expect(rebuilt.metadata?.labels).toEqual(agent.metadata?.labels);
137
+ });
138
+
139
+ it("populates every AgentSpec field in the fixture (new-field tripwire)", () => {
140
+ const spec = fullyPopulatedAgent().spec!;
141
+
142
+ const unsetFields = AgentSpecSchema.fields
143
+ .filter((field) => !isFieldSet(spec, field))
144
+ .map((field) => field.name);
145
+
146
+ expect(
147
+ unsetFields,
148
+ `AgentSpec gained field(s) [${unsetFields.join(", ")}] that the ` +
149
+ "fixture does not populate. Set them in fullyPopulatedAgent() AND " +
150
+ "map them in agentToInput() — an unmapped spec field is cleared " +
151
+ "by every console inline edit (stigmer/stigmer#319).",
152
+ ).toEqual([]);
153
+ });
154
+ });
@@ -1,16 +1,12 @@
1
1
  import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
2
2
  import type {
3
3
  AgentInput,
4
+ DatastoreUsageInput,
5
+ EnvVarDeclarationInput,
4
6
  McpServerUsageInput,
5
7
  SubAgentInput,
6
8
  } from "@stigmer/sdk";
7
9
 
8
- interface EnvVarDeclarationInput {
9
- isSecret?: boolean;
10
- description?: string;
11
- optional?: boolean;
12
- }
13
-
14
10
  /**
15
11
  * Converts a fetched Agent proto to the AgentInput shape expected by
16
12
  * `stigmer.agent.update()`. This enables inline field editing: read the
@@ -18,16 +14,23 @@ interface EnvVarDeclarationInput {
18
14
  *
19
15
  * Must be kept exhaustive — any spec field not mapped here will be
20
16
  * cleared on the next update (the backend does full spec replacement).
17
+ * Enforced by `__tests__/agentToInput.test.ts`: a schema tripwire fails
18
+ * when `AgentSpec` gains a field this converter does not round-trip
19
+ * (stigmer/stigmer#319 was exactly such a drop).
21
20
  */
22
21
  export function agentToInput(agent: Agent): AgentInput {
23
22
  const meta = agent.metadata;
24
23
  const spec = agent.spec;
25
24
 
25
+ // Resource refs preserve `version` (the tag/hash pin on versioned
26
+ // resources like Skills) — dropping it would silently reset a pinned
27
+ // reference to "latest" on the next inline edit.
26
28
  const mcpServerUsages: McpServerUsageInput[] | undefined =
27
29
  spec?.mcpServerUsages?.map((usage) => ({
28
30
  mcpServerRef: {
29
31
  org: usage.mcpServerRef?.org ?? "",
30
32
  slug: usage.mcpServerRef?.slug ?? "",
33
+ version: usage.mcpServerRef?.version || undefined,
31
34
  },
32
35
  enabledTools: usage.enabledTools.length > 0 ? [...usage.enabledTools] : undefined,
33
36
  toolApprovalOverrides:
@@ -44,6 +47,7 @@ export function agentToInput(agent: Agent): AgentInput {
44
47
  spec?.skillRefs?.map((ref) => ({
45
48
  org: ref.org || "",
46
49
  slug: ref.slug,
50
+ version: ref.version || undefined,
47
51
  }));
48
52
 
49
53
  const subAgents: SubAgentInput[] | undefined =
@@ -60,11 +64,24 @@ export function agentToInput(agent: Agent): AgentInput {
60
64
  : undefined,
61
65
  skillRefs:
62
66
  sa.skillRefs.length > 0
63
- ? sa.skillRefs.map((r) => ({ org: r.org || "", slug: r.slug }))
67
+ ? sa.skillRefs.map((r) => ({
68
+ org: r.org || "",
69
+ slug: r.slug,
70
+ version: r.version || undefined,
71
+ }))
64
72
  : undefined,
65
73
  modelOverride: sa.modelOverride || undefined,
66
74
  }));
67
75
 
76
+ const datastoreUsages: DatastoreUsageInput[] | undefined =
77
+ spec?.datastoreUsages?.map((usage) => ({
78
+ datastoreRef: {
79
+ org: usage.datastoreRef?.org ?? "",
80
+ slug: usage.datastoreRef?.slug ?? "",
81
+ version: usage.datastoreRef?.version || undefined,
82
+ },
83
+ }));
84
+
68
85
  let env: Record<string, EnvVarDeclarationInput> | undefined;
69
86
  if (spec?.env && Object.keys(spec.env).length > 0) {
70
87
  env = {};
@@ -91,5 +108,6 @@ export function agentToInput(agent: Agent): AgentInput {
91
108
  skillRefs: skillRefs?.length ? skillRefs : undefined,
92
109
  subAgents: subAgents?.length ? subAgents : undefined,
93
110
  env,
111
+ datastoreUsages: datastoreUsages?.length ? datastoreUsages : undefined,
94
112
  };
95
113
  }
@@ -17,6 +17,8 @@ export interface UseAgentCountOptions {
17
17
  * @default "org"
18
18
  */
19
19
  readonly scope?: ResourceListScope;
20
+ /** Opaque token that forces a recount when its value changes. */
21
+ readonly refetchToken?: unknown;
20
22
  }
21
23
 
22
24
  /** Return value of {@link useAgentCount}. */
@@ -18,6 +18,13 @@ export interface ApiKeyListPanelProps {
18
18
  readonly onRefetchRef?: (refetch: () => void) => void;
19
19
  /** Additional CSS class names for the root container. */
20
20
  readonly className?: string;
21
+ /**
22
+ * Reference instant for the "last used" relative stamp. Defaults to
23
+ * the live clock. **Deterministic hosts (documentation embeds, video
24
+ * export) must pass a frozen instant** — otherwise a depicted key's
25
+ * "2h ago" drifts with every replay.
26
+ */
27
+ readonly now?: Date;
21
28
  }
22
29
 
23
30
  /**
@@ -46,6 +53,7 @@ export interface ApiKeyListPanelProps {
46
53
  export function ApiKeyListPanel({
47
54
  onRefetchRef,
48
55
  className,
56
+ now,
49
57
  }: ApiKeyListPanelProps) {
50
58
  const { apiKeys, isLoading, error, refetch } = useApiKeyList();
51
59
  const [confirmingId, setConfirmingId] = useState<string | null>(null);
@@ -104,6 +112,7 @@ export function ApiKeyListPanel({
104
112
  <ApiKeyRow
105
113
  key={id}
106
114
  apiKey={key}
115
+ now={now}
107
116
  isConfirming={confirmingId === id}
108
117
  onConfirmDelete={() => setConfirmingId(id)}
109
118
  onCancelDelete={() => setConfirmingId(null)}
@@ -124,12 +133,14 @@ export function ApiKeyListPanel({
124
133
 
125
134
  function ApiKeyRow({
126
135
  apiKey,
136
+ now,
127
137
  isConfirming,
128
138
  onConfirmDelete,
129
139
  onCancelDelete,
130
140
  onDeleted,
131
141
  }: {
132
142
  apiKey: ApiKey;
143
+ now?: Date;
133
144
  isConfirming: boolean;
134
145
  onConfirmDelete: () => void;
135
146
  onCancelDelete: () => void;
@@ -243,7 +254,9 @@ function ApiKeyRow({
243
254
  : "No expiry"}
244
255
  </span>
245
256
  <span>
246
- {lastUsedAt ? formatRelativeTime(timestampDate(lastUsedAt)) : "Never used"}
257
+ {lastUsedAt
258
+ ? formatRelativeTime(timestampDate(lastUsedAt), now)
259
+ : "Never used"}
247
260
  </span>
248
261
  </div>
249
262
 
@@ -276,9 +289,9 @@ function formatShortDate(date: Date): string {
276
289
  });
277
290
  }
278
291
 
279
- function formatRelativeTime(date: Date): string {
280
- const now = Date.now();
281
- const diffMs = now - date.getTime();
292
+ function formatRelativeTime(date: Date, now?: Date): string {
293
+ const ref = now?.getTime() ?? Date.now();
294
+ const diffMs = ref - date.getTime();
282
295
 
283
296
  if (diffMs < 0) return "Just now";
284
297
 
@@ -22,6 +22,12 @@ export interface CreateApiKeyFormProps {
22
22
  readonly onCreated?: (apiKey: ApiKey) => void;
23
23
  /** Fired when the user cancels creation. */
24
24
  readonly onCancel?: () => void;
25
+ /**
26
+ * Seeds the name field on mount (e.g. a suggested key name, or a demo
27
+ * scenario depicting the form mid-fill). One-time: consumed on mount;
28
+ * subsequent changes are ignored. The field stays fully editable.
29
+ */
30
+ readonly initialName?: string;
25
31
  /** Additional CSS class names for the root container. */
26
32
  readonly className?: string;
27
33
  }
@@ -51,11 +57,12 @@ export function CreateApiKeyForm({
51
57
  org,
52
58
  onCreated,
53
59
  onCancel,
60
+ initialName = "",
54
61
  className,
55
62
  }: CreateApiKeyFormProps) {
56
63
  const { create, isCreating, error, clearError } = useCreateApiKey();
57
64
 
58
- const [name, setName] = useState("");
65
+ const [name, setName] = useState(initialName);
59
66
  const [expiry, setExpiry] = useState<ExpiryOption>("never");
60
67
 
61
68
  const trimmedName = name.trim();
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { render, screen, fireEvent, cleanup } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import type { Stigmer } from "@stigmer/sdk";
5
+ import { StigmerContext } from "../../context";
6
+ import { CreateApiKeyForm } from "../CreateApiKeyForm";
7
+
8
+ afterEach(cleanup);
9
+
10
+ function createWrapper() {
11
+ const mockClient = {
12
+ apiKey: { create: vi.fn() },
13
+ } as unknown as Stigmer;
14
+
15
+ return ({ children }: { children: ReactNode }) => (
16
+ <StigmerContext.Provider value={mockClient}>
17
+ {children}
18
+ </StigmerContext.Provider>
19
+ );
20
+ }
21
+
22
+ function nameInput(): HTMLInputElement {
23
+ return screen.getByLabelText("Name") as HTMLInputElement;
24
+ }
25
+
26
+ function submitButton(): HTMLButtonElement {
27
+ return screen.getByRole("button", {
28
+ name: "Create API key",
29
+ }) as HTMLButtonElement;
30
+ }
31
+
32
+ describe("CreateApiKeyForm initialName", () => {
33
+ it("starts with an empty name and a disabled submit by default", () => {
34
+ render(<CreateApiKeyForm org="acme" />, { wrapper: createWrapper() });
35
+
36
+ expect(nameInput().value).toBe("");
37
+ expect(submitButton().disabled).toBe(true);
38
+ });
39
+
40
+ it("seeds the name field and enables submit when initialName is set", () => {
41
+ render(<CreateApiKeyForm org="acme" initialName="quickstart-key" />, {
42
+ wrapper: createWrapper(),
43
+ });
44
+
45
+ expect(nameInput().value).toBe("quickstart-key");
46
+ expect(submitButton().disabled).toBe(false);
47
+ });
48
+
49
+ it("keeps the seeded field editable — initialName is a seed, not a lock", () => {
50
+ render(<CreateApiKeyForm org="acme" initialName="quickstart-key" />, {
51
+ wrapper: createWrapper(),
52
+ });
53
+
54
+ fireEvent.change(nameInput(), { target: { value: "renamed-key" } });
55
+
56
+ expect(nameInput().value).toBe("renamed-key");
57
+ });
58
+ });
@@ -17,6 +17,8 @@ export interface UseDatastoreCountOptions {
17
17
  * @default "org"
18
18
  */
19
19
  readonly scope?: ResourceListScope;
20
+ /** Opaque token that forces a recount when its value changes. */
21
+ readonly refetchToken?: unknown;
20
22
  }
21
23
 
22
24
  /** Return value of {@link useDatastoreCount}. */
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useCallback, useState, type KeyboardEvent } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
+ import { DatastoreIcon } from "../datastore/DatastoreDetailView.js";
5
6
  import type { DependencyNode, NodeKind } from "./types.js";
6
7
 
7
8
  interface DependencyTreeNodeProps {
@@ -15,6 +16,7 @@ const KIND_LABELS: Record<NodeKind, string> = {
15
16
  agent: "Agent",
16
17
  "mcp-server": "MCP",
17
18
  skill: "Skill",
19
+ datastore: "Datastore",
18
20
  "sub-agent": "Sub-Agent",
19
21
  };
20
22
 
@@ -179,6 +181,10 @@ function kindBadgeClasses(kind: NodeKind): string {
179
181
  return "bg-[var(--stgm-status-running-subtle)] text-[var(--stgm-status-running)]";
180
182
  case "skill":
181
183
  return "bg-[var(--stgm-status-pending-subtle)] text-[var(--stgm-status-pending)]";
184
+ // Badges borrow status hues as category colors (skill=pending,
185
+ // sub-agent=ready); datastore takes the remaining distinct hue.
186
+ case "datastore":
187
+ return "bg-[var(--stgm-status-degraded-subtle)] text-[var(--stgm-status-degraded)]";
182
188
  case "sub-agent":
183
189
  return "bg-[var(--stgm-status-ready-subtle)] text-[var(--stgm-status-ready)]";
184
190
  case "agent":
@@ -205,6 +211,8 @@ function NodeIcon({
205
211
  return <McpServerIcon className={className} />;
206
212
  case "skill":
207
213
  return <SkillIcon className={className} />;
214
+ case "datastore":
215
+ return <DatastoreIcon className={className} />;
208
216
  }
209
217
  }
210
218
 
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { useDependencyGraph } from "../useDependencyGraph";
4
+
5
+ const emptySpec = {
6
+ mcpServerUsages: [],
7
+ skillRefs: [],
8
+ subAgents: [],
9
+ };
10
+
11
+ function graphFor(spec: Parameters<typeof useDependencyGraph>[0]["spec"]) {
12
+ const { result } = renderHook(() =>
13
+ useDependencyGraph({ agentName: "clinic-assistant", agentOrg: "acme", spec }),
14
+ );
15
+ return result.current;
16
+ }
17
+
18
+ describe("useDependencyGraph — datastore nodes", () => {
19
+ it("derives a datastore node with a navigation ref", () => {
20
+ const { tree, isEmpty } = graphFor({
21
+ ...emptySpec,
22
+ datastoreUsages: [
23
+ { datastoreRef: { org: "acme", slug: "clinic-records" } },
24
+ ],
25
+ });
26
+
27
+ // A datastore-only agent has dependencies — the tab must appear.
28
+ expect(isEmpty).toBe(false);
29
+ expect(tree).not.toBeNull();
30
+ expect(tree!.nodeCount).toBe(2);
31
+
32
+ const node = tree!.root.children[0];
33
+ expect(node).toMatchObject({
34
+ id: "datastore:clinic-records",
35
+ kind: "datastore",
36
+ label: "clinic-records",
37
+ ref: { org: "acme", slug: "clinic-records" },
38
+ });
39
+ // Same-org ref gets no qualified label.
40
+ expect(node.qualifiedLabel).toBeUndefined();
41
+ });
42
+
43
+ it("qualifies cross-org datastore labels and falls back to the agent org for empty refs", () => {
44
+ const { tree } = graphFor({
45
+ ...emptySpec,
46
+ datastoreUsages: [
47
+ { datastoreRef: { org: "partner", slug: "shared-records" } },
48
+ { datastoreRef: { org: "", slug: "local-records" } },
49
+ ],
50
+ });
51
+
52
+ const [crossOrg, relative] = tree!.root.children;
53
+ expect(crossOrg.qualifiedLabel).toBe("partner/shared-records");
54
+ expect(crossOrg.ref).toEqual({ org: "partner", slug: "shared-records" });
55
+ expect(relative.qualifiedLabel).toBeUndefined();
56
+ expect(relative.ref).toEqual({ org: "acme", slug: "local-records" });
57
+ });
58
+
59
+ it("orders children to mirror the Overview sections: MCP, skills, datastores, sub-agents", () => {
60
+ const { tree } = graphFor({
61
+ mcpServerUsages: [
62
+ {
63
+ mcpServerRef: { org: "acme", slug: "github" },
64
+ enabledTools: [],
65
+ toolApprovalOverrides: [],
66
+ },
67
+ ],
68
+ skillRefs: [{ org: "acme", slug: "triage-guide" }],
69
+ subAgents: [
70
+ {
71
+ name: "researcher",
72
+ description: "",
73
+ mcpAccess: [],
74
+ skillRefs: [],
75
+ modelOverride: "",
76
+ },
77
+ ],
78
+ datastoreUsages: [
79
+ { datastoreRef: { org: "acme", slug: "clinic-records" } },
80
+ ],
81
+ });
82
+
83
+ expect(tree!.root.children.map((c) => c.kind)).toEqual([
84
+ "mcp-server",
85
+ "skill",
86
+ "datastore",
87
+ "sub-agent",
88
+ ]);
89
+ expect(tree!.nodeCount).toBe(5);
90
+ });
91
+
92
+ it("accepts specs without the datastoreUsages field (pre-existing public callers)", () => {
93
+ const { tree, isEmpty } = graphFor({
94
+ ...emptySpec,
95
+ skillRefs: [{ org: "acme", slug: "triage-guide" }],
96
+ });
97
+
98
+ expect(isEmpty).toBe(false);
99
+ expect(tree!.root.children.map((c) => c.kind)).toEqual(["skill"]);
100
+ });
101
+
102
+ it("stays empty when no dependencies exist at all", () => {
103
+ const { tree, isEmpty } = graphFor({ ...emptySpec, datastoreUsages: [] });
104
+
105
+ expect(isEmpty).toBe(true);
106
+ expect(tree).toBeNull();
107
+ });
108
+ });
@@ -4,9 +4,10 @@
4
4
  * - `"agent"` — the root agent (always the tree root)
5
5
  * - `"mcp-server"` — an MCP server referenced via `mcpServerUsages` or `mcpAccess`
6
6
  * - `"skill"` — a skill referenced via `skillRefs`
7
+ * - `"datastore"` — a datastore referenced via `datastoreUsages`
7
8
  * - `"sub-agent"` — an inline sub-agent definition with its own dependencies
8
9
  */
9
- export type NodeKind = "agent" | "mcp-server" | "skill" | "sub-agent";
10
+ export type NodeKind = "agent" | "mcp-server" | "skill" | "datastore" | "sub-agent";
10
11
 
11
12
  /**
12
13
  * A single node in the agent dependency tree.
@@ -15,9 +16,9 @@ export type NodeKind = "agent" | "mcp-server" | "skill" | "sub-agent";
15
16
  * (MCP servers and skills they access). Edges are implicit in the
16
17
  * parent-child relationship — no separate edge type is needed.
17
18
  *
18
- * Nodes are navigable when `ref` is defined (MCP servers, skills).
19
- * Sub-agent nodes have no `ref` because they are inline definitions
20
- * within the agent spec, not standalone resources.
19
+ * Nodes are navigable when `ref` is defined (MCP servers, skills,
20
+ * datastores). Sub-agent nodes have no `ref` because they are inline
21
+ * definitions within the agent spec, not standalone resources.
21
22
  */
22
23
  export interface DependencyNode {
23
24
  /**
@@ -46,9 +47,9 @@ export interface DependencyNode {
46
47
  /** Child nodes. Empty array for leaf nodes (MCP servers, skills). */
47
48
  readonly children: readonly DependencyNode[];
48
49
  /**
49
- * Navigation reference for clickable nodes. Defined for MCP servers
50
- * and skills (standalone resources). Undefined for sub-agents (inline
51
- * definitions) and the root agent.
50
+ * Navigation reference for clickable nodes. Defined for MCP servers,
51
+ * skills, and datastores (standalone resources). Undefined for
52
+ * sub-agents (inline definitions) and the root agent.
52
53
  */
53
54
  readonly ref?: { readonly org: string; readonly slug: string };
54
55
  }
@@ -70,7 +71,7 @@ export interface DependencyGraphProps {
70
71
  /**
71
72
  * Called when a navigable node is clicked. The `node.ref` field
72
73
  * contains the `org` and `slug` for routing. Only fired for nodes
73
- * where `ref` is defined (MCP servers, skills).
74
+ * where `ref` is defined (MCP servers, skills, datastores).
74
75
  */
75
76
  readonly onNodeClick?: (node: DependencyNode) => void;
76
77
  /**
@@ -119,6 +120,17 @@ export interface UseDependencyGraphOptions {
119
120
  }[];
120
121
  readonly modelOverride: string;
121
122
  }[];
123
+ /**
124
+ * Datastore attachments (`spec.datastoreUsages`). Optional because
125
+ * this hook is a public SDK export: callers passing hand-built spec
126
+ * objects written before the field existed must keep compiling.
127
+ */
128
+ readonly datastoreUsages?: readonly {
129
+ readonly datastoreRef?: {
130
+ readonly org: string;
131
+ readonly slug: string;
132
+ };
133
+ }[];
122
134
  } | undefined;
123
135
  }
124
136
 
@@ -41,10 +41,12 @@ export function useDependencyGraph({
41
41
  if (!spec) return { tree: null, isEmpty: true };
42
42
 
43
43
  const { mcpServerUsages, skillRefs, subAgents } = spec;
44
+ const datastoreUsages = spec.datastoreUsages ?? [];
44
45
 
45
46
  const hasDeps =
46
47
  mcpServerUsages.length > 0 ||
47
48
  skillRefs.length > 0 ||
49
+ datastoreUsages.length > 0 ||
48
50
  subAgents.length > 0;
49
51
 
50
52
  if (!hasDeps) return { tree: null, isEmpty: true };
@@ -92,6 +94,24 @@ export function useDependencyGraph({
92
94
  };
93
95
  });
94
96
 
97
+ const datastoreNodes: DependencyNode[] = datastoreUsages
98
+ .filter((u) => u.datastoreRef)
99
+ .map((usage) => {
100
+ nodeCount++;
101
+ const ref = usage.datastoreRef!;
102
+ return {
103
+ id: `datastore:${ref.slug}`,
104
+ kind: "datastore" as const,
105
+ label: ref.slug,
106
+ qualifiedLabel:
107
+ ref.org && ref.org !== agentOrg
108
+ ? `${ref.org}/${ref.slug}`
109
+ : undefined,
110
+ children: [],
111
+ ref: { org: ref.org || agentOrg, slug: ref.slug },
112
+ };
113
+ });
114
+
95
115
  const subAgentNodes: DependencyNode[] = subAgents.map((sa) => {
96
116
  nodeCount++;
97
117
 
@@ -143,11 +163,13 @@ export function useDependencyGraph({
143
163
  };
144
164
  });
145
165
 
166
+ // Child ordering mirrors the Overview sections: MCP servers,
167
+ // skills, datastores, then sub-agents.
146
168
  const root: DependencyNode = {
147
169
  id: `agent:${agentName}`,
148
170
  kind: "agent",
149
171
  label: agentName,
150
- children: [...mcpNodes, ...skillNodes, ...subAgentNodes],
172
+ children: [...mcpNodes, ...skillNodes, ...datastoreNodes, ...subAgentNodes],
151
173
  };
152
174
 
153
175
  return {