@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.
- package/agent/AgentDetailView.d.ts +14 -4
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +73 -11
- package/agent/AgentDetailView.js.map +1 -1
- package/agent/internal/agentToInput.d.ts +3 -0
- package/agent/internal/agentToInput.d.ts.map +1 -1
- package/agent/internal/agentToInput.js +21 -1
- package/agent/internal/agentToInput.js.map +1 -1
- package/agent/useAgentCount.d.ts +2 -0
- package/agent/useAgentCount.d.ts.map +1 -1
- package/agent/useAgentCount.js.map +1 -1
- package/api-key/ApiKeyListPanel.d.ts +8 -1
- package/api-key/ApiKeyListPanel.d.ts.map +1 -1
- package/api-key/ApiKeyListPanel.js +9 -7
- package/api-key/ApiKeyListPanel.js.map +1 -1
- package/api-key/CreateApiKeyForm.d.ts +7 -1
- package/api-key/CreateApiKeyForm.d.ts.map +1 -1
- package/api-key/CreateApiKeyForm.js +2 -2
- package/api-key/CreateApiKeyForm.js.map +1 -1
- package/datastore/useDatastoreCount.d.ts +2 -0
- package/datastore/useDatastoreCount.d.ts.map +1 -1
- package/datastore/useDatastoreCount.js.map +1 -1
- package/dependency-graph/DependencyTreeNode.d.ts.map +1 -1
- package/dependency-graph/DependencyTreeNode.js +8 -0
- package/dependency-graph/DependencyTreeNode.js.map +1 -1
- package/dependency-graph/types.d.ts +20 -8
- package/dependency-graph/types.d.ts.map +1 -1
- package/dependency-graph/useDependencyGraph.d.ts.map +1 -1
- package/dependency-graph/useDependencyGraph.js +21 -1
- package/dependency-graph/useDependencyGraph.js.map +1 -1
- package/index.d.ts +6 -4
- package/index.d.ts.map +1 -1
- package/index.js +6 -2
- package/index.js.map +1 -1
- package/internal/scroll-area.d.ts +4 -0
- package/internal/scroll-area.d.ts.map +1 -0
- package/internal/scroll-area.js +15 -0
- package/internal/scroll-area.js.map +1 -0
- package/internal/tooltip.d.ts +7 -0
- package/internal/tooltip.d.ts.map +1 -0
- package/internal/tooltip.js +31 -0
- package/internal/tooltip.js.map +1 -0
- package/manifest/ApplyManifestDialog.d.ts +10 -5
- package/manifest/ApplyManifestDialog.d.ts.map +1 -1
- package/manifest/ApplyManifestDialog.js +30 -13
- package/manifest/ApplyManifestDialog.js.map +1 -1
- package/manifest/useApplyManifest.d.ts +6 -2
- package/manifest/useApplyManifest.d.ts.map +1 -1
- package/manifest/useApplyManifest.js +5 -5
- package/manifest/useApplyManifest.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts +38 -2
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +21 -5
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/StdioSandboxNotice.d.ts +12 -11
- package/mcp-server/StdioSandboxNotice.d.ts.map +1 -1
- package/mcp-server/StdioSandboxNotice.js +13 -12
- package/mcp-server/StdioSandboxNotice.js.map +1 -1
- package/mcp-server/index.d.ts +7 -0
- package/mcp-server/index.d.ts.map +1 -1
- package/mcp-server/index.js +9 -0
- package/mcp-server/index.js.map +1 -1
- package/mcp-server/steps/EnvironmentAuthStep.d.ts +4 -0
- package/mcp-server/steps/EnvironmentAuthStep.d.ts.map +1 -1
- package/mcp-server/steps/EnvironmentAuthStep.js +4 -0
- package/mcp-server/steps/EnvironmentAuthStep.js.map +1 -1
- package/mcp-server/steps/IdentityTransportStep.d.ts +6 -0
- package/mcp-server/steps/IdentityTransportStep.d.ts.map +1 -1
- package/mcp-server/steps/IdentityTransportStep.js +7 -1
- package/mcp-server/steps/IdentityTransportStep.js.map +1 -1
- package/mcp-server/steps/ReviewStep.d.ts +5 -0
- package/mcp-server/steps/ReviewStep.d.ts.map +1 -1
- package/mcp-server/steps/ReviewStep.js +16 -2
- package/mcp-server/steps/ReviewStep.js.map +1 -1
- package/mcp-server/useMcpServerCount.d.ts +2 -0
- package/mcp-server/useMcpServerCount.d.ts.map +1 -1
- package/mcp-server/useMcpServerCount.js.map +1 -1
- package/package.json +4 -4
- package/resource-creation/CreationPicker.d.ts.map +1 -1
- package/resource-creation/CreationPicker.js +3 -3
- package/resource-creation/CreationPicker.js.map +1 -1
- package/resource-creation/WizardNav.d.ts.map +1 -1
- package/resource-creation/WizardNav.js +1 -1
- package/resource-creation/WizardNav.js.map +1 -1
- package/resource-creation/templates/mcp-server-templates.d.ts.map +1 -1
- package/resource-creation/templates/mcp-server-templates.js +17 -14
- package/resource-creation/templates/mcp-server-templates.js.map +1 -1
- package/resource-detail/Section.d.ts +8 -1
- package/resource-detail/Section.d.ts.map +1 -1
- package/resource-detail/Section.js +2 -2
- package/resource-detail/Section.js.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts +9 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.js +2 -1
- package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.d.ts +9 -0
- package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.js +2 -2
- package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
- package/search/useResourceCount.d.ts +7 -0
- package/search/useResourceCount.d.ts.map +1 -1
- package/search/useResourceCount.js +2 -1
- package/search/useResourceCount.js.map +1 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +17 -3
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +2 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +19 -11
- package/session/SessionViewer.js.map +1 -1
- package/session/SessionViewerLayout.d.ts +90 -0
- package/session/SessionViewerLayout.d.ts.map +1 -0
- package/session/SessionViewerLayout.js +32 -0
- package/session/SessionViewerLayout.js.map +1 -0
- package/session/index.d.ts +4 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +10 -0
- package/session/index.js.map +1 -1
- package/session/useSessionRailViews.d.ts +2 -2
- package/sidebar/SettingsSidebar.d.ts +62 -0
- package/sidebar/SettingsSidebar.d.ts.map +1 -0
- package/sidebar/SettingsSidebar.js +60 -0
- package/sidebar/SettingsSidebar.js.map +1 -0
- package/sidebar/WorkspaceSidebar.d.ts +87 -0
- package/sidebar/WorkspaceSidebar.d.ts.map +1 -0
- package/sidebar/WorkspaceSidebar.js +95 -0
- package/sidebar/WorkspaceSidebar.js.map +1 -0
- package/sidebar/chrome.d.ts +48 -0
- package/sidebar/chrome.d.ts.map +1 -0
- package/sidebar/chrome.js +32 -0
- package/sidebar/chrome.js.map +1 -0
- package/sidebar/index.d.ts +6 -0
- package/sidebar/index.d.ts.map +1 -0
- package/sidebar/index.js +9 -0
- package/sidebar/index.js.map +1 -0
- package/sidebar/types.d.ts +54 -0
- package/sidebar/types.d.ts.map +1 -0
- package/sidebar/types.js +2 -0
- package/sidebar/types.js.map +1 -0
- package/skill/useSkillCount.d.ts +2 -0
- package/skill/useSkillCount.d.ts.map +1 -1
- package/skill/useSkillCount.js.map +1 -1
- package/src/agent/AgentDetailView.tsx +198 -11
- package/src/agent/__tests__/AgentDetailView.datastores.test.tsx +166 -0
- package/src/agent/internal/__tests__/agentToInput.test.ts +154 -0
- package/src/agent/internal/agentToInput.ts +25 -7
- package/src/agent/useAgentCount.ts +2 -0
- package/src/api-key/ApiKeyListPanel.tsx +17 -4
- package/src/api-key/CreateApiKeyForm.tsx +8 -1
- package/src/api-key/__tests__/CreateApiKeyForm.test.tsx +58 -0
- package/src/datastore/useDatastoreCount.ts +2 -0
- package/src/dependency-graph/DependencyTreeNode.tsx +8 -0
- package/src/dependency-graph/__tests__/useDependencyGraph.test.ts +108 -0
- package/src/dependency-graph/types.ts +20 -8
- package/src/dependency-graph/useDependencyGraph.ts +23 -1
- package/src/index.ts +24 -0
- package/src/internal/scroll-area.tsx +37 -0
- package/src/internal/tooltip.tsx +66 -0
- package/src/manifest/ApplyManifestDialog.tsx +40 -16
- package/src/manifest/__tests__/ApplyManifestDialog.test.tsx +147 -0
- package/src/manifest/__tests__/useApplyManifest.test.tsx +6 -6
- package/src/manifest/useApplyManifest.ts +12 -8
- package/src/mcp-server/McpServerDetailView.tsx +52 -4
- package/src/mcp-server/StdioSandboxNotice.tsx +16 -14
- package/src/mcp-server/__tests__/McpServerDetailView.test.tsx +345 -0
- package/src/mcp-server/__tests__/StdioSandboxNotice.test.tsx +3 -2
- package/src/mcp-server/index.ts +13 -0
- package/src/mcp-server/steps/EnvironmentAuthStep.tsx +4 -0
- package/src/mcp-server/steps/IdentityTransportStep.tsx +16 -1
- package/src/mcp-server/steps/ReviewStep.tsx +18 -1
- package/src/mcp-server/useMcpServerCount.ts +2 -0
- package/src/resource-creation/CreationPicker.tsx +7 -0
- package/src/resource-creation/WizardNav.tsx +1 -0
- package/src/resource-creation/templates/mcp-server-templates.ts +17 -14
- package/src/resource-detail/Section.tsx +9 -1
- package/src/resource-workbench/components/ResourceWorkbench.tsx +13 -0
- package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
- package/src/search/useResourceCount.ts +9 -1
- package/src/session/NewSessionViewer.tsx +48 -61
- package/src/session/SessionViewer.tsx +71 -86
- package/src/session/SessionViewerLayout.tsx +131 -0
- package/src/session/__tests__/SessionViewerLayout.test.tsx +140 -0
- package/src/session/index.ts +14 -0
- package/src/session/useSessionRailViews.tsx +2 -2
- package/src/sidebar/SettingsSidebar.tsx +173 -0
- package/src/sidebar/WorkspaceSidebar.tsx +386 -0
- package/src/sidebar/__tests__/SettingsSidebar.test.tsx +107 -0
- package/src/sidebar/__tests__/WorkspaceSidebar.test.tsx +236 -0
- package/src/sidebar/chrome.tsx +129 -0
- package/src/sidebar/index.ts +16 -0
- package/src/sidebar/types.ts +55 -0
- package/src/skill/useSkillCount.ts +2 -0
- package/src/test/__tests__/samples.test.ts +161 -2
- package/src/test/index.ts +1 -1
- package/src/test/samples.ts +157 -14
- package/src/workflow/useWorkflowCount.ts +2 -0
- package/styles.css +1 -1
- package/test/index.d.ts +1 -1
- package/test/index.d.ts.map +1 -1
- package/test/index.js +1 -1
- package/test/index.js.map +1 -1
- package/test/samples.d.ts +100 -7
- package/test/samples.d.ts.map +1 -1
- package/test/samples.js +133 -14
- package/test/samples.js.map +1 -1
- package/workflow/useWorkflowCount.d.ts +2 -0
- package/workflow/useWorkflowCount.d.ts.map +1 -1
- package/workflow/useWorkflowCount.js.map +1 -1
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
5
|
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
6
6
|
import type {
|
|
7
|
+
DatastoreUsage,
|
|
7
8
|
McpServerUsage,
|
|
8
9
|
SubAgent,
|
|
9
10
|
} from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
@@ -14,7 +15,9 @@ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/
|
|
|
14
15
|
import { useAgent } from "./useAgent.js";
|
|
15
16
|
import { useUpdateAgent } from "./useUpdateAgent.js";
|
|
16
17
|
import { agentToInput } from "./internal/agentToInput.js";
|
|
18
|
+
import { toError } from "../internal/toError.js";
|
|
17
19
|
import { ErrorMessage } from "../error/ErrorMessage.js";
|
|
20
|
+
import { DatastoreIcon } from "../datastore/DatastoreDetailView.js";
|
|
18
21
|
import { VisibilityBadge } from "../library/VisibilitySelector.js";
|
|
19
22
|
import { useManageAccess } from "../access/useManageAccess.js";
|
|
20
23
|
import { AgentShareList } from "../sharing/AgentShareList.js";
|
|
@@ -58,6 +61,13 @@ export interface AgentDetailViewProps {
|
|
|
58
61
|
* Provides `org` and `slug` of the referenced skill.
|
|
59
62
|
*/
|
|
60
63
|
readonly onSkillClick?: (ref: { org: string; slug: string }) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Called when a datastore reference is clicked.
|
|
66
|
+
* Provides `org` and `slug` of the referenced datastore so the
|
|
67
|
+
* consumer can wire navigation. When the reference has no explicit
|
|
68
|
+
* org, the agent's own org is used as fallback.
|
|
69
|
+
*/
|
|
70
|
+
readonly onDatastoreClick?: (ref: { org: string; slug: string }) => void;
|
|
61
71
|
/**
|
|
62
72
|
* Called once when the agent resource has been fetched successfully.
|
|
63
73
|
* Provides the resource display name for use cases like breadcrumbs,
|
|
@@ -176,9 +186,9 @@ export interface AgentDetailViewProps {
|
|
|
176
186
|
* Fetches the agent via {@link useAgent} internally and renders its
|
|
177
187
|
* full configuration inside a {@link ResourceDetailShell}: a
|
|
178
188
|
* standardized header with action bar, followed by structured content
|
|
179
|
-
* sections (instructions, MCP server usages, skills,
|
|
180
|
-
* environment variables). Sections with no data are
|
|
181
|
-
* — reducing visual noise per Nielsen heuristic #8.
|
|
189
|
+
* sections (instructions, MCP server usages, skills, datastores,
|
|
190
|
+
* sub-agents, and environment variables). Sections with no data are
|
|
191
|
+
* omitted entirely — reducing visual noise per Nielsen heuristic #8.
|
|
182
192
|
*
|
|
183
193
|
* The action bar transforms this from a read-only view into an
|
|
184
194
|
* operational hub. Actions are provided by the consumer via
|
|
@@ -215,6 +225,7 @@ export function AgentDetailView({
|
|
|
215
225
|
slug,
|
|
216
226
|
onMcpServerClick,
|
|
217
227
|
onSkillClick,
|
|
228
|
+
onDatastoreClick,
|
|
218
229
|
onResourceLoad,
|
|
219
230
|
primaryAction,
|
|
220
231
|
actions,
|
|
@@ -236,12 +247,22 @@ export function AgentDetailView({
|
|
|
236
247
|
const { agent, isLoading, error, refetch } = useAgent(org, slug);
|
|
237
248
|
const { update, isUpdating } = useUpdateAgent();
|
|
238
249
|
|
|
250
|
+
// Last failed inline save, attributed to the field that was edited so
|
|
251
|
+
// only that section shows the message. The backend's message is the
|
|
252
|
+
// UX (DD-006): e.g. the datastore exposure guard refuses public +
|
|
253
|
+
// datastore_usages with an actionable FAILED_PRECONDITION message.
|
|
254
|
+
const [saveError, setSaveError] = useState<{
|
|
255
|
+
field: string;
|
|
256
|
+
message: string;
|
|
257
|
+
} | null>(null);
|
|
258
|
+
|
|
239
259
|
const saveField = useCallback(
|
|
240
260
|
async <K extends keyof import("@stigmer/sdk").AgentInput>(
|
|
241
261
|
field: K,
|
|
242
262
|
value: import("@stigmer/sdk").AgentInput[K],
|
|
243
263
|
): Promise<boolean> => {
|
|
244
264
|
if (!agent) return false;
|
|
265
|
+
setSaveError(null);
|
|
245
266
|
const input = agentToInput(agent);
|
|
246
267
|
(input as unknown as Record<string, unknown>)[field] = value;
|
|
247
268
|
try {
|
|
@@ -249,13 +270,16 @@ export function AgentDetailView({
|
|
|
249
270
|
onResourceUpdated?.(updated);
|
|
250
271
|
refetch();
|
|
251
272
|
return true;
|
|
252
|
-
} catch {
|
|
273
|
+
} catch (err) {
|
|
274
|
+
setSaveError({ field, message: toError(err).message });
|
|
253
275
|
return false;
|
|
254
276
|
}
|
|
255
277
|
},
|
|
256
278
|
[agent, update, onResourceUpdated, refetch],
|
|
257
279
|
);
|
|
258
280
|
|
|
281
|
+
const clearSaveError = useCallback(() => setSaveError(null), []);
|
|
282
|
+
|
|
259
283
|
const { tree, isEmpty: noDeps } = useDependencyGraph({
|
|
260
284
|
agentName: agent?.metadata?.name || agent?.metadata?.slug || slug,
|
|
261
285
|
agentOrg: agent?.metadata?.org || org,
|
|
@@ -290,9 +314,11 @@ export function AgentDetailView({
|
|
|
290
314
|
onMcpServerClick?.(node.ref);
|
|
291
315
|
} else if (node.kind === "skill") {
|
|
292
316
|
onSkillClick?.(node.ref);
|
|
317
|
+
} else if (node.kind === "datastore") {
|
|
318
|
+
onDatastoreClick?.(node.ref);
|
|
293
319
|
}
|
|
294
320
|
},
|
|
295
|
-
[onMcpServerClick, onSkillClick],
|
|
321
|
+
[onMcpServerClick, onSkillClick, onDatastoreClick],
|
|
296
322
|
);
|
|
297
323
|
|
|
298
324
|
const onResourceLoadRef = useRef(onResourceLoad);
|
|
@@ -430,9 +456,12 @@ export function AgentDetailView({
|
|
|
430
456
|
description={spec?.description}
|
|
431
457
|
onMcpServerClick={onMcpServerClick}
|
|
432
458
|
onSkillClick={onSkillClick}
|
|
459
|
+
onDatastoreClick={onDatastoreClick}
|
|
433
460
|
editable={editable}
|
|
434
461
|
isSaving={isUpdating}
|
|
435
462
|
saveField={saveField}
|
|
463
|
+
saveError={saveError}
|
|
464
|
+
clearSaveError={clearSaveError}
|
|
436
465
|
/>
|
|
437
466
|
);
|
|
438
467
|
}
|
|
@@ -467,22 +496,33 @@ function AgentOverview({
|
|
|
467
496
|
description,
|
|
468
497
|
onMcpServerClick,
|
|
469
498
|
onSkillClick,
|
|
499
|
+
onDatastoreClick,
|
|
470
500
|
editable,
|
|
471
501
|
isSaving,
|
|
472
502
|
saveField,
|
|
503
|
+
saveError,
|
|
504
|
+
clearSaveError,
|
|
473
505
|
}: {
|
|
474
506
|
readonly spec: NonNullable<ReturnType<typeof useAgent>["agent"]>["spec"];
|
|
475
507
|
readonly agentOrg: string;
|
|
476
508
|
readonly description?: string;
|
|
477
509
|
readonly onMcpServerClick?: (ref: { org: string; slug: string }) => void;
|
|
478
510
|
readonly onSkillClick?: (ref: { org: string; slug: string }) => void;
|
|
511
|
+
readonly onDatastoreClick?: (ref: { org: string; slug: string }) => void;
|
|
479
512
|
readonly editable?: boolean;
|
|
480
513
|
readonly isSaving?: boolean;
|
|
481
514
|
readonly saveField?: <K extends keyof import("@stigmer/sdk").AgentInput>(
|
|
482
515
|
field: K,
|
|
483
516
|
value: import("@stigmer/sdk").AgentInput[K],
|
|
484
517
|
) => Promise<boolean>;
|
|
518
|
+
readonly saveError?: { field: string; message: string } | null;
|
|
519
|
+
readonly clearSaveError?: () => void;
|
|
485
520
|
}) {
|
|
521
|
+
// The failed save's message, shown only under the section that owns
|
|
522
|
+
// the field — a rejection of a datastore edit must not appear under
|
|
523
|
+
// the skills editor.
|
|
524
|
+
const errorFor = (field: keyof import("@stigmer/sdk").AgentInput) =>
|
|
525
|
+
saveError?.field === field ? saveError.message : undefined;
|
|
486
526
|
const handleInstructionsSave = useCallback(
|
|
487
527
|
async (v: string) => saveField?.("instructions", v || undefined) ?? false,
|
|
488
528
|
[saveField],
|
|
@@ -508,6 +548,15 @@ function AgentOverview({
|
|
|
508
548
|
[saveField],
|
|
509
549
|
);
|
|
510
550
|
|
|
551
|
+
const handleDatastoresSave = useCallback(
|
|
552
|
+
async (refs: ResourceRefRow[]) =>
|
|
553
|
+
saveField?.(
|
|
554
|
+
"datastoreUsages",
|
|
555
|
+
refs.map((r) => ({ datastoreRef: { org: r.org, slug: r.slug } })),
|
|
556
|
+
) ?? false,
|
|
557
|
+
[saveField],
|
|
558
|
+
);
|
|
559
|
+
|
|
511
560
|
const handleEnvSave = useCallback(
|
|
512
561
|
async (rows: KeyValueRow[]) => {
|
|
513
562
|
const env: Record<string, { isSecret?: boolean; description?: string; optional?: boolean }> = {};
|
|
@@ -551,6 +600,19 @@ function AgentOverview({
|
|
|
551
600
|
[spec?.skillRefs, agentOrg],
|
|
552
601
|
);
|
|
553
602
|
|
|
603
|
+
const datastoreRefRows: ResourceRefRow[] = useMemo(
|
|
604
|
+
() =>
|
|
605
|
+
(spec?.datastoreUsages ?? []).map((usage) => ({
|
|
606
|
+
org: usage.datastoreRef?.org || agentOrg,
|
|
607
|
+
slug: usage.datastoreRef?.slug ?? "",
|
|
608
|
+
label:
|
|
609
|
+
usage.datastoreRef?.org && usage.datastoreRef.org !== agentOrg
|
|
610
|
+
? `${usage.datastoreRef.org}/${usage.datastoreRef.slug}`
|
|
611
|
+
: usage.datastoreRef?.slug ?? "",
|
|
612
|
+
})),
|
|
613
|
+
[spec?.datastoreUsages, agentOrg],
|
|
614
|
+
);
|
|
615
|
+
|
|
554
616
|
const envRows: KeyValueRow[] = useMemo(
|
|
555
617
|
() =>
|
|
556
618
|
Object.entries(spec?.env ?? {})
|
|
@@ -569,13 +631,46 @@ function AgentOverview({
|
|
|
569
631
|
const showInstructions = editable || !!spec?.instructions;
|
|
570
632
|
const showMcpServers = editable || (spec && spec.mcpServerUsages.length > 0);
|
|
571
633
|
const showSkills = editable || (spec && spec.skillRefs.length > 0);
|
|
634
|
+
const showDatastores = editable || (spec && spec.datastoreUsages.length > 0);
|
|
572
635
|
const showSubAgents = editable || (spec && spec.subAgents.length > 0);
|
|
573
636
|
const showEnv = editable || (spec?.env && Object.keys(spec.env).length > 0);
|
|
574
637
|
|
|
575
638
|
const [mcpEditing, setMcpEditing] = useState(false);
|
|
576
639
|
const [skillsEditing, setSkillsEditing] = useState(false);
|
|
640
|
+
const [datastoresEditing, setDatastoresEditing] = useState(false);
|
|
577
641
|
const [envEditing, setEnvEditing] = useState(false);
|
|
578
642
|
|
|
643
|
+
// Entering edit mode discards the previous attempt's error — the
|
|
644
|
+
// message describes a stale draft, not the one being composed.
|
|
645
|
+
const handleMcpEditingChange = useCallback(
|
|
646
|
+
(editing: boolean) => {
|
|
647
|
+
if (editing) clearSaveError?.();
|
|
648
|
+
setMcpEditing(editing);
|
|
649
|
+
},
|
|
650
|
+
[clearSaveError],
|
|
651
|
+
);
|
|
652
|
+
const handleSkillsEditingChange = useCallback(
|
|
653
|
+
(editing: boolean) => {
|
|
654
|
+
if (editing) clearSaveError?.();
|
|
655
|
+
setSkillsEditing(editing);
|
|
656
|
+
},
|
|
657
|
+
[clearSaveError],
|
|
658
|
+
);
|
|
659
|
+
const handleDatastoresEditingChange = useCallback(
|
|
660
|
+
(editing: boolean) => {
|
|
661
|
+
if (editing) clearSaveError?.();
|
|
662
|
+
setDatastoresEditing(editing);
|
|
663
|
+
},
|
|
664
|
+
[clearSaveError],
|
|
665
|
+
);
|
|
666
|
+
const handleEnvEditingChange = useCallback(
|
|
667
|
+
(editing: boolean) => {
|
|
668
|
+
if (editing) clearSaveError?.();
|
|
669
|
+
setEnvEditing(editing);
|
|
670
|
+
},
|
|
671
|
+
[clearSaveError],
|
|
672
|
+
);
|
|
673
|
+
|
|
579
674
|
return (
|
|
580
675
|
<div className="flex flex-col gap-6">
|
|
581
676
|
{showDescription && (
|
|
@@ -586,6 +681,7 @@ function AgentOverview({
|
|
|
586
681
|
value={spec?.description || ""}
|
|
587
682
|
onSave={(v) => saveField?.("description", v || undefined) ?? Promise.resolve(false)}
|
|
588
683
|
isSaving={isSaving}
|
|
684
|
+
error={errorFor("description")}
|
|
589
685
|
placeholder="Add a description"
|
|
590
686
|
minRows={2}
|
|
591
687
|
/>
|
|
@@ -608,6 +704,7 @@ function AgentOverview({
|
|
|
608
704
|
value={spec?.instructions ?? ""}
|
|
609
705
|
onSave={handleInstructionsSave}
|
|
610
706
|
isSaving={isSaving}
|
|
707
|
+
error={errorFor("instructions")}
|
|
611
708
|
placeholder="Add instructions for the agent"
|
|
612
709
|
minRows={4}
|
|
613
710
|
/>
|
|
@@ -619,14 +716,15 @@ function AgentOverview({
|
|
|
619
716
|
)}
|
|
620
717
|
|
|
621
718
|
{showMcpServers && (
|
|
622
|
-
<Section title="MCP Servers" count={spec?.mcpServerUsages.length} onEdit={editable ? () =>
|
|
719
|
+
<Section title="MCP Servers" count={spec?.mcpServerUsages.length} onEdit={editable ? () => handleMcpEditingChange(!mcpEditing) : undefined}>
|
|
623
720
|
{editable ? (
|
|
624
721
|
<InlineEditResourceList
|
|
625
722
|
value={mcpRefRows}
|
|
626
723
|
onSave={handleMcpServersSave}
|
|
627
724
|
isSaving={isSaving}
|
|
725
|
+
error={errorFor("mcpServerUsages")}
|
|
628
726
|
editing={mcpEditing}
|
|
629
|
-
onEditingChange={
|
|
727
|
+
onEditingChange={handleMcpEditingChange}
|
|
630
728
|
onItemClick={onMcpServerClick ? (ref) => onMcpServerClick({ org: ref.org, slug: ref.slug }) : undefined}
|
|
631
729
|
itemIcon={<McpServerIcon className="size-4" />}
|
|
632
730
|
resourceLabel="MCP server"
|
|
@@ -643,14 +741,15 @@ function AgentOverview({
|
|
|
643
741
|
)}
|
|
644
742
|
|
|
645
743
|
{showSkills && (
|
|
646
|
-
<Section title="Skills" count={spec?.skillRefs.length} onEdit={editable ? () =>
|
|
744
|
+
<Section title="Skills" count={spec?.skillRefs.length} onEdit={editable ? () => handleSkillsEditingChange(!skillsEditing) : undefined}>
|
|
647
745
|
{editable ? (
|
|
648
746
|
<InlineEditResourceList
|
|
649
747
|
value={skillRefRows}
|
|
650
748
|
onSave={handleSkillsSave}
|
|
651
749
|
isSaving={isSaving}
|
|
750
|
+
error={errorFor("skillRefs")}
|
|
652
751
|
editing={skillsEditing}
|
|
653
|
-
onEditingChange={
|
|
752
|
+
onEditingChange={handleSkillsEditingChange}
|
|
654
753
|
onItemClick={onSkillClick ? (ref) => onSkillClick({ org: ref.org, slug: ref.slug }) : undefined}
|
|
655
754
|
itemIcon={<SkillIcon className="size-4" />}
|
|
656
755
|
resourceLabel="skill"
|
|
@@ -666,24 +765,51 @@ function AgentOverview({
|
|
|
666
765
|
</Section>
|
|
667
766
|
)}
|
|
668
767
|
|
|
768
|
+
{showDatastores && (
|
|
769
|
+
<Section title="Datastores" count={spec?.datastoreUsages.length} onEdit={editable ? () => handleDatastoresEditingChange(!datastoresEditing) : undefined}>
|
|
770
|
+
{editable ? (
|
|
771
|
+
<InlineEditResourceList
|
|
772
|
+
value={datastoreRefRows}
|
|
773
|
+
onSave={handleDatastoresSave}
|
|
774
|
+
isSaving={isSaving}
|
|
775
|
+
error={errorFor("datastoreUsages")}
|
|
776
|
+
editing={datastoresEditing}
|
|
777
|
+
onEditingChange={handleDatastoresEditingChange}
|
|
778
|
+
onItemClick={onDatastoreClick ? (ref) => onDatastoreClick({ org: ref.org, slug: ref.slug }) : undefined}
|
|
779
|
+
itemIcon={<DatastoreIcon className="size-4" />}
|
|
780
|
+
resourceLabel="datastore"
|
|
781
|
+
defaultOrg={agentOrg}
|
|
782
|
+
/>
|
|
783
|
+
) : (
|
|
784
|
+
<DatastoresContent
|
|
785
|
+
usages={spec?.datastoreUsages ?? []}
|
|
786
|
+
defaultOrg={agentOrg}
|
|
787
|
+
onDatastoreClick={onDatastoreClick}
|
|
788
|
+
/>
|
|
789
|
+
)}
|
|
790
|
+
</Section>
|
|
791
|
+
)}
|
|
792
|
+
|
|
669
793
|
{showSubAgents && (
|
|
670
794
|
<SubAgentsSection
|
|
671
795
|
subAgents={spec?.subAgents ?? []}
|
|
672
796
|
editable={editable}
|
|
673
797
|
isSaving={isSaving}
|
|
798
|
+
error={errorFor("subAgents")}
|
|
674
799
|
onSave={(subs) => saveField?.("subAgents", subs.length > 0 ? subs : undefined) ?? Promise.resolve(false)}
|
|
675
800
|
/>
|
|
676
801
|
)}
|
|
677
802
|
|
|
678
803
|
{showEnv && (
|
|
679
|
-
<Section title="Environment Variables" count={Object.keys(spec?.env ?? {}).length} onEdit={editable ? () =>
|
|
804
|
+
<Section title="Environment Variables" count={Object.keys(spec?.env ?? {}).length} onEdit={editable ? () => handleEnvEditingChange(!envEditing) : undefined}>
|
|
680
805
|
{editable ? (
|
|
681
806
|
<InlineEditKeyValue
|
|
682
807
|
value={envRows}
|
|
683
808
|
onSave={handleEnvSave}
|
|
684
809
|
isSaving={isSaving}
|
|
810
|
+
error={errorFor("env")}
|
|
685
811
|
editing={envEditing}
|
|
686
|
-
onEditingChange={
|
|
812
|
+
onEditingChange={handleEnvEditingChange}
|
|
687
813
|
showSecretToggle
|
|
688
814
|
showOptionalToggle
|
|
689
815
|
showDescription
|
|
@@ -860,6 +986,59 @@ function SkillsContent({
|
|
|
860
986
|
);
|
|
861
987
|
}
|
|
862
988
|
|
|
989
|
+
function DatastoresContent({
|
|
990
|
+
usages,
|
|
991
|
+
defaultOrg,
|
|
992
|
+
onDatastoreClick,
|
|
993
|
+
}: {
|
|
994
|
+
readonly usages: readonly DatastoreUsage[];
|
|
995
|
+
readonly defaultOrg: string;
|
|
996
|
+
readonly onDatastoreClick?: (ref: { org: string; slug: string }) => void;
|
|
997
|
+
}) {
|
|
998
|
+
return (
|
|
999
|
+
<div className="flex flex-col">
|
|
1000
|
+
{usages.map((usage, index) => {
|
|
1001
|
+
const ref = usage.datastoreRef;
|
|
1002
|
+
if (!ref) return null;
|
|
1003
|
+
|
|
1004
|
+
const refOrg = ref.org || defaultOrg;
|
|
1005
|
+
const label =
|
|
1006
|
+
ref.org && ref.org !== defaultOrg
|
|
1007
|
+
? `${ref.org}/${ref.slug}`
|
|
1008
|
+
: ref.slug;
|
|
1009
|
+
|
|
1010
|
+
const row = (
|
|
1011
|
+
<div className="flex items-center gap-3">
|
|
1012
|
+
<DatastoreIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
1013
|
+
<span className="text-sm font-medium text-foreground">
|
|
1014
|
+
{label}
|
|
1015
|
+
</span>
|
|
1016
|
+
</div>
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
return onDatastoreClick ? (
|
|
1020
|
+
<button
|
|
1021
|
+
key={ref.slug || index}
|
|
1022
|
+
type="button"
|
|
1023
|
+
onClick={() => onDatastoreClick({ org: refOrg, slug: ref.slug })}
|
|
1024
|
+
className={cn(
|
|
1025
|
+
"w-full rounded-md px-3 py-2 text-left transition-colors",
|
|
1026
|
+
"hover:bg-accent-hover",
|
|
1027
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
|
|
1028
|
+
)}
|
|
1029
|
+
>
|
|
1030
|
+
{row}
|
|
1031
|
+
</button>
|
|
1032
|
+
) : (
|
|
1033
|
+
<div key={ref.slug || index} className="px-3 py-2">
|
|
1034
|
+
{row}
|
|
1035
|
+
</div>
|
|
1036
|
+
);
|
|
1037
|
+
})}
|
|
1038
|
+
</div>
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
863
1042
|
interface SubAgentDraft {
|
|
864
1043
|
name: string;
|
|
865
1044
|
description: string;
|
|
@@ -870,11 +1049,13 @@ function SubAgentsSection({
|
|
|
870
1049
|
subAgents,
|
|
871
1050
|
editable,
|
|
872
1051
|
isSaving,
|
|
1052
|
+
error,
|
|
873
1053
|
onSave,
|
|
874
1054
|
}: {
|
|
875
1055
|
readonly subAgents: readonly SubAgent[];
|
|
876
1056
|
readonly editable?: boolean;
|
|
877
1057
|
readonly isSaving?: boolean;
|
|
1058
|
+
readonly error?: string | null;
|
|
878
1059
|
readonly onSave?: (subs: import("@stigmer/sdk").SubAgentInput[]) => Promise<boolean>;
|
|
879
1060
|
}) {
|
|
880
1061
|
const [expanded, setExpanded] = useState<Set<number>>(new Set());
|
|
@@ -1025,6 +1206,12 @@ function SubAgentsSection({
|
|
|
1025
1206
|
</div>
|
|
1026
1207
|
)}
|
|
1027
1208
|
|
|
1209
|
+
{error && (
|
|
1210
|
+
<p className="border-t border-border px-3 py-2 text-xs text-destructive" role="alert">
|
|
1211
|
+
{error}
|
|
1212
|
+
</p>
|
|
1213
|
+
)}
|
|
1214
|
+
|
|
1028
1215
|
{editable && showAddForm && (
|
|
1029
1216
|
<div className="border-t border-border p-3 space-y-2">
|
|
1030
1217
|
<input
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Tests for the Datastores section on AgentDetailView (stigmer/stigmer#319):
|
|
2
|
+
// read-mode rendering and navigation, the inline-edit save payload (which
|
|
3
|
+
// must round-trip the full spec through agentToInput), and the DD-006
|
|
4
|
+
// surfacing of the backend's datastore exposure guard refusal.
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, beforeAll, afterEach, beforeEach, type Mock } from "vitest";
|
|
7
|
+
import { render, screen, cleanup, waitFor } from "@testing-library/react";
|
|
8
|
+
import userEvent from "@testing-library/user-event";
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
|
+
import { create } from "@bufbuild/protobuf";
|
|
11
|
+
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
12
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
13
|
+
import type { AgentInput } from "@stigmer/sdk";
|
|
14
|
+
import { StigmerContext } from "../../context";
|
|
15
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
16
|
+
import { AgentDetailView } from "../AgentDetailView";
|
|
17
|
+
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
HTMLDialogElement.prototype.showModal = function showModal() {
|
|
20
|
+
this.open = true;
|
|
21
|
+
};
|
|
22
|
+
HTMLDialogElement.prototype.close = function close() {
|
|
23
|
+
this.open = false;
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => cleanup());
|
|
28
|
+
beforeEach(() => vi.restoreAllMocks());
|
|
29
|
+
|
|
30
|
+
function makeAgent() {
|
|
31
|
+
return create(AgentSchema, {
|
|
32
|
+
metadata: {
|
|
33
|
+
id: "agt_1",
|
|
34
|
+
name: "Clinic Assistant",
|
|
35
|
+
slug: "clinic-assistant",
|
|
36
|
+
org: "acme",
|
|
37
|
+
},
|
|
38
|
+
spec: {
|
|
39
|
+
instructions: "You are a careful clinic assistant.",
|
|
40
|
+
datastoreUsages: [
|
|
41
|
+
{
|
|
42
|
+
datastoreRef: {
|
|
43
|
+
org: "acme",
|
|
44
|
+
slug: "clinic-records",
|
|
45
|
+
kind: ApiResourceKind.datastore,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
datastoreRef: {
|
|
50
|
+
org: "partner",
|
|
51
|
+
slug: "shared-records",
|
|
52
|
+
kind: ApiResourceKind.datastore,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderView(overrides: {
|
|
61
|
+
update?: Mock;
|
|
62
|
+
onDatastoreClick?: (ref: { org: string; slug: string }) => void;
|
|
63
|
+
editable?: boolean;
|
|
64
|
+
}) {
|
|
65
|
+
const agent = makeAgent();
|
|
66
|
+
const update = overrides.update ?? vi.fn().mockResolvedValue(agent);
|
|
67
|
+
const stigmer = {
|
|
68
|
+
agent: {
|
|
69
|
+
getByReference: vi.fn().mockResolvedValue(agent),
|
|
70
|
+
update,
|
|
71
|
+
},
|
|
72
|
+
iamPolicy: {
|
|
73
|
+
checkMyPermission: vi.fn().mockResolvedValue({ isAuthorized: false }),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function Wrapper({ children }: { children: ReactNode }) {
|
|
78
|
+
return (
|
|
79
|
+
<FetchCacheContext.Provider value={null}>
|
|
80
|
+
<StigmerContext.Provider value={stigmer as never}>
|
|
81
|
+
{children}
|
|
82
|
+
</StigmerContext.Provider>
|
|
83
|
+
</FetchCacheContext.Provider>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = render(
|
|
88
|
+
<AgentDetailView
|
|
89
|
+
org="acme"
|
|
90
|
+
slug="clinic-assistant"
|
|
91
|
+
editable={overrides.editable}
|
|
92
|
+
onDatastoreClick={overrides.onDatastoreClick}
|
|
93
|
+
/>,
|
|
94
|
+
{ wrapper: Wrapper },
|
|
95
|
+
);
|
|
96
|
+
return { ...result, update };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe("AgentDetailView — Datastores section (read mode)", () => {
|
|
100
|
+
it("renders attached datastores with org-qualified labels for cross-org refs", async () => {
|
|
101
|
+
renderView({});
|
|
102
|
+
|
|
103
|
+
expect(await screen.findByText("Datastores")).toBeTruthy();
|
|
104
|
+
expect(screen.getByText("clinic-records")).toBeTruthy();
|
|
105
|
+
expect(screen.getByText("partner/shared-records")).toBeTruthy();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("fires onDatastoreClick with the resolved org and slug", async () => {
|
|
109
|
+
const user = userEvent.setup();
|
|
110
|
+
const onDatastoreClick = vi.fn();
|
|
111
|
+
renderView({ onDatastoreClick });
|
|
112
|
+
|
|
113
|
+
await user.click(await screen.findByText("clinic-records"));
|
|
114
|
+
expect(onDatastoreClick).toHaveBeenCalledWith({
|
|
115
|
+
org: "acme",
|
|
116
|
+
slug: "clinic-records",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await user.click(screen.getByText("partner/shared-records"));
|
|
120
|
+
expect(onDatastoreClick).toHaveBeenCalledWith({
|
|
121
|
+
org: "partner",
|
|
122
|
+
slug: "shared-records",
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("AgentDetailView — Datastores section (inline edit)", () => {
|
|
128
|
+
it("saves the full reconstructed input with correctly-shaped datastoreUsages", async () => {
|
|
129
|
+
const user = userEvent.setup();
|
|
130
|
+
const { update } = renderView({ editable: true });
|
|
131
|
+
|
|
132
|
+
await user.click(await screen.findByLabelText("Edit datastores"));
|
|
133
|
+
await user.click(screen.getByLabelText("Remove partner/shared-records"));
|
|
134
|
+
await user.click(screen.getByRole("button", { name: "Save changes" }));
|
|
135
|
+
|
|
136
|
+
await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
|
|
137
|
+
const input = update.mock.calls[0][0] as AgentInput;
|
|
138
|
+
|
|
139
|
+
expect(input.datastoreUsages).toEqual([
|
|
140
|
+
{ datastoreRef: { org: "acme", slug: "clinic-records" } },
|
|
141
|
+
]);
|
|
142
|
+
// The rest of the spec must ride along — a section save is a full
|
|
143
|
+
// spec replacement reconstructed via agentToInput().
|
|
144
|
+
expect(input.instructions).toBe("You are a careful clinic assistant.");
|
|
145
|
+
expect(input.org).toBe("acme");
|
|
146
|
+
expect(input.slug).toBe("clinic-assistant");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("surfaces the backend's exposure-guard refusal in the section editor (DD-006)", async () => {
|
|
150
|
+
const user = userEvent.setup();
|
|
151
|
+
const guardMessage =
|
|
152
|
+
'agent "clinic-assistant" cannot be public while it uses datastores ' +
|
|
153
|
+
"(clinic-records): multi-tenant datastore sharing is not supported — " +
|
|
154
|
+
"keep the agent private or org-visible, or remove its datastore_usages";
|
|
155
|
+
renderView({
|
|
156
|
+
editable: true,
|
|
157
|
+
update: vi.fn().mockRejectedValue(new Error(guardMessage)),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await user.click(await screen.findByLabelText("Edit datastores"));
|
|
161
|
+
await user.click(screen.getByRole("button", { name: "Save changes" }));
|
|
162
|
+
|
|
163
|
+
const alert = await screen.findByRole("alert");
|
|
164
|
+
expect(alert.textContent).toContain(guardMessage);
|
|
165
|
+
});
|
|
166
|
+
});
|