@stigmer/react 0.2.2 → 0.3.0

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 (178) hide show
  1. package/composer/ComposerToolbar.d.ts +5 -1
  2. package/composer/ComposerToolbar.d.ts.map +1 -1
  3. package/composer/ComposerToolbar.js +6 -3
  4. package/composer/ComposerToolbar.js.map +1 -1
  5. package/composer/SessionComposer.d.ts +17 -1
  6. package/composer/SessionComposer.d.ts.map +1 -1
  7. package/composer/SessionComposer.js +32 -35
  8. package/composer/SessionComposer.js.map +1 -1
  9. package/execution/MessageEntry.d.ts +3 -1
  10. package/execution/MessageEntry.d.ts.map +1 -1
  11. package/execution/MessageEntry.js +30 -1
  12. package/execution/MessageEntry.js.map +1 -1
  13. package/github/index.d.ts +1 -1
  14. package/github/index.d.ts.map +1 -1
  15. package/github/index.js.map +1 -1
  16. package/github/useGitHubConnection.d.ts +70 -1
  17. package/github/useGitHubConnection.d.ts.map +1 -1
  18. package/github/useGitHubConnection.js +99 -20
  19. package/github/useGitHubConnection.js.map +1 -1
  20. package/identity-provider/IdentityProviderWizard.d.ts.map +1 -1
  21. package/identity-provider/IdentityProviderWizard.js +19 -3
  22. package/identity-provider/IdentityProviderWizard.js.map +1 -1
  23. package/index.d.ts +4 -4
  24. package/index.d.ts.map +1 -1
  25. package/index.js +2 -2
  26. package/index.js.map +1 -1
  27. package/models/HarnessSelector.d.ts +41 -0
  28. package/models/HarnessSelector.d.ts.map +1 -0
  29. package/models/HarnessSelector.js +74 -0
  30. package/models/HarnessSelector.js.map +1 -0
  31. package/models/ModelSelector.d.ts +26 -16
  32. package/models/ModelSelector.d.ts.map +1 -1
  33. package/models/ModelSelector.js +128 -48
  34. package/models/ModelSelector.js.map +1 -1
  35. package/models/__tests__/HarnessSelector.test.d.ts +2 -0
  36. package/models/__tests__/HarnessSelector.test.d.ts.map +1 -0
  37. package/models/__tests__/HarnessSelector.test.js +160 -0
  38. package/models/__tests__/HarnessSelector.test.js.map +1 -0
  39. package/models/__tests__/harness.test.d.ts +2 -0
  40. package/models/__tests__/harness.test.d.ts.map +1 -0
  41. package/models/__tests__/harness.test.js +50 -0
  42. package/models/__tests__/harness.test.js.map +1 -0
  43. package/models/__tests__/useModelRegistry.test.d.ts +2 -0
  44. package/models/__tests__/useModelRegistry.test.d.ts.map +1 -0
  45. package/models/__tests__/useModelRegistry.test.js +148 -0
  46. package/models/__tests__/useModelRegistry.test.js.map +1 -0
  47. package/models/harness.d.ts +21 -0
  48. package/models/harness.d.ts.map +1 -0
  49. package/models/harness.js +34 -0
  50. package/models/harness.js.map +1 -0
  51. package/models/index.d.ts +7 -2
  52. package/models/index.d.ts.map +1 -1
  53. package/models/index.js +3 -1
  54. package/models/index.js.map +1 -1
  55. package/models/registry.d.ts +53 -13
  56. package/models/registry.d.ts.map +1 -1
  57. package/models/registry.js +51 -40
  58. package/models/registry.js.map +1 -1
  59. package/models/useModelRegistry.d.ts +39 -19
  60. package/models/useModelRegistry.d.ts.map +1 -1
  61. package/models/useModelRegistry.js +45 -23
  62. package/models/useModelRegistry.js.map +1 -1
  63. package/organization/OrgProfilePanel.d.ts.map +1 -1
  64. package/organization/OrgProfilePanel.js +23 -2
  65. package/organization/OrgProfilePanel.js.map +1 -1
  66. package/package.json +4 -4
  67. package/runner/RunnerFileBrowser.d.ts +11 -1
  68. package/runner/RunnerFileBrowser.d.ts.map +1 -1
  69. package/runner/RunnerFileBrowser.js +70 -7
  70. package/runner/RunnerFileBrowser.js.map +1 -1
  71. package/runner/RunnerListPanel.js +2 -1
  72. package/runner/RunnerListPanel.js.map +1 -1
  73. package/runner/WorkspaceRunnerSelector.d.ts +36 -0
  74. package/runner/WorkspaceRunnerSelector.d.ts.map +1 -0
  75. package/runner/WorkspaceRunnerSelector.js +63 -0
  76. package/runner/WorkspaceRunnerSelector.js.map +1 -0
  77. package/runner/__tests__/phase.test.js +6 -2
  78. package/runner/__tests__/phase.test.js.map +1 -1
  79. package/runner/index.d.ts +2 -0
  80. package/runner/index.d.ts.map +1 -1
  81. package/runner/index.js +1 -0
  82. package/runner/index.js.map +1 -1
  83. package/runner/phase.d.ts +9 -7
  84. package/runner/phase.d.ts.map +1 -1
  85. package/runner/phase.js +18 -12
  86. package/runner/phase.js.map +1 -1
  87. package/runner/useRunnerFileBrowser.d.ts.map +1 -1
  88. package/runner/useRunnerFileBrowser.js +26 -2
  89. package/runner/useRunnerFileBrowser.js.map +1 -1
  90. package/session/__tests__/useCreateSession.test.d.ts +2 -0
  91. package/session/__tests__/useCreateSession.test.d.ts.map +1 -0
  92. package/session/__tests__/useCreateSession.test.js +232 -0
  93. package/session/__tests__/useCreateSession.test.js.map +1 -0
  94. package/session/__tests__/useNewSessionFlow.test.d.ts +2 -0
  95. package/session/__tests__/useNewSessionFlow.test.d.ts.map +1 -0
  96. package/session/__tests__/useNewSessionFlow.test.js +199 -0
  97. package/session/__tests__/useNewSessionFlow.test.js.map +1 -0
  98. package/session/__tests__/useSessionConversation.test.js +37 -0
  99. package/session/__tests__/useSessionConversation.test.js.map +1 -1
  100. package/session/index.d.ts +1 -1
  101. package/session/index.d.ts.map +1 -1
  102. package/session/useCreateSession.d.ts +8 -0
  103. package/session/useCreateSession.d.ts.map +1 -1
  104. package/session/useCreateSession.js +2 -0
  105. package/session/useCreateSession.js.map +1 -1
  106. package/session/useNewSessionFlow.d.ts +6 -1
  107. package/session/useNewSessionFlow.d.ts.map +1 -1
  108. package/session/useNewSessionFlow.js +34 -8
  109. package/session/useNewSessionFlow.js.map +1 -1
  110. package/session/usePersistedModel.d.ts +16 -1
  111. package/session/usePersistedModel.d.ts.map +1 -1
  112. package/session/usePersistedModel.js +15 -6
  113. package/session/usePersistedModel.js.map +1 -1
  114. package/session/useSessionConversation.d.ts.map +1 -1
  115. package/session/useSessionConversation.js +6 -1
  116. package/session/useSessionConversation.js.map +1 -1
  117. package/session/useSessionPageFlow.d.ts +11 -0
  118. package/session/useSessionPageFlow.d.ts.map +1 -1
  119. package/session/useSessionPageFlow.js +11 -2
  120. package/session/useSessionPageFlow.js.map +1 -1
  121. package/settings/MembersSection.d.ts.map +1 -1
  122. package/settings/MembersSection.js +7 -2
  123. package/settings/MembersSection.js.map +1 -1
  124. package/src/composer/ComposerToolbar.tsx +24 -1
  125. package/src/composer/SessionComposer.tsx +81 -44
  126. package/src/execution/MessageEntry.tsx +134 -1
  127. package/src/github/index.ts +1 -0
  128. package/src/github/useGitHubConnection.ts +162 -22
  129. package/src/identity-provider/IdentityProviderWizard.tsx +112 -3
  130. package/src/index.ts +16 -1
  131. package/src/models/HarnessSelector.tsx +130 -0
  132. package/src/models/ModelSelector.tsx +285 -81
  133. package/src/models/__tests__/HarnessSelector.test.tsx +190 -0
  134. package/src/models/__tests__/harness.test.ts +66 -0
  135. package/src/models/__tests__/useModelRegistry.test.tsx +209 -0
  136. package/src/models/harness.ts +45 -0
  137. package/src/models/index.ts +7 -2
  138. package/src/models/registry.ts +122 -50
  139. package/src/models/useModelRegistry.ts +74 -24
  140. package/src/organization/OrgProfilePanel.tsx +98 -0
  141. package/src/runner/RunnerFileBrowser.tsx +227 -8
  142. package/src/runner/RunnerListPanel.tsx +13 -5
  143. package/src/runner/WorkspaceRunnerSelector.tsx +180 -0
  144. package/src/runner/__tests__/phase.test.ts +6 -2
  145. package/src/runner/index.ts +3 -0
  146. package/src/runner/phase.ts +18 -12
  147. package/src/runner/useRunnerFileBrowser.ts +39 -3
  148. package/src/session/__tests__/useCreateSession.test.tsx +296 -0
  149. package/src/session/__tests__/useNewSessionFlow.test.tsx +258 -0
  150. package/src/session/__tests__/useSessionConversation.test.tsx +53 -0
  151. package/src/session/index.ts +1 -1
  152. package/src/session/useCreateSession.ts +9 -0
  153. package/src/session/useNewSessionFlow.ts +46 -9
  154. package/src/session/usePersistedModel.ts +30 -6
  155. package/src/session/useSessionConversation.ts +6 -1
  156. package/src/session/useSessionPageFlow.ts +26 -2
  157. package/src/settings/MembersSection.tsx +23 -1
  158. package/src/workspace/WorkspaceEditor.tsx +176 -126
  159. package/src/workspace/index.ts +5 -0
  160. package/src/workspace/useRecentWorkspaces.ts +162 -0
  161. package/src/workspace/useWorkspaceEntries.ts +13 -0
  162. package/styles.css +1 -1
  163. package/workspace/WorkspaceEditor.d.ts +25 -22
  164. package/workspace/WorkspaceEditor.d.ts.map +1 -1
  165. package/workspace/WorkspaceEditor.js +64 -43
  166. package/workspace/WorkspaceEditor.js.map +1 -1
  167. package/workspace/index.d.ts +2 -0
  168. package/workspace/index.d.ts.map +1 -1
  169. package/workspace/index.js +1 -0
  170. package/workspace/index.js.map +1 -1
  171. package/workspace/useRecentWorkspaces.d.ts +31 -0
  172. package/workspace/useRecentWorkspaces.d.ts.map +1 -0
  173. package/workspace/useRecentWorkspaces.js +117 -0
  174. package/workspace/useRecentWorkspaces.js.map +1 -0
  175. package/workspace/useWorkspaceEntries.d.ts +8 -0
  176. package/workspace/useWorkspaceEntries.d.ts.map +1 -1
  177. package/workspace/useWorkspaceEntries.js +4 -0
  178. package/workspace/useWorkspaceEntries.js.map +1 -1
