@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.
Files changed (92) hide show
  1. package/agent/AgentDetailView.d.ts +7 -5
  2. package/agent/AgentDetailView.d.ts.map +1 -1
  3. package/agent/AgentDetailView.js +1 -1
  4. package/agent/AgentDetailView.js.map +1 -1
  5. package/agent-instance/AgentInstanceEmptyState.d.ts +4 -1
  6. package/agent-instance/AgentInstanceEmptyState.d.ts.map +1 -1
  7. package/agent-instance/AgentInstanceEmptyState.js +12 -5
  8. package/agent-instance/AgentInstanceEmptyState.js.map +1 -1
  9. package/agent-instance/AgentInstanceList.d.ts +13 -2
  10. package/agent-instance/AgentInstanceList.d.ts.map +1 -1
  11. package/agent-instance/AgentInstanceList.js +8 -4
  12. package/agent-instance/AgentInstanceList.js.map +1 -1
  13. package/agent-instance/useAgentInstances.d.ts +10 -3
  14. package/agent-instance/useAgentInstances.d.ts.map +1 -1
  15. package/agent-instance/useAgentInstances.js +12 -4
  16. package/agent-instance/useAgentInstances.js.map +1 -1
  17. package/button/Button.d.ts +54 -0
  18. package/button/Button.d.ts.map +1 -0
  19. package/button/Button.js +42 -0
  20. package/button/Button.js.map +1 -0
  21. package/button/index.d.ts +3 -0
  22. package/button/index.d.ts.map +1 -0
  23. package/button/index.js +2 -0
  24. package/button/index.js.map +1 -0
  25. package/empty-state/EmptyState.d.ts.map +1 -1
  26. package/empty-state/EmptyState.js +8 -3
  27. package/empty-state/EmptyState.js.map +1 -1
  28. package/empty-state/types.d.ts +2 -0
  29. package/empty-state/types.d.ts.map +1 -1
  30. package/execution/index.d.ts +1 -1
  31. package/execution/index.d.ts.map +1 -1
  32. package/execution/index.js.map +1 -1
  33. package/execution/useCreateAgentExecution.d.ts +81 -9
  34. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  35. package/execution/useCreateAgentExecution.js +42 -4
  36. package/execution/useCreateAgentExecution.js.map +1 -1
  37. package/index.d.ts +3 -1
  38. package/index.d.ts.map +1 -1
  39. package/index.js +2 -0
  40. package/index.js.map +1 -1
  41. package/package.json +4 -4
  42. package/session/useNewSessionFlow.d.ts +6 -3
  43. package/session/useNewSessionFlow.d.ts.map +1 -1
  44. package/session/useNewSessionFlow.js +34 -35
  45. package/session/useNewSessionFlow.js.map +1 -1
  46. package/sharing/AgentShareList.d.ts +9 -7
  47. package/sharing/AgentShareList.d.ts.map +1 -1
  48. package/sharing/AgentShareList.js +17 -7
  49. package/sharing/AgentShareList.js.map +1 -1
  50. package/sharing/useAgentShares.d.ts +15 -9
  51. package/sharing/useAgentShares.d.ts.map +1 -1
  52. package/sharing/useAgentShares.js +17 -11
  53. package/sharing/useAgentShares.js.map +1 -1
  54. package/src/agent/AgentDetailView.tsx +8 -5
  55. package/src/agent-instance/AgentInstanceEmptyState.tsx +24 -43
  56. package/src/agent-instance/AgentInstanceList.tsx +29 -16
  57. package/src/agent-instance/__tests__/agent-instance-hooks.test.tsx +19 -0
  58. package/src/agent-instance/useAgentInstances.ts +12 -3
  59. package/src/button/Button.tsx +104 -0
  60. package/src/button/__tests__/Button.test.tsx +82 -0
  61. package/src/button/index.ts +2 -0
  62. package/src/empty-state/EmptyState.tsx +8 -10
  63. package/src/empty-state/types.ts +2 -0
  64. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +92 -0
  65. package/src/execution/index.ts +2 -0
  66. package/src/execution/useCreateAgentExecution.ts +116 -10
  67. package/src/index.ts +6 -0
  68. package/src/session/__tests__/useNewSessionFlow.test.tsx +121 -52
  69. package/src/session/useNewSessionFlow.ts +38 -37
  70. package/src/sharing/AgentShareList.tsx +38 -43
  71. package/src/sharing/__tests__/useAgentShares.test.tsx +42 -1
  72. package/src/sharing/useAgentShares.ts +17 -11
  73. package/src/workflow/WorkflowDetailView.tsx +9 -0
  74. package/src/workflow/instance/WorkflowInstanceEmptyState.tsx +24 -43
  75. package/src/workflow/instance/WorkflowInstanceList.tsx +29 -16
  76. package/src/workflow/useWorkflowInstances.ts +10 -3
  77. package/workflow/WorkflowDetailView.d.ts +8 -1
  78. package/workflow/WorkflowDetailView.d.ts.map +1 -1
  79. package/workflow/WorkflowDetailView.js +2 -2
  80. package/workflow/WorkflowDetailView.js.map +1 -1
  81. package/workflow/instance/WorkflowInstanceEmptyState.d.ts +4 -1
  82. package/workflow/instance/WorkflowInstanceEmptyState.d.ts.map +1 -1
  83. package/workflow/instance/WorkflowInstanceEmptyState.js +12 -5
  84. package/workflow/instance/WorkflowInstanceEmptyState.js.map +1 -1
  85. package/workflow/instance/WorkflowInstanceList.d.ts +13 -2
  86. package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
  87. package/workflow/instance/WorkflowInstanceList.js +8 -4
  88. package/workflow/instance/WorkflowInstanceList.js.map +1 -1
  89. package/workflow/useWorkflowInstances.d.ts +8 -3
  90. package/workflow/useWorkflowInstances.d.ts.map +1 -1
  91. package/workflow/useWorkflowInstances.js +10 -4
  92. 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 the session and execution
