@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.
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
@@ -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
- /** Organization slug (needed for environment resolution and create flow). */
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
- const { instances, isLoading, error, refetch } = useAgentInstances(agentId);
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
- <button
112
- type="button"
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
- <PlusIcon />
123
- Create
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/50">
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/50 animate-pulse" />
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 all instances for a specific agent.
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
+ });
@@ -0,0 +1,2 @@
1
+ export { Button } from "./Button.js";
2
+ export type { ButtonProps, ButtonSize, ButtonVariant } from "./Button.js";
@@ -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
- <button
75
- type="button"
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={cn(
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
- </button>
84
+ </Button>
87
85
  )}
88
86
  </div>
89
87
  );
@@ -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(), {
@@ -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