@@ -0,0 +1,258 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { DEFAULT_MODEL_ID, DEFAULT_CURSOR_MODEL_ID } from "../../models/registry";
4
+ import type { UseCreateSessionReturn } from "../useCreateSession";
5
+
6
+ const mockCreateSession = vi.fn<UseCreateSessionReturn["create"]>();
7
+ vi.mock("../useCreateSession", () => ({
8
+ useCreateSession: () => ({
9
+ create: mockCreateSession,
10
+ isCreating: false,
11
+ error: null,
12
+ clearError: vi.fn(),
13
+ }),
14
+ }));
15
+
16
+ const mockCreateExecution = vi.fn();
17
+ vi.mock("../../execution/useCreateAgentExecution", () => ({
18
+ useCreateAgentExecution: () => ({
19
+ create: mockCreateExecution,
20
+ isCreating: false,
21
+ error: null,
22
+ clearError: vi.fn(),
23
+ }),
24
+ }));
25
+
26
+ const mockDefaultAgent = {
27
+ agent: null as { status?: { defaultInstanceId?: string } } | null,
28
+ isLoading: false,
29
+ error: null,
30
+ refetch: vi.fn(),
31
+ };
32
+ vi.mock("../../agent", () => ({
33
+ useDefaultAgent: () => mockDefaultAgent,
34
+ }));
35
+
36
+ const mockWorkspace = {
37
+ entries: [],
38
+ hasEntries: false,
39
+ toInput: vi.fn().mockReturnValue([]),
40
+ addGitRepo: vi.fn(),
41
+ addLocalPath: vi.fn(),
42
+ removeEntry: vi.fn(),
43
+ clear: vi.fn(),
44
+ };
45
+ vi.mock("../../workspace", () => ({
46
+ useWorkspaceEntries: () => mockWorkspace,
47
+ }));
48
+
49
+ const mockSessionVariables = {
50
+ variables: [],
51
+ hasVariables: false,
52
+ setVariable: vi.fn(),
53
+ removeVariable: vi.fn(),
54
+ clear: vi.fn(),
55
+ toMap: vi.fn().mockReturnValue(new Map()),
56
+ };
57
+ vi.mock("../../execution/useSessionVariables", () => ({
58
+ useSessionVariables: () => mockSessionVariables,
59
+ }));
60
+
61
+ import { useNewSessionFlow } from "../useNewSessionFlow";
62
+
63
+ const STORAGE_KEY_HARNESS = "stigmer:session:harness";
64
+ const STORAGE_KEY_MODEL_NATIVE = "stigmer:session:model";
65
+ const STORAGE_KEY_MODEL_CURSOR = "stigmer:session:model:cursor";
66
+
67
+ function defaultOptions() {
68
+ return {
69
+ org: "acme",
70
+ onSessionCreated: vi.fn(),
71
+ onError: vi.fn(),
72
+ };
73
+ }
74
+
75
+ describe("useNewSessionFlow", () => {
76
+ beforeEach(() => {
77
+ localStorage.clear();
78
+ mockDefaultAgent.agent = {
79
+ status: { defaultInstanceId: "default-inst" },
80
+ };
81
+ mockDefaultAgent.isLoading = false;
82
+ mockDefaultAgent.error = null;
83
+ mockCreateSession.mockResolvedValue({ sessionId: "sess-new" });
84
+ mockCreateExecution.mockResolvedValue({});
85
+ });
86
+
87
+ afterEach(() => {
88
+ vi.restoreAllMocks();
89
+ localStorage.clear();
90
+ });
91
+
92
+ describe("harness state", () => {
93
+ it("defaults to native when localStorage is empty", () => {
94
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
95
+ expect(result.current.harness).toBe("native");
96
+ });
97
+
98
+ it("restores cursor harness from localStorage", () => {
99
+ localStorage.setItem(STORAGE_KEY_HARNESS, "cursor");
100
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
101
+ expect(result.current.harness).toBe("cursor");
102
+ });
103
+
104
+ it("falls back to native for unknown localStorage values", () => {
105
+ localStorage.setItem(STORAGE_KEY_HARNESS, "unknown-value");
106
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
107
+ expect(result.current.harness).toBe("native");
108
+ });
109
+
110
+ it("persists harness to localStorage on change", () => {
111
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
112
+
113
+ act(() => result.current.setHarness("cursor"));
114
+
115
+ expect(localStorage.getItem(STORAGE_KEY_HARNESS)).toBe("cursor");
116
+ expect(result.current.harness).toBe("cursor");
117
+ });
118
+
119
+ it("persists native harness to localStorage", () => {
120
+ localStorage.setItem(STORAGE_KEY_HARNESS, "cursor");
121
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
122
+
123
+ act(() => result.current.setHarness("native"));
124
+
125
+ expect(localStorage.getItem(STORAGE_KEY_HARNESS)).toBe("native");
126
+ });
127
+ });
128
+
129
+ describe("per-harness model persistence", () => {
130
+ it("uses separate storage keys for native and cursor models", () => {
131
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
132
+
133
+ act(() => result.current.setModelId(DEFAULT_MODEL_ID));
134
+
135
+ expect(localStorage.getItem(STORAGE_KEY_MODEL_NATIVE)).toBe(
136
+ DEFAULT_MODEL_ID,
137
+ );
138
+ expect(localStorage.getItem(STORAGE_KEY_MODEL_CURSOR)).toBeNull();
139
+ });
140
+
141
+ it("persists cursor model to cursor-specific key", () => {
142
+ localStorage.setItem(STORAGE_KEY_HARNESS, "cursor");
143
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
144
+
145
+ act(() => result.current.setModelId(DEFAULT_CURSOR_MODEL_ID));
146
+
147
+ expect(localStorage.getItem(STORAGE_KEY_MODEL_CURSOR)).toBe(
148
+ DEFAULT_CURSOR_MODEL_ID,
149
+ );
150
+ });
151
+
152
+ it("restores per-harness model when switching harness", () => {
153
+ localStorage.setItem(STORAGE_KEY_MODEL_CURSOR, DEFAULT_CURSOR_MODEL_ID);
154
+
155
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
156
+
157
+ act(() => result.current.setHarness("cursor"));
158
+
159
+ expect(result.current.modelId).toBe(DEFAULT_CURSOR_MODEL_ID);
160
+ });
161
+
162
+ it("clears modelId when switching to a harness with no stored model", () => {
163
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
164
+
165
+ act(() => result.current.setModelId(DEFAULT_MODEL_ID));
166
+ expect(result.current.modelId).toBe(DEFAULT_MODEL_ID);
167
+
168
+ act(() => result.current.setHarness("cursor"));
169
+
170
+ // No stored cursor model → modelId should be undefined
171
+ // (unless the model happens to be valid in the cursor registry)
172
+ if (result.current.modelId !== undefined) {
173
+ // If it has a value, it must be valid for cursor harness
174
+ expect(result.current.modelId).toBe(
175
+ localStorage.getItem(STORAGE_KEY_MODEL_CURSOR),
176
+ );
177
+ }
178
+ });
179
+
180
+ it("invalidates modelId when it is not in the active harness registry", () => {
181
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
182
+
183
+ // Set a native-only model
184
+ act(() => result.current.setModelId(DEFAULT_MODEL_ID));
185
+ expect(result.current.modelId).toBe(DEFAULT_MODEL_ID);
186
+
187
+ // Switch to cursor → native model should be invalidated
188
+ act(() => result.current.setHarness("cursor"));
189
+
190
+ // DEFAULT_MODEL_ID (anthropic) is not in cursor registry
191
+ expect(result.current.modelId).not.toBe(DEFAULT_MODEL_ID);
192
+ });
193
+ });
194
+
195
+ describe("submit with harness", () => {
196
+ it("passes harness field to createSession", async () => {
197
+ const opts = defaultOptions();
198
+ const { result } = renderHook(() => useNewSessionFlow(opts));
199
+
200
+ await act(async () => {
201
+ await result.current.submit("Hello");
202
+ });
203
+
204
+ expect(mockCreateSession).toHaveBeenCalledOnce();
205
+ const sessionInput = mockCreateSession.mock.calls[0][0];
206
+ expect(sessionInput.harness).toBe("native");
207
+ });
208
+
209
+ it("passes cursor harness to createSession after switching", async () => {
210
+ const opts = defaultOptions();
211
+ const { result } = renderHook(() => useNewSessionFlow(opts));
212
+
213
+ act(() => result.current.setHarness("cursor"));
214
+
215
+ await act(async () => {
216
+ await result.current.submit("Hello");
217
+ });
218
+
219
+ const sessionInput = mockCreateSession.mock.calls[0][0];
220
+ expect(sessionInput.harness).toBe("cursor");
221
+ });
222
+
223
+ it("calls onSessionCreated on success", async () => {
224
+ const opts = defaultOptions();
225
+ const { result } = renderHook(() => useNewSessionFlow(opts));
226
+
227
+ await act(async () => {
228
+ await result.current.submit("Hello");
229
+ });
230
+
231
+ expect(opts.onSessionCreated).toHaveBeenCalledWith("sess-new");
232
+ });
233
+
234
+ it("sets submitError and calls onError on failure", async () => {
235
+ mockCreateSession.mockRejectedValueOnce(new Error("RPC fail"));
236
+ const opts = defaultOptions();
237
+ const { result } = renderHook(() => useNewSessionFlow(opts));
238
+
239
+ await act(async () => {
240
+ await result.current.submit("Hello");
241
+ });
242
+
243
+ expect(result.current.submitError).not.toBeNull();
244
+ expect(opts.onError).toHaveBeenCalled();
245
+ });
246
+
247
+ it("resets isSubmitting after completion", async () => {
248
+ const opts = defaultOptions();
249
+ const { result } = renderHook(() => useNewSessionFlow(opts));
250
+
251
+ await act(async () => {
252
+ await result.current.submit("Hello");
253
+ });
254
+
255
+ expect(result.current.isSubmitting).toBe(false);
256
+ });
257
+ });
258
+ });
@@ -302,4 +302,57 @@ describe("useSessionConversation", () => {
302
302
  expect(result.current.pendingUserMessage).toBeNull();
303
303
  });
