@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.
- package/composer/ComposerToolbar.d.ts +5 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +6 -3
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +17 -1
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +32 -35
- package/composer/SessionComposer.js.map +1 -1
- package/execution/MessageEntry.d.ts +3 -1
- package/execution/MessageEntry.d.ts.map +1 -1
- package/execution/MessageEntry.js +30 -1
- package/execution/MessageEntry.js.map +1 -1
- package/github/index.d.ts +1 -1
- package/github/index.d.ts.map +1 -1
- package/github/index.js.map +1 -1
- package/github/useGitHubConnection.d.ts +70 -1
- package/github/useGitHubConnection.d.ts.map +1 -1
- package/github/useGitHubConnection.js +99 -20
- package/github/useGitHubConnection.js.map +1 -1
- package/identity-provider/IdentityProviderWizard.d.ts.map +1 -1
- package/identity-provider/IdentityProviderWizard.js +19 -3
- package/identity-provider/IdentityProviderWizard.js.map +1 -1
- package/index.d.ts +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/models/HarnessSelector.d.ts +41 -0
- package/models/HarnessSelector.d.ts.map +1 -0
- package/models/HarnessSelector.js +74 -0
- package/models/HarnessSelector.js.map +1 -0
- package/models/ModelSelector.d.ts +26 -16
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +128 -48
- package/models/ModelSelector.js.map +1 -1
- package/models/__tests__/HarnessSelector.test.d.ts +2 -0
- package/models/__tests__/HarnessSelector.test.d.ts.map +1 -0
- package/models/__tests__/HarnessSelector.test.js +160 -0
- package/models/__tests__/HarnessSelector.test.js.map +1 -0
- package/models/__tests__/harness.test.d.ts +2 -0
- package/models/__tests__/harness.test.d.ts.map +1 -0
- package/models/__tests__/harness.test.js +50 -0
- package/models/__tests__/harness.test.js.map +1 -0
- package/models/__tests__/useModelRegistry.test.d.ts +2 -0
- package/models/__tests__/useModelRegistry.test.d.ts.map +1 -0
- package/models/__tests__/useModelRegistry.test.js +148 -0
- package/models/__tests__/useModelRegistry.test.js.map +1 -0
- package/models/harness.d.ts +21 -0
- package/models/harness.d.ts.map +1 -0
- package/models/harness.js +34 -0
- package/models/harness.js.map +1 -0
- package/models/index.d.ts +7 -2
- package/models/index.d.ts.map +1 -1
- package/models/index.js +3 -1
- package/models/index.js.map +1 -1
- package/models/registry.d.ts +53 -13
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +51 -40
- package/models/registry.js.map +1 -1
- package/models/useModelRegistry.d.ts +39 -19
- package/models/useModelRegistry.d.ts.map +1 -1
- package/models/useModelRegistry.js +45 -23
- package/models/useModelRegistry.js.map +1 -1
- package/organization/OrgProfilePanel.d.ts.map +1 -1
- package/organization/OrgProfilePanel.js +23 -2
- package/organization/OrgProfilePanel.js.map +1 -1
- package/package.json +4 -4
- package/runner/RunnerFileBrowser.d.ts +11 -1
- package/runner/RunnerFileBrowser.d.ts.map +1 -1
- package/runner/RunnerFileBrowser.js +70 -7
- package/runner/RunnerFileBrowser.js.map +1 -1
- package/runner/RunnerListPanel.js +2 -1
- package/runner/RunnerListPanel.js.map +1 -1
- package/runner/WorkspaceRunnerSelector.d.ts +36 -0
- package/runner/WorkspaceRunnerSelector.d.ts.map +1 -0
- package/runner/WorkspaceRunnerSelector.js +63 -0
- package/runner/WorkspaceRunnerSelector.js.map +1 -0
- package/runner/__tests__/phase.test.js +6 -2
- package/runner/__tests__/phase.test.js.map +1 -1
- package/runner/index.d.ts +2 -0
- package/runner/index.d.ts.map +1 -1
- package/runner/index.js +1 -0
- package/runner/index.js.map +1 -1
- package/runner/phase.d.ts +9 -7
- package/runner/phase.d.ts.map +1 -1
- package/runner/phase.js +18 -12
- package/runner/phase.js.map +1 -1
- package/runner/useRunnerFileBrowser.d.ts.map +1 -1
- package/runner/useRunnerFileBrowser.js +26 -2
- package/runner/useRunnerFileBrowser.js.map +1 -1
- package/session/__tests__/useCreateSession.test.d.ts +2 -0
- package/session/__tests__/useCreateSession.test.d.ts.map +1 -0
- package/session/__tests__/useCreateSession.test.js +232 -0
- package/session/__tests__/useCreateSession.test.js.map +1 -0
- package/session/__tests__/useNewSessionFlow.test.d.ts +2 -0
- package/session/__tests__/useNewSessionFlow.test.d.ts.map +1 -0
- package/session/__tests__/useNewSessionFlow.test.js +199 -0
- package/session/__tests__/useNewSessionFlow.test.js.map +1 -0
- package/session/__tests__/useSessionConversation.test.js +37 -0
- package/session/__tests__/useSessionConversation.test.js.map +1 -1
- package/session/index.d.ts +1 -1
- package/session/index.d.ts.map +1 -1
- package/session/useCreateSession.d.ts +8 -0
- package/session/useCreateSession.d.ts.map +1 -1
- package/session/useCreateSession.js +2 -0
- package/session/useCreateSession.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +6 -1
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +34 -8
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/usePersistedModel.d.ts +16 -1
- package/session/usePersistedModel.d.ts.map +1 -1
- package/session/usePersistedModel.js +15 -6
- package/session/usePersistedModel.js.map +1 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +6 -1
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +11 -0
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +11 -2
- package/session/useSessionPageFlow.js.map +1 -1
- package/settings/MembersSection.d.ts.map +1 -1
- package/settings/MembersSection.js +7 -2
- package/settings/MembersSection.js.map +1 -1
- package/src/composer/ComposerToolbar.tsx +24 -1
- package/src/composer/SessionComposer.tsx +81 -44
- package/src/execution/MessageEntry.tsx +134 -1
- package/src/github/index.ts +1 -0
- package/src/github/useGitHubConnection.ts +162 -22
- package/src/identity-provider/IdentityProviderWizard.tsx +112 -3
- package/src/index.ts +16 -1
- package/src/models/HarnessSelector.tsx +130 -0
- package/src/models/ModelSelector.tsx +285 -81
- package/src/models/__tests__/HarnessSelector.test.tsx +190 -0
- package/src/models/__tests__/harness.test.ts +66 -0
- package/src/models/__tests__/useModelRegistry.test.tsx +209 -0
- package/src/models/harness.ts +45 -0
- package/src/models/index.ts +7 -2
- package/src/models/registry.ts +122 -50
- package/src/models/useModelRegistry.ts +74 -24
- package/src/organization/OrgProfilePanel.tsx +98 -0
- package/src/runner/RunnerFileBrowser.tsx +227 -8
- package/src/runner/RunnerListPanel.tsx +13 -5
- package/src/runner/WorkspaceRunnerSelector.tsx +180 -0
- package/src/runner/__tests__/phase.test.ts +6 -2
- package/src/runner/index.ts +3 -0
- package/src/runner/phase.ts +18 -12
- package/src/runner/useRunnerFileBrowser.ts +39 -3
- package/src/session/__tests__/useCreateSession.test.tsx +296 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +258 -0
- package/src/session/__tests__/useSessionConversation.test.tsx +53 -0
- package/src/session/index.ts +1 -1
- package/src/session/useCreateSession.ts +9 -0
- package/src/session/useNewSessionFlow.ts +46 -9
- package/src/session/usePersistedModel.ts +30 -6
- package/src/session/useSessionConversation.ts +6 -1
- package/src/session/useSessionPageFlow.ts +26 -2
- package/src/settings/MembersSection.tsx +23 -1
- package/src/workspace/WorkspaceEditor.tsx +176 -126
- package/src/workspace/index.ts +5 -0
- package/src/workspace/useRecentWorkspaces.ts +162 -0
- package/src/workspace/useWorkspaceEntries.ts +13 -0
- package/styles.css +1 -1
- package/workspace/WorkspaceEditor.d.ts +25 -22
- package/workspace/WorkspaceEditor.d.ts.map +1 -1
- package/workspace/WorkspaceEditor.js +64 -43
- package/workspace/WorkspaceEditor.js.map +1 -1
- package/workspace/index.d.ts +2 -0
- package/workspace/index.d.ts.map +1 -1
- package/workspace/index.js +1 -0
- package/workspace/index.js.map +1 -1
- package/workspace/useRecentWorkspaces.d.ts +31 -0
- package/workspace/useRecentWorkspaces.d.ts.map +1 -0
- package/workspace/useRecentWorkspaces.js +117 -0
- package/workspace/useRecentWorkspaces.js.map +1 -0
- package/workspace/useWorkspaceEntries.d.ts +8 -0
- package/workspace/useWorkspaceEntries.d.ts.map +1 -1
- package/workspace/useWorkspaceEntries.js +4 -0
- 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
|
});
|
package/src/session/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
26
|
-
|
|
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(
|
|
54
|
+
return localStorage.getItem(key) ?? undefined;
|
|
31
55
|
});
|
|
32
56
|
|
|
33
57
|
useEffect(() => {
|
|
34
58
|
if (modelId) {
|
|
35
|
-
localStorage.setItem(
|
|
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
|
|
139
|
-
|
|
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
|
-
|
|
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
|
);
|