144
- * creation RPCs, then calls `onSessionCreated` on success.
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
- * in sequence, then notifies the consumer via `onSessionCreated`.
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 sessionFields = {
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
- let sessionId: string;
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
- ({ sessionId } = await createSession({
325
- ...sessionFields,
326
- agentInstanceId: resolution.instanceId,
327
- }));
330
+ agentInstanceId = resolution.instanceId;
328
331
  } else {
329
- ({ sessionId } = await createSession({
330
- ...sessionFields,
331
- agentRef,
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
- ({ sessionId } = await createSession({
364
- ...sessionFields,
365
- agentInstanceId: resolvedInstanceId,
366
- }));
367
+ agentInstanceId = resolvedInstanceId;
367
368
  }
368
369
 
369
- // Local execution: attach the session's runner worker before creating
370
- // the first execution. The session view (useSessionConversation) owns
371
- // the steady-state lifecycle, but it is not mounted yet — navigation
372
- // happens after onSessionCreated below so without this eager attach
373
- // the first execution would have no poller until the view mounts.
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
- createSession,
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. A share created from this
34
- * list lands in this org its URL, billing, and credentials belong
35
- * to it (a **cross-org share** when it differs from the agent's org,
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 every share the viewer can see (the server FGA-filters
56
- * `getByAgent`, so each org's members see their own org's channels and
57
- * never another org's — decision 013), with per-row copy link, edit,
58
- * pause/resume, reset link, and delete. Creation goes through the same
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
- const { shares, isLoading, error, refetch } = useAgentShares(agentId);
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
- <button
146
- type="button"
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
- </button>
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
- <div className="flex flex-col items-center gap-2 py-12 text-center">
432
- <ShareIcon className="size-10 text-muted-foreground-faint" />
433
- <p className="text-sm font-medium text-muted-foreground">No shares yet</p>
434
- <p className="max-w-sm text-xs text-muted-foreground-subtle">
435
- A share gives this agent a hosted chat link you can send or embed —
436
- each share is its own channel with its own audience, embed origins,
437
- and tool credentials.
438
- </p>
439
- {canCreate && (
440
- <button
441
- type="button"
442
- onClick={onCreateClick}
443
- className={cn(
444
- "mt-2 inline-flex items-center gap-1 rounded-md px-3 py-1.5",
445
- "text-xs font-medium",
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 all of an agent's {@link AgentShare} resources —
30
- * the channels carrying its hosted-chat configuration (audience, allowed
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). This
36
- * hook returns the full list; the server already scopes it to shares the
37
- * caller can view (FGA-filtered in cloud, unfiltered in the single-user
38
- * OSS edition), so a viewer sees their own org's channels and never
39
- * another org's.
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(agent?.metadata?.id ?? "");
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 { cn } from "@stigmer/theme";
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 Instance". */
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
- <div
25
- className={cn(
26
- "flex flex-col items-center justify-center py-12 px-6 text-center",
27
- className,
28
- )}
29
- >
30
- <div className="mb-4 rounded-full bg-muted p-3">
31
- <LayersIcon />
32
- </div>
33
-
34
- <h3 className="text-sm font-semibold text-foreground mb-1">
35
- No instances yet
36
- </h3>
37
-
38
- <p className="text-sm text-muted-foreground max-w-sm mb-6">
39
- Create instances to run this workflow with different configurations
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="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
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="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
82
- <path d="M7 2v10M2 7h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
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
- /** Organization slug (needed for environment resolution and create flow). */
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
- const { instances, isLoading, error, refetch } = useWorkflowInstances(workflowId);
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
- <button
104
- type="button"
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
- <PlusIcon />
115
- Create
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/50">
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/50 animate-pulse" />
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 all instances for a specific workflow.
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
  }