304
304
  });
305
+
306
+ it("full follow-up lifecycle: active execution completes → canSendFollowUp → sendFollowUp succeeds", async () => {
307
+ // Start with one IN_PROGRESS execution (simulates a Cursor execution running)
308
+ const activeExec = makeExecution("exec-1", ExecutionPhase.EXECUTION_IN_PROGRESS);
309
+ methods.listBySession.mockResolvedValue({ entries: [activeExec] });
310
+
311
+ const execStream = createControllableStream<AgentExecution>();
312
+ methods.subscribe.mockReturnValue(execStream.generator);
313
+
314
+ const { result } = renderHook(
315
+ () => useSessionConversation("session-1", "org"),
316
+ { wrapper: createWrapper(mockStigmer) },
317
+ );
318
+
319
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
320
+
321
+ // Initially canSendFollowUp should be false (execution is active)
322
+ expect(result.current.canSendFollowUp).toBe(false);
323
+
324
+ // Stream delivers EXECUTION_COMPLETED — simulating Cursor run finishing
325
+ const completedExec = makeExecution("exec-1", ExecutionPhase.EXECUTION_COMPLETED);
326
+ act(() => {
327
+ execStream.push(completedExec);
328
+ });
329
+
330
+ // After stream completes, the hook should refetch executions.
331
+ // Mock the refetch to return the completed execution.
332
+ methods.listBySession.mockResolvedValue({ entries: [completedExec] });
333
+
334
+ // Wait for canSendFollowUp to become true
335
+ await waitFor(() => {
336
+ expect(result.current.canSendFollowUp).toBe(true);
337
+ });
338
+
339
+ // Now send a follow-up message
340
+ const followUpExec = makeExecution("exec-2", ExecutionPhase.EXECUTION_PENDING);
341
+ followUpExec.metadata!.id = "exec-2";
342
+ methods.executionCreate.mockResolvedValue(followUpExec);
343
+
344
+ await act(async () => {
345
+ await result.current.sendFollowUp("Follow-up message", { modelName: "default" });
346
+ });
347
+
348
+ // Verify the create call was made with the correct session and message
349
+ expect(methods.executionCreate).toHaveBeenCalledWith(
350
+ expect.objectContaining({
351
+ org: "org",
352
+ sessionId: "session-1",
353
+ message: "Follow-up message",
354
+ executionConfig: expect.objectContaining({ modelName: "default" }),
355
+ }),
356
+ );
357
+ });
305
358
  });
