@stigmer/react 3.1.13 → 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 (139) hide show
  1. package/agent/AgentDetailView.d.ts +9 -9
  2. package/agent/AgentDetailView.d.ts.map +1 -1
  3. package/agent/AgentDetailView.js +23 -31
  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 +5 -3
  38. package/index.d.ts.map +1 -1
  39. package/index.js +4 -2
  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 +46 -0
  47. package/sharing/AgentShareList.d.ts.map +1 -0
  48. package/sharing/AgentShareList.js +159 -0
  49. package/sharing/AgentShareList.js.map +1 -0
  50. package/sharing/ShareAgentDialog.d.ts +38 -25
  51. package/sharing/ShareAgentDialog.d.ts.map +1 -1
  52. package/sharing/ShareAgentDialog.js +131 -62
  53. package/sharing/ShareAgentDialog.js.map +1 -1
  54. package/sharing/index.d.ts +10 -8
  55. package/sharing/index.d.ts.map +1 -1
  56. package/sharing/index.js +5 -4
  57. package/sharing/index.js.map +1 -1
  58. package/sharing/useAgentShares.d.ts +48 -0
  59. package/sharing/useAgentShares.d.ts.map +1 -0
  60. package/sharing/useAgentShares.js +44 -0
  61. package/sharing/useAgentShares.js.map +1 -0
  62. package/sharing/useCanCreateAgentShare.d.ts +43 -0
  63. package/sharing/useCanCreateAgentShare.d.ts.map +1 -0
  64. package/sharing/useCanCreateAgentShare.js +44 -0
  65. package/sharing/useCanCreateAgentShare.js.map +1 -0
  66. package/sharing/useDeleteAgentShare.d.ts +42 -0
  67. package/sharing/useDeleteAgentShare.d.ts.map +1 -0
  68. package/sharing/useDeleteAgentShare.js +45 -0
  69. package/sharing/useDeleteAgentShare.js.map +1 -0
  70. package/sharing/useSaveAgentShare.d.ts +43 -18
  71. package/sharing/useSaveAgentShare.d.ts.map +1 -1
  72. package/sharing/useSaveAgentShare.js +55 -22
  73. package/sharing/useSaveAgentShare.js.map +1 -1
  74. package/src/agent/AgentDetailView.tsx +37 -43
  75. package/src/agent-instance/AgentInstanceEmptyState.tsx +24 -43
  76. package/src/agent-instance/AgentInstanceList.tsx +29 -16
  77. package/src/agent-instance/__tests__/agent-instance-hooks.test.tsx +19 -0
  78. package/src/agent-instance/useAgentInstances.ts +12 -3
  79. package/src/button/Button.tsx +104 -0
  80. package/src/button/__tests__/Button.test.tsx +82 -0
  81. package/src/button/index.ts +2 -0
  82. package/src/empty-state/EmptyState.tsx +8 -10
  83. package/src/empty-state/types.ts +2 -0
  84. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +92 -0
  85. package/src/execution/index.ts +2 -0
  86. package/src/execution/useCreateAgentExecution.ts +116 -10
  87. package/src/index.ts +17 -9
  88. package/src/session/__tests__/useNewSessionFlow.test.tsx +121 -52
  89. package/src/session/useNewSessionFlow.ts +38 -37
  90. package/src/sharing/AgentShareList.tsx +502 -0
  91. package/src/sharing/ShareAgentDialog.tsx +310 -112
  92. package/src/sharing/__tests__/AgentShareList.test.tsx +388 -0
  93. package/src/sharing/__tests__/ShareAgentDialog.test.tsx +311 -312
  94. package/src/sharing/__tests__/useAgentShares.test.tsx +186 -0
  95. package/src/sharing/__tests__/useCanCreateAgentShare.test.tsx +190 -0
  96. package/src/sharing/index.ts +10 -12
  97. package/src/sharing/useAgentShares.ts +74 -0
  98. package/src/sharing/useCanCreateAgentShare.ts +74 -0
  99. package/src/sharing/useDeleteAgentShare.ts +73 -0
  100. package/src/sharing/useSaveAgentShare.ts +73 -23
  101. package/src/workflow/WorkflowDetailView.tsx +9 -0
  102. package/src/workflow/instance/WorkflowInstanceEmptyState.tsx +24 -43
  103. package/src/workflow/instance/WorkflowInstanceList.tsx +29 -16
  104. package/src/workflow/useWorkflowInstances.ts +10 -3
  105. package/styles.css +1 -1
  106. package/workflow/WorkflowDetailView.d.ts +8 -1
  107. package/workflow/WorkflowDetailView.d.ts.map +1 -1
  108. package/workflow/WorkflowDetailView.js +2 -2
  109. package/workflow/WorkflowDetailView.js.map +1 -1
  110. package/workflow/instance/WorkflowInstanceEmptyState.d.ts +4 -1
  111. package/workflow/instance/WorkflowInstanceEmptyState.d.ts.map +1 -1
  112. package/workflow/instance/WorkflowInstanceEmptyState.js +12 -5
  113. package/workflow/instance/WorkflowInstanceEmptyState.js.map +1 -1
  114. package/workflow/instance/WorkflowInstanceList.d.ts +13 -2
  115. package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
  116. package/workflow/instance/WorkflowInstanceList.js +8 -4
  117. package/workflow/instance/WorkflowInstanceList.js.map +1 -1
  118. package/workflow/useWorkflowInstances.d.ts +8 -3
  119. package/workflow/useWorkflowInstances.d.ts.map +1 -1
  120. package/workflow/useWorkflowInstances.js +10 -4
  121. package/workflow/useWorkflowInstances.js.map +1 -1
  122. package/sharing/useAgentShare.d.ts +0 -49
  123. package/sharing/useAgentShare.d.ts.map +0 -1
  124. package/sharing/useAgentShare.js +0 -64
  125. package/sharing/useAgentShare.js.map +0 -1
  126. package/sharing/useCreateExternalShareLink.d.ts +0 -90
  127. package/sharing/useCreateExternalShareLink.d.ts.map +0 -1
  128. package/sharing/useCreateExternalShareLink.js +0 -65
  129. package/sharing/useCreateExternalShareLink.js.map +0 -1
  130. package/sharing/useShareAgent.d.ts +0 -69
  131. package/sharing/useShareAgent.d.ts.map +0 -1
  132. package/sharing/useShareAgent.js +0 -42
  133. package/sharing/useShareAgent.js.map +0 -1
  134. package/src/sharing/__tests__/useAgentShare.test.tsx +0 -210
  135. package/src/sharing/__tests__/useCreateExternalShareLink.test.tsx +0 -194
  136. package/src/sharing/__tests__/useShareAgent.test.tsx +0 -131
  137. package/src/sharing/useAgentShare.ts +0 -105
  138. package/src/sharing/useCreateExternalShareLink.tsx +0 -141
  139. package/src/sharing/useShareAgent.tsx +0 -107
@@ -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();
@@ -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,