@stigmer/react 3.1.14 → 3.1.15
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
|
@@ -5,6 +5,7 @@ import { cn } from "@stigmer/theme";
|
|
|
5
5
|
import type { AgentInstance } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
|
|
6
6
|
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
7
7
|
import { useAgentInstances } from "./useAgentInstances.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";
|
|
@@ -19,8 +20,19 @@ export interface AgentInstanceListProps {
|
|
|
19
20
|
readonly agentId: string;
|
|
20
21
|
/** The default instance ID (from agent.status.defaultInstanceId) — filtered out of the list. */
|
|
21
22
|
readonly defaultInstanceId?: string;
|
|
22
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* The AGENT's organization slug (needed for environment resolution
|
|
25
|
+
* and the create flow). Not the list scope — that is `viewerOrg`,
|
|
26
|
+
* which differs from `org` when viewing another org's agent.
|
|
27
|
+
*/
|
|
23
28
|
readonly org: string;
|
|
29
|
+
/**
|
|
30
|
+
* The viewer's active organization slug. Scopes the list to this
|
|
31
|
+
* org's instances of the agent, so a member of several orgs sees
|
|
32
|
+
* exactly the current org context's instances. Omit to default to
|
|
33
|
+
* the agent's own org.
|
|
34
|
+
*/
|
|
35
|
+
readonly viewerOrg?: string;
|
|
24
36
|
/** Called when the user wants to create a new instance. */
|
|
25
37
|
readonly onCreateClick?: () => void;
|
|
26
38
|
/** Called when the user clicks an instance row (opens detail). */
|
|
@@ -53,6 +65,7 @@ export function AgentInstanceList({
|
|
|
53
65
|
agentId,
|
|
54
66
|
defaultInstanceId,
|
|
55
67
|
org,
|
|
68
|
+
viewerOrg,
|
|
56
69
|
onCreateClick,
|
|
57
70
|
onInstanceClick,
|
|
58
71
|
onStartSessionClick,
|
|
@@ -60,7 +73,13 @@ export function AgentInstanceList({
|
|
|
60
73
|
refreshKey,
|
|
61
74
|
className,
|
|
62
75
|
}: AgentInstanceListProps) {
|
|
63
|
-
|
|
76
|
+
// Scope the list to the org whose context the viewer is in; when the
|
|
77
|
+
// host passes no viewerOrg the scope falls back to the agent's own org
|
|
78
|
+
// (the same-org owner flow).
|
|
79
|
+
const { instances, isLoading, error, refetch } = useAgentInstances(
|
|
80
|
+
agentId,
|
|
81
|
+
viewerOrg || org,
|
|
82
|
+
);
|
|
64
83
|
const { environments } = useEnvironmentList(org);
|
|
65
84
|
|
|
66
85
|
// Refetch when refreshKey changes (signals external mutation like create/delete)
|
|
@@ -108,27 +127,21 @@ export function AgentInstanceList({
|
|
|
108
127
|
{userInstances.length} {userInstances.length === 1 ? "instance" : "instances"}
|
|
109
128
|
</h3>
|
|
110
129
|
{onCreateClick && (
|
|
111
|
-
<
|
|
112
|
-
|
|
130
|
+
<Button
|
|
131
|
+
variant="outline"
|
|
132
|
+
size="xs"
|
|
133
|
+
icon={<PlusIcon />}
|
|
113
134
|
onClick={onCreateClick}
|
|
114
|
-
className={cn(
|
|
115
|
-
"inline-flex items-center gap-1 rounded-md px-2.5 py-1",
|
|
116
|
-
"text-xs font-medium",
|
|
117
|
-
"border border-border text-foreground",
|
|
118
|
-
"hover:bg-accent-hover",
|
|
119
|
-
"focus:outline-none focus:ring-2 focus:ring-ring",
|
|
120
|
-
)}
|
|
121
135
|
>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
</button>
|
|
136
|
+
Create instance
|
|
137
|
+
</Button>
|
|
125
138
|
)}
|
|
126
139
|
</div>
|
|
127
140
|
|
|
128
141
|
<div className="overflow-hidden rounded-lg border border-border">
|
|
129
142
|
<table className="w-full text-sm">
|
|
130
143
|
<thead>
|
|
131
|
-
<tr className="border-b border-border bg-muted
|
|
144
|
+
<tr className="border-b border-border bg-muted-subtle">
|
|
132
145
|
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Name</th>
|
|
133
146
|
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Environments</th>
|
|
134
147
|
<th className="px-4 py-2 text-left font-medium text-muted-foreground">Visibility</th>
|
|
@@ -289,7 +302,7 @@ function LoadingSkeleton() {
|
|
|
289
302
|
return (
|
|
290
303
|
<div className="space-y-2 py-4">
|
|
291
304
|
{[1, 2, 3].map((i) => (
|
|
292
|
-
<div key={i} className="h-12 rounded-md bg-muted
|
|
305
|
+
<div key={i} className="h-12 animate-pulse rounded-md bg-muted-faint" />
|
|
293
306
|
))}
|
|
294
307
|
</div>
|
|
295
308
|
);
|
|
@@ -200,6 +200,25 @@ describe("useAgentInstances", () => {
|
|
|
200
200
|
expect(result.current.instances).toHaveLength(2);
|
|
201
201
|
expect(result.current.instances[0]?.metadata?.id).toBe("ain-1");
|
|
202
202
|
expect(mockGetByAgent).toHaveBeenCalledTimes(1);
|
|
203
|
+
expect(mockGetByAgent).toHaveBeenCalledWith(
|
|
204
|
+
expect.objectContaining({ agentId: "agent-001", org: "" }),
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("threads the org scope into the request", async () => {
|
|
209
|
+
// The org scope belongs in the RPC, never client-side filtering: the
|
|
210
|
+
// server narrows a multi-org member's view to one org's instances.
|
|
211
|
+
mockGetByAgent.mockResolvedValueOnce({ items: [], totalCount: 0 });
|
|
212
|
+
|
|
213
|
+
const { result } = renderHook(
|
|
214
|
+
() => useAgentInstances("agent-001", "acme"),
|
|
215
|
+
{ wrapper: createWrapper(makeMockClient()) },
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
219
|
+
expect(mockGetByAgent).toHaveBeenCalledWith(
|
|
220
|
+
expect.objectContaining({ agentId: "agent-001", org: "acme" }),
|
|
221
|
+
);
|
|
203
222
|
});
|
|
204
223
|
|
|
205
224
|
it("does not fetch when agentId is null", async () => {
|
|
@@ -21,12 +21,19 @@ export interface UseAgentInstancesReturn {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Data hook that fetches
|
|
24
|
+
* Data hook that fetches instances for a specific agent.
|
|
25
25
|
*
|
|
26
26
|
* Wraps `stigmer.agentInstance.getByAgent()` with loading and error
|
|
27
27
|
* state management. Used on the Agent detail page "Instances" tab to
|
|
28
28
|
* show environment-bound deployments of the agent blueprint.
|
|
29
29
|
*
|
|
30
|
+
* Pass `org` to scope the list to one organization's instances — the
|
|
31
|
+
* org-context view a console tab needs. Permissions alone cannot provide
|
|
32
|
+
* this scope: a member of several orgs can view all of their orgs'
|
|
33
|
+
* instances of the agent, so the unscoped list merges them. The server
|
|
34
|
+
* applies the scope (it only ever narrows the permission-bounded view);
|
|
35
|
+
* omit `org` for the full permission-bounded list.
|
|
36
|
+
*
|
|
30
37
|
* Pass `null` as `agentId` to skip fetching (stable no-op).
|
|
31
38
|
*
|
|
32
39
|
* Note: the `getByAgent` RPC returns an `AgentInstanceList` whose
|
|
@@ -35,11 +42,12 @@ export interface UseAgentInstancesReturn {
|
|
|
35
42
|
*
|
|
36
43
|
* @example
|
|
37
44
|
* ```tsx
|
|
38
|
-
* const { instances, isLoading } = useAgentInstances(agent.metadata?.id);
|
|
45
|
+
* const { instances, isLoading } = useAgentInstances(agent.metadata?.id, viewerOrg);
|
|
39
46
|
* ```
|
|
40
47
|
*/
|
|
41
48
|
export function useAgentInstances(
|
|
42
49
|
agentId: string | null | undefined,
|
|
50
|
+
org?: string,
|
|
43
51
|
): UseAgentInstancesReturn {
|
|
44
52
|
const stigmer = useStigmer();
|
|
45
53
|
|
|
@@ -48,6 +56,7 @@ export function useAgentInstances(
|
|
|
48
56
|
const resp = await stigmer.agentInstance.getByAgent(
|
|
49
57
|
create(GetAgentInstancesByAgentRequestSchema, {
|
|
50
58
|
agentId,
|
|
59
|
+
org: org ?? "",
|
|
51
60
|
}),
|
|
52
61
|
);
|
|
53
62
|
return resp.items ? [...resp.items] : [];
|
|
@@ -60,7 +69,7 @@ export function useAgentInstances(
|
|
|
60
69
|
isRefetching,
|
|
61
70
|
error,
|
|
62
71
|
refetch,
|
|
63
|
-
} = useFetch<readonly AgentInstance[]>(fetchFn, [agentId, stigmer], []);
|
|
72
|
+
} = useFetch<readonly AgentInstance[]>(fetchFn, [agentId, org, stigmer], []);
|
|
64
73
|
|
|
65
74
|
return { instances, isLoading, isRefetching, error, refetch };
|
|
66
75
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Visual weight of a {@link Button}, from most to least prominent.
|
|
8
|
+
*
|
|
9
|
+
* - `primary` — filled call to action (one per view: empty-state CTAs,
|
|
10
|
+
* dialog confirms).
|
|
11
|
+
* - `outline` — bordered pill for secondary actions that share a row
|
|
12
|
+
* with content (list-header actions like "Create share").
|
|
13
|
+
* - `ghost` — borderless, hover-surfaced; for repeated inline actions
|
|
14
|
+
* (table row actions) where borders would add noise.
|
|
15
|
+
* - `destructive` — filled danger action (dialog confirms for deletes).
|
|
16
|
+
* For inline destructive row actions prefer `ghost` with destructive
|
|
17
|
+
* text styling via `className`.
|
|
18
|
+
*/
|
|
19
|
+
export type ButtonVariant = "primary" | "outline" | "ghost" | "destructive";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Size of a {@link Button}.
|
|
23
|
+
*
|
|
24
|
+
* - `xs` — compact pill (list headers, table rows).
|
|
25
|
+
* - `sm` — standard (empty-state CTAs, dialog actions).
|
|
26
|
+
*/
|
|
27
|
+
export type ButtonSize = "xs" | "sm";
|
|
28
|
+
|
|
29
|
+
/** Props for {@link Button}. Extends the native button attributes. */
|
|
30
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
31
|
+
/** Visual weight. @default "primary" */
|
|
32
|
+
readonly variant?: ButtonVariant;
|
|
33
|
+
/** Size. @default "sm" */
|
|
34
|
+
readonly size?: ButtonSize;
|
|
35
|
+
/** Optional leading icon, rendered before the label. */
|
|
36
|
+
readonly icon?: ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// One source of truth for each tier's classes: every mapping below is the
|
|
40
|
+
// established idiom already used across the SDK (list-header pills,
|
|
41
|
+
// EmptyState CTAs, ConfirmDialog actions) — collected here so surfaces
|
|
42
|
+
// can stop hand-rolling drifting copies of the same button.
|
|
43
|
+
const VARIANT_CLASSES: Record<ButtonVariant, string> = {
|
|
44
|
+
primary: "bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
45
|
+
outline: "border border-border text-foreground hover:bg-accent-hover",
|
|
46
|
+
ghost: "text-foreground hover:bg-accent-hover",
|
|
47
|
+
destructive:
|
|
48
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive-hover",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const SIZE_CLASSES: Record<ButtonSize, string> = {
|
|
52
|
+
xs: "px-2.5 py-1 text-xs",
|
|
53
|
+
sm: "px-3 py-1.5 text-xs",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The SDK's shared button primitive.
|
|
58
|
+
*
|
|
59
|
+
* A thin, token-compliant wrapper over the native `<button>`: variants
|
|
60
|
+
* and sizes cover the console's established action tiers, everything
|
|
61
|
+
* else (handlers, ARIA, `disabled`, `form`) passes through natively.
|
|
62
|
+
* `type` defaults to `"button"` so a button inside a form never submits
|
|
63
|
+
* it accidentally.
|
|
64
|
+
*
|
|
65
|
+
* All visual properties flow through `--stgm-*` design tokens (DD-005);
|
|
66
|
+
* hover states use dedicated hover tokens, never opacity modifiers.
|
|
67
|
+
*
|
|
68
|
+
* This is an SDK component (DD-001) — embeddable by platform builders.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* <Button variant="outline" size="xs" icon={<PlusIcon />} onClick={onCreate}>
|
|
73
|
+
* Create share
|
|
74
|
+
* </Button>
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function Button({
|
|
78
|
+
variant = "primary",
|
|
79
|
+
size = "sm",
|
|
80
|
+
icon,
|
|
81
|
+
type = "button",
|
|
82
|
+
className,
|
|
83
|
+
children,
|
|
84
|
+
...rest
|
|
85
|
+
}: ButtonProps) {
|
|
86
|
+
return (
|
|
87
|
+
<button
|
|
88
|
+
type={type}
|
|
89
|
+
className={cn(
|
|
90
|
+
"inline-flex items-center gap-1 rounded-md font-medium transition-colors",
|
|
91
|
+
"motion-reduce:transition-none",
|
|
92
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
93
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
94
|
+
SIZE_CLASSES[size],
|
|
95
|
+
VARIANT_CLASSES[variant],
|
|
96
|
+
className,
|
|
97
|
+
)}
|
|
98
|
+
{...rest}
|
|
99
|
+
>
|
|
100
|
+
{icon}
|
|
101
|
+
{children}
|
|
102
|
+
</button>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
import { Button } from "../Button";
|
|
4
|
+
|
|
5
|
+
afterEach(cleanup);
|
|
6
|
+
|
|
7
|
+
describe("Button", () => {
|
|
8
|
+
it("renders a native button with the label and fires onClick", () => {
|
|
9
|
+
const onClick = vi.fn();
|
|
10
|
+
render(<Button onClick={onClick}>Create share</Button>);
|
|
11
|
+
|
|
12
|
+
const el = screen.getByRole("button", { name: "Create share" });
|
|
13
|
+
expect(el.tagName).toBe("BUTTON");
|
|
14
|
+
el.click();
|
|
15
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("defaults type to button so it never submits an enclosing form", () => {
|
|
19
|
+
render(<Button>Save</Button>);
|
|
20
|
+
expect(screen.getByRole("button").getAttribute("type")).toBe("button");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("allows an explicit type override", () => {
|
|
24
|
+
render(<Button type="submit">Submit</Button>);
|
|
25
|
+
expect(screen.getByRole("button").getAttribute("type")).toBe("submit");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("applies the variant's token classes", () => {
|
|
29
|
+
const { rerender } = render(<Button>CTA</Button>);
|
|
30
|
+
const el = screen.getByRole("button");
|
|
31
|
+
expect(el.className).toContain("bg-primary");
|
|
32
|
+
|
|
33
|
+
rerender(<Button variant="outline">CTA</Button>);
|
|
34
|
+
expect(el.className).toContain("border-border");
|
|
35
|
+
expect(el.className).not.toContain("bg-primary");
|
|
36
|
+
|
|
37
|
+
rerender(<Button variant="ghost">CTA</Button>);
|
|
38
|
+
expect(el.className).toContain("hover:bg-accent-hover");
|
|
39
|
+
|
|
40
|
+
rerender(<Button variant="destructive">CTA</Button>);
|
|
41
|
+
expect(el.className).toContain("bg-destructive");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("applies the size classes", () => {
|
|
45
|
+
const { rerender } = render(<Button size="xs">CTA</Button>);
|
|
46
|
+
const el = screen.getByRole("button");
|
|
47
|
+
expect(el.className).toContain("px-2.5");
|
|
48
|
+
|
|
49
|
+
rerender(<Button size="sm">CTA</Button>);
|
|
50
|
+
expect(el.className).toContain("px-3");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("renders a leading icon before the label", () => {
|
|
54
|
+
render(
|
|
55
|
+
<Button icon={<svg data-testid="icon" aria-hidden="true" />}>
|
|
56
|
+
Create instance
|
|
57
|
+
</Button>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const el = screen.getByRole("button", { name: "Create instance" });
|
|
61
|
+
expect(el.firstElementChild).toBe(screen.getByTestId("icon"));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("passes native attributes through (disabled, aria-label)", () => {
|
|
65
|
+
const onClick = vi.fn();
|
|
66
|
+
render(
|
|
67
|
+
<Button disabled aria-label="Reset link" onClick={onClick}>
|
|
68
|
+
Reset
|
|
69
|
+
</Button>,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const el = screen.getByRole("button", { name: "Reset link" });
|
|
73
|
+
expect(el.hasAttribute("disabled")).toBe(true);
|
|
74
|
+
el.click();
|
|
75
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("merges caller classes after the variant classes", () => {
|
|
79
|
+
render(<Button className="mt-2">CTA</Button>);
|
|
80
|
+
expect(screen.getByRole("button").className).toContain("mt-2");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { cn } from "@stigmer/theme";
|
|
4
|
+
import { Button } from "../button/Button.js";
|
|
4
5
|
import { useEmptyState } from "./useEmptyState.js";
|
|
5
6
|
import type { EmptyStateProps } from "./types.js";
|
|
6
7
|
|
|
@@ -71,19 +72,16 @@ export function EmptyState({
|
|
|
71
72
|
{children
|
|
72
73
|
? <div className="mt-1">{children}</div>
|
|
73
74
|
: action && (
|
|
74
|
-
<
|
|
75
|
-
|
|
75
|
+
<Button
|
|
76
|
+
// A retry after an error is a recovery affordance, not the
|
|
77
|
+
// view's call to action — outline keeps it appropriately quiet.
|
|
78
|
+
variant={variant === "error" ? "outline" : "primary"}
|
|
79
|
+
icon={action.icon}
|
|
76
80
|
onClick={action.onClick}
|
|
77
|
-
className=
|
|
78
|
-
"mt-1 inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium",
|
|
79
|
-
variant === "error"
|
|
80
|
-
? "border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground"
|
|
81
|
-
: "bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
82
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
83
|
-
)}
|
|
81
|
+
className="mt-1"
|
|
84
82
|
>
|
|
85
83
|
{action.label}
|
|
86
|
-
</
|
|
84
|
+
</Button>
|
|
87
85
|
)}
|
|
88
86
|
</div>
|
|
89
87
|
);
|
package/src/empty-state/types.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface EmptyStateAction {
|
|
|
16
16
|
readonly label: string;
|
|
17
17
|
/** Click handler. */
|
|
18
18
|
readonly onClick: () => void;
|
|
19
|
+
/** Optional leading icon (e.g. a plus glyph on create CTAs). */
|
|
20
|
+
readonly icon?: ReactNode;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/** Options for the {@link useEmptyState} behavior hook. */
|
|
@@ -3,6 +3,7 @@ import { renderHook, act } from "@testing-library/react";
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import type { Stigmer } from "@stigmer/sdk";
|
|
5
5
|
import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
6
|
+
import { Harness, ExecutionTarget } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
6
7
|
import { StigmerContext } from "../../context";
|
|
7
8
|
import { useCreateAgentExecution } from "../useCreateAgentExecution";
|
|
8
9
|
|
|
@@ -84,6 +85,97 @@ describe("useCreateAgentExecution — executionConfig mapping", () => {
|
|
|
84
85
|
});
|
|
85
86
|
});
|
|
86
87
|
|
|
88
|
+
describe("useCreateAgentExecution — one-call session bootstrap (sessionSpec)", () => {
|
|
89
|
+
it("converts harness and executionTarget options to proto enums", async () => {
|
|
90
|
+
const { result } = renderHook(() => useCreateAgentExecution(), {
|
|
91
|
+
wrapper: createWrapper(makeMockClient()),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await act(async () => {
|
|
95
|
+
await result.current.create({
|
|
96
|
+
org: "acme",
|
|
97
|
+
message: "Customize the landing page",
|
|
98
|
+
sessionSpec: {
|
|
99
|
+
agentInstanceId: "ain-1",
|
|
100
|
+
workspaceEntries: [
|
|
101
|
+
{ name: "site", source: { localPath: { path: "/repos/site" } } },
|
|
102
|
+
],
|
|
103
|
+
harness: "native",
|
|
104
|
+
executionTarget: "local",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(mockCreate).toHaveBeenCalledTimes(1);
|
|
110
|
+
const input = mockCreate.mock.calls[0][0];
|
|
111
|
+
expect(input.sessionId).toBeUndefined();
|
|
112
|
+
expect(input.sessionSpec).toMatchObject({
|
|
113
|
+
agentInstanceId: "ain-1",
|
|
114
|
+
harness: Harness.NATIVE,
|
|
115
|
+
executionTarget: ExecutionTarget.LOCAL,
|
|
116
|
+
});
|
|
117
|
+
expect(input.sessionSpec.workspaceEntries).toEqual([
|
|
118
|
+
{ name: "site", source: { localPath: { path: "/repos/site" } } },
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("leaves harness and executionTarget undefined when not chosen (server decides)", async () => {
|
|
123
|
+
const { result } = renderHook(() => useCreateAgentExecution(), {
|
|
124
|
+
wrapper: createWrapper(makeMockClient()),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await act(async () => {
|
|
128
|
+
await result.current.create({
|
|
129
|
+
org: "acme",
|
|
130
|
+
message: "Hello",
|
|
131
|
+
sessionSpec: { agentInstanceId: "ain-1" },
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const input = mockCreate.mock.calls[0][0];
|
|
136
|
+
expect(input.sessionSpec.harness).toBeUndefined();
|
|
137
|
+
expect(input.sessionSpec.executionTarget).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("returns the server-assigned session id from the bootstrap response", async () => {
|
|
141
|
+
mockCreate.mockResolvedValueOnce({
|
|
142
|
+
metadata: { id: "aex-1" },
|
|
143
|
+
spec: { sessionId: "ses-created" },
|
|
144
|
+
});
|
|
145
|
+
const { result } = renderHook(() => useCreateAgentExecution(), {
|
|
146
|
+
wrapper: createWrapper(makeMockClient()),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
let created: { executionId: string; sessionId: string } | undefined;
|
|
150
|
+
await act(async () => {
|
|
151
|
+
created = await result.current.create({
|
|
152
|
+
org: "acme",
|
|
153
|
+
message: "Hello",
|
|
154
|
+
sessionSpec: { agentInstanceId: "ain-1" },
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(created).toEqual({ executionId: "aex-1", sessionId: "ses-created" });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("echoes the input session id on the existing-session path", async () => {
|
|
162
|
+
const { result } = renderHook(() => useCreateAgentExecution(), {
|
|
163
|
+
wrapper: createWrapper(makeMockClient()),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
let created: { executionId: string; sessionId: string } | undefined;
|
|
167
|
+
await act(async () => {
|
|
168
|
+
created = await result.current.create({
|
|
169
|
+
org: "acme",
|
|
170
|
+
sessionId: "ses-1",
|
|
171
|
+
message: "Hello",
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(created).toEqual({ executionId: "aex-1", sessionId: "ses-1" });
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
87
179
|
describe("useCreateAgentExecution — supersede link (edit-and-resubmit)", () => {
|
|
88
180
|
it("maps supersedesExecutionId into the create call", async () => {
|
|
89
181
|
const { result } = renderHook(() => useCreateAgentExecution(), {
|
package/src/execution/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { useCreateAgentExecution } from "./useCreateAgentExecution.js";
|
|
2
2
|
export type {
|
|
3
|
+
BootstrapSessionSpec,
|
|
3
4
|
CreateAgentExecutionInput,
|
|
4
5
|
CreateAgentExecutionResult,
|
|
6
|
+
SharedAgentExecutionFields,
|
|
5
7
|
UseCreateAgentExecutionReturn,
|
|
6
8
|
} from "./useCreateAgentExecution.js";
|
|
7
9
|
|