@@ -61,7 +61,7 @@ export type {
61
61
  } from "./useSessionPageFlow";
62
62
 
63
63
  export { usePersistedModel } from "./usePersistedModel";
64
- export type { UsePersistedModelReturn } from "./usePersistedModel";
64
+ export type { UsePersistedModelReturn, UsePersistedModelOptions } from "./usePersistedModel";
65
65
 
66
66
  export { useEditSessionPrep } from "./useEditSessionPrep";
67
67
  export type { UseEditSessionPrepReturn } from "./useEditSessionPrep";
@@ -9,6 +9,7 @@ import {
9
9
  } from "@stigmer/sdk";
10
10
  import { useStigmer } from "../hooks";
11
11
  import { toError } from "../internal/toError";
12
+ import { toProtoHarness, type HarnessOption } from "../models/harness";
12
13
 
13
14
  /** Shared fields present in both variants of {@link CreateSessionInput}. */
14
15
  export interface SharedSessionFields {
@@ -30,6 +31,13 @@ export interface SharedSessionFields {
30
31
  * auto-bind in OSS, cloud auto-provisioning in Cloud).
31
32
  */
32
33
  readonly runnerId?: string;
34
+ /**
35
+ * Execution harness for this session.
36
+ *
37
+ * Determines which execution engine processes agent activities.
38
+ * Immutable after the first execution runs. Defaults to `"native"`.
39
+ */
40
+ readonly harness?: HarnessOption;
33
41
  }
