@stigmer/react 3.1.14 → 3.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/AgentDetailView.d.ts +7 -5
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +1 -1
- package/agent/AgentDetailView.js.map +1 -1
- package/agent-instance/AgentInstanceEmptyState.d.ts +4 -1
- package/agent-instance/AgentInstanceEmptyState.d.ts.map +1 -1
- package/agent-instance/AgentInstanceEmptyState.js +12 -5
- package/agent-instance/AgentInstanceEmptyState.js.map +1 -1
- package/agent-instance/AgentInstanceList.d.ts +13 -2
- package/agent-instance/AgentInstanceList.d.ts.map +1 -1
- package/agent-instance/AgentInstanceList.js +8 -4
- package/agent-instance/AgentInstanceList.js.map +1 -1
- package/agent-instance/useAgentInstances.d.ts +10 -3
- package/agent-instance/useAgentInstances.d.ts.map +1 -1
- package/agent-instance/useAgentInstances.js +12 -4
- package/agent-instance/useAgentInstances.js.map +1 -1
- package/button/Button.d.ts +54 -0
- package/button/Button.d.ts.map +1 -0
- package/button/Button.js +42 -0
- package/button/Button.js.map +1 -0
- package/button/index.d.ts +3 -0
- package/button/index.d.ts.map +1 -0
- package/button/index.js +2 -0
- package/button/index.js.map +1 -0
- package/empty-state/EmptyState.d.ts.map +1 -1
- package/empty-state/EmptyState.js +8 -3
- package/empty-state/EmptyState.js.map +1 -1
- package/empty-state/types.d.ts +2 -0
- package/empty-state/types.d.ts.map +1 -1
- package/execution/index.d.ts +1 -1
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js.map +1 -1
- package/execution/useCreateAgentExecution.d.ts +81 -9
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +42 -4
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +3 -1
- package/index.d.ts.map +1 -1
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/session/useNewSessionFlow.d.ts +6 -3
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +34 -35
- package/session/useNewSessionFlow.js.map +1 -1
- package/sharing/AgentShareList.d.ts +9 -7
- package/sharing/AgentShareList.d.ts.map +1 -1
- package/sharing/AgentShareList.js +17 -7
- package/sharing/AgentShareList.js.map +1 -1
- package/sharing/useAgentShares.d.ts +15 -9
- package/sharing/useAgentShares.d.ts.map +1 -1
- package/sharing/useAgentShares.js +17 -11
- package/sharing/useAgentShares.js.map +1 -1
- package/src/agent/AgentDetailView.tsx +8 -5
- package/src/agent-instance/AgentInstanceEmptyState.tsx +24 -43
- package/src/agent-instance/AgentInstanceList.tsx +29 -16
- package/src/agent-instance/__tests__/agent-instance-hooks.test.tsx +19 -0
- package/src/agent-instance/useAgentInstances.ts +12 -3
- package/src/button/Button.tsx +104 -0
- package/src/button/__tests__/Button.test.tsx +82 -0
- package/src/button/index.ts +2 -0
- package/src/empty-state/EmptyState.tsx +8 -10
- package/src/empty-state/types.ts +2 -0
- package/src/execution/__tests__/useCreateAgentExecution.test.tsx +92 -0
- package/src/execution/index.ts +2 -0
- package/src/execution/useCreateAgentExecution.ts +116 -10
- package/src/index.ts +6 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +121 -52
- package/src/session/useNewSessionFlow.ts +38 -37
- package/src/sharing/AgentShareList.tsx +38 -43
- package/src/sharing/__tests__/useAgentShares.test.tsx +42 -1
- package/src/sharing/useAgentShares.ts +17 -11
- package/src/workflow/WorkflowDetailView.tsx +9 -0
- package/src/workflow/instance/WorkflowInstanceEmptyState.tsx +24 -43
- package/src/workflow/instance/WorkflowInstanceList.tsx +29 -16
- package/src/workflow/useWorkflowInstances.ts +10 -3
- package/workflow/WorkflowDetailView.d.ts +8 -1
- package/workflow/WorkflowDetailView.d.ts.map +1 -1
- package/workflow/WorkflowDetailView.js +2 -2
- package/workflow/WorkflowDetailView.js.map +1 -1
- package/workflow/instance/WorkflowInstanceEmptyState.d.ts +4 -1
- package/workflow/instance/WorkflowInstanceEmptyState.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceEmptyState.js +12 -5
- package/workflow/instance/WorkflowInstanceEmptyState.js.map +1 -1
- package/workflow/instance/WorkflowInstanceList.d.ts +13 -2
- package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceList.js +8 -4
- package/workflow/instance/WorkflowInstanceList.js.map +1 -1
- package/workflow/useWorkflowInstances.d.ts +8 -3
- package/workflow/useWorkflowInstances.d.ts.map +1 -1
- package/workflow/useWorkflowInstances.js +10 -4
- package/workflow/useWorkflowInstances.js.map +1 -1
|
@@ -2,17 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useState } from "react";
|
|
4
4
|
import type { JsonObject } from "@bufbuild/protobuf";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
AttachmentInput,
|
|
7
|
+
EnvVarInput,
|
|
8
|
+
McpServerUsageInput,
|
|
9
|
+
ResourceRef,
|
|
10
|
+
WorkspaceEntryInput,
|
|
11
|
+
} from "@stigmer/sdk";
|
|
6
12
|
import { useStigmer } from "../hooks.js";
|
|
7
13
|
import { toError } from "../internal/toError.js";
|
|
8
14
|
import { toProtoInteractionMode } from "../composer/interaction-mode.js";
|
|
15
|
+
import { toProtoHarness, type HarnessOption } from "../models/harness.js";
|
|
16
|
+
import {
|
|
17
|
+
toProtoExecutionTarget,
|
|
18
|
+
type ExecutionTargetOption,
|
|
19
|
+
} from "../session/execution-target.js";
|
|
9
20
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Spec for the session the server auto-creates on the one-call bootstrap
|
|
23
|
+
* path (maps to `AgentExecutionSpec.session_spec` in the proto).
|
|
24
|
+
*
|
|
25
|
+
* Carries the session shape — workspace, harness, execution target, MCP
|
|
26
|
+
* servers, skills — alongside the first message, so starting a session
|
|
27
|
+
* with a configured workspace is a single API call (stigmer/stigmer#249).
|
|
28
|
+
*
|
|
29
|
+
* When `agentInstanceId` is omitted, the server resolves the agent's
|
|
30
|
+
* default instance from the execution's `agentId` (creating the instance
|
|
31
|
+
* if missing), or falls back to the platform default agent.
|
|
32
|
+
*/
|
|
33
|
+
export interface BootstrapSessionSpec {
|
|
34
|
+
/** Agent instance the session runs against. Omit to let the server resolve it. */
|
|
35
|
+
readonly agentInstanceId?: string;
|
|
36
|
+
/** Initial conversation subject. Omit for an async LLM-generated title. */
|
|
37
|
+
readonly subject?: string;
|
|
38
|
+
/** Workspace source entries to attach to the session. */
|
|
39
|
+
readonly workspaceEntries?: WorkspaceEntryInput[];
|
|
40
|
+
/** MCP server configurations to include for tool access. */
|
|
41
|
+
readonly mcpServerUsages?: McpServerUsageInput[];
|
|
42
|
+
/** Skill references to enable for executions in this session. */
|
|
43
|
+
readonly skillRefs?: ResourceRef[];
|
|
44
|
+
/** Execution harness. Immutable after the first execution runs. */
|
|
45
|
+
readonly harness?: HarnessOption;
|
|
46
|
+
/** Where session activities execute. Immutable after the first execution runs. */
|
|
47
|
+
readonly executionTarget?: ExecutionTargetOption;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Fields shared by both variants of {@link CreateAgentExecutionInput}. */
|
|
51
|
+
export interface SharedAgentExecutionFields {
|
|
12
52
|
/** Organization slug that owns the session. */
|
|
13
53
|
readonly org: string;
|
|
14
|
-
/** Session to create the execution within. */
|
|
15
|
-
readonly sessionId: string;
|
|
16
54
|
/** User message that initiates the execution. */
|
|
17
55
|
readonly message: string;
|
|
18
56
|
/** Override the default model for this execution. */
|
|
@@ -106,11 +144,42 @@ export interface CreateAgentExecutionInput {
|
|
|
106
144
|
readonly supersedesExecutionId?: string;
|
|
107
145
|
}
|
|
108
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Input for {@link UseCreateAgentExecutionReturn.create}. Exactly one
|
|
149
|
+
* session strategy must be provided:
|
|
150
|
+
*
|
|
151
|
+
* - **`sessionId`** — Create the execution within an existing session.
|
|
152
|
+
* - **`sessionSpec`** — One-call bootstrap: the server creates a session
|
|
153
|
+
* from the embedded spec and dispatches this execution in it. The new
|
|
154
|
+
* session's ID is returned on {@link CreateAgentExecutionResult.sessionId}.
|
|
155
|
+
*
|
|
156
|
+
* Providing both is a type error (and an `INVALID_ARGUMENT` server-side).
|
|
157
|
+
*/
|
|
158
|
+
export type CreateAgentExecutionInput = SharedAgentExecutionFields &
|
|
159
|
+
(
|
|
160
|
+
| {
|
|
161
|
+
/** Session to create the execution within. */
|
|
162
|
+
readonly sessionId: string;
|
|
163
|
+
/** @internal Discriminant — excluded when `sessionId` is provided. */
|
|
164
|
+
readonly sessionSpec?: never;
|
|
165
|
+
}
|
|
166
|
+
| {
|
|
167
|
+
/** Spec for the session to auto-create (one-call bootstrap). */
|
|
168
|
+
readonly sessionSpec: BootstrapSessionSpec;
|
|
169
|
+
/** @internal Discriminant — excluded when `sessionSpec` is provided. */
|
|
170
|
+
readonly sessionId?: never;
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
|
|
109
174
|
/** Resolved output of {@link UseCreateAgentExecutionReturn.create}. */
|
|
110
175
|
export interface CreateAgentExecutionResult {
|
|
111
176
|
/** Server-assigned identifier for the newly created execution. */
|
|
112
177
|
readonly executionId: string;
|
|
113
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Session the execution belongs to. On the `sessionId` path this echoes
|
|
180
|
+
* the input; on the `sessionSpec` bootstrap path it is the server-created
|
|
181
|
+
* session's ID.
|
|
182
|
+
*/
|
|
114
183
|
readonly sessionId: string;
|
|
115
184
|
}
|
|
116
185
|
|
|
@@ -132,9 +201,12 @@ export interface UseCreateAgentExecutionReturn {
|
|
|
132
201
|
* Behavior hook that wraps `agentExecution.create()` with loading/error
|
|
133
202
|
* state.
|
|
134
203
|
*
|
|
135
|
-
* Maps 1:1 to the AgentExecution aggregate — a single run of an agent
|
|
136
|
-
*
|
|
137
|
-
*
|
|
204
|
+
* Maps 1:1 to the AgentExecution aggregate — a single run of an agent.
|
|
205
|
+
* Two session strategies are supported, mirroring the RPC contract:
|
|
206
|
+
* pass `sessionId` to execute within an existing session (use
|
|
207
|
+
* {@link useCreateSession} to create one), or pass `sessionSpec` to
|
|
208
|
+
* bootstrap a new session (workspace, harness, execution target) and
|
|
209
|
+
* dispatch the first message in a single call.
|
|
138
210
|
*
|
|
139
211
|
* Supports both secret delivery flows:
|
|
140
212
|
*
|
|
@@ -165,6 +237,21 @@ export interface UseCreateAgentExecutionReturn {
|
|
|
165
237
|
* },
|
|
166
238
|
* });
|
|
167
239
|
* ```
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```tsx
|
|
243
|
+
* // One-call bootstrap: session with a workspace + first message
|
|
244
|
+
* const { create } = useCreateAgentExecution();
|
|
245
|
+
* const { sessionId } = await create({
|
|
246
|
+
* org: "acme",
|
|
247
|
+
* message: "Customize the landing page",
|
|
248
|
+
* sessionSpec: {
|
|
249
|
+
* agentInstanceId: "ain_abc",
|
|
250
|
+
* workspaceEntries: [{ name: "site", source: { localPath: { path: "/repos/site" } } }],
|
|
251
|
+
* executionTarget: "local",
|
|
252
|
+
* },
|
|
253
|
+
* });
|
|
254
|
+
* ```
|
|
168
255
|
*/
|
|
169
256
|
export function useCreateAgentExecution(): UseCreateAgentExecutionReturn {
|
|
170
257
|
const stigmer = useStigmer();
|
|
@@ -199,10 +286,27 @@ export function useCreateAgentExecution(): UseCreateAgentExecutionReturn {
|
|
|
199
286
|
}
|
|
200
287
|
: undefined;
|
|
201
288
|
|
|
289
|
+
const sessionSpec = input.sessionSpec
|
|
290
|
+
? {
|
|
291
|
+
agentInstanceId: input.sessionSpec.agentInstanceId,
|
|
292
|
+
subject: input.sessionSpec.subject,
|
|
293
|
+
workspaceEntries: input.sessionSpec.workspaceEntries,
|
|
294
|
+
mcpServerUsages: input.sessionSpec.mcpServerUsages,
|
|
295
|
+
skillRefs: input.sessionSpec.skillRefs,
|
|
296
|
+
harness: input.sessionSpec.harness
|
|
297
|
+
? toProtoHarness(input.sessionSpec.harness)
|
|
298
|
+
: undefined,
|
|
299
|
+
executionTarget: input.sessionSpec.executionTarget
|
|
300
|
+
? toProtoExecutionTarget(input.sessionSpec.executionTarget)
|
|
301
|
+
: undefined,
|
|
302
|
+
}
|
|
303
|
+
: undefined;
|
|
304
|
+
|
|
202
305
|
const execution = await stigmer.agentExecution.create({
|
|
203
306
|
name: `execution-${Date.now()}`,
|
|
204
307
|
org: input.org,
|
|
205
308
|
sessionId: input.sessionId,
|
|
309
|
+
sessionSpec,
|
|
206
310
|
agentId: input.agentId,
|
|
207
311
|
message: input.message,
|
|
208
312
|
executionConfig,
|
|
@@ -215,7 +319,9 @@ export function useCreateAgentExecution(): UseCreateAgentExecutionReturn {
|
|
|
215
319
|
|
|
216
320
|
return {
|
|
217
321
|
executionId: execution.metadata!.id,
|
|
218
|
-
|
|
322
|
+
// On the bootstrap path the server assigns the session; trust the
|
|
323
|
+
// response first so both variants report the real session.
|
|
324
|
+
sessionId: execution.spec?.sessionId ?? input.sessionId ?? "",
|
|
219
325
|
};
|
|
220
326
|
} catch (err) {
|
|
221
327
|
setError(toError(err));
|
package/src/index.ts
CHANGED
|
@@ -336,8 +336,10 @@ export {
|
|
|
336
336
|
getArtifactRenderMode,
|
|
337
337
|
} from "./execution/index.js";
|
|
338
338
|
export type {
|
|
339
|
+
BootstrapSessionSpec,
|
|
339
340
|
CreateAgentExecutionInput,
|
|
340
341
|
CreateAgentExecutionResult,
|
|
342
|
+
SharedAgentExecutionFields,
|
|
341
343
|
UseCreateAgentExecutionReturn,
|
|
342
344
|
UseExecutionStreamReturn,
|
|
343
345
|
UseAgentExecutionActionsOptions,
|
|
@@ -1179,6 +1181,10 @@ export type { TabsProps, TabItem } from "./tabs/index.js";
|
|
|
1179
1181
|
export { Switch } from "./switch/index.js";
|
|
1180
1182
|
export type { SwitchProps } from "./switch/index.js";
|
|
1181
1183
|
|
|
1184
|
+
// Button — shared action primitive (variants for the console's action tiers)
|
|
1185
|
+
export { Button } from "./button/index.js";
|
|
1186
|
+
export type { ButtonProps, ButtonSize, ButtonVariant } from "./button/index.js";
|
|
1187
|
+
|
|
1182
1188
|
// Resource Detail — headless hooks, action bar, and composed shell for resource detail pages
|
|
1183
1189
|
export {
|
|
1184
1190
|
useCopyResource,
|
|
@@ -7,16 +7,9 @@ import type { ModelRegistryState } from "../../models/ModelRegistryContext";
|
|
|
7
7
|
import { ExecutionTargetContext } from "../../execution-target-context";
|
|
8
8
|
import { RunnerAdapterContext } from "../../runner-adapter";
|
|
9
9
|
import type { RunnerAdapter } from "../../runner-adapter";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
|
300
|
-
it("
|
|
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(
|
|
309
|
-
const
|
|
310
|
-
expect(
|
|
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
|
|
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
|
|
324
|
-
expect(
|
|
333
|
+
const execInput = mockCreateExecution.mock.calls[0][0];
|
|
334
|
+
expect(execInput.sessionSpec.harness).toBe("cursor");
|
|
325
335
|
});
|
|
326
336
|
|
|
327
|
-
it("calls onSessionCreated
|
|
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
|
-
|
|
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
|
|
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(
|
|
373
|
-
const
|
|
374
|
-
expect(
|
|
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(
|
|
386
|
-
const
|
|
387
|
-
expect(
|
|
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(
|
|
399
|
-
const
|
|
400
|
-
expect(
|
|
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(
|
|
412
|
-
const
|
|
413
|
-
expect(
|
|
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 (
|
|
418
|
-
it("attaches the worker
|
|
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
|
|
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(
|
|
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("
|
|
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
|
-
|
|
466
|
-
|
|
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(
|
|
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(
|
|
611
|
-
expect(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
715
|
-
expect(
|
|
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();
|