@stigmer/react 3.12.0 → 3.12.1
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/execution/useCreateAgentExecution.d.ts +3 -2
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/internal/isUnimplemented.d.ts +17 -0
- package/internal/isUnimplemented.d.ts.map +1 -0
- package/internal/isUnimplemented.js +26 -0
- package/internal/isUnimplemented.js.map +1 -0
- package/mcp-server/McpServerConnectDialog.js +7 -3
- package/mcp-server/McpServerConnectDialog.js.map +1 -1
- package/mcp-server/useMcpServerCredentials.d.ts +38 -10
- package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
- package/mcp-server/useMcpServerCredentials.js +50 -7
- package/mcp-server/useMcpServerCredentials.js.map +1 -1
- package/mcp-server/useOrgOAuthApp.d.ts +22 -0
- package/mcp-server/useOrgOAuthApp.d.ts.map +1 -1
- package/mcp-server/useOrgOAuthApp.js +39 -7
- package/mcp-server/useOrgOAuthApp.js.map +1 -1
- package/package.json +4 -4
- package/session/useSessionConversation.d.ts +4 -2
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js.map +1 -1
- package/skill/useSkillVersions.d.ts.map +1 -1
- package/skill/useSkillVersions.js +1 -9
- package/skill/useSkillVersions.js.map +1 -1
- package/src/execution/useCreateAgentExecution.ts +3 -2
- package/src/internal/isUnimplemented.ts +26 -0
- package/src/mcp-server/McpServerConnectDialog.tsx +29 -11
- package/src/mcp-server/__tests__/VendorApprovalBlocked.surfaces.test.tsx +103 -5
- package/src/mcp-server/__tests__/byoaEditionDegradation.test.tsx +323 -0
- package/src/mcp-server/__tests__/useMcpServerCredentials.test.tsx +306 -0
- package/src/mcp-server/useMcpServerCredentials.ts +87 -19
- package/src/mcp-server/useOrgOAuthApp.ts +54 -8
- package/src/session/useSessionConversation.ts +4 -2
- package/src/skill/useSkillVersions.ts +1 -11
- package/src/workflow/WorkflowCanvasEditor.tsx +37 -7
- package/src/workflow/WorkflowExecutionGraph.tsx +26 -1
- package/src/workflow/WorkflowExecutionViewer.tsx +27 -13
- package/src/workflow/WorkflowNode.tsx +5 -2
- package/src/workflow/WorkflowRunDialog.tsx +28 -1
- package/src/workflow/__tests__/WorkflowExecutionViewer.reconciliation.test.tsx +48 -1
- package/src/workflow/__tests__/WorkflowNode.test.tsx +93 -0
- package/src/workflow/__tests__/WorkflowRunDialog.test.tsx +127 -0
- package/src/workflow/__tests__/graph-history.test.ts +103 -0
- package/src/workflow/__tests__/overview-graph.test.ts +17 -0
- package/src/workflow/__tests__/useCanvasKeyboardShortcuts.undo.test.ts +115 -0
- package/src/workflow/__tests__/useWorkflowCanvas.undo.test.tsx +133 -0
- package/src/workflow/instance/CreateWorkflowInstanceDialog.tsx +16 -7
- package/src/workflow/instance/useDeleteWorkflowInstance.ts +9 -4
- package/src/workflow/layout/registry-dimensions.ts +2 -5
- package/src/workflow/node-shell/NodeShell.tsx +4 -0
- package/src/workflow/task-type-visual-registry.ts +5 -3
- package/src/workflow/useCanvasKeyboardShortcuts.ts +33 -3
- package/src/workflow/useGraphHistory.ts +22 -41
- package/src/workflow/useWorkflowCanvas.ts +12 -10
- package/src/workflow/useWorkflowSave.ts +2 -15
- package/src/workflow/workflow-graph-conversions.ts +25 -3
- package/workflow/WorkflowCanvasEditor.d.ts.map +1 -1
- package/workflow/WorkflowCanvasEditor.js +31 -6
- package/workflow/WorkflowCanvasEditor.js.map +1 -1
- package/workflow/WorkflowExecutionGraph.d.ts.map +1 -1
- package/workflow/WorkflowExecutionGraph.js +27 -1
- package/workflow/WorkflowExecutionGraph.js.map +1 -1
- package/workflow/WorkflowExecutionViewer.d.ts.map +1 -1
- package/workflow/WorkflowExecutionViewer.js +26 -12
- package/workflow/WorkflowExecutionViewer.js.map +1 -1
- package/workflow/WorkflowNode.d.ts.map +1 -1
- package/workflow/WorkflowNode.js +2 -2
- package/workflow/WorkflowNode.js.map +1 -1
- package/workflow/WorkflowRunDialog.d.ts +11 -1
- package/workflow/WorkflowRunDialog.d.ts.map +1 -1
- package/workflow/WorkflowRunDialog.js +16 -2
- package/workflow/WorkflowRunDialog.js.map +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.d.ts.map +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.js +10 -2
- package/workflow/instance/CreateWorkflowInstanceDialog.js.map +1 -1
- package/workflow/instance/useDeleteWorkflowInstance.d.ts +9 -4
- package/workflow/instance/useDeleteWorkflowInstance.d.ts.map +1 -1
- package/workflow/instance/useDeleteWorkflowInstance.js +4 -2
- package/workflow/instance/useDeleteWorkflowInstance.js.map +1 -1
- package/workflow/layout/registry-dimensions.d.ts.map +1 -1
- package/workflow/layout/registry-dimensions.js +2 -5
- package/workflow/layout/registry-dimensions.js.map +1 -1
- package/workflow/node-shell/NodeShell.js +1 -1
- package/workflow/node-shell/NodeShell.js.map +1 -1
- package/workflow/task-type-visual-registry.d.ts.map +1 -1
- package/workflow/task-type-visual-registry.js +5 -3
- package/workflow/task-type-visual-registry.js.map +1 -1
- package/workflow/useCanvasKeyboardShortcuts.d.ts +15 -4
- package/workflow/useCanvasKeyboardShortcuts.d.ts.map +1 -1
- package/workflow/useCanvasKeyboardShortcuts.js +25 -4
- package/workflow/useCanvasKeyboardShortcuts.js.map +1 -1
- package/workflow/useGraphHistory.d.ts +16 -9
- package/workflow/useGraphHistory.d.ts.map +1 -1
- package/workflow/useGraphHistory.js +19 -33
- package/workflow/useGraphHistory.js.map +1 -1
- package/workflow/useWorkflowCanvas.d.ts +1 -3
- package/workflow/useWorkflowCanvas.d.ts.map +1 -1
- package/workflow/useWorkflowCanvas.js +15 -9
- package/workflow/useWorkflowCanvas.js.map +1 -1
- package/workflow/useWorkflowSave.d.ts.map +1 -1
- package/workflow/useWorkflowSave.js +2 -15
- package/workflow/useWorkflowSave.js.map +1 -1
- package/workflow/workflow-graph-conversions.d.ts +19 -1
- package/workflow/workflow-graph-conversions.d.ts.map +1 -1
- package/workflow/workflow-graph-conversions.js +17 -3
- package/workflow/workflow-graph-conversions.js.map +1 -1
|
@@ -11,6 +11,7 @@ import { diffEnv } from "../environment/diffEnv.js";
|
|
|
11
11
|
import { SYSTEM_ENV_VAR_KEYS } from "../environment/systemEnvVars.js";
|
|
12
12
|
import type { EnvVarFormVariable } from "../environment/EnvVarForm.js";
|
|
13
13
|
import { useOAuthGrantStatus } from "./useOAuthGrantStatus.js";
|
|
14
|
+
import { useOrgOAuthApp } from "./useOrgOAuthApp.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Credential acquisition mode for an MCP server.
|
|
@@ -93,9 +94,15 @@ export interface UseMcpServerCredentialsReturn {
|
|
|
93
94
|
* Servers whose env vars are all optional are always ready.
|
|
94
95
|
*/
|
|
95
96
|
readonly isReady: boolean;
|
|
96
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* `true` while the personal environment, grant status, or org-override
|
|
99
|
+
* lookup is being fetched.
|
|
100
|
+
*/
|
|
97
101
|
readonly isLoading: boolean;
|
|
98
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Error from the personal environment, grant status, or org-override
|
|
104
|
+
* fetch, or `null`.
|
|
105
|
+
*/
|
|
99
106
|
readonly error: Error | null;
|
|
100
107
|
/**
|
|
101
108
|
* Save the provided credentials to the user's personal environment.
|
|
@@ -131,22 +138,35 @@ export interface UseMcpServerCredentialsReturn {
|
|
|
131
138
|
*/
|
|
132
139
|
readonly isVendorApprovalBlocked: boolean;
|
|
133
140
|
/**
|
|
134
|
-
* Where the effective OAuth app
|
|
141
|
+
* Where the effective OAuth app is resolved from for the caller's org
|
|
142
|
+
* — the same source the connect flow will use.
|
|
135
143
|
*
|
|
136
|
-
*
|
|
137
|
-
* `
|
|
138
|
-
*
|
|
144
|
+
* Resolved client-side from the `getOrgOAuthApp` RPC keyed on this
|
|
145
|
+
* hook's `org` parameter. It cannot come from the fetched McpServer:
|
|
146
|
+
* the caller's active org is client-side context the read RPCs never
|
|
147
|
+
* carry, so `status.oauth_status` fields 3-4 are never populated by
|
|
148
|
+
* any backend (see the OAuthStatus proto).
|
|
149
|
+
*
|
|
150
|
+
* `UNSPECIFIED` when `authMode` is `"manual"` or while the override
|
|
151
|
+
* lookup has not yet resolved.
|
|
139
152
|
*/
|
|
140
153
|
readonly effectiveOAuthSource: OAuthAppSource;
|
|
141
154
|
/**
|
|
142
|
-
* `true` when an org-level BYOA override is active for this server
|
|
143
|
-
* Derived from `
|
|
155
|
+
* `true` when an org-level BYOA override is active for this server
|
|
156
|
+
* and org. Derived from the `getOrgOAuthApp` lookup.
|
|
144
157
|
*/
|
|
145
158
|
readonly isOrgOAuthApp: boolean;
|
|
146
159
|
/**
|
|
147
160
|
* `true` when the BYOA option is relevant for this server: the server
|
|
148
|
-
* uses vendor OAuth (`authMode === "oauth"` with an `oauth_app_ref`)
|
|
149
|
-
*
|
|
161
|
+
* uses vendor OAuth (`authMode === "oauth"` with an `oauth_app_ref`),
|
|
162
|
+
* no org override is currently active, AND the backend supports the
|
|
163
|
+
* org-override surface at all.
|
|
164
|
+
*
|
|
165
|
+
* The last condition is an edition capability, confirmed by probing
|
|
166
|
+
* `getOrgOAuthApp` (see {@link useOrgOAuthApp}): the surface is
|
|
167
|
+
* hosted-only, so on OSS deployments — where the submit could only
|
|
168
|
+
* answer UNIMPLEMENTED — this stays `false` and no BYOA affordance
|
|
169
|
+
* renders (stigmer/stigmer#558).
|
|
150
170
|
*
|
|
151
171
|
* When `true`, the UI should offer "Use your own OAuth app" as either
|
|
152
172
|
* a primary action (when vendor approval is blocked) or a secondary
|
|
@@ -201,6 +221,15 @@ export interface UseMcpServerCredentialsReturn {
|
|
|
201
221
|
* form. Additional non-OAuth vars still appear in `missingVariables`
|
|
202
222
|
* (mixed mode).
|
|
203
223
|
*
|
|
224
|
+
* **Org-override-aware**: for vendor-OAuth servers with an
|
|
225
|
+
* `oauth_app_ref`, the hook also composes {@link useOrgOAuthApp} to
|
|
226
|
+
* resolve which OAuth app the connect flow will use for `org`
|
|
227
|
+
* (`effectiveOAuthSource` / `isOrgOAuthApp` / `canBringOwnApp`).
|
|
228
|
+
* The `org` parameter must therefore be the caller's ACTIVE org — the
|
|
229
|
+
* org credentials are stored in and the org whose BYOA override
|
|
230
|
+
* applies — not necessarily the org that owns the MCP server (they
|
|
231
|
+
* differ when browsing another org's public server).
|
|
232
|
+
*
|
|
204
233
|
* Unlike {@link useMcpServerSetup} which manages multi-server setup
|
|
205
234
|
* for session creation, this hook is scoped to a single server and
|
|
206
235
|
* always persists to the personal environment (no one-time option).
|
|
@@ -281,14 +310,51 @@ export function useMcpServerCredentials(
|
|
|
281
310
|
oauthStatus?.vendorApprovalStatus === VendorApprovalStatus.REJECTED);
|
|
282
311
|
const vendorApprovalDocsUrl = oauthStatus?.vendorApprovalDocsUrl || null;
|
|
283
312
|
|
|
284
|
-
const effectiveOAuthSource =
|
|
285
|
-
oauthStatus?.effectiveOauthSource ??
|
|
286
|
-
OAuthAppSource.OAUTH_APP_SOURCE_UNSPECIFIED;
|
|
287
|
-
const isOrgOAuthApp =
|
|
288
|
-
effectiveOAuthSource === OAuthAppSource.OAUTH_APP_SOURCE_ORG_OVERRIDE;
|
|
289
313
|
const hasOAuthAppRef = Boolean(auth?.oauthAppRef?.slug);
|
|
314
|
+
|
|
315
|
+
// Org-override lookup, two duties in one fetch (shared cross-mount via
|
|
316
|
+
// the hook's fetch cache):
|
|
317
|
+
// 1. Capability probe (`isSupported`) — the org-override surface is
|
|
318
|
+
// hosted-only, so BYOA affordances must hide on OSS (#558).
|
|
319
|
+
// 2. The override signal (`hasOverride`) — which OAuthApp the connect
|
|
320
|
+
// flow will actually use for this org. Resolved client-side because
|
|
321
|
+
// the caller's active org is client-side context the read RPCs never
|
|
322
|
+
// carry (get has no org; getByReference's org is the server's OWNING
|
|
323
|
+
// org, which differs from the caller's org when browsing another
|
|
324
|
+
// org's public server), so the fetched McpServer cannot answer this
|
|
325
|
+
// (stigmer-cloud#401).
|
|
326
|
+
// Scoped to vendor-OAuth servers with a platform app ref — the only
|
|
327
|
+
// shape an override can exist for — so manual/DCR servers pay no RPC.
|
|
328
|
+
const orgOverride = useOrgOAuthApp(
|
|
329
|
+
authMode === "oauth" && hasOAuthAppRef
|
|
330
|
+
? (mcpServer?.metadata?.id ?? null)
|
|
331
|
+
: null,
|
|
332
|
+
org,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const isOrgOAuthApp = orgOverride.hasOverride;
|
|
336
|
+
// While the lookup is in flight, failed, or unposable (an app ref exists
|
|
337
|
+
// but no org to resolve an override for), the override state is unknown —
|
|
338
|
+
// report UNSPECIFIED rather than guessing PLATFORM, matching the
|
|
339
|
+
// documented "not yet resolved" contract. UNSUPPORTED is not unknown:
|
|
340
|
+
// on OSS the platform-ref app is truthfully the effective source.
|
|
341
|
+
const overrideUnknown =
|
|
342
|
+
orgOverride.isLoading ||
|
|
343
|
+
orgOverride.error !== null ||
|
|
344
|
+
(hasOAuthAppRef && !org);
|
|
345
|
+
const effectiveOAuthSource =
|
|
346
|
+
authMode !== "oauth" || overrideUnknown
|
|
347
|
+
? OAuthAppSource.OAUTH_APP_SOURCE_UNSPECIFIED
|
|
348
|
+
: isOrgOAuthApp
|
|
349
|
+
? OAuthAppSource.OAUTH_APP_SOURCE_ORG_OVERRIDE
|
|
350
|
+
: hasOAuthAppRef
|
|
351
|
+
? OAuthAppSource.OAUTH_APP_SOURCE_PLATFORM
|
|
352
|
+
: OAuthAppSource.OAUTH_APP_SOURCE_NONE;
|
|
290
353
|
const canBringOwnApp =
|
|
291
|
-
authMode === "oauth" &&
|
|
354
|
+
authMode === "oauth" &&
|
|
355
|
+
hasOAuthAppRef &&
|
|
356
|
+
!isOrgOAuthApp &&
|
|
357
|
+
orgOverride.isSupported;
|
|
292
358
|
|
|
293
359
|
const grantStatus = useOAuthGrantStatus(
|
|
294
360
|
authMode === "oauth" ? (mcpServer?.metadata?.id ?? null) : null,
|
|
@@ -339,7 +405,8 @@ export function useMcpServerCredentials(
|
|
|
339
405
|
const refetch = useCallback(() => {
|
|
340
406
|
personalEnv.refetch();
|
|
341
407
|
grantStatus.refetch();
|
|
342
|
-
|
|
408
|
+
orgOverride.refetch();
|
|
409
|
+
}, [personalEnv, grantStatus, orgOverride]);
|
|
343
410
|
|
|
344
411
|
return {
|
|
345
412
|
authMode,
|
|
@@ -358,8 +425,9 @@ export function useMcpServerCredentials(
|
|
|
358
425
|
manualEntrySupported,
|
|
359
426
|
missingVariables,
|
|
360
427
|
isReady,
|
|
361
|
-
isLoading:
|
|
362
|
-
|
|
428
|
+
isLoading:
|
|
429
|
+
personalEnv.isLoading || grantStatus.isLoading || orgOverride.isLoading,
|
|
430
|
+
error: personalEnv.error ?? grantStatus.error ?? orgOverride.error,
|
|
363
431
|
saveCredentials,
|
|
364
432
|
isSaving: personalEnv.isMutating,
|
|
365
433
|
refetch,
|
|
@@ -8,11 +8,26 @@ import {
|
|
|
8
8
|
DeleteOrgOAuthAppInputSchema,
|
|
9
9
|
} from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
|
|
10
10
|
import { useStigmer } from "../hooks.js";
|
|
11
|
+
import { isUnimplemented } from "../internal/isUnimplemented.js";
|
|
11
12
|
import { toError } from "../internal/toError.js";
|
|
12
13
|
import { useFetch } from "../internal/useFetch.js";
|
|
13
14
|
|
|
14
15
|
/** Return value of {@link useOrgOAuthApp}. */
|
|
15
16
|
export interface UseOrgOAuthAppReturn {
|
|
17
|
+
/**
|
|
18
|
+
* Whether the backend supports the org-OAuth-app override surface at all.
|
|
19
|
+
*
|
|
20
|
+
* The surface is hosted-only by design: the OSS server answers
|
|
21
|
+
* UNIMPLEMENTED for all three org-override RPCs because its flat
|
|
22
|
+
* OAuthApp store has no override binding (stigmer/stigmer#558 — the
|
|
23
|
+
* self-hosted equivalent is applying an OAuthApp resource and
|
|
24
|
+
* referencing it from `spec.auth.oauth_app_ref`). The fetch doubles
|
|
25
|
+
* as the capability probe: `isSupported` is `false` until the first
|
|
26
|
+
* successful response confirms the surface exists, so BYOA
|
|
27
|
+
* affordances gated on it never flash on deployments where the
|
|
28
|
+
* submit could only fail.
|
|
29
|
+
*/
|
|
30
|
+
readonly isSupported: boolean;
|
|
16
31
|
/** Whether an org-level BYOA override exists for this resource + org. */
|
|
17
32
|
readonly hasOverride: boolean;
|
|
18
33
|
/** System-generated ID of the override's OAuthApp, or `null` when absent. */
|
|
@@ -72,12 +87,25 @@ export interface UseOrgOAuthAppReturn {
|
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
interface OrgOAuthAppData {
|
|
90
|
+
isSupported: boolean;
|
|
75
91
|
hasOverride: boolean;
|
|
76
92
|
oauthAppId: string | null;
|
|
77
93
|
clientId: string | null;
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
const IDLE_DATA: OrgOAuthAppData = {
|
|
97
|
+
isSupported: false,
|
|
98
|
+
hasOverride: false,
|
|
99
|
+
oauthAppId: null,
|
|
100
|
+
clientId: null,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The UNIMPLEMENTED answer resolved into data rather than an error:
|
|
105
|
+
* the backend is healthy, it just doesn't have this edition capability.
|
|
106
|
+
*/
|
|
107
|
+
const UNSUPPORTED_DATA: OrgOAuthAppData = {
|
|
108
|
+
isSupported: false,
|
|
81
109
|
hasOverride: false,
|
|
82
110
|
oauthAppId: null,
|
|
83
111
|
clientId: null,
|
|
@@ -110,6 +138,14 @@ const IDLE: UseOrgOAuthAppReturn = {
|
|
|
110
138
|
* mutations bound to the hook's resource + org context, eliminating
|
|
111
139
|
* parameter repetition at call sites.
|
|
112
140
|
*
|
|
141
|
+
* **Edition awareness**: the fetch doubles as a capability probe. The
|
|
142
|
+
* org-override surface is hosted-only; the OSS server answers
|
|
143
|
+
* UNIMPLEMENTED for it, which this hook resolves into
|
|
144
|
+
* `isSupported: false` with no error. Gate every BYOA affordance on
|
|
145
|
+
* `isSupported` (as {@link useMcpServerCredentials} does for
|
|
146
|
+
* `canBringOwnApp`) — the mutations can only fail where the probe
|
|
147
|
+
* reports the surface absent.
|
|
148
|
+
*
|
|
113
149
|
* Pass `null` for either parameter to skip fetching (stable no-op).
|
|
114
150
|
*
|
|
115
151
|
* @example
|
|
@@ -136,18 +172,28 @@ export function useOrgOAuthApp(
|
|
|
136
172
|
const { data, isLoading, isRefetching, error, refetch } = useFetch<OrgOAuthAppData>(
|
|
137
173
|
resourceId && org
|
|
138
174
|
? async () => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
175
|
+
try {
|
|
176
|
+
const result = await stigmer.mcpServer.getOrgOAuthApp(
|
|
177
|
+
create(GetOrgOAuthAppInputSchema, { resourceId, org }),
|
|
178
|
+
);
|
|
179
|
+
return {
|
|
180
|
+
isSupported: true,
|
|
181
|
+
hasOverride: result.hasOverride,
|
|
182
|
+
oauthAppId: result.oauthAppId || null,
|
|
183
|
+
clientId: result.clientId || null,
|
|
184
|
+
};
|
|
185
|
+
} catch (err) {
|
|
186
|
+
if (isUnimplemented(err)) return UNSUPPORTED_DATA;
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
147
189
|
}
|
|
148
190
|
: null,
|
|
149
191
|
[resourceId, org, stigmer],
|
|
150
192
|
IDLE_DATA,
|
|
193
|
+
// Cross-mount cache: the credentials hook composes this fetch as its
|
|
194
|
+
// capability probe, so a detail page mounts two instances — the cache
|
|
195
|
+
// lets the second render from the first's result.
|
|
196
|
+
{ cacheKey: `org-oauth-app:${org}:${resourceId}` },
|
|
151
197
|
);
|
|
152
198
|
|
|
153
199
|
const [isSetting, setIsSetting] = useState(false);
|
|
@@ -74,8 +74,10 @@ export interface SendFollowUpOptions {
|
|
|
74
74
|
* Execution-scoped secrets and configuration (Execution Flow).
|
|
75
75
|
*
|
|
76
76
|
* Values are injected into the agent sandbox for this execution only
|
|
77
|
-
* and deleted when the execution completes. They
|
|
78
|
-
* Environment values
|
|
77
|
+
* and deleted when the execution completes. They take the highest
|
|
78
|
+
* merge priority, overriding Environment values bound via the
|
|
79
|
+
* instance. Keys must be declared in the agent's env declarations
|
|
80
|
+
* (a whitelist, not a value source) or they are dropped.
|
|
79
81
|
*
|
|
80
82
|
* @see {@link CreateAgentExecutionInput.runtimeEnv}
|
|
81
83
|
*/
|
|
@@ -3,18 +3,15 @@
|
|
|
3
3
|
import { useCallback, useMemo } from "react";
|
|
4
4
|
import { create } from "@bufbuild/protobuf";
|
|
5
5
|
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
6
|
-
import { StigmerError } from "@stigmer/sdk";
|
|
7
6
|
import {
|
|
8
7
|
ListSkillVersionsInputSchema,
|
|
9
8
|
type SkillVersionEntry as ProtoSkillVersionEntry,
|
|
10
9
|
} from "@stigmer/protos/ai/stigmer/agentic/skill/v1/io_pb";
|
|
11
10
|
import { useStigmer } from "../hooks.js";
|
|
11
|
+
import { isUnimplemented } from "../internal/isUnimplemented.js";
|
|
12
12
|
import { useFetch } from "../internal/useFetch.js";
|
|
13
13
|
import type { VersionEntry } from "../version-history/types.js";
|
|
14
14
|
|
|
15
|
-
// gRPC/Connect Code.Unimplemented = 12
|
|
16
|
-
const CODE_UNIMPLEMENTED = 12;
|
|
17
|
-
|
|
18
15
|
// ---------------------------------------------------------------------------
|
|
19
16
|
// Public types
|
|
20
17
|
// ---------------------------------------------------------------------------
|
|
@@ -156,10 +153,3 @@ function mapProtoToVersionEntry(proto: ProtoSkillVersionEntry): VersionEntry {
|
|
|
156
153
|
};
|
|
157
154
|
}
|
|
158
155
|
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
// Error detection
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
|
|
163
|
-
function isUnimplemented(err: unknown): boolean {
|
|
164
|
-
return err instanceof StigmerError && err.connectCode === CODE_UNIMPLEMENTED;
|
|
165
|
-
}
|
|
@@ -87,7 +87,7 @@ const WorkflowCanvasEditorInner = memo(function WorkflowCanvasEditorInner({
|
|
|
87
87
|
layoutEngine,
|
|
88
88
|
}: WorkflowCanvasEditorProps) {
|
|
89
89
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
90
|
-
const canvas = useWorkflowCanvas(yaml,
|
|
90
|
+
const canvas = useWorkflowCanvas(yaml, { layoutEngine: layoutEngine ?? undefined });
|
|
91
91
|
const [paletteCollapsed, setPaletteCollapsed] = useState(false);
|
|
92
92
|
const togglePalette = useCallback(() => setPaletteCollapsed((p) => !p), []);
|
|
93
93
|
|
|
@@ -161,8 +161,35 @@ const WorkflowCanvasEditorInner = memo(function WorkflowCanvasEditorInner({
|
|
|
161
161
|
copySelection: canvas.copySelection,
|
|
162
162
|
pasteAtCenter: canvas.pasteAtCenter,
|
|
163
163
|
cutSelection: canvas.cutSelection,
|
|
164
|
+
undo: canvas.undo,
|
|
165
|
+
redo: canvas.redo,
|
|
164
166
|
});
|
|
165
167
|
|
|
168
|
+
// All canvas shortcuts are gated on focus being inside the container,
|
|
169
|
+
// but React Flow's pane prevents default on mousedown, so pane clicks
|
|
170
|
+
// never move focus into it on their own — without this, Cmd+Z (etc.)
|
|
171
|
+
// after a pane interaction silently does nothing (oss#588). Focusing
|
|
172
|
+
// unconditionally also commits-and-blurs an inspector field when the
|
|
173
|
+
// user clicks back into the canvas. Scoped to the canvas column so
|
|
174
|
+
// palette and inspector clicks keep their native focus flow; when the
|
|
175
|
+
// press lands on a real focusable (node, toolbar button, picker
|
|
176
|
+
// input), the default mousedown focus takes over right after.
|
|
177
|
+
const handleCanvasPointerDownCapture = useCallback(() => {
|
|
178
|
+
containerRef.current?.focus({ preventScroll: true });
|
|
179
|
+
}, []);
|
|
180
|
+
|
|
181
|
+
// Deleting the focused node drops DOM focus to <body> when React Flow
|
|
182
|
+
// unmounts the element, disarming every canvas shortcut at exactly the
|
|
183
|
+
// moment the user reaches for Cmd+Z (oss#588). Return focus to the
|
|
184
|
+
// container after any node deletion so undo stays reachable.
|
|
185
|
+
const handleNodesDelete = useCallback(
|
|
186
|
+
(deleted: Parameters<typeof canvas.onNodesDelete>[0]) => {
|
|
187
|
+
canvas.onNodesDelete(deleted);
|
|
188
|
+
containerRef.current?.focus({ preventScroll: true });
|
|
189
|
+
},
|
|
190
|
+
[canvas.onNodesDelete],
|
|
191
|
+
);
|
|
192
|
+
|
|
166
193
|
useEffect(() => {
|
|
167
194
|
onDirtyChange?.(canvas.isDirty);
|
|
168
195
|
}, [canvas.isDirty, onDirtyChange]);
|
|
@@ -239,10 +266,10 @@ const WorkflowCanvasEditorInner = memo(function WorkflowCanvasEditorInner({
|
|
|
239
266
|
|
|
240
267
|
const handleContextMenuDeleteNode = useCallback(
|
|
241
268
|
(nodeId: string) => {
|
|
242
|
-
|
|
269
|
+
handleNodesDelete([{ id: nodeId } as import("@xyflow/react").Node]);
|
|
243
270
|
setContextMenu(null);
|
|
244
271
|
},
|
|
245
|
-
[
|
|
272
|
+
[handleNodesDelete],
|
|
246
273
|
);
|
|
247
274
|
|
|
248
275
|
const handleContextMenuDuplicateNode = useCallback(
|
|
@@ -371,6 +398,9 @@ const WorkflowCanvasEditorInner = memo(function WorkflowCanvasEditorInner({
|
|
|
371
398
|
const handleContextMenuDeleteSelection = useCallback(() => {
|
|
372
399
|
canvas.deleteSelection();
|
|
373
400
|
setContextMenu(null);
|
|
401
|
+
// Same focus restoration as handleNodesDelete — the selection may
|
|
402
|
+
// include the focused node element.
|
|
403
|
+
containerRef.current?.focus({ preventScroll: true });
|
|
374
404
|
}, [canvas.deleteSelection]);
|
|
375
405
|
|
|
376
406
|
const handleSelectionContextMenu = useCallback(
|
|
@@ -475,9 +505,9 @@ const WorkflowCanvasEditorInner = memo(function WorkflowCanvasEditorInner({
|
|
|
475
505
|
|
|
476
506
|
const handleDeleteNode = useCallback(
|
|
477
507
|
(nodeId: string) => {
|
|
478
|
-
|
|
508
|
+
handleNodesDelete([{ id: nodeId } as import("@xyflow/react").Node]);
|
|
479
509
|
},
|
|
480
|
-
[
|
|
510
|
+
[handleNodesDelete],
|
|
481
511
|
);
|
|
482
512
|
|
|
483
513
|
const handleDuplicateNode = useCallback(
|
|
@@ -580,7 +610,7 @@ const WorkflowCanvasEditorInner = memo(function WorkflowCanvasEditorInner({
|
|
|
580
610
|
|
|
581
611
|
{showPalette && !paletteCollapsed && <WorkflowTaskPalette />}
|
|
582
612
|
|
|
583
|
-
<div className="stg:relative stg:flex-1">
|
|
613
|
+
<div className="stg:relative stg:flex-1" onPointerDownCapture={handleCanvasPointerDownCapture}>
|
|
584
614
|
{showPalette && (
|
|
585
615
|
<PaletteToggle
|
|
586
616
|
collapsed={paletteCollapsed}
|
|
@@ -613,7 +643,7 @@ const WorkflowCanvasEditorInner = memo(function WorkflowCanvasEditorInner({
|
|
|
613
643
|
isValidConnection={canvas.isValidConnection}
|
|
614
644
|
onDrop={canvas.onDrop}
|
|
615
645
|
onDragOver={canvas.onDragOver}
|
|
616
|
-
onNodesDelete={
|
|
646
|
+
onNodesDelete={handleNodesDelete}
|
|
617
647
|
onEdgesDelete={canvas.onEdgesDelete}
|
|
618
648
|
onNodeContextMenu={handleNodeContextMenu}
|
|
619
649
|
onEdgeContextMenu={handleEdgeContextMenu}
|
|
@@ -152,6 +152,7 @@ function WorkflowExecutionGraphInner({
|
|
|
152
152
|
const [didInitialFit, setDidInitialFit] = useState(false);
|
|
153
153
|
const isInitializedRef = useRef(false);
|
|
154
154
|
const didFitGuardRef = useRef(false);
|
|
155
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
155
156
|
|
|
156
157
|
// ── Ephemeral drag positions ───────────────────────────────────────
|
|
157
158
|
// Resets when the execution changes (navigating to a different run).
|
|
@@ -188,6 +189,14 @@ function WorkflowExecutionGraphInner({
|
|
|
188
189
|
|
|
189
190
|
const performInitialFit = useCallback(() => {
|
|
190
191
|
if (didFitGuardRef.current) return;
|
|
192
|
+
// A CSS-hidden mount (the execution viewer keeps BOTH center views
|
|
193
|
+
// mounted with the inactive one `display:none`) measures 0×0 —
|
|
194
|
+
// `fitView` against that computes a degenerate transform that never
|
|
195
|
+
// self-corrects, so the first toggle to Graph would show an
|
|
196
|
+
// apparently empty canvas (oss#571). Defer: the ResizeObserver below
|
|
197
|
+
// re-attempts on the hidden→visible transition.
|
|
198
|
+
const el = containerRef.current;
|
|
199
|
+
if (!el || el.offsetWidth === 0 || el.offsetHeight === 0) return;
|
|
191
200
|
didFitGuardRef.current = true;
|
|
192
201
|
fitView({ padding: 0.15, duration: getAnimationDuration(300) });
|
|
193
202
|
setDidInitialFit(true);
|
|
@@ -209,6 +218,22 @@ function WorkflowExecutionGraphInner({
|
|
|
209
218
|
}
|
|
210
219
|
}, [nodes.length, performInitialFit]);
|
|
211
220
|
|
|
221
|
+
// The deferred-fit path for hidden mounts: when the container gains
|
|
222
|
+
// real dimensions (first reveal, or a late layout pass), run the
|
|
223
|
+
// pending initial fit. One-shot — `performInitialFit` self-guards, so
|
|
224
|
+
// user pan/zoom is never overridden by later resizes.
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
const el = containerRef.current;
|
|
227
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
228
|
+
const observer = new ResizeObserver(() => {
|
|
229
|
+
if (isInitializedRef.current && nodes.length > 0) {
|
|
230
|
+
performInitialFit();
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
observer.observe(el);
|
|
234
|
+
return () => observer.disconnect();
|
|
235
|
+
}, [nodes.length, performInitialFit]);
|
|
236
|
+
|
|
212
237
|
// Derive active task name stably from taskStates (not from nodes array — DD-010)
|
|
213
238
|
const activeTaskInfo = useActiveTaskName(taskStates);
|
|
214
239
|
const isTerminal = executionPhase != null && TERMINAL_EXECUTION_PHASES.has(executionPhase);
|
|
@@ -276,7 +301,7 @@ function WorkflowExecutionGraphInner({
|
|
|
276
301
|
|
|
277
302
|
return (
|
|
278
303
|
<WorkflowGraphModeProvider mode="execution">
|
|
279
|
-
<div className={cn("stg:relative stg:h-full stg:w-full", className)}>
|
|
304
|
+
<div ref={containerRef} className={cn("stg:relative stg:h-full stg:w-full", className)}>
|
|
280
305
|
{versionMismatch && (
|
|
281
306
|
<div className="stg:absolute stg:left-3 stg:right-3 stg:top-3 stg:z-50 stg:rounded-md stg:border stg:border-[var(--stgm-warning,#f59e0b)]/30 stg:bg-[var(--stgm-warning,#f59e0b)]/5 stg:px-3 stg:py-2 stg:text-xs stg:text-[var(--stgm-foreground,#1a1a2e)]">
|
|
282
307
|
{versionMismatch}
|
|
@@ -344,24 +344,38 @@ export const WorkflowExecutionViewer = memo(function WorkflowExecutionViewer({
|
|
|
344
344
|
// Thread view. One always-visible announcer serves both center views.
|
|
345
345
|
const announcement = useExecutionAnnouncements(effectiveTaskStates);
|
|
346
346
|
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
// the
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
347
|
+
// Boundary-transition snapshot refetches. The snapshot fetch is one-shot
|
|
348
|
+
// (`useWorkflowExecution` never polls), so the stream store's stage
|
|
349
|
+
// transitions are the only live signals that the mount snapshot went
|
|
350
|
+
// stale — the snapshot's own `phase` cannot be the trigger, since it
|
|
351
|
+
// only moves on a refetch (the circularity behind the stale "Pending"
|
|
352
|
+
// header, R1-5). Two transitions matter, one per direction:
|
|
353
|
+
//
|
|
354
|
+
// - ENTERING "complete" (T04, DD-T04-4): while a run streams, the cards'
|
|
355
|
+
// collapsed lines and bodies work from the TRUNCATED event summaries;
|
|
356
|
+
// the full per-task output lands on `status.tasks[]` only when the
|
|
357
|
+
// runner persists the terminal snapshot.
|
|
358
|
+
// - ENTERING "streaming" (oss#571): the store flips connecting→streaming
|
|
359
|
+
// on the FIRST delivered event, so this transition is the "run
|
|
360
|
+
// started" signal for a page that landed on a still-pending execution
|
|
361
|
+
// — without the refetch, the phase badge and the phase-gated
|
|
362
|
+
// Pause/Cancel would freeze at "Pending" for the entire run. It also
|
|
363
|
+
// covers reconnecting→streaming, where the snapshot slept through the
|
|
364
|
+
// outage.
|
|
365
|
+
//
|
|
366
|
+
// Both are TRANSITION-scoped (never fire on mount): a terminal-replay
|
|
367
|
+
// mount starts at "complete" and a mid-run mount starts at "streaming",
|
|
368
|
+
// and each already fetched a fresh snapshot.
|
|
357
369
|
const prevStreamStageRef = useRef(streamState.stage);
|
|
358
370
|
useEffect(() => {
|
|
359
371
|
const prev = prevStreamStageRef.current;
|
|
360
372
|
prevStreamStageRef.current = streamState.stage;
|
|
361
|
-
|
|
373
|
+
const enteredComplete =
|
|
362
374
|
streamState.stage === "complete" &&
|
|
363
|
-
(prev === "streaming" || prev === "reconnecting")
|
|
364
|
-
|
|
375
|
+
(prev === "streaming" || prev === "reconnecting");
|
|
376
|
+
const enteredStreaming =
|
|
377
|
+
streamState.stage === "streaming" && prev !== "streaming";
|
|
378
|
+
if (enteredComplete || enteredStreaming) {
|
|
365
379
|
refetchExecution();
|
|
366
380
|
}
|
|
367
381
|
}, [streamState.stage, refetchExecution]);
|
|
@@ -28,7 +28,7 @@ export const WorkflowNode = memo(function WorkflowNode({
|
|
|
28
28
|
selected,
|
|
29
29
|
}: NodeProps & { data: CanvasTaskNodeData }) {
|
|
30
30
|
const mode = useWorkflowGraphMode();
|
|
31
|
-
const visualSpec = getVisualSpec(data.
|
|
31
|
+
const visualSpec = getVisualSpec(data.kindString);
|
|
32
32
|
const categoryColor = CATEGORY_COLORS[data.category];
|
|
33
33
|
const errorCount = data.errorCount ?? 0;
|
|
34
34
|
const isNested = NESTED_TASK_KINDS.has(data.kindString);
|
|
@@ -38,7 +38,10 @@ export const WorkflowNode = memo(function WorkflowNode({
|
|
|
38
38
|
return (
|
|
39
39
|
<div
|
|
40
40
|
data-visual-class={data.visualClass}
|
|
41
|
-
data-task-kind
|
|
41
|
+
// data-task-kind is a sanctioned e2e/embedding hook meaning "real
|
|
42
|
+
// workflow task kind" — sentinels omit it and are targeted via
|
|
43
|
+
// data-id + data-visual-class instead (oss#581).
|
|
44
|
+
data-task-kind={data.isSentinel ? undefined : data.kindString}
|
|
42
45
|
data-execution-status={executionState?.status}
|
|
43
46
|
data-diff-status={diffState?.status}
|
|
44
47
|
aria-label={buildAriaLabel(data, errorCount, executionState?.status)}
|
|
@@ -24,6 +24,16 @@ export interface WorkflowRunDialogProps {
|
|
|
24
24
|
* Passed through to the form's instance picker to control visibility threshold.
|
|
25
25
|
*/
|
|
26
26
|
readonly defaultInstanceId?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Instance to preselect when the dialog opens — wire this from a
|
|
29
|
+
* row-level "Run" action so the form reflects the instance the user
|
|
30
|
+
* clicked. Applied on each open transition (after the form resets);
|
|
31
|
+
* ignored when the id is not in `instances`, so a stale id degrades
|
|
32
|
+
* to the default option. Omit (or pass `null`) for the
|
|
33
|
+
* server-resolved default. Same convention as
|
|
34
|
+
* {@link SessionComposer}'s `initialInstanceId`.
|
|
35
|
+
*/
|
|
36
|
+
readonly initialInstanceId?: string | null;
|
|
27
37
|
/**
|
|
28
38
|
* Called after the execution is created successfully.
|
|
29
39
|
* Receives the execution ID — use for navigation.
|
|
@@ -67,6 +77,7 @@ export function WorkflowRunDialog({
|
|
|
67
77
|
workflow,
|
|
68
78
|
instances,
|
|
69
79
|
defaultInstanceId,
|
|
80
|
+
initialInstanceId,
|
|
70
81
|
onSuccess,
|
|
71
82
|
onError,
|
|
72
83
|
}: WorkflowRunDialogProps) {
|
|
@@ -94,11 +105,27 @@ export function WorkflowRunDialog({
|
|
|
94
105
|
if (!dialog) return;
|
|
95
106
|
if (open && !dialog.open) {
|
|
96
107
|
flow.reset();
|
|
108
|
+
// Preselection must follow reset (reset clears the selection back to
|
|
109
|
+
// the server-resolved default). Guarded against stale ids: an instance
|
|
110
|
+
// deleted between the caller capturing the id and this open falls back
|
|
111
|
+
// to the default option instead of a <select> value with no option.
|
|
112
|
+
if (
|
|
113
|
+
initialInstanceId &&
|
|
114
|
+
instances.some((i) => i.metadata?.id === initialInstanceId)
|
|
115
|
+
) {
|
|
116
|
+
flow.setSelectedInstanceId(initialInstanceId);
|
|
117
|
+
}
|
|
97
118
|
dialog.showModal();
|
|
98
119
|
} else if (!open && dialog.open) {
|
|
99
120
|
dialog.close();
|
|
100
121
|
}
|
|
101
|
-
}, [
|
|
122
|
+
}, [
|
|
123
|
+
open,
|
|
124
|
+
flow.reset,
|
|
125
|
+
flow.setSelectedInstanceId,
|
|
126
|
+
initialInstanceId,
|
|
127
|
+
instances,
|
|
128
|
+
]);
|
|
102
129
|
|
|
103
130
|
const handleDialogCancel = useCallback(
|
|
104
131
|
(e: React.SyntheticEvent) => {
|
|
@@ -571,7 +571,9 @@ describe("WorkflowExecutionViewer (terminal-phase refetch, T04)", () => {
|
|
|
571
571
|
|
|
572
572
|
// Re-mocks ONLY the stream stage (the execution mock — and its refetch
|
|
573
573
|
// spy — must stay stable across the flip; the assertions count its calls).
|
|
574
|
-
function mockStreamStage(
|
|
574
|
+
function mockStreamStage(
|
|
575
|
+
stage: "connecting" | "streaming" | "reconnecting" | "complete",
|
|
576
|
+
) {
|
|
575
577
|
mockedUseEventStream.mockReturnValue({
|
|
576
578
|
events: [],
|
|
577
579
|
taskStates: new Map([
|
|
@@ -630,4 +632,49 @@ describe("WorkflowExecutionViewer (terminal-phase refetch, T04)", () => {
|
|
|
630
632
|
|
|
631
633
|
expect(refetch).not.toHaveBeenCalled();
|
|
632
634
|
});
|
|
635
|
+
|
|
636
|
+
// The other stale direction (oss#571's live-run finding): a page that
|
|
637
|
+
// lands BEFORE the run starts fetches a PENDING snapshot, and nothing
|
|
638
|
+
// refetched it when the run began — the phase badge and the phase-gated
|
|
639
|
+
// Pause/Cancel froze at "Pending" for the entire run. The store flips
|
|
640
|
+
// connecting → streaming exactly when the FIRST event is delivered, so
|
|
641
|
+
// that transition IS the "run started" signal.
|
|
642
|
+
it("the stream's first delivery refetches a pending-landing snapshot — the phase badge and Pause/Cancel catch up to the started run", () => {
|
|
643
|
+
const { refetch } = arrange(ExecutionPhase.EXECUTION_PENDING);
|
|
644
|
+
mockStreamStage("connecting");
|
|
645
|
+
const { rerender } = render(<WorkflowExecutionViewer executionId="wex_1" />);
|
|
646
|
+
expect(refetch).not.toHaveBeenCalled();
|
|
647
|
+
|
|
648
|
+
mockStreamStage("streaming");
|
|
649
|
+
rerender(
|
|
650
|
+
<WorkflowExecutionViewer executionId="wex_1" className="pass-2" />,
|
|
651
|
+
);
|
|
652
|
+
expect(refetch).toHaveBeenCalledTimes(1);
|
|
653
|
+
|
|
654
|
+
// Idempotent: staying in streaming never re-triggers.
|
|
655
|
+
rerender(
|
|
656
|
+
<WorkflowExecutionViewer executionId="wex_1" className="pass-3" />,
|
|
657
|
+
);
|
|
658
|
+
expect(refetch).toHaveBeenCalledTimes(1);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it("a reconnect recovery (reconnecting → streaming) refetches — the snapshot may have moved during the outage", () => {
|
|
662
|
+
const { refetch } = arrange(ExecutionPhase.EXECUTION_IN_PROGRESS);
|
|
663
|
+
mockStreamStage("streaming");
|
|
664
|
+
const { rerender } = render(<WorkflowExecutionViewer executionId="wex_1" />);
|
|
665
|
+
// Mid-run mount: the snapshot was just fetched — no refetch.
|
|
666
|
+
expect(refetch).not.toHaveBeenCalled();
|
|
667
|
+
|
|
668
|
+
mockStreamStage("reconnecting");
|
|
669
|
+
rerender(
|
|
670
|
+
<WorkflowExecutionViewer executionId="wex_1" className="pass-2" />,
|
|
671
|
+
);
|
|
672
|
+
expect(refetch).not.toHaveBeenCalled();
|
|
673
|
+
|
|
674
|
+
mockStreamStage("streaming");
|
|
675
|
+
rerender(
|
|
676
|
+
<WorkflowExecutionViewer executionId="wex_1" className="pass-3" />,
|
|
677
|
+
);
|
|
678
|
+
expect(refetch).toHaveBeenCalledTimes(1);
|
|
679
|
+
});
|
|
633
680
|
});
|