34
42
 
35
43
  /**
@@ -157,6 +165,7 @@ export function useCreateSession(): UseCreateSessionReturn {
157
165
  skillRefs: input.skillRefs,
158
166
  runnerId: input.runnerId,
159
167
  agentInstanceId: resolvedInstanceId,
168
+ harness: input.harness ? toProtoHarness(input.harness) : undefined,
160
169
  });
161
170
 
162
171
  return { sessionId: session.metadata!.id };
@@ -5,15 +5,22 @@ import { getUserMessage, type McpServerUsageInput, type ResourceRef } from "@sti
5
5
  import type { AgentResolution } from "../agent";
6
6
  import { useDefaultAgent } from "../agent";
7
7
  import { useModelRegistry } from "../models";
8
+ import { DEFAULT_HARNESS, type HarnessOption } from "../models/harness";
8
9
  import { useWorkspaceEntries, type UseWorkspaceEntriesReturn } from "../workspace";
9
10
  import { useSessionVariables, type UseSessionVariablesReturn } from "../execution/useSessionVariables";
10
11
  import type { SessionComposerSubmitContext } from "../composer";
11
12
  import { useCreateSession } from "./useCreateSession";
12
13
  import { useCreateAgentExecution } from "../execution/useCreateAgentExecution";
13
14
 
14
- const STORAGE_KEY_MODEL = "stigmer:session:model";
15
+ const STORAGE_KEY_HARNESS = "stigmer:session:harness";
15
16
  const STORAGE_KEY_RUNNER = "stigmer:session:runner";
16
17
 
18
+ function modelStorageKey(harness: HarnessOption): string {
19
+ return harness === "cursor"
20
+ ? "stigmer:session:model:cursor"
21
+ : "stigmer:session:model";
22
+ }
23
+
17
24
  /** Options for {@link useNewSessionFlow}. */
