@stigmer/react 3.1.14 → 3.1.16
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 +7 -5
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +1 -1
- package/agent/AgentDetailView.js.map +1 -1
- package/agent-instance/AgentInstanceEmptyState.d.ts +4 -1
- package/agent-instance/AgentInstanceEmptyState.d.ts.map +1 -1
- package/agent-instance/AgentInstanceEmptyState.js +12 -5
- package/agent-instance/AgentInstanceEmptyState.js.map +1 -1
- package/agent-instance/AgentInstanceList.d.ts +13 -2
- package/agent-instance/AgentInstanceList.d.ts.map +1 -1
- package/agent-instance/AgentInstanceList.js +8 -4
- package/agent-instance/AgentInstanceList.js.map +1 -1
- package/agent-instance/useAgentInstances.d.ts +10 -3
- package/agent-instance/useAgentInstances.d.ts.map +1 -1
- package/agent-instance/useAgentInstances.js +12 -4
- package/agent-instance/useAgentInstances.js.map +1 -1
- package/button/Button.d.ts +54 -0
- package/button/Button.d.ts.map +1 -0
- package/button/Button.js +42 -0
- package/button/Button.js.map +1 -0
- package/button/index.d.ts +3 -0
- package/button/index.d.ts.map +1 -0
- package/button/index.js +2 -0
- package/button/index.js.map +1 -0
- package/empty-state/EmptyState.d.ts.map +1 -1
- package/empty-state/EmptyState.js +8 -3
- package/empty-state/EmptyState.js.map +1 -1
- package/empty-state/types.d.ts +2 -0
- package/empty-state/types.d.ts.map +1 -1
- package/execution/index.d.ts +1 -1
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js.map +1 -1
- package/execution/useCreateAgentExecution.d.ts +81 -9
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +42 -4
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +3 -1
- package/index.d.ts.map +1 -1
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/session/useNewSessionFlow.d.ts +6 -3
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +34 -35
- package/session/useNewSessionFlow.js.map +1 -1
- package/sharing/AgentShareList.d.ts +9 -7
- package/sharing/AgentShareList.d.ts.map +1 -1
- package/sharing/AgentShareList.js +17 -7
- package/sharing/AgentShareList.js.map +1 -1
- package/sharing/useAgentShares.d.ts +15 -9
- package/sharing/useAgentShares.d.ts.map +1 -1
- package/sharing/useAgentShares.js +17 -11
- package/sharing/useAgentShares.js.map +1 -1
- package/src/agent/AgentDetailView.tsx +8 -5
- package/src/agent-instance/AgentInstanceEmptyState.tsx +24 -43
- package/src/agent-instance/AgentInstanceList.tsx +29 -16
- package/src/agent-instance/__tests__/agent-instance-hooks.test.tsx +19 -0
- package/src/agent-instance/useAgentInstances.ts +12 -3
- package/src/button/Button.tsx +104 -0
- package/src/button/__tests__/Button.test.tsx +82 -0
- package/src/button/index.ts +2 -0
- package/src/empty-state/EmptyState.tsx +8 -10
- package/src/empty-state/types.ts +2 -0
- package/src/execution/__tests__/useCreateAgentExecution.test.tsx +92 -0
- package/src/execution/index.ts +2 -0
- package/src/execution/useCreateAgentExecution.ts +116 -10
- package/src/index.ts +6 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +121 -52
- package/src/session/useNewSessionFlow.ts +38 -37
- package/src/sharing/AgentShareList.tsx +38 -43
- package/src/sharing/__tests__/useAgentShares.test.tsx +42 -1
- package/src/sharing/useAgentShares.ts +17 -11
- package/src/workflow/WorkflowDetailView.tsx +9 -0
- package/src/workflow/instance/WorkflowInstanceEmptyState.tsx +24 -43
- package/src/workflow/instance/WorkflowInstanceList.tsx +29 -16
- package/src/workflow/useWorkflowInstances.ts +10 -3
- package/workflow/WorkflowDetailView.d.ts +8 -1
- package/workflow/WorkflowDetailView.d.ts.map +1 -1
- package/workflow/WorkflowDetailView.js +2 -2
- package/workflow/WorkflowDetailView.js.map +1 -1
- package/workflow/instance/WorkflowInstanceEmptyState.d.ts +4 -1
- package/workflow/instance/WorkflowInstanceEmptyState.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceEmptyState.js +12 -5
- package/workflow/instance/WorkflowInstanceEmptyState.js.map +1 -1
- package/workflow/instance/WorkflowInstanceList.d.ts +13 -2
- package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceList.js +8 -4
- package/workflow/instance/WorkflowInstanceList.js.map +1 -1
- package/workflow/useWorkflowInstances.d.ts +8 -3
- package/workflow/useWorkflowInstances.d.ts.map +1 -1
- package/workflow/useWorkflowInstances.js +10 -4
- package/workflow/useWorkflowInstances.js.map +1 -1
|
@@ -10,8 +10,8 @@ import { DEFAULT_HARNESS, type HarnessOption } from "../models/harness.js";
|
|
|
10
10
|
import { useWorkspaceEntries, type UseWorkspaceEntriesReturn } from "../workspace/index.js";
|
|
11
11
|
import { useSessionVariables, type UseSessionVariablesReturn } from "../execution/useSessionVariables.js";
|
|
12
12
|
import type { SessionComposerSubmitContext } from "../composer/index.js";
|
|
13
|
+
import { useStigmer } from "../hooks.js";
|
|
13
14
|
import { withTimeout } from "../internal/withTimeout.js";
|
|
14
|
-
import { useCreateSession } from "./useCreateSession.js";
|
|
15
15
|
import { useCreateAgentExecution } from "../execution/useCreateAgentExecution.js";
|
|
16
16
|
import type { ExecutionTargetOption } from "./execution-target.js";
|
|
17
17
|
import { useExecutionTarget } from "../execution-target-context.js";
|
|
@@ -140,8 +140,9 @@ export interface UseNewSessionFlowReturn {
|
|
|
140
140
|
* Create a session with the first execution.
|
|
141
141
|
*
|
|
142
142
|
* Composes all managed state (agent, workspace, MCP servers, skills,
|
|
143
|
-
* model, session variables) into
|
|
144
|
-
*
|
|
143
|
+
* model, session variables) into a single bootstrap RPC —
|
|
144
|
+
* `agentExecution.create` with an embedded session spec — then calls
|
|
145
|
+
* `onSessionCreated` on success.
|
|
145
146
|
*
|
|
146
147
|
* The `model` parameter overrides `modelId` for this submission only
|
|
147
148
|
* (used when SessionComposer passes a per-message model selection).
|
|
@@ -160,7 +161,9 @@ export interface UseNewSessionFlowReturn {
|
|
|
160
161
|
* model selection (with localStorage persistence), agent resolution,
|
|
161
162
|
* MCP server/skill selection, workspace entries, and session
|
|
162
163
|
* variables. On submission, creates the session and its first execution
|
|
163
|
-
*
|
|
164
|
+
* with a single one-call bootstrap RPC (`agentExecution.create` with an
|
|
165
|
+
* embedded session spec), then notifies the consumer via
|
|
166
|
+
* `onSessionCreated`.
|
|
164
167
|
*
|
|
165
168
|
* This hook is framework-agnostic — it works identically in Next.js,
|
|
166
169
|
* Vite, Tauri, or any React environment. Navigation, toast notifications,
|
|
@@ -210,8 +213,8 @@ export function useNewSessionFlow(
|
|
|
210
213
|
return defaultHarness ?? DEFAULT_HARNESS;
|
|
211
214
|
});
|
|
212
215
|
|
|
216
|
+
const stigmer = useStigmer();
|
|
213
217
|
const { getModel, isLoading: isModelsLoading } = useModelRegistry({ harness });
|
|
214
|
-
const { create: createSession } = useCreateSession();
|
|
215
218
|
const { create: createExecution } = useCreateAgentExecution();
|
|
216
219
|
// Guests cannot read the org default agent (and must never run it) —
|
|
217
220
|
// `null` puts the hook in its stable no-op mode, and the submit path
|
|
@@ -296,8 +299,7 @@ export function useNewSessionFlow(
|
|
|
296
299
|
? await resolveExecutionRuntimeEnv(getRuntimeEnv, context?.runtimeEnv)
|
|
297
300
|
: context?.runtimeEnv;
|
|
298
301
|
|
|
299
|
-
const
|
|
300
|
-
org,
|
|
302
|
+
const sessionSpecBase = {
|
|
301
303
|
workspaceEntries: workspace.hasEntries
|
|
302
304
|
? workspace.toInput()
|
|
303
305
|
: undefined,
|
|
@@ -317,19 +319,21 @@ export function useNewSessionFlow(
|
|
|
317
319
|
workspaceFileRefs: context?.workspaceFileRefs,
|
|
318
320
|
};
|
|
319
321
|
|
|
320
|
-
|
|
322
|
+
// Resolve which agent the bootstrapped session runs against: an
|
|
323
|
+
// explicit instance when one is known, otherwise an agent ID the
|
|
324
|
+
// server resolves to its default instance (creating it if missing).
|
|
325
|
+
let agentInstanceId: string | undefined;
|
|
326
|
+
let agentId: string | undefined;
|
|
321
327
|
|
|
322
328
|
if (agentRef && resolution) {
|
|
323
329
|
if (resolution.mode === "saved") {
|
|
324
|
-
|
|
325
|
-
...sessionFields,
|
|
326
|
-
agentInstanceId: resolution.instanceId,
|
|
327
|
-
}));
|
|
330
|
+
agentInstanceId = resolution.instanceId;
|
|
328
331
|
} else {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
332
|
+
// Slug reference → agent ID; the server handles instance
|
|
333
|
+
// resolution, including auto-creating a missing default
|
|
334
|
+
// instance (which a client-side lookup cannot).
|
|
335
|
+
const agent = await stigmer.agent.getByReference(agentRef);
|
|
336
|
+
agentId = agent.metadata!.id;
|
|
333
337
|
}
|
|
334
338
|
} else if (isGuest) {
|
|
335
339
|
// Fail closed: a guest session is only ever created against the
|
|
@@ -360,32 +364,29 @@ export function useNewSessionFlow(
|
|
|
360
364
|
);
|
|
361
365
|
}
|
|
362
366
|
}
|
|
363
|
-
|
|
364
|
-
...sessionFields,
|
|
365
|
-
agentInstanceId: resolvedInstanceId,
|
|
366
|
-
}));
|
|
367
|
+
agentInstanceId = resolvedInstanceId;
|
|
367
368
|
}
|
|
368
369
|
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
370
|
+
// One-call bootstrap: the embedded session spec and the first
|
|
371
|
+
// message travel in a single create — the server creates the
|
|
372
|
+
// session and dispatches the execution atomically.
|
|
373
|
+
const { sessionId } = await createExecution({
|
|
374
|
+
...executionFields,
|
|
375
|
+
agentId,
|
|
376
|
+
sessionSpec: { ...sessionSpecBase, agentInstanceId },
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Local execution: attach the session's runner worker now that the
|
|
380
|
+
// session ID is known. The session view (useSessionConversation)
|
|
381
|
+
// owns the steady-state lifecycle, but it is not mounted yet —
|
|
382
|
+
// navigation happens after onSessionCreated below. Attaching after
|
|
383
|
+
// the create is safe: the first activity waits on the session's
|
|
384
|
+
// task queue for a worker (5-minute ScheduleToStart window), so the
|
|
385
|
+
// worker attached here picks it up immediately.
|
|
374
386
|
if (adapter && executionTarget === "local") {
|
|
375
387
|
await adapter.onSessionOpened(sessionId);
|
|
376
388
|
}
|
|
377
389
|
|
|
378
|
-
try {
|
|
379
|
-
await createExecution({ ...executionFields, sessionId });
|
|
380
|
-
} catch (err) {
|
|
381
|
-
// The first execution failed and the session view will not mount,
|
|
382
|
-
// so detach the worker we just attached to avoid leaking it.
|
|
383
|
-
if (adapter && executionTarget === "local") {
|
|
384
|
-
adapter.onSessionClosed(sessionId).catch(() => {});
|
|
385
|
-
}
|
|
386
|
-
throw err;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
390
|
sessionVariables.clear();
|
|
390
391
|
onSessionCreated(sessionId);
|
|
391
392
|
} catch (err) {
|
|
@@ -414,7 +415,7 @@ export function useNewSessionFlow(
|
|
|
414
415
|
isDefaultAgentLoading,
|
|
415
416
|
defaultAgentError,
|
|
416
417
|
waitForDefaultAgent,
|
|
417
|
-
|
|
418
|
+
stigmer.agent,
|
|
418
419
|
createExecution,
|
|
419
420
|
sessionVariables,
|
|
420
421
|
onSessionCreated,
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
11
11
|
import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
|
|
12
12
|
import { toast } from "../feedback/toast.js";
|
|
13
|
+
import { Button } from "../button/Button.js";
|
|
14
|
+
import { EmptyState } from "../empty-state/EmptyState.js";
|
|
13
15
|
import { PermissionGate } from "../iam-policy/PermissionGate.js";
|
|
14
16
|
import { ConfirmDialog } from "../resource-detail/ConfirmDialog.js";
|
|
15
17
|
import { useConfirmAction } from "../resource-detail/useConfirmAction.js";
|
|
@@ -30,9 +32,10 @@ export interface AgentShareListProps {
|
|
|
30
32
|
/** The agent whose shares are managed. */
|
|
31
33
|
readonly agent: Agent;
|
|
32
34
|
/**
|
|
33
|
-
* The viewer's active organization slug.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* The viewer's active organization slug. Scopes the list to this
|
|
36
|
+
* org's channels of the agent, and a share created from this list
|
|
37
|
+
* lands in this org — its URL, billing, and credentials belong to it
|
|
38
|
+
* (a **cross-org share** when it differs from the agent's org,
|
|
36
39
|
* decision 013). Omit to default to the agent's own org.
|
|
37
40
|
*/
|
|
38
41
|
readonly viewerOrg?: string;
|
|
@@ -52,10 +55,11 @@ export interface AgentShareListProps {
|
|
|
52
55
|
* agent analog of {@link AgentInstanceList}, rendered in the agent
|
|
53
56
|
* detail view's Shares tab.
|
|
54
57
|
*
|
|
55
|
-
* Lists
|
|
56
|
-
* `getByAgent
|
|
57
|
-
*
|
|
58
|
-
*
|
|
58
|
+
* Lists the viewer's active org's channels of the agent: `viewerOrg`
|
|
59
|
+
* scopes the `getByAgent` call server-side, so a member of several orgs
|
|
60
|
+
* sees exactly the current org context's channels — never a merged list
|
|
61
|
+
* of every org's (decision 013 amendment). Per-row actions: copy link,
|
|
62
|
+
* edit, pause/resume, reset link, delete. Creation goes through the same
|
|
59
63
|
* {@link ShareAgentDialog} and always lands in the viewer's active org;
|
|
60
64
|
* the create bar mirrors the server's via
|
|
61
65
|
* {@link useCanCreateAgentShare}, so the button never appears to a user
|
|
@@ -74,7 +78,13 @@ export function AgentShareList({
|
|
|
74
78
|
className,
|
|
75
79
|
}: AgentShareListProps) {
|
|
76
80
|
const agentId = agent.metadata?.id ?? "";
|
|
77
|
-
|
|
81
|
+
// Scope the list to the org whose context the viewer is in; when the
|
|
82
|
+
// host passes no viewerOrg the scope falls back to the agent's own org
|
|
83
|
+
// (the same-org owner flow), matching where creation would land.
|
|
84
|
+
const { shares, isLoading, error, refetch } = useAgentShares(
|
|
85
|
+
agentId,
|
|
86
|
+
viewerOrg || (agent.metadata?.org ?? ""),
|
|
87
|
+
);
|
|
78
88
|
const { allowed: canCreate, shareOrg } = useCanCreateAgentShare(
|
|
79
89
|
agent,
|
|
80
90
|
viewerOrg,
|
|
@@ -142,20 +152,14 @@ export function AgentShareList({
|
|
|
142
152
|
{shares.length} {shares.length === 1 ? "share" : "shares"}
|
|
143
153
|
</h3>
|
|
144
154
|
{canCreate && (
|
|
145
|
-
<
|
|
146
|
-
|
|
155
|
+
<Button
|
|
156
|
+
variant="outline"
|
|
157
|
+
size="xs"
|
|
158
|
+
icon={<PlusIcon />}
|
|
147
159
|
onClick={() => setEditor({ mode: "create" })}
|
|
148
|
-
className={cn(
|
|
149
|
-
"inline-flex items-center gap-1 rounded-md px-2.5 py-1",
|
|
150
|
-
"text-xs font-medium",
|
|
151
|
-
"border border-border text-foreground",
|
|
152
|
-
"hover:bg-accent-hover",
|
|
153
|
-
"focus:outline-none focus:ring-2 focus:ring-ring",
|
|
154
|
-
)}
|
|
155
160
|
>
|
|
156
|
-
<PlusIcon />
|
|
157
161
|
Create share
|
|
158
|
-
</
|
|
162
|
+
</Button>
|
|
159
163
|
)}
|
|
160
164
|
</div>
|
|
161
165
|
|
|
@@ -428,30 +432,21 @@ function ShareEmptyState({
|
|
|
428
432
|
readonly onCreateClick: () => void;
|
|
429
433
|
}) {
|
|
430
434
|
return (
|
|
431
|
-
<
|
|
432
|
-
|
|
433
|
-
<
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
onClick
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
"bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
447
|
-
"focus:outline-none focus:ring-2 focus:ring-ring",
|
|
448
|
-
)}
|
|
449
|
-
>
|
|
450
|
-
<PlusIcon />
|
|
451
|
-
Create share
|
|
452
|
-
</button>
|
|
453
|
-
)}
|
|
454
|
-
</div>
|
|
435
|
+
<EmptyState
|
|
436
|
+
variant="first-use"
|
|
437
|
+
icon={<ShareIcon className="size-10" />}
|
|
438
|
+
title="No shares yet"
|
|
439
|
+
description={
|
|
440
|
+
"A share gives this agent a hosted chat link you can send or embed — " +
|
|
441
|
+
"each share is its own channel with its own audience, embed origins, " +
|
|
442
|
+
"and tool credentials."
|
|
443
|
+
}
|
|
444
|
+
action={
|
|
445
|
+
canCreate
|
|
446
|
+
? { label: "Create share", onClick: onCreateClick, icon: <PlusIcon /> }
|
|
447
|
+
: undefined
|
|
448
|
+
}
|
|
449
|
+
/>
|
|
455
450
|
);
|
|
456
451
|
}
|
|
457
452
|
|
|
@@ -59,7 +59,48 @@ describe("useAgentShares", () => {
|
|
|
59
59
|
expect(result.current.shares).toEqual([share]);
|
|
60
60
|
expect(result.current.error).toBeNull();
|
|
61
61
|
expect(getByAgent).toHaveBeenCalledWith(
|
|
62
|
-
expect.objectContaining({ agentId: "agt_1" }),
|
|
62
|
+
expect.objectContaining({ agentId: "agt_1", org: "" }),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("threads the org scope into the request", async () => {
|
|
67
|
+
// The org scope belongs in the RPC, never client-side filtering: the
|
|
68
|
+
// server narrows a multi-org member's view to one org's channels.
|
|
69
|
+
const getByAgent = vi
|
|
70
|
+
.fn()
|
|
71
|
+
.mockResolvedValue({ totalCount: 0, items: [] });
|
|
72
|
+
const client = createMockStigmer({ getByAgent });
|
|
73
|
+
|
|
74
|
+
const { result } = renderHook(() => useAgentShares("agt_1", "acme"), {
|
|
75
|
+
wrapper: wrapper(client),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
79
|
+
expect(getByAgent).toHaveBeenCalledWith(
|
|
80
|
+
expect.objectContaining({ agentId: "agt_1", org: "acme" }),
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("refetches when the org scope changes", async () => {
|
|
85
|
+
const getByAgent = vi
|
|
86
|
+
.fn()
|
|
87
|
+
.mockResolvedValue({ totalCount: 0, items: [] });
|
|
88
|
+
const client = createMockStigmer({ getByAgent });
|
|
89
|
+
|
|
90
|
+
const { result, rerender } = renderHook(
|
|
91
|
+
({ org }: { org: string }) => useAgentShares("agt_1", org),
|
|
92
|
+
{ wrapper: wrapper(client), initialProps: { org: "acme" } },
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
96
|
+
expect(getByAgent).toHaveBeenCalledTimes(1);
|
|
97
|
+
|
|
98
|
+
// Switching org context must re-query — a stale other-org list is
|
|
99
|
+
// exactly the bug the scope exists to prevent.
|
|
100
|
+
rerender({ org: "consumer-org" });
|
|
101
|
+
await waitFor(() => expect(getByAgent).toHaveBeenCalledTimes(2));
|
|
102
|
+
expect(getByAgent).toHaveBeenLastCalledWith(
|
|
103
|
+
expect.objectContaining({ agentId: "agt_1", org: "consumer-org" }),
|
|
63
104
|
);
|
|
64
105
|
});
|
|
65
106
|
|
|
@@ -26,33 +26,39 @@ export interface UseAgentSharesReturn {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Data hook that loads
|
|
30
|
-
*
|
|
29
|
+
* Data hook that loads an agent's {@link AgentShare} resources — the
|
|
30
|
+
* channels carrying its hosted-chat configuration (audience, allowed
|
|
31
31
|
* origins, visitor messages, tool credentials, link token).
|
|
32
32
|
*
|
|
33
33
|
* Sharing is channel configuration, not agent behavior (decision 011):
|
|
34
34
|
* an agent can carry N shares, each with its own URL, billing org, and
|
|
35
|
-
* credentials (decision 011 D3 + decision 013 cross-org shares).
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* credentials (decision 011 D3 + decision 013 cross-org shares).
|
|
36
|
+
*
|
|
37
|
+
* Pass `org` to scope the list to one organization's channels — the
|
|
38
|
+
* org-context view a console tab needs. Permissions alone cannot provide
|
|
39
|
+
* this scope: a member of several orgs can view all of their orgs'
|
|
40
|
+
* channels of the agent, so the unscoped list merges them. The server
|
|
41
|
+
* applies the scope (it only ever narrows the permission-bounded view);
|
|
42
|
+
* omit `org` for the full permission-bounded list.
|
|
40
43
|
*
|
|
41
44
|
* Pass an empty `agentId` to skip fetching (stable no-op) — useful
|
|
42
45
|
* while the agent is still loading.
|
|
43
46
|
*
|
|
44
47
|
* @example
|
|
45
48
|
* ```tsx
|
|
46
|
-
* const { shares, isLoading, refetch } = useAgentShares(
|
|
49
|
+
* const { shares, isLoading, refetch } = useAgentShares(
|
|
50
|
+
* agent?.metadata?.id ?? "",
|
|
51
|
+
* viewerOrg,
|
|
52
|
+
* );
|
|
47
53
|
* ```
|
|
48
54
|
*/
|
|
49
|
-
export function useAgentShares(agentId: string): UseAgentSharesReturn {
|
|
55
|
+
export function useAgentShares(agentId: string, org?: string): UseAgentSharesReturn {
|
|
50
56
|
const stigmer = useStigmer();
|
|
51
57
|
|
|
52
58
|
const fetchFn = agentId
|
|
53
59
|
? async () => {
|
|
54
60
|
const result = await stigmer.agentShare.getByAgent(
|
|
55
|
-
create(GetAgentSharesByAgentRequestSchema, { agentId }),
|
|
61
|
+
create(GetAgentSharesByAgentRequestSchema, { agentId, org: org ?? "" }),
|
|
56
62
|
);
|
|
57
63
|
return result.items;
|
|
58
64
|
}
|
|
@@ -60,7 +66,7 @@ export function useAgentShares(agentId: string): UseAgentSharesReturn {
|
|
|
60
66
|
|
|
61
67
|
const { data: shares, isLoading, isRefetching, error, refetch } = useFetch(
|
|
62
68
|
fetchFn,
|
|
63
|
-
[agentId, stigmer],
|
|
69
|
+
[agentId, org, stigmer],
|
|
64
70
|
[] as AgentShare[],
|
|
65
71
|
);
|
|
66
72
|
|
|
@@ -76,6 +76,13 @@ export interface WorkflowDetailViewProps {
|
|
|
76
76
|
* Receives the execution ID — use for navigation to the execution viewer.
|
|
77
77
|
*/
|
|
78
78
|
readonly onExecutionClick?: (executionId: string) => void;
|
|
79
|
+
/**
|
|
80
|
+
* The viewer's active organization slug, feeding the Instances tab:
|
|
81
|
+
* it scopes the list to this org's instances of the workflow, so a
|
|
82
|
+
* member of several orgs sees the current org context only. Omit to
|
|
83
|
+
* default to the workflow's own org.
|
|
84
|
+
*/
|
|
85
|
+
readonly viewerOrg?: string;
|
|
79
86
|
/**
|
|
80
87
|
* Called when the user clicks "Create Instance" in the Instances tab.
|
|
81
88
|
* Opens the create instance dialog.
|
|
@@ -157,6 +164,7 @@ export function WorkflowDetailView({
|
|
|
157
164
|
onTabChange,
|
|
158
165
|
defaultTab,
|
|
159
166
|
onExecutionClick,
|
|
167
|
+
viewerOrg,
|
|
160
168
|
onCreateInstanceClick,
|
|
161
169
|
onInstanceClick,
|
|
162
170
|
onInstanceRunClick,
|
|
@@ -300,6 +308,7 @@ export function WorkflowDetailView({
|
|
|
300
308
|
workflowId={meta?.id ?? ""}
|
|
301
309
|
defaultInstanceId={workflow?.status?.defaultInstanceId}
|
|
302
310
|
org={org}
|
|
311
|
+
viewerOrg={viewerOrg}
|
|
303
312
|
onCreateClick={onCreateInstanceClick}
|
|
304
313
|
onInstanceClick={onInstanceClick}
|
|
305
314
|
onRunClick={onInstanceRunClick}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { EmptyState } from "../../empty-state/EmptyState.js";
|
|
4
4
|
|
|
5
5
|
/** Props for {@link WorkflowInstanceEmptyState}. */
|
|
6
6
|
export interface WorkflowInstanceEmptyStateProps {
|
|
7
|
-
/** Called when the user clicks "Create
|
|
7
|
+
/** Called when the user clicks "Create instance". */
|
|
8
8
|
readonly onCreateClick?: () => void;
|
|
9
9
|
/** Additional CSS class names. */
|
|
10
10
|
readonly className?: string;
|
|
@@ -15,62 +15,43 @@ export interface WorkflowInstanceEmptyStateProps {
|
|
|
15
15
|
*
|
|
16
16
|
* Communicates the value proposition of workflow instances without
|
|
17
17
|
* mentioning the platform-managed default instance (implementation detail).
|
|
18
|
+
*
|
|
19
|
+
* A thin composition over the shared {@link EmptyState} primitive: only the
|
|
20
|
+
* icon and copy are specific to workflow instances.
|
|
18
21
|
*/
|
|
19
22
|
export function WorkflowInstanceEmptyState({
|
|
20
23
|
onCreateClick,
|
|
21
24
|
className,
|
|
22
25
|
}: WorkflowInstanceEmptyStateProps) {
|
|
23
26
|
return (
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
and access controls. Each instance binds environments (credentials,
|
|
41
|
-
secrets, settings) and can be shared independently.
|
|
42
|
-
</p>
|
|
43
|
-
|
|
44
|
-
{onCreateClick && (
|
|
45
|
-
<button
|
|
46
|
-
type="button"
|
|
47
|
-
onClick={onCreateClick}
|
|
48
|
-
className={cn(
|
|
49
|
-
"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5",
|
|
50
|
-
"text-sm font-medium",
|
|
51
|
-
"bg-primary text-primary-foreground",
|
|
52
|
-
"hover:bg-primary/90",
|
|
53
|
-
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
54
|
-
)}
|
|
55
|
-
>
|
|
56
|
-
<PlusIcon />
|
|
57
|
-
Create Instance
|
|
58
|
-
</button>
|
|
59
|
-
)}
|
|
60
|
-
</div>
|
|
27
|
+
<EmptyState
|
|
28
|
+
variant="first-use"
|
|
29
|
+
icon={<LayersIcon />}
|
|
30
|
+
title="No instances yet"
|
|
31
|
+
description={
|
|
32
|
+
"Create instances to run this workflow with different configurations " +
|
|
33
|
+
"and access controls. Each instance binds environments (credentials, " +
|
|
34
|
+
"secrets, settings) and can be shared independently."
|
|
35
|
+
}
|
|
36
|
+
action={
|
|
37
|
+
onCreateClick
|
|
38
|
+
? { label: "Create instance", onClick: onCreateClick, icon: <PlusIcon /> }
|
|
39
|
+
: undefined
|
|
40
|
+
}
|
|
41
|
+
className={className}
|
|
42
|
+
/>
|
|
61
43
|
);
|
|
62
44
|
}
|
|
63
45
|
|
|
64
46
|
function LayersIcon() {
|
|
65
47
|
return (
|
|
66
|
-
<svg width="
|
|
48
|
+
<svg width="24" height="24" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
67
49
|
<path
|
|
68
50
|
d="M10 2L2 6l8 4 8-4-8-4zM2 10l8 4 8-4M2 14l8 4 8-4"
|
|
69
51
|
stroke="currentColor"
|
|
70
52
|
strokeWidth="1.5"
|
|
71
53
|
strokeLinecap="round"
|
|
72
54
|
strokeLinejoin="round"
|
|
73
|
-
className="text-muted-foreground"
|
|
74
55
|
/>
|
|
75
56
|
</svg>
|
|
76
57
|
);
|
|
@@ -78,8 +59,8 @@ function LayersIcon() {
|
|
|
78
59
|
|
|
79
60
|
function PlusIcon() {
|
|
80
61
|
return (
|
|
81
|
-
<svg width="
|
|
82
|
-
<path d="
|
|
62
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
63
|
+
<path d="M6 2v8M2 6h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
83
64
|
</svg>
|
|
84
65
|
);
|
|
85
66
|
}
|
|
@@ -5,6 +5,7 @@ import { cn } from "@stigmer/theme";
|
|
|
5
5
|
import type { WorkflowInstance } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/api_pb";
|
|
6
6
|
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
7
7
|
import { useWorkflowInstances } from "../useWorkflowInstances.js";
|
|
8
|
+
import { Button } from "../../button/Button.js";
|
|
8
9
|
import { useEnvironmentList } from "../../environment/useEnvironmentList.js";
|
|
9
10
|
import { ResourceVisibilityControl } from "../../library/ResourceVisibilityControl.js";
|
|
10
11
|
import { PermissionGate } from "../../iam-policy/PermissionGate.js";
|
|
@@ -16,8 +17,19 @@ export interface WorkflowInstanceListProps {
|
|
|
16
17
|
readonly workflowId: string;
|
|
17
18
|
/** The default instance ID (from workflow.status.defaultInstanceId) — filtered out of the list. */
|
|
18
19
|
readonly defaultInstanceId?: string;
|
|
19
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The WORKFLOW's organization slug (needed for environment resolution
|
|
22
|
+
* and the create flow). Not the list scope — that is `viewerOrg`,
|
|
23
|
+
* which differs from `org` when viewing another org's workflow.
|
|
24
|
+
*/
|
|
20
25
|
readonly org: string;
|
|
26
|
+
/**
|
|
27
|
+
* The viewer's active organization slug. Scopes the list to this
|
|
28
|
+
* org's instances of the workflow, so a member of several orgs sees
|
|
29
|
+
* exactly the current org context's instances. Omit to default to
|
|
30
|
+
* the workflow's own org.
|
|
31
|
+
*/
|
|
32
|
+
readonly viewerOrg?: string;
|
|
21
33
|
/** Called when the user wants to create a new instance. */
|
|
22
34
|
readonly onCreateClick?: () => void;
|
|
23
35
|
/** Called when the user clicks an instance row. */
|
|
@@ -45,6 +57,7 @@ export function WorkflowInstanceList({
|
|
|
45
57
|
workflowId,
|
|
46
58
|
defaultInstanceId,
|
|
47
59
|
org,
|
|
60
|
+
viewerOrg,
|
|
48
61
|
onCreateClick,
|
|
49
62
|
onInstanceClick,
|
|
50
63
|
onRunClick,
|
|
@@ -52,7 +65,13 @@ export function WorkflowInstanceList({
|
|
|
52
65
|
refreshKey,
|
|
53
66
|
className,
|
|
54
67
|
}: WorkflowInstanceListProps) {
|
|
55
|
-
|
|
68
|
+
// Scope the list to the org whose context the viewer is in; when the
|
|
69
|
+
// host passes no viewerOrg the scope falls back to the workflow's own
|
|
70
|
+
// org (the same-org owner flow).
|
|
71
|
+
const { instances, isLoading, error, refetch } = useWorkflowInstances(
|
|
72
|
+
workflowId,
|
|
73
|
+
viewerOrg || org,
|
|
74
|
+
);
|
|
56
75
|
const { environments } = useEnvironmentList(org);
|
|
57
76
|
|
|
58
77
|
// Refetch when refreshKey changes (signals external mutation like create/delete)
|
|
@@ -100,27 +119,21 @@ export function WorkflowInstanceList({
|
|
|
100
119
|
{userInstances.length} {userInstances.length === 1 ? "instance" : "instances"}
|
|
101
120
|
</h3>
|
|
102
121
|
{onCreateClick && (
|
|
103
|
-
<
|
|
104
|
-
|
|
122
|
+
<Button
|
|
123
|
+
variant="outline"
|
|
124
|
+
size="xs"
|
|
125
|
+
icon={<PlusIcon />}
|
|
105
126
|
onClick={onCreateClick}
|
|
106
|
-
className={cn(
|
|
107
|
-
"inline-flex items-center gap-1 rounded-md px-2.5 py-1",
|
|
108
|
-
"text-xs font-medium",
|
|
109
|
-
"border border-border text-foreground",
|
|
110
|
-
"hover:bg-accent-hover",
|
|
111
|
-
"focus:outline-none focus:ring-2 focus:ring-ring",
|
|
112
|
-
)}
|
|
113
127
|
>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
</button>
|
|
128
|
+
Create instance
|
|
129
|
+
</Button>
|
|
117
130
|
)}
|
|
118
131
|
</div>
|
|
119
132
|
|
|
120
133
|
<div className="overflow-hidden rounded-lg border border-border">
|
|
121
134
|
<table className="w-full text-sm">
|
|
122
135
|
<thead>
|
|
123
|
-
<tr className="border-b border-border bg-muted
|
|
136
|
+
<tr className="border-b border-border bg-muted-subtle">
|
|
124
137
|
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Name</th>
|
|
125
138
|
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Environments</th>
|
|
126
139
|
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Visibility</th>
|
|
@@ -267,7 +280,7 @@ function LoadingSkeleton() {
|
|
|
267
280
|
return (
|
|
268
281
|
<div className="space-y-2 py-4">
|
|
269
282
|
{[1, 2, 3].map((i) => (
|
|
270
|
-
<div key={i} className="h-12 rounded-md bg-muted
|
|
283
|
+
<div key={i} className="h-12 animate-pulse rounded-md bg-muted-faint" />
|
|
271
284
|
))}
|
|
272
285
|
</div>
|
|
273
286
|
);
|
|
@@ -21,21 +21,27 @@ export interface UseWorkflowInstancesReturn {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Data hook that fetches
|
|
24
|
+
* Data hook that fetches instances for a specific workflow.
|
|
25
25
|
*
|
|
26
26
|
* Wraps `stigmer.workflowInstance.getByWorkflow()` with loading
|
|
27
27
|
* and error state management. Used on the Workflow detail page
|
|
28
28
|
* "Instances" tab to show environment-bound deployments.
|
|
29
29
|
*
|
|
30
|
+
* Pass `org` to scope the list to one organization's instances — the
|
|
31
|
+
* org-context view a console tab needs. The server applies the scope
|
|
32
|
+
* (it only ever narrows the permission-bounded view); omit `org` for
|
|
33
|
+
* the full permission-bounded list.
|
|
34
|
+
*
|
|
30
35
|
* Pass `null` as `workflowId` to skip fetching (stable no-op).
|
|
31
36
|
*
|
|
32
37
|
* @example
|
|
33
38
|
* ```tsx
|
|
34
|
-
* const { instances, isLoading } = useWorkflowInstances(workflow.metadata?.id);
|
|
39
|
+
* const { instances, isLoading } = useWorkflowInstances(workflow.metadata?.id, viewerOrg);
|
|
35
40
|
* ```
|
|
36
41
|
*/
|
|
37
42
|
export function useWorkflowInstances(
|
|
38
43
|
workflowId: string | null | undefined,
|
|
44
|
+
org?: string,
|
|
39
45
|
): UseWorkflowInstancesReturn {
|
|
40
46
|
const stigmer = useStigmer();
|
|
41
47
|
|
|
@@ -44,6 +50,7 @@ export function useWorkflowInstances(
|
|
|
44
50
|
const resp = await stigmer.workflowInstance.getByWorkflow(
|
|
45
51
|
create(GetWorkflowInstancesByWorkflowRequestSchema, {
|
|
46
52
|
workflowId,
|
|
53
|
+
org: org ?? "",
|
|
47
54
|
}),
|
|
48
55
|
);
|
|
49
56
|
return resp.entries ? [...resp.entries] : [];
|
|
@@ -56,7 +63,7 @@ export function useWorkflowInstances(
|
|
|
56
63
|
isRefetching,
|
|
57
64
|
error,
|
|
58
65
|
refetch,
|
|
59
|
-
} = useFetch<readonly WorkflowInstance[]>(fetchFn, [workflowId, stigmer], []);
|
|
66
|
+
} = useFetch<readonly WorkflowInstance[]>(fetchFn, [workflowId, org, stigmer], []);
|
|
60
67
|
|
|
61
68
|
return { instances, isLoading, isRefetching, error, refetch };
|
|
62
69
|
}
|