@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
@@ -2,17 +2,55 @@
2
2
 
3
3
  import { useCallback, useState } from "react";
4
4
  import type { JsonObject } from "@bufbuild/protobuf";
5
- import type { AttachmentInput, EnvVarInput } from "@stigmer/sdk";
5
+ import type {
6
+ AttachmentInput,
7
+ EnvVarInput,
8
+ McpServerUsageInput,
9
+ ResourceRef,
10
+ WorkspaceEntryInput,
11
+ } from "@stigmer/sdk";
6
12
  import { useStigmer } from "../hooks.js";
7
13
  import { toError } from "../internal/toError.js";
8
14
  import { toProtoInteractionMode } from "../composer/interaction-mode.js";
15
+ import { toProtoHarness, type HarnessOption } from "../models/harness.js";
16
+ import {
17
+ toProtoExecutionTarget,
18
+ type ExecutionTargetOption,
19
+ } from "../session/execution-target.js";
9
20
 
10
- /** Input for {@link UseCreateAgentExecutionReturn.create}. */
11
- export interface CreateAgentExecutionInput {
21
+ /**
22
+ * Spec for the session the server auto-creates on the one-call bootstrap
23
+ * path (maps to `AgentExecutionSpec.session_spec` in the proto).
24
+ *
25
+ * Carries the session shape — workspace, harness, execution target, MCP
26
+ * servers, skills — alongside the first message, so starting a session
27
+ * with a configured workspace is a single API call (stigmer/stigmer#249).
28
+ *
29
+ * When `agentInstanceId` is omitted, the server resolves the agent's
30
+ * default instance from the execution's `agentId` (creating the instance
31
+ * if missing), or falls back to the platform default agent.
32
+ */
33
+ export interface BootstrapSessionSpec {
34
+ /** Agent instance the session runs against. Omit to let the server resolve it. */
35
+ readonly agentInstanceId?: string;
36
+ /** Initial conversation subject. Omit for an async LLM-generated title. */
37
+ readonly subject?: string;
38
+ /** Workspace source entries to attach to the session. */
39
+ readonly workspaceEntries?: WorkspaceEntryInput[];
40
+ /** MCP server configurations to include for tool access. */
41
+ readonly mcpServerUsages?: McpServerUsageInput[];
42
+ /** Skill references to enable for executions in this session. */
43
+ readonly skillRefs?: ResourceRef[];
44
+ /** Execution harness. Immutable after the first execution runs. */
45
+ readonly harness?: HarnessOption;
46
+ /** Where session activities execute. Immutable after the first execution runs. */
47
+ readonly executionTarget?: ExecutionTargetOption;
48
+ }
49
+
50
+ /** Fields shared by both variants of {@link CreateAgentExecutionInput}. */
51
+ export interface SharedAgentExecutionFields {
12
52
  /** Organization slug that owns the session. */
13
53
  readonly org: string;
14
- /** Session to create the execution within. */
15
- readonly sessionId: string;
16
54
  /** User message that initiates the execution. */
17
55
  readonly message: string;
18
56
  /** Override the default model for this execution. */
@@ -106,11 +144,42 @@ export interface CreateAgentExecutionInput {
106
144
  readonly supersedesExecutionId?: string;
107
145
  }
108
146
 
147
+ /**
148
+ * Input for {@link UseCreateAgentExecutionReturn.create}. Exactly one
149
+ * session strategy must be provided:
150
+ *
151
+ * - **`sessionId`** — Create the execution within an existing session.
152
+ * - **`sessionSpec`** — One-call bootstrap: the server creates a session
153
+ * from the embedded spec and dispatches this execution in it. The new
154
+ * session's ID is returned on {@link CreateAgentExecutionResult.sessionId}.
155
+ *
156
+ * Providing both is a type error (and an `INVALID_ARGUMENT` server-side).
157
+ */
158
+ export type CreateAgentExecutionInput = SharedAgentExecutionFields &
159
+ (
160
+ | {
161
+ /** Session to create the execution within. */
162
+ readonly sessionId: string;
163
+ /** @internal Discriminant — excluded when `sessionId` is provided. */
164
+ readonly sessionSpec?: never;
165
+ }
166
+ | {
167
+ /** Spec for the session to auto-create (one-call bootstrap). */
168
+ readonly sessionSpec: BootstrapSessionSpec;
169
+ /** @internal Discriminant — excluded when `sessionSpec` is provided. */
170
+ readonly sessionId?: never;
171
+ }
172
+ );
173
+
109
174
  /** Resolved output of {@link UseCreateAgentExecutionReturn.create}. */
110
175
  export interface CreateAgentExecutionResult {
111
176
  /** Server-assigned identifier for the newly created execution. */
112
177
  readonly executionId: string;
113
- /** Session the execution belongs to (echoed from input). */
178
+ /**
179
+ * Session the execution belongs to. On the `sessionId` path this echoes
180
+ * the input; on the `sessionSpec` bootstrap path it is the server-created
181
+ * session's ID.
182
+ */
114
183
  readonly sessionId: string;
115
184
  }
116
185
 
@@ -132,9 +201,12 @@ export interface UseCreateAgentExecutionReturn {
132
201
  * Behavior hook that wraps `agentExecution.create()` with loading/error
133
202
  * state.
134
203
  *
135
- * Maps 1:1 to the AgentExecution aggregate — a single run of an agent
136
- * within an existing session. Requires a `sessionId`; use
137
- * {@link useCreateSession} to create the session first.
204
+ * Maps 1:1 to the AgentExecution aggregate — a single run of an agent.
205
+ * Two session strategies are supported, mirroring the RPC contract:
206
+ * pass `sessionId` to execute within an existing session (use
207
+ * {@link useCreateSession} to create one), or pass `sessionSpec` to
208
+ * bootstrap a new session (workspace, harness, execution target) and
209
+ * dispatch the first message in a single call.
138
210
  *
139
211
  * Supports both secret delivery flows:
140
212
  *
@@ -165,6 +237,21 @@ export interface UseCreateAgentExecutionReturn {
165
237
  * },
166
238
  * });
167
239
  * ```
240
+ *
241
+ * @example
242
+ * ```tsx
243
+ * // One-call bootstrap: session with a workspace + first message
244
+ * const { create } = useCreateAgentExecution();
245
+ * const { sessionId } = await create({
246
+ * org: "acme",
247
+ * message: "Customize the landing page",
248
+ * sessionSpec: {
249
+ * agentInstanceId: "ain_abc",
250
+ * workspaceEntries: [{ name: "site", source: { localPath: { path: "/repos/site" } } }],
251
+ * executionTarget: "local",
252
+ * },
253
+ * });
254
+ * ```
168
255
  */
169
256
  export function useCreateAgentExecution(): UseCreateAgentExecutionReturn {
170
257
  const stigmer = useStigmer();
@@ -199,10 +286,27 @@ export function useCreateAgentExecution(): UseCreateAgentExecutionReturn {
199
286
  }
200
287
  : undefined;
201
288
 
289
+ const sessionSpec = input.sessionSpec
290
+ ? {
291
+ agentInstanceId: input.sessionSpec.agentInstanceId,
292
+ subject: input.sessionSpec.subject,
293
+ workspaceEntries: input.sessionSpec.workspaceEntries,
294
+ mcpServerUsages: input.sessionSpec.mcpServerUsages,
295
+ skillRefs: input.sessionSpec.skillRefs,
296
+ harness: input.sessionSpec.harness
297
+ ? toProtoHarness(input.sessionSpec.harness)
298
+ : undefined,
299
+ executionTarget: input.sessionSpec.executionTarget
300
+ ? toProtoExecutionTarget(input.sessionSpec.executionTarget)
301
+ : undefined,
302
+ }
303
+ : undefined;
304
+
202
305
  const execution = await stigmer.agentExecution.create({
203
306
  name: `execution-${Date.now()}`,
204
307
  org: input.org,
205
308
  sessionId: input.sessionId,
309
+ sessionSpec,
206
310
  agentId: input.agentId,
207
311
  message: input.message,
208
312
  executionConfig,
@@ -215,7 +319,9 @@ export function useCreateAgentExecution(): UseCreateAgentExecutionReturn {
215
319
 
216
320
  return {
217
321
  executionId: execution.metadata!.id,
218
- sessionId: input.sessionId,
322
+ // On the bootstrap path the server assigns the session; trust the
323
+ // response first so both variants report the real session.
324
+ sessionId: execution.spec?.sessionId ?? input.sessionId ?? "",
219
325
  };
220
326
  } catch (err) {
221
327
  setError(toError(err));
package/src/index.ts CHANGED
@@ -336,8 +336,10 @@ export {
336
336
  getArtifactRenderMode,
337
337
  } from "./execution/index.js";
338
338
  export type {
339
+ BootstrapSessionSpec,
339
340
  CreateAgentExecutionInput,
340
341
  CreateAgentExecutionResult,
342
+ SharedAgentExecutionFields,
341
343
  UseCreateAgentExecutionReturn,
342
344
  UseExecutionStreamReturn,
343
345
  UseAgentExecutionActionsOptions,
@@ -1179,6 +1181,10 @@ export type { TabsProps, TabItem } from "./tabs/index.js";
1179
1181
  export { Switch } from "./switch/index.js";
1180
1182
  export type { SwitchProps } from "./switch/index.js";
1181
1183
 
1184
+ // Button — shared action primitive (variants for the console's action tiers)
1185
+ export { Button } from "./button/index.js";
1186
+ export type { ButtonProps, ButtonSize, ButtonVariant } from "./button/index.js";
1187
+
1182
1188
  // Resource Detail — headless hooks, action bar, and composed shell for resource detail pages
1183
1189
  export {
1184
1190
  useCopyResource,
@@ -7,16 +7,9 @@ import type { ModelRegistryState } from "../../models/ModelRegistryContext";
7
7
  import { ExecutionTargetContext } from "../../execution-target-context";
8
8
  import { RunnerAdapterContext } from "../../runner-adapter";
9
9
  import type { RunnerAdapter } from "../../runner-adapter";
10
- import type { UseCreateSessionReturn } from "../useCreateSession";
11
-
12
- const mockCreateSession = vi.fn<UseCreateSessionReturn["create"]>();
13
- vi.mock("../useCreateSession", () => ({
14
- useCreateSession: () => ({
15
- create: mockCreateSession,
16
- isCreating: false,
17
- error: null,
18
- clearError: vi.fn(),
19
- }),
10
+ const mockGetByReference = vi.fn();
11
+ vi.mock("../../hooks", () => ({
12
+ useStigmer: () => ({ agent: { getByReference: mockGetByReference } }),
20
13
  }));
21
14
 
22
15
  const mockCreateExecution = vi.fn();
@@ -127,8 +120,10 @@ describe("useNewSessionFlow", () => {
127
120
  };
128
121
  mockDefaultAgent.isLoading = false;
129
122
  mockDefaultAgent.error = null;
130
- mockCreateSession.mockResolvedValue({ sessionId: "sess-new" });
131
- mockCreateExecution.mockResolvedValue({});
123
+ mockCreateExecution.mockResolvedValue({
124
+ executionId: "exec-new",
125
+ sessionId: "sess-new",
126
+ });
132
127
  });
133
128
 
134
129
  afterEach(() => {
@@ -296,8 +291,8 @@ describe("useNewSessionFlow", () => {
296
291
  });
297
292
  });
298
293
 
299
- describe("submit with harness", () => {
300
- it("passes harness field to createSession", async () => {
294
+ describe("submit one-call bootstrap", () => {
295
+ it("creates the execution with an embedded sessionSpec in a single call", async () => {
301
296
  const opts = defaultOptions();
302
297
  const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
303
298
 
@@ -305,12 +300,27 @@ describe("useNewSessionFlow", () => {
305
300
  await result.current.submit("Hello");
306
301
  });
307
302
 
308
- expect(mockCreateSession).toHaveBeenCalledOnce();
309
- const sessionInput = mockCreateSession.mock.calls[0][0];
310
- expect(sessionInput.harness).toBe("native");
303
+ expect(mockCreateExecution).toHaveBeenCalledOnce();
304
+ const execInput = mockCreateExecution.mock.calls[0][0];
305
+ expect(execInput.message).toBe("Hello");
306
+ expect(execInput.sessionSpec).toBeDefined();
307
+ expect(execInput.sessionSpec.agentInstanceId).toBe("default-inst");
308
+ expect(execInput.sessionId).toBeUndefined();
309
+ });
310
+
311
+ it("passes harness in the sessionSpec", async () => {
312
+ const opts = defaultOptions();
313
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
314
+
315
+ await act(async () => {
316
+ await result.current.submit("Hello");
317
+ });
318
+
319
+ const execInput = mockCreateExecution.mock.calls[0][0];
320
+ expect(execInput.sessionSpec.harness).toBe("native");
311
321
  });
312
322
 
313
- it("passes cursor harness to createSession after switching", async () => {
323
+ it("passes cursor harness in the sessionSpec after switching", async () => {
314
324
  const opts = defaultOptions();
315
325
  const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
316
326
 
@@ -320,11 +330,11 @@ describe("useNewSessionFlow", () => {
320
330
  await result.current.submit("Hello");
321
331
  });
322
332
 
323
- const sessionInput = mockCreateSession.mock.calls[0][0];
324
- expect(sessionInput.harness).toBe("cursor");
333
+ const execInput = mockCreateExecution.mock.calls[0][0];
334
+ expect(execInput.sessionSpec.harness).toBe("cursor");
325
335
  });
326
336
 
327
- it("calls onSessionCreated on success", async () => {
337
+ it("calls onSessionCreated with the server-assigned session id", async () => {
328
338
  const opts = defaultOptions();
329
339
  const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
330
340
 
@@ -336,7 +346,7 @@ describe("useNewSessionFlow", () => {
336
346
  });
337
347
 
338
348
  it("sets submitError and calls onError on failure", async () => {
339
- mockCreateSession.mockRejectedValueOnce(new Error("RPC fail"));
349
+ mockCreateExecution.mockRejectedValueOnce(new Error("RPC fail"));
340
350
  const opts = defaultOptions();
341
351
  const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
342
352
 
@@ -360,8 +370,48 @@ describe("useNewSessionFlow", () => {
360
370
  });
361
371
  });
362
372
 
373
+ describe("submit — agent resolution strategies", () => {
374
+ it("uses the saved resolution's instance directly (no agent lookup)", async () => {
375
+ const opts = defaultOptions();
376
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
377
+
378
+ act(() => {
379
+ result.current.setAgentRef({ org: "acme", slug: "reviewer" });
380
+ result.current.setResolution({ mode: "saved", instanceId: "saved-inst" });
381
+ });
382
+ await act(async () => {
383
+ await result.current.submit("Hello");
384
+ });
385
+
386
+ expect(mockGetByReference).not.toHaveBeenCalled();
387
+ const execInput = mockCreateExecution.mock.calls[0][0];
388
+ expect(execInput.sessionSpec.agentInstanceId).toBe("saved-inst");
389
+ expect(execInput.agentId).toBeUndefined();
390
+ });
391
+
392
+ it("resolves a non-saved agentRef to agentId and lets the server pick the instance", async () => {
393
+ mockGetByReference.mockResolvedValueOnce({ metadata: { id: "agt-resolved" } });
394
+ const opts = defaultOptions();
395
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
396
+
397
+ act(() => {
398
+ result.current.setAgentRef({ org: "acme", slug: "reviewer" });
399
+ result.current.setResolution({ mode: "direct" });
400
+ });
401
+ await act(async () => {
402
+ await result.current.submit("Hello");
403
+ });
404
+
405
+ expect(mockGetByReference).toHaveBeenCalledWith({ org: "acme", slug: "reviewer" });
406
+ const execInput = mockCreateExecution.mock.calls[0][0];
407
+ expect(execInput.agentId).toBe("agt-resolved");
408
+ // The server resolves (and if needed creates) the default instance.
409
+ expect(execInput.sessionSpec.agentInstanceId).toBeUndefined();
410
+ });
411
+ });
412
+
363
413
  describe("submit with executionTarget", () => {
364
- it("passes executionTarget to createSession when provided", async () => {
414
+ it("passes executionTarget in the sessionSpec when provided", async () => {
365
415
  const opts = { ...defaultOptions(), executionTarget: "local" as const };
366
416
  const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
367
417
 
@@ -369,9 +419,9 @@ describe("useNewSessionFlow", () => {
369
419
  await result.current.submit("Hello");
370
420
  });
371
421
 
372
- expect(mockCreateSession).toHaveBeenCalledOnce();
373
- const sessionInput = mockCreateSession.mock.calls[0][0];
374
- expect(sessionInput.executionTarget).toBe("local");
422
+ expect(mockCreateExecution).toHaveBeenCalledOnce();
423
+ const execInput = mockCreateExecution.mock.calls[0][0];
424
+ expect(execInput.sessionSpec.executionTarget).toBe("local");
375
425
  });
376
426
 
377
427
  it("does not include executionTarget when not provided", async () => {
@@ -382,9 +432,9 @@ describe("useNewSessionFlow", () => {
382
432
  await result.current.submit("Hello");
383
433
  });
384
434
 
385
- expect(mockCreateSession).toHaveBeenCalledOnce();
386
- const sessionInput = mockCreateSession.mock.calls[0][0];
387
- expect(sessionInput.executionTarget).toBeUndefined();
435
+ expect(mockCreateExecution).toHaveBeenCalledOnce();
436
+ const execInput = mockCreateExecution.mock.calls[0][0];
437
+ expect(execInput.sessionSpec.executionTarget).toBeUndefined();
388
438
  });
389
439
 
390
440
  it("uses context executionTarget when per-hook option is omitted", async () => {
@@ -395,9 +445,9 @@ describe("useNewSessionFlow", () => {
395
445
  await result.current.submit("Hello");
396
446
  });
397
447
 
398
- expect(mockCreateSession).toHaveBeenCalledOnce();
399
- const sessionInput = mockCreateSession.mock.calls[0][0];
400
- expect(sessionInput.executionTarget).toBe("local");
448
+ expect(mockCreateExecution).toHaveBeenCalledOnce();
449
+ const execInput = mockCreateExecution.mock.calls[0][0];
450
+ expect(execInput.sessionSpec.executionTarget).toBe("local");
401
451
  });
402
452
 
403
453
  it("per-hook executionTarget option overrides context", async () => {
@@ -408,14 +458,14 @@ describe("useNewSessionFlow", () => {
408
458
  await result.current.submit("Hello");
409
459
  });
410
460
 
411
- expect(mockCreateSession).toHaveBeenCalledOnce();
412
- const sessionInput = mockCreateSession.mock.calls[0][0];
413
- expect(sessionInput.executionTarget).toBe("local");
461
+ expect(mockCreateExecution).toHaveBeenCalledOnce();
462
+ const execInput = mockCreateExecution.mock.calls[0][0];
463
+ expect(execInput.sessionSpec.executionTarget).toBe("local");
414
464
  });
415
465
  });
416
466
 
417
- describe("submit — local runner worker (eager attach)", () => {
418
- it("attaches the worker before creating the first execution", async () => {
467
+ describe("submit — local runner worker (post-create attach)", () => {
468
+ it("attaches the worker after the bootstrap create returns the session id", async () => {
419
469
  const adapter = createMockAdapter();
420
470
  const opts = defaultOptions();
421
471
  const { result } = renderHook(() => useNewSessionFlow(opts), {
@@ -430,10 +480,12 @@ describe("useNewSessionFlow", () => {
430
480
  expect(adapter.onSessionOpened).toHaveBeenCalledWith("sess-new");
431
481
  expect(adapter.onSessionClosed).not.toHaveBeenCalled();
432
482
 
433
- // The worker must be polling before the first execution exists.
483
+ // The session ID only exists after the one-call create, so the attach
484
+ // follows it. This is safe: the first activity waits on the session's
485
+ // task queue for a worker (5-minute ScheduleToStart window).
434
486
  const openedOrder = adapter.onSessionOpened.mock.invocationCallOrder[0];
435
487
  const execOrder = mockCreateExecution.mock.invocationCallOrder[0];
436
- expect(openedOrder).toBeLessThan(execOrder);
488
+ expect(execOrder).toBeLessThan(openedOrder);
437
489
  });
438
490
 
439
491
  it("does not attach a worker when the target is cloud", async () => {
@@ -450,7 +502,7 @@ describe("useNewSessionFlow", () => {
450
502
  expect(adapter.onSessionOpened).not.toHaveBeenCalled();
451
503
  });
452
504
 
453
- it("detaches the worker when the first execution fails (no leak)", async () => {
505
+ it("never attaches a worker when the bootstrap create fails (no leak)", async () => {
454
506
  const adapter = createMockAdapter();
455
507
  mockCreateExecution.mockRejectedValueOnce(new Error("execution boom"));
456
508
  const opts = defaultOptions();
@@ -462,11 +514,30 @@ describe("useNewSessionFlow", () => {
462
514
  await result.current.submit("Hello");
463
515
  });
464
516
 
465
- expect(adapter.onSessionOpened).toHaveBeenCalledWith("sess-new");
466
- expect(adapter.onSessionClosed).toHaveBeenCalledWith("sess-new");
517
+ // The create failed before a session existed, so there is no worker
518
+ // to attach — and therefore nothing to compensate/detach.
519
+ expect(adapter.onSessionOpened).not.toHaveBeenCalled();
520
+ expect(adapter.onSessionClosed).not.toHaveBeenCalled();
467
521
  expect(result.current.submitError).not.toBeNull();
468
522
  expect(opts.onError).toHaveBeenCalled();
469
523
  });
524
+
525
+ it("surfaces an attach failure without navigating", async () => {
526
+ const adapter = createMockAdapter();
527
+ adapter.onSessionOpened.mockRejectedValueOnce(new Error("runner down"));
528
+ const opts = defaultOptions();
529
+ const { result } = renderHook(() => useNewSessionFlow(opts), {
530
+ wrapper: createWrapper("local", adapter),
531
+ });
532
+
533
+ await act(async () => {
534
+ await result.current.submit("Hello");
535
+ });
536
+
537
+ expect(result.current.submitError).not.toBeNull();
538
+ expect(opts.onError).toHaveBeenCalled();
539
+ expect(opts.onSessionCreated).not.toHaveBeenCalled();
540
+ });
470
541
  });
471
542
 
472
543
  describe("host runtime env (getRuntimeEnv)", () => {
@@ -528,7 +599,6 @@ describe("useNewSessionFlow", () => {
528
599
  });
529
600
 
530
601
  // No orphan session, no execution — the failure is fully pre-flight.
531
- expect(mockCreateSession).not.toHaveBeenCalled();
532
602
  expect(mockCreateExecution).not.toHaveBeenCalled();
533
603
  expect(result.current.submitError).toContain("token mint failed");
534
604
  expect(opts.onError).toHaveBeenCalled();
@@ -587,7 +657,7 @@ describe("useNewSessionFlow", () => {
587
657
  await result.current.submit("Hello");
588
658
  });
589
659
 
590
- expect(mockCreateSession.mock.calls[0][0].harness).toBe("cursor");
660
+ expect(mockCreateExecution.mock.calls[0][0].sessionSpec.harness).toBe("cursor");
591
661
  });
592
662
  });
593
663
 
@@ -607,8 +677,8 @@ describe("useNewSessionFlow", () => {
607
677
  });
608
678
 
609
679
  expect(mockDefaultAgent.waitForResolution).toHaveBeenCalledOnce();
610
- expect(mockCreateSession).toHaveBeenCalledOnce();
611
- expect(mockCreateSession.mock.calls[0][0].agentInstanceId).toBe("awaited-inst");
680
+ expect(mockCreateExecution).toHaveBeenCalledOnce();
681
+ expect(mockCreateExecution.mock.calls[0][0].sessionSpec.agentInstanceId).toBe("awaited-inst");
612
682
  expect(opts.onSessionCreated).toHaveBeenCalledWith("sess-new");
613
683
  expect(result.current.submitError).toBeNull();
614
684
  });
@@ -630,7 +700,7 @@ describe("useNewSessionFlow", () => {
630
700
 
631
701
  expect(result.current.submitError).toBeTruthy();
632
702
  expect(opts.onError).toHaveBeenCalled();
633
- expect(mockCreateSession).not.toHaveBeenCalled();
703
+ expect(mockCreateExecution).not.toHaveBeenCalled();
634
704
  });
635
705
 
636
706
  it("surfaces timeout error when fetch never resolves", async () => {
@@ -661,7 +731,7 @@ describe("useNewSessionFlow", () => {
661
731
 
662
732
  expect(result.current.submitError).toContain("did not load in time");
663
733
  expect(opts.onError).toHaveBeenCalled();
664
- expect(mockCreateSession).not.toHaveBeenCalled();
734
+ expect(mockCreateExecution).not.toHaveBeenCalled();
665
735
 
666
736
  vi.useRealTimers();
667
737
  });
@@ -680,7 +750,7 @@ describe("useNewSessionFlow", () => {
680
750
 
681
751
  expect(result.current.submitError).toContain("Failed to load default agent");
682
752
  expect(opts.onError).toHaveBeenCalled();
683
- expect(mockCreateSession).not.toHaveBeenCalled();
753
+ expect(mockCreateExecution).not.toHaveBeenCalled();
684
754
  expect(mockDefaultAgent.waitForResolution).not.toHaveBeenCalled();
685
755
  });
686
756
  });
@@ -711,8 +781,8 @@ describe("useNewSessionFlow", () => {
711
781
  await result.current.submit("Hello");
712
782
  });
713
783
 
714
- expect(mockCreateSession).toHaveBeenCalledOnce();
715
- expect(mockCreateSession.mock.calls[0][0].agentInstanceId).toBe("shared-inst");
784
+ expect(mockCreateExecution).toHaveBeenCalledOnce();
785
+ expect(mockCreateExecution.mock.calls[0][0].sessionSpec.agentInstanceId).toBe("shared-inst");
716
786
  expect(opts.onSessionCreated).toHaveBeenCalledWith("sess-new");
717
787
  });
718
788
 
@@ -726,7 +796,6 @@ describe("useNewSessionFlow", () => {
726
796
  await result.current.submit("Hello");
727
797
  });
728
798
 
729
- expect(mockCreateSession).not.toHaveBeenCalled();
730
799
  expect(mockCreateExecution).not.toHaveBeenCalled();
731
800
  expect(result.current.submitError).toContain("still loading");
732
801
  expect(opts.onError).toHaveBeenCalled();