18
25
  export interface UseNewSessionFlowOptions {
19
26
  /** Organization slug. Required for session and execution creation. */
@@ -33,7 +40,12 @@ export interface UseNewSessionFlowOptions {
33
40
 
34
41
  /** Return value of {@link useNewSessionFlow}. */
35
42
  export interface UseNewSessionFlowReturn {
36
- /** Currently selected model ID (persisted to localStorage). */
43
+ /** Currently selected harness (persisted to localStorage). */
44
+ readonly harness: HarnessOption;
45
+ /** Switch the harness. Resets the model if invalid for the new harness. */
46
+ readonly setHarness: (harness: HarnessOption) => void;
47
+
48
+ /** Currently selected model ID (persisted per-harness to localStorage). */
37
49
  readonly modelId: string | undefined;
38
50
  /** Update the selected model. Automatically persists to localStorage. */
39
51
  readonly setModelId: (id: string) => void;
@@ -136,7 +148,13 @@ export function useNewSessionFlow(
136
148
  ): UseNewSessionFlowReturn {
137
149
  const { org, onSessionCreated, onError } = options;
138
150
 
139
- const { getModel } = useModelRegistry();
151
+ const [harness, setHarnessRaw] = useState<HarnessOption>(() => {
152
+ if (typeof window === "undefined") return DEFAULT_HARNESS;
153
+ const stored = localStorage.getItem(STORAGE_KEY_HARNESS);
154
+ return stored === "cursor" ? "cursor" : DEFAULT_HARNESS;
155
+ });
156
+
157
+ const { getModel } = useModelRegistry({ harness });
140
158
  const { create: createSession } = useCreateSession();
141
159
  const { create: createExecution } = useCreateAgentExecution();
142
160
  const {
@@ -158,20 +176,35 @@ export function useNewSessionFlow(
158
176
 
159
177
  const validModelId = modelId && getModel(modelId) ? modelId : undefined;
160
178
 
161
- // Restore persisted model on mount
179
+ // Persist harness on change
180
+ useEffect(() => {
181
+ localStorage.setItem(STORAGE_KEY_HARNESS, harness);
182
+ }, [harness]);
183
+
184
+ const setHarness = useCallback(
185
+ (h: HarnessOption) => {
186
+ setHarnessRaw(h);
187
+ // Restore per-harness model preference, or clear if none stored
188
+ const storedModel = localStorage.getItem(modelStorageKey(h));
189
+ setModelId(storedModel ?? undefined);
190
+ },
191
+ [],
192
+ );
193
+
194
+ // Restore persisted model on mount (using current harness key)
162
195
  useEffect(() => {
163
- const stored = localStorage.getItem(STORAGE_KEY_MODEL);
196
+ const stored = localStorage.getItem(modelStorageKey(harness));
164
197
  if (stored && getModel(stored)) {
165
198
  setModelId(stored);
166
199
  }
167
- }, [getModel]);
200
+ }, [getModel, harness]);
168
201
 
169
- // Persist model on change
202
+ // Persist model on change (using current harness key)
170
203
  useEffect(() => {
171
204
  if (modelId) {
172
- localStorage.setItem(STORAGE_KEY_MODEL, modelId);
205
+ localStorage.setItem(modelStorageKey(harness), modelId);
173
206
  }
174
- }, [modelId]);
207
+ }, [modelId, harness]);
175
208
 
176
209
  // Restore persisted runner on mount
177
210
  useEffect(() => {
@@ -216,6 +249,7 @@ export function useNewSessionFlow(
216
249
  mcpServerUsages: mcpServerUsages.length > 0 ? mcpServerUsages : undefined,
217
250
  skillRefs: skillRefs.length > 0 ? skillRefs : undefined,
218
251
  runnerId: runnerId ?? undefined,
252
+ harness,
219
253
  };
220
254
 
221
255
  const executionFields = {
@@ -277,6 +311,7 @@ export function useNewSessionFlow(
277
311
  [
278
312
  isSubmitting,
279
313
  org,
314
+ harness,
280
315
  validModelId,
281
316
  workspace,
282
317
  mcpServerUsages,
@@ -296,6 +331,8 @@ export function useNewSessionFlow(
296
331
  );
297
332
 
298
333
  return {
334
+ harness,
335
+ setHarness,
299
336
  modelId: validModelId,
300
337
  setModelId,
301
338
  agentRef,
@@ -2,8 +2,19 @@
2
2
 
3
3
  import { useEffect, useState } from "react";
4
4
  import { useModelRegistry } from "../models";
5
+ import type { HarnessOption } from "../models/harness";
5
6
 
6
- const STORAGE_KEY_MODEL = "stigmer:session:model";
7
+ /** Options for {@link usePersistedModel}. */
8
+ export interface UsePersistedModelOptions {
9
+ /**
10
+ * Filter the model registry by harness before validating the stored model.
11
+ *
12
+ * When `"cursor"`, the persisted model is read from a cursor-specific
13
+ * localStorage key and validated against cursor-provider models.
14
+ * When `"native"` or omitted, the default key and registry apply.
15
+ */
16
+ readonly harness?: HarnessOption;
17
+ }
7
18
 
8
19
  /** Return value of {@link usePersistedModel}. */
9
20
  export type UsePersistedModelReturn = readonly [
@@ -11,6 +22,12 @@ export type UsePersistedModelReturn = readonly [
11
22
  setModelId: (id: string) => void,
12
23
  ];
13
24
 
25
+ function storageKey(harness?: HarnessOption): string {
26
+ return harness === "cursor"
27
+ ? "stigmer:session:model:cursor"
28
+ : "stigmer:session:model";
29
+ }
30
+
14
31
  /**
15
32
  * Model selection with localStorage persistence.
16
33
  *
@@ -19,22 +36,29 @@ export type UsePersistedModelReturn = readonly [
19
36
  * `undefined`). On change, persists the new selection to localStorage so
20
37
  * it survives page reloads and navigation.
21
38
  *
39
+ * When `options.harness` is provided, the stored model is read from a
40
+ * harness-specific key and validated against the harness-filtered registry.
41
+ *
22
42
  * Used by both the session launcher (new session) and session page
23
43
  * (follow-up messages) to maintain a consistent model preference.
24
44
  */
25
- export function usePersistedModel(): UsePersistedModelReturn {
26
- const { getModel } = useModelRegistry();
45
+ export function usePersistedModel(
46
+ options?: UsePersistedModelOptions,
47
+ ): UsePersistedModelReturn {
48
+ const harness = options?.harness;
49
+ const { getModel } = useModelRegistry({ harness });
50
+ const key = storageKey(harness);
27
51
 
28
52
  const [modelId, setModelId] = useState<string | undefined>(() => {
29
53
  if (typeof window === "undefined") return undefined;
30
- return localStorage.getItem(STORAGE_KEY_MODEL) ?? undefined;
54
+ return localStorage.getItem(key) ?? undefined;
31
55
  });
32
56
 
33
57
  useEffect(() => {
34
58
  if (modelId) {
35
- localStorage.setItem(STORAGE_KEY_MODEL, modelId);
59
+ localStorage.setItem(key, modelId);
36
60
  }
37
- }, [modelId]);
61
+ }, [modelId, key]);
38
62
 
39
63
  const validModelId = modelId && getModel(modelId) ? modelId : undefined;
40
64
  return [validModelId, setModelId] as const;
@@ -354,8 +354,11 @@ export function useSessionConversation(
354
354
  });
355
355
  setPendingExecutionId(result.executionId);
356
356
  refetch();
357
- } catch {
357
+ } catch (err) {
358
358
  setPendingUserMessage(null);
359
+ if (process.env.NODE_ENV !== "production") {
360
+ console.error("[useSessionConversation] sendFollowUp failed:", err);
361
+ }
359
362
  }
360
363
  },
361
364
  [sessionId, session, org, stigmer, create, updateSession, refetch, refetchSession],
@@ -455,6 +458,8 @@ function buildUpdateInput(
455
458
  subject: spec?.subject || undefined,
456
459
  threadId: spec?.threadId || undefined,
457
460
  sandboxId: spec?.sandboxId || undefined,
461
+ runnerId: spec?.runnerId || undefined,
462
+ harness: spec?.harness,
458
463
  metadata:
459
464
  spec?.metadata && Object.keys(spec.metadata).length > 0
460
465
  ? { ...spec.metadata }
@@ -8,6 +8,8 @@ import { useStigmer } from "../hooks";
8
8
  import { useWorkspaceEntries, type UseWorkspaceEntriesReturn } from "../workspace";
9
9
  import { useSessionVariables, type UseSessionVariablesReturn } from "../execution/useSessionVariables";
10
10
  import type { SessionComposerSubmitContext } from "../composer";
11
+ import { fromProtoHarness, type HarnessOption } from "../models/harness";
12
+ import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
11
13
  import { useSessionConversation, type UseSessionConversationReturn } from "./useSessionConversation";
12
14
  import { useAgentRefFromSession } from "./useAgentRefFromSession";
13
15
  import { usePersistedModel, type UsePersistedModelReturn } from "./usePersistedModel";
@@ -31,6 +33,17 @@ export interface UseSessionPageFlowReturn {
31
33
  /** Full conversation state from `useSessionConversation`. */
32
34
  readonly conv: UseSessionConversationReturn;
33
35
 
36
+ /**
37
+ * Session's execution harness (read-only, derived from session spec).
38
+ *
39
+ * Use this to:
40
+ * - Filter the model selector for follow-up messages
41
+ * - Render a harness badge in the session header
42
+ *
43
+ * Defaults to `"native"` while the session is loading.
44
+ */
45
+ readonly harness: HarnessOption;
46
+
34
47
  /** Persisted model selection: `[modelId, setModelId]`. */
35
48
  readonly model: UsePersistedModelReturn;
36
49
 
@@ -135,8 +148,18 @@ export function useSessionPageFlow(
135
148
 
136
149
  const stigmer = useStigmer();
137
150
  const conv = useSessionConversation(sessionId, org);
138
- const model = usePersistedModel();
139
- const [modelId] = model;
151
+ const harness: HarnessOption = fromProtoHarness(
152
+ conv.session?.spec?.harness ?? Harness.UNSPECIFIED,
153
+ );
154
+ const [persistedModelId, setPersistedModelId] = usePersistedModel({ harness });
155
+
156
+ const lastExecModelId = useMemo(() => {
157
+ const lastExec = conv.completedExecutions.at(-1);
158
+ return lastExec?.spec?.executionConfig?.modelName || undefined;
159
+ }, [conv.completedExecutions]);
160
+
161
+ const modelId = persistedModelId ?? lastExecModelId;
162
+ const model: UsePersistedModelReturn = [modelId, setPersistedModelId] as const;
140
163
 
141
164
  const workspace = useWorkspaceEntries();
142
165
  const sessionVariables = useSessionVariables();
@@ -261,6 +284,7 @@ export function useSessionPageFlow(
261
284
 
262
285
  return {
263
286
  conv,
287
+ harness,
264
288
  model,
265
289
  agentRef,
266
290
  setAgentRef,
@@ -4,12 +4,23 @@ import { OrgMembersPanel } from "../iam-policy/OrgMembersPanel";
4
4
  import { useResourceAvailable, ApiResourceKind } from "../deployment-mode";
5
5
  import { CloudFeatureNotice } from "../internal/CloudFeatureNotice";
6
6
  import { useOrg } from "../organization/OrgProvider";
7
+ import { useIdentityProviderList } from "../identity-provider/useIdentityProviderList";
7
8
 
8
9
  /** Settings section for organization membership and role management. */
9
10
  export function MembersSection() {
10
11
  const { activeOrg } = useOrg();
11
12
  const membersAvailable = useResourceAvailable(ApiResourceKind.iam_policy);
13
+ const idpAvailable = useResourceAvailable(ApiResourceKind.identity_provider);
12
14
  const orgId = activeOrg?.metadata?.id ?? "";
15
+ const orgSlug = activeOrg?.metadata?.slug ?? "";
16
+
17
+ const { identityProviders } = useIdentityProviderList(
18
+ idpAvailable && orgSlug ? orgSlug : null,
19
+ );
20
+
21
+ const hasJitProviders = identityProviders.some(
22
+ (idp) => idp.spec?.autoProvisionAccounts || idp.spec?.isSsoProvider,
23
+ );
13
24
 
14
25
  return (
15
26
  <section aria-labelledby="members-heading">
@@ -34,7 +45,18 @@ export function MembersSection() {
34
45
  Select an organization to manage members.
35
46
  </p>
36
47
  ) : (
37
- <OrgMembersPanel orgId={orgId} />
48
+ <>
49
+ {hasJitProviders && (
50
+ <div className="mb-3 rounded-md border border-border-muted bg-muted-faint px-3 py-2">
51
+ <p className="text-[0.65rem] text-muted-foreground">
52
+ This organization has identity providers with auto-provisioning
53
+ enabled. Members may appear here automatically when users
54
+ authenticate via federated identity.
55
+ </p>
56
+ </div>
57
+ )}
58
+ <OrgMembersPanel orgId={orgId} />
59
+ </>
38
60
  )}
39
61
  </section>
40
62
  );