@stigmer/react 3.15.3-dev.20260914121148 → 3.15.3-dev.20260916211208
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/ApprovalCard.d.ts +18 -5
- package/execution/ApprovalCard.d.ts.map +1 -1
- package/execution/ApprovalCard.js +24 -25
- package/execution/ApprovalCard.js.map +1 -1
- package/execution/ToolArgsView.js +1 -17
- package/execution/ToolArgsView.js.map +1 -1
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +4 -1
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/tool-categories.d.ts +29 -6
- package/execution/tool-categories.d.ts.map +1 -1
- package/execution/tool-categories.js +61 -15
- package/execution/tool-categories.js.map +1 -1
- package/organization/OrgSwitcher.d.ts +3 -1
- package/organization/OrgSwitcher.d.ts.map +1 -1
- package/organization/OrgSwitcher.js +3 -1
- package/organization/OrgSwitcher.js.map +1 -1
- package/package.json +4 -4
- package/sharing/useCanCreateAgentShare.d.ts +5 -4
- package/sharing/useCanCreateAgentShare.d.ts.map +1 -1
- package/sharing/useCanCreateAgentShare.js +5 -4
- package/sharing/useCanCreateAgentShare.js.map +1 -1
- package/src/execution/ApprovalCard.tsx +38 -23
- package/src/execution/ToolArgsView.tsx +1 -20
- package/src/execution/ToolCallItem.tsx +4 -0
- package/src/execution/__tests__/ApprovalCard.test.tsx +110 -1
- package/src/execution/__tests__/ToolCallItem.test.tsx +38 -0
- package/src/execution/__tests__/write-content.test.ts +82 -0
- package/src/execution/tool-categories.ts +77 -17
- package/src/organization/OrgSwitcher.tsx +3 -1
- package/src/sharing/useCanCreateAgentShare.ts +5 -4
- package/src/user/UserMenu.tsx +3 -1
- package/user/UserMenu.d.ts +3 -1
- package/user/UserMenu.d.ts.map +1 -1
- package/user/UserMenu.js +3 -1
- package/user/UserMenu.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/react",
|
|
3
|
-
"version": "3.15.3-dev.
|
|
3
|
+
"version": "3.15.3-dev.20260916211208",
|
|
4
4
|
"description": "React provider and client hook for the Stigmer platform SDK",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@bufbuild/protovalidate": "^1.1.1",
|
|
39
39
|
"@dagrejs/dagre": "^1.1.4",
|
|
40
|
-
"@stigmer/theme": "3.15.3-dev.
|
|
40
|
+
"@stigmer/theme": "3.15.3-dev.20260916211208",
|
|
41
41
|
"diff": "^8.0.3",
|
|
42
42
|
"fflate": "^0.8.2",
|
|
43
43
|
"hast-util-to-jsx-runtime": "^2.3.6",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"@codemirror/state": "^6.0.0",
|
|
61
61
|
"@codemirror/view": "^6.0.0",
|
|
62
62
|
"@lezer/highlight": "^1.0.0",
|
|
63
|
-
"@stigmer/protos": "3.15.3-dev.
|
|
64
|
-
"@stigmer/sdk": "3.15.3-dev.
|
|
63
|
+
"@stigmer/protos": "3.15.3-dev.20260916211208",
|
|
64
|
+
"@stigmer/sdk": "3.15.3-dev.20260916211208",
|
|
65
65
|
"@tanstack/react-table": "^8.20.0",
|
|
66
66
|
"@xyflow/react": "^12.0.0",
|
|
67
67
|
"elkjs": "^0.9.0",
|
|
@@ -28,10 +28,11 @@ export interface UseCanCreateAgentShareReturn {
|
|
|
28
28
|
* resource id — an Organization's id equals its slug
|
|
29
29
|
* (ApiResourceMetadata.id).
|
|
30
30
|
*
|
|
31
|
-
* On the open-source edition
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* On the open-source edition the answer follows the server's posture. A
|
|
32
|
+
* server without sign-in has the permissive single-team authorizer and
|
|
33
|
+
* answers allowed (decision 011 D4). A server with sign-in enforces the
|
|
34
|
+
* same organization roles as the cloud, so `can_create_agent_share` is
|
|
35
|
+
* the admin's and a member sees no share affordance.
|
|
35
36
|
*
|
|
36
37
|
* Pass `null` for `agent` while it loads — `allowed` stays `false` so
|
|
37
38
|
* no affordance flashes before the gate can be evaluated.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCanCreateAgentShare.d.ts","sourceRoot":"","sources":["../../src/sharing/useCanCreateAgentShare.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oDAAoD,CAAC;AAIhF,sDAAsD;AACtD,MAAM,WAAW,4BAA4B;IAC3C,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED
|
|
1
|
+
{"version":3,"file":"useCanCreateAgentShare.d.ts","sourceRoot":"","sources":["../../src/sharing/useCanCreateAgentShare.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oDAAoD,CAAC;AAIhF,sDAAsD;AACtD,MAAM,WAAW,4BAA4B;IAC3C,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,KAAK,GAAG,IAAI,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,4BAA4B,CAuB9B"}
|
|
@@ -17,10 +17,11 @@ import { useCheckPermission } from "../iam-policy/useCheckPermission.js";
|
|
|
17
17
|
* resource id — an Organization's id equals its slug
|
|
18
18
|
* (ApiResourceMetadata.id).
|
|
19
19
|
*
|
|
20
|
-
* On the open-source edition
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* On the open-source edition the answer follows the server's posture. A
|
|
21
|
+
* server without sign-in has the permissive single-team authorizer and
|
|
22
|
+
* answers allowed (decision 011 D4). A server with sign-in enforces the
|
|
23
|
+
* same organization roles as the cloud, so `can_create_agent_share` is
|
|
24
|
+
* the admin's and a member sees no share affordance.
|
|
24
25
|
*
|
|
25
26
|
* Pass `null` for `agent` while it loads — `allowed` stays `false` so
|
|
26
27
|
* no affordance flashes before the gate can be evaluated.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCanCreateAgentShare.js","sourceRoot":"","sources":["../../src/sharing/useCanCreateAgentShare.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAGb,OAAO,EAAE,qBAAqB,EAAE,MAAM,wDAAwD,CAAC;AAC/F,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAgBzE
|
|
1
|
+
{"version":3,"file":"useCanCreateAgentShare.js","sourceRoot":"","sources":["../../src/sharing/useCanCreateAgentShare.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAGb,OAAO,EAAE,qBAAqB,EAAE,MAAM,wDAAwD,CAAC;AAC/F,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAgBzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,sBAAsB,CACpC,KAAmB,EACnB,SAAkB;IAElB,MAAM,OAAO,GAAG,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC;IAC1C,MAAM,QAAQ,GAAG,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC;IAC5C,MAAM,QAAQ,GAAG,SAAS,IAAI,QAAQ,CAAC;IACvC,MAAM,UAAU,GAAG,QAAQ,KAAK,EAAE,IAAI,QAAQ,KAAK,QAAQ,CAAC;IAC5D,MAAM,QAAQ,GACZ,KAAK,EAAE,QAAQ,EAAE,UAAU,KAAK,qBAAqB,CAAC,iBAAiB,CAAC;IAE1E,oEAAoE;IACpE,oEAAoE;IACpE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,kBAAkB,CAClD,CAAC,UAAU,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,EAC9D,UAAU,CACX,CAAC;IACF,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,kBAAkB,CACpD,UAAU,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,EACtE,wBAAwB,CACzB,CAAC;IAEF,MAAM,OAAO,GACX,CAAC,CAAC,KAAK,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,IAAI,cAAc,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;IAEtE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AAC3C,CAAC"}
|
|
@@ -13,8 +13,10 @@ import {
|
|
|
13
13
|
resolveToolCategoryFromKind,
|
|
14
14
|
extractPrimaryArgFromPreview,
|
|
15
15
|
extractShellIntentFromPreview,
|
|
16
|
-
|
|
16
|
+
extractWriteContentFromArgs,
|
|
17
17
|
isFileCategory,
|
|
18
|
+
parseArgsPreview,
|
|
19
|
+
withAuthoritativeWriteContent,
|
|
18
20
|
type ToolCategory,
|
|
19
21
|
} from "./tool-categories.js";
|
|
20
22
|
import { CATEGORY_ICON } from "./ToolCallItem.js";
|
|
@@ -233,6 +235,17 @@ export function ApprovalCardHeader({
|
|
|
233
235
|
export interface ApprovalCardBodyProps {
|
|
234
236
|
/** The pending approval to render a decision panel for. */
|
|
235
237
|
readonly pendingApproval: PendingApproval;
|
|
238
|
+
/**
|
|
239
|
+
* The gated tool row's authoritative arguments (`ToolCall.args`), when the
|
|
240
|
+
* caller holds the row — the inline gate on a {@link ToolCallItem} does.
|
|
241
|
+
* They supply the proposed write/edit content, which the pending approval's
|
|
242
|
+
* `argsPreview` (the runner's sanitized summary) cannot carry for a file of
|
|
243
|
+
* any real size; every other field the card shows still comes from the
|
|
244
|
+
* preview, so nothing the runner redacted becomes visible. Absent, the card
|
|
245
|
+
* renders from the preview alone and a large write shows the "no preview"
|
|
246
|
+
* notice rather than its content.
|
|
247
|
+
*/
|
|
248
|
+
readonly args?: Record<string, unknown>;
|
|
236
249
|
/** Called when the user picks Approve / Approve-all / Skip / Reject. */
|
|
237
250
|
readonly onSubmit: (action: ApprovalAction, comment?: string) => void;
|
|
238
251
|
/** True while the RPC for this tool call is in flight. */
|
|
@@ -245,13 +258,16 @@ export interface ApprovalCardBodyProps {
|
|
|
245
258
|
|
|
246
259
|
/**
|
|
247
260
|
* The decision panel of an approval: the optional message and gate reason, the
|
|
248
|
-
*
|
|
249
|
-
* the
|
|
250
|
-
*
|
|
251
|
-
*
|
|
261
|
+
* proposed change (the write/edit content from the row's `args` when the caller
|
|
262
|
+
* holds the row, the sanitized args preview otherwise), and the action buttons.
|
|
263
|
+
* Reused in two places — the standalone {@link ApprovalCard} (with
|
|
264
|
+
* {@link ApprovalCardHeader} above it) and inline inside a gated
|
|
265
|
+
* {@link ToolCallItem}'s expanded panel, where the item's own row is the header.
|
|
266
|
+
* One body, one preview rule in both.
|
|
252
267
|
*/
|
|
253
268
|
export function ApprovalCardBody({
|
|
254
269
|
pendingApproval,
|
|
270
|
+
args,
|
|
255
271
|
onSubmit,
|
|
256
272
|
isSubmitting = false,
|
|
257
273
|
error = null,
|
|
@@ -279,18 +295,17 @@ export function ApprovalCardBody({
|
|
|
279
295
|
pendingApproval.mcpServerSlug,
|
|
280
296
|
);
|
|
281
297
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}, [pendingApproval.argsPreview]);
|
|
298
|
+
// What the card shows: the preview's fields (the runner's sanitized summary),
|
|
299
|
+
// with the proposed write/edit content overlaid from the row's authoritative
|
|
300
|
+
// `args` when the caller holds the row — the one value the summary cannot
|
|
301
|
+
// carry for a file of any real size, and the one the user is asked to approve.
|
|
302
|
+
// A preview that does not parse (a record persisted under an older runner's
|
|
303
|
+
// whole-string truncation) stays `null`: nothing is overlaid onto a summary
|
|
304
|
+
// the card cannot read, and the notice below says so honestly.
|
|
305
|
+
const displayArgs = useMemo<Record<string, unknown> | null>(() => {
|
|
306
|
+
const previewArgs = parseArgsPreview(pendingApproval.argsPreview);
|
|
307
|
+
return previewArgs ? withAuthoritativeWriteContent(previewArgs, args) : null;
|
|
308
|
+
}, [pendingApproval.argsPreview, args]);
|
|
294
309
|
|
|
295
310
|
// APPROVE_ALL now grants a run-lifetime lease scoped to the clicked tool's
|
|
296
311
|
// CLASS — its MCP server for an MCP tool, else its approval category (where
|
|
@@ -327,11 +342,11 @@ export function ApprovalCardBody({
|
|
|
327
342
|
const isWriteEdit =
|
|
328
343
|
categoryInfo.category === "write" || categoryInfo.category === "edit";
|
|
329
344
|
|
|
330
|
-
// The proposed write/edit content
|
|
331
|
-
//
|
|
345
|
+
// The proposed write/edit content, when the gate carries no authoritative
|
|
346
|
+
// file_changes capture. Drives the three-way fallback below.
|
|
332
347
|
const writeContent = useMemo(
|
|
333
|
-
() =>
|
|
334
|
-
[
|
|
348
|
+
() => (displayArgs ? extractWriteContentFromArgs(displayArgs) : null),
|
|
349
|
+
[displayArgs],
|
|
335
350
|
);
|
|
336
351
|
|
|
337
352
|
// The decision-relevant preview. The pre-execution deny-gate is an
|
|
@@ -353,11 +368,11 @@ export function ApprovalCardBody({
|
|
|
353
368
|
const kind =
|
|
354
369
|
pendingApproval.toolKind === ToolKind.FILE_WRITE ? "create" : "no-preview";
|
|
355
370
|
preview = <EmptyChangeNotice kind={kind} />;
|
|
356
|
-
} else if (
|
|
371
|
+
} else if (displayArgs) {
|
|
357
372
|
preview = (
|
|
358
373
|
<ToolArgsView
|
|
359
374
|
toolName={pendingApproval.toolName}
|
|
360
|
-
args={
|
|
375
|
+
args={displayArgs}
|
|
361
376
|
mcpServerSlug={pendingApproval.mcpServerSlug}
|
|
362
377
|
showFileName={!isWriteEdit}
|
|
363
378
|
/>
|
|
@@ -4,7 +4,7 @@ import { useMemo } from "react";
|
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
5
|
import {
|
|
6
6
|
resolveToolCategory,
|
|
7
|
-
|
|
7
|
+
extractWriteContentFromArgs,
|
|
8
8
|
} from "./tool-categories.js";
|
|
9
9
|
import type { ToolCategory, ToolCategoryInfo } from "./tool-categories.js";
|
|
10
10
|
import { FilePathLink } from "./FilePathLink.js";
|
|
@@ -239,25 +239,6 @@ function GenericArgsView({ args }: { args: Record<string, unknown> }) {
|
|
|
239
239
|
// Utilities
|
|
240
240
|
// ---------------------------------------------------------------------------
|
|
241
241
|
|
|
242
|
-
const WRITE_CONTENT_FIELDS = [
|
|
243
|
-
"contents",
|
|
244
|
-
"content",
|
|
245
|
-
"file_content",
|
|
246
|
-
"new_text",
|
|
247
|
-
"new_string",
|
|
248
|
-
"replacement",
|
|
249
|
-
] as const;
|
|
250
|
-
|
|
251
|
-
function extractWriteContentFromArgs(
|
|
252
|
-
args: Record<string, unknown>,
|
|
253
|
-
): string | null {
|
|
254
|
-
for (const field of WRITE_CONTENT_FIELDS) {
|
|
255
|
-
const val = args[field];
|
|
256
|
-
if (typeof val === "string" && val.length > 0) return val;
|
|
257
|
-
}
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
242
|
function extractPrimaryArgValue(
|
|
262
243
|
args: Record<string, unknown> | null,
|
|
263
244
|
info: ToolCategoryInfo,
|
|
@@ -360,8 +360,12 @@ export const ToolCallItem = memo(function ToolCallItem({
|
|
|
360
360
|
// The row above is this approval's header, so render only the body (preview
|
|
361
361
|
// + actions). The enclosing card already carries the border + warning/
|
|
362
362
|
// destructive accent for a pending gate, so the body renders borderless.
|
|
363
|
+
// This row holds the call's authoritative `args`; the body reads the
|
|
364
|
+
// proposed write content from them, which the approval's sanitized preview
|
|
365
|
+
// cannot carry for a file of any real size (stigmer#1107).
|
|
363
366
|
<ApprovalCardBody
|
|
364
367
|
pendingApproval={approval.pendingApproval}
|
|
368
|
+
args={toolCall.args}
|
|
365
369
|
onSubmit={approval.onSubmit}
|
|
366
370
|
isSubmitting={approval.isSubmitting}
|
|
367
371
|
error={approval.error}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
ApprovalPolicySource,
|
|
8
8
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
9
9
|
import { ToolKind } from "@stigmer/sdk";
|
|
10
|
-
import { ApprovalCard } from "../ApprovalCard";
|
|
10
|
+
import { ApprovalCard, ApprovalCardBody } from "../ApprovalCard";
|
|
11
11
|
|
|
12
12
|
// Under apply-then-review the deny-gate carries no captured `file_changes`
|
|
13
13
|
// (message.proto field 14 was removed in Phase 5 Slice 4); the gate renders the
|
|
@@ -451,6 +451,115 @@ describe("ApprovalCard file-change preview", () => {
|
|
|
451
451
|
});
|
|
452
452
|
});
|
|
453
453
|
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// The proposed content comes from the row's authoritative args (stigmer#1107)
|
|
456
|
+
//
|
|
457
|
+
// `argsPreview` is the runner's sanitized summary: a value over its per-field
|
|
458
|
+
// cap is not carried whole (older runners wrote an in-band `[N chars]` marker;
|
|
459
|
+
// current ones leave the field out). The body therefore reads the proposed
|
|
460
|
+
// write/edit content from the row's `args` when the caller holds the row, and
|
|
461
|
+
// only that — every other field still renders from the redacted preview.
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
describe("ApprovalCardBody proposed content from args", () => {
|
|
465
|
+
const BIG = Array.from({ length: 30 }, (_, i) => `gate line ${i + 1}`).join("\n") + "\n";
|
|
466
|
+
|
|
467
|
+
function bigWriteApproval(argsPreview: string) {
|
|
468
|
+
return create(PendingApprovalSchema, {
|
|
469
|
+
toolCallId: "tc-big",
|
|
470
|
+
toolName: "write_file",
|
|
471
|
+
toolKind: ToolKind.FILE_WRITE,
|
|
472
|
+
argsPreview,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
it("renders the whole content from args when the preview carried an elision marker (older runner)", () => {
|
|
477
|
+
render(
|
|
478
|
+
<ApprovalCardBody
|
|
479
|
+
pendingApproval={bigWriteApproval('{"file_path":"/tmp/big.txt","content":"[381 chars]"}')}
|
|
480
|
+
args={{ file_path: "/tmp/big.txt", content: BIG }}
|
|
481
|
+
onSubmit={noop}
|
|
482
|
+
/>,
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
expect(screen.getByText("Content")).toBeTruthy();
|
|
486
|
+
// The head renders, bounded by lines, with the in-place reveal; the marker
|
|
487
|
+
// is never shown as if it were the file.
|
|
488
|
+
expect(screen.getByText(/gate line 1\b/)).toBeTruthy();
|
|
489
|
+
expect(screen.queryByText(/gate line 30/)).toBeNull();
|
|
490
|
+
expect(screen.getByRole("button", { name: /Show all \d+ lines/ })).toBeTruthy();
|
|
491
|
+
expect(screen.queryByText(/\[381 chars\]/)).toBeNull();
|
|
492
|
+
expect(screen.queryByText("New file — preview unavailable")).toBeNull();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("renders the whole content from args when the preview left the field out (current runner)", () => {
|
|
496
|
+
render(
|
|
497
|
+
<ApprovalCardBody
|
|
498
|
+
pendingApproval={bigWriteApproval('{"file_path":"/tmp/big.txt"}')}
|
|
499
|
+
args={{ file_path: "/tmp/big.txt", content: BIG }}
|
|
500
|
+
onSubmit={noop}
|
|
501
|
+
/>,
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
expect(screen.getByText("Content")).toBeTruthy();
|
|
505
|
+
expect(screen.getByText(/gate line 1\b/)).toBeTruthy();
|
|
506
|
+
expect(screen.queryByText("New file — preview unavailable")).toBeNull();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("without the row, a write whose content the preview left out shows the honest notice", () => {
|
|
510
|
+
// The row-less surfaces (the workflow approval list, an embedded card):
|
|
511
|
+
// nothing is invented from a preview that carries no content.
|
|
512
|
+
render(
|
|
513
|
+
<ApprovalCardBody
|
|
514
|
+
pendingApproval={bigWriteApproval('{"file_path":"/tmp/big.txt"}')}
|
|
515
|
+
onSubmit={noop}
|
|
516
|
+
/>,
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
expect(screen.getByText("New file — preview unavailable")).toBeTruthy();
|
|
520
|
+
expect(screen.queryByText("Content")).toBeNull();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("overlays the content only — a field the preview redacted stays redacted when args carry it raw", () => {
|
|
524
|
+
// An MCP gate renders every scalar argument, so this is where a raw value
|
|
525
|
+
// crossing over from `args` would show. It must not.
|
|
526
|
+
const approval = create(PendingApprovalSchema, {
|
|
527
|
+
toolCallId: "tc-mcp-secret",
|
|
528
|
+
toolName: "publish",
|
|
529
|
+
toolKind: ToolKind.MCP,
|
|
530
|
+
mcpServerSlug: "notes",
|
|
531
|
+
argsPreview: '{"title":"hello","token":"[REDACTED]"}',
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
render(
|
|
535
|
+
<ApprovalCardBody
|
|
536
|
+
pendingApproval={approval}
|
|
537
|
+
args={{ title: "hello", token: "s3cr3t-raw" }}
|
|
538
|
+
onSubmit={noop}
|
|
539
|
+
/>,
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
expect(screen.getByText("[REDACTED]")).toBeTruthy();
|
|
543
|
+
expect(screen.queryByText(/s3cr3t-raw/)).toBeNull();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("does not overlay onto a preview the card cannot parse (a record from an older runner's truncation)", () => {
|
|
547
|
+
// Pre-unification native previews truncated the whole JSON string at 500
|
|
548
|
+
// chars; such a record's preview does not parse. The body keeps today's
|
|
549
|
+
// honest notice rather than rendering a content block with no path row.
|
|
550
|
+
render(
|
|
551
|
+
<ApprovalCardBody
|
|
552
|
+
pendingApproval={bigWriteApproval('{"file_path":"/tmp/big.txt","content":"gate li…')}
|
|
553
|
+
args={{ file_path: "/tmp/big.txt", content: BIG }}
|
|
554
|
+
onSubmit={noop}
|
|
555
|
+
/>,
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
expect(screen.getByText("New file — preview unavailable")).toBeTruthy();
|
|
559
|
+
expect(screen.queryByText("Content")).toBeNull();
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
454
563
|
describe("ApprovalCard in-card decision error", () => {
|
|
455
564
|
function makeApproval() {
|
|
456
565
|
return create(PendingApprovalSchema, {
|
|
@@ -494,6 +494,44 @@ describe("ToolCallItem disclosure", () => {
|
|
|
494
494
|
expect(row.className).not.toContain("stg:bg-warning");
|
|
495
495
|
});
|
|
496
496
|
|
|
497
|
+
it("shows a gated write's whole proposed content from the row's args, never the preview's elision marker (stigmer#1107)", () => {
|
|
498
|
+
// The row holds the authoritative `args`; the approval's `argsPreview` is
|
|
499
|
+
// the runner's sanitized summary and cannot carry a file of any real size
|
|
500
|
+
// (an older runner wrote `[N chars]`; the current one leaves the field
|
|
501
|
+
// out). The inline gate must read the content from the row.
|
|
502
|
+
const big = Array.from({ length: 30 }, (_, i) => `gate line ${i + 1}`).join("\n") + "\n";
|
|
503
|
+
const tc = makeToolCall({
|
|
504
|
+
id: "tc-big-write",
|
|
505
|
+
name: "write_file",
|
|
506
|
+
args: { file_path: "/tmp/e2e-big-gate.txt", content: big },
|
|
507
|
+
status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
|
|
508
|
+
});
|
|
509
|
+
const approval: PendingApproval = create(PendingApprovalSchema, {
|
|
510
|
+
toolCallId: "tc-big-write",
|
|
511
|
+
toolName: "write_file",
|
|
512
|
+
argsPreview: '{"file_path":"/tmp/e2e-big-gate.txt","content":"[381 chars]"}',
|
|
513
|
+
});
|
|
514
|
+
const ctx: ApprovalContextValue = {
|
|
515
|
+
approvalsByToolCallId: new Map([["tc-big-write", approval]]),
|
|
516
|
+
onSubmit: () => {},
|
|
517
|
+
submittingIds: new Set(),
|
|
518
|
+
errorsByToolCallId: new Map(),
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const { container } = render(
|
|
522
|
+
<ApprovalContext.Provider value={ctx}>
|
|
523
|
+
<ToolCallItem toolCall={tc} />
|
|
524
|
+
</ApprovalContext.Provider>,
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const row = container.querySelector('[data-cursor-target="tool-call-row"]')!;
|
|
528
|
+
expect(row.textContent).toContain("gate line 1");
|
|
529
|
+
expect(row.textContent).not.toContain("gate line 30"); // bounded by lines
|
|
530
|
+
expect(row.textContent).not.toContain("[381 chars]");
|
|
531
|
+
expect(screen.getByRole("button", { name: /Show all \d+ lines/ })).toBeTruthy();
|
|
532
|
+
expect(screen.getByLabelText("Approve")).toBeTruthy();
|
|
533
|
+
});
|
|
534
|
+
|
|
497
535
|
it("routes an inline APPROVE_ALL decision with the gated tool's id", () => {
|
|
498
536
|
const tc = makeToolCall({
|
|
499
537
|
id: "tc-gated",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one write-content rule for the gate and the settled row (stigmer#1107):
|
|
3
|
+
* which argument field carries a write's proposed content, and how the gate
|
|
4
|
+
* overlays that content from the row's authoritative `args` onto the
|
|
5
|
+
* sanitized preview without letting any other field cross over.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
extractWriteContentFromArgs,
|
|
10
|
+
parseArgsPreview,
|
|
11
|
+
withAuthoritativeWriteContent,
|
|
12
|
+
} from "../tool-categories";
|
|
13
|
+
|
|
14
|
+
describe("extractWriteContentFromArgs", () => {
|
|
15
|
+
it("returns the first content field holding a non-empty string, in list order", () => {
|
|
16
|
+
expect(extractWriteContentFromArgs({ contents: "a", content: "b" })).toBe("a");
|
|
17
|
+
expect(extractWriteContentFromArgs({ content: "", new_string: "b" })).toBe("b");
|
|
18
|
+
expect(extractWriteContentFromArgs({ new_text: "cursor", replacement: "x" })).toBe("cursor");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns null when no content field holds a string", () => {
|
|
22
|
+
expect(extractWriteContentFromArgs({ file_path: "/x" })).toBeNull();
|
|
23
|
+
expect(extractWriteContentFromArgs({ content: 42 })).toBeNull();
|
|
24
|
+
expect(extractWriteContentFromArgs({})).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("parseArgsPreview", () => {
|
|
29
|
+
it("parses an object preview and rejects everything else", () => {
|
|
30
|
+
expect(parseArgsPreview('{"path":"/x"}')).toEqual({ path: "/x" });
|
|
31
|
+
expect(parseArgsPreview("")).toBeNull();
|
|
32
|
+
expect(parseArgsPreview('"a string"')).toBeNull();
|
|
33
|
+
expect(parseArgsPreview("[1,2]")).toBeNull();
|
|
34
|
+
expect(parseArgsPreview('{"path":"/x","content":"trunc…')).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("withAuthoritativeWriteContent", () => {
|
|
39
|
+
const preview = { file_path: "/x", token: "[REDACTED]", content: "[381 chars]" };
|
|
40
|
+
|
|
41
|
+
it("replaces an elided content value under the field args carry it in", () => {
|
|
42
|
+
const out = withAuthoritativeWriteContent(preview, {
|
|
43
|
+
file_path: "/x",
|
|
44
|
+
token: "raw-secret",
|
|
45
|
+
content: "the whole file\n",
|
|
46
|
+
});
|
|
47
|
+
expect(out.content).toBe("the whole file\n");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("adds a content field the preview left out", () => {
|
|
51
|
+
const out = withAuthoritativeWriteContent(
|
|
52
|
+
{ file_path: "/x" },
|
|
53
|
+
{ file_path: "/x", new_string: "after" },
|
|
54
|
+
);
|
|
55
|
+
expect(out).toEqual({ file_path: "/x", new_string: "after" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("copies nothing but the content: a redacted field stays redacted", () => {
|
|
59
|
+
const out = withAuthoritativeWriteContent(preview, {
|
|
60
|
+
file_path: "/y",
|
|
61
|
+
token: "raw-secret",
|
|
62
|
+
content: "c",
|
|
63
|
+
extra: "never shown",
|
|
64
|
+
});
|
|
65
|
+
expect(out.token).toBe("[REDACTED]");
|
|
66
|
+
expect(out.file_path).toBe("/x");
|
|
67
|
+
expect("extra" in out).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns the preview object itself when args carry no content, or are absent", () => {
|
|
71
|
+
expect(withAuthoritativeWriteContent(preview, { file_path: "/x" })).toBe(preview);
|
|
72
|
+
expect(withAuthoritativeWriteContent(preview, undefined)).toBe(preview);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("never mutates its inputs", () => {
|
|
76
|
+
const args = { file_path: "/x", content: "c" };
|
|
77
|
+
const out = withAuthoritativeWriteContent(preview, args);
|
|
78
|
+
expect(out).not.toBe(preview);
|
|
79
|
+
expect(preview.content).toBe("[381 chars]");
|
|
80
|
+
expect(args.content).toBe("c");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -545,6 +545,14 @@ export function extractShellIntentFromPreview(
|
|
|
545
545
|
}
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
+
/**
|
|
549
|
+
* The argument fields under which a write or edit tool carries the content it
|
|
550
|
+
* proposes, in the order they are tried. One list for every reader — the
|
|
551
|
+
* pre-execution gate ({@link ApprovalCardBody}) and the post-execution row
|
|
552
|
+
* ({@link ToolArgsView}) — so the two never disagree on what "the content" is.
|
|
553
|
+
* Spans both harnesses' file-tool shapes (deepagents' `content`/`new_string`,
|
|
554
|
+
* Cursor's `contents`/`new_text`).
|
|
555
|
+
*/
|
|
548
556
|
const WRITE_CONTENT_FIELDS = [
|
|
549
557
|
"contents",
|
|
550
558
|
"content",
|
|
@@ -554,29 +562,81 @@ const WRITE_CONTENT_FIELDS = [
|
|
|
554
562
|
"replacement",
|
|
555
563
|
] as const;
|
|
556
564
|
|
|
565
|
+
/** A proposed write's content and the argument field it was found under. */
|
|
566
|
+
interface WriteContent {
|
|
567
|
+
readonly field: (typeof WRITE_CONTENT_FIELDS)[number];
|
|
568
|
+
readonly value: string;
|
|
569
|
+
}
|
|
570
|
+
|
|
557
571
|
/**
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
* the approval preview matches the completed tool call display.
|
|
562
|
-
*
|
|
563
|
-
* Returns `null` if parsing fails or no content field is found.
|
|
572
|
+
* Finds the content a write/edit tool proposes in its parsed arguments: the
|
|
573
|
+
* first {@link WRITE_CONTENT_FIELDS} entry holding a non-empty string, with the
|
|
574
|
+
* field it sat under. `null` when the args carry no content.
|
|
564
575
|
*/
|
|
565
|
-
|
|
566
|
-
|
|
576
|
+
function findWriteContent(
|
|
577
|
+
args: Record<string, unknown>,
|
|
578
|
+
): WriteContent | null {
|
|
579
|
+
for (const field of WRITE_CONTENT_FIELDS) {
|
|
580
|
+
const value = args[field];
|
|
581
|
+
if (typeof value === "string" && value.length > 0) return { field, value };
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* The content a write/edit tool proposes, from its parsed arguments, or `null`.
|
|
588
|
+
* The value half of {@link findWriteContent}.
|
|
589
|
+
*/
|
|
590
|
+
export function extractWriteContentFromArgs(
|
|
591
|
+
args: Record<string, unknown>,
|
|
567
592
|
): string | null {
|
|
568
|
-
|
|
593
|
+
return findWriteContent(args)?.value ?? null;
|
|
594
|
+
}
|
|
569
595
|
|
|
596
|
+
/**
|
|
597
|
+
* Parses a JSON `argsPreview` string into an args object, or `null` when the
|
|
598
|
+
* string is empty, unparseable, or not an object. The preview is the runner's
|
|
599
|
+
* sanitized summary of a tool call's arguments — secrets redacted, a value too
|
|
600
|
+
* large to carry whole left out, paths and commands kept verbatim — so what
|
|
601
|
+
* this returns is what a surface may SHOW; the proposed content of a write is
|
|
602
|
+
* read from the row's `args` (see {@link withAuthoritativeWriteContent}).
|
|
603
|
+
*/
|
|
604
|
+
export function parseArgsPreview(
|
|
605
|
+
argsPreview: string,
|
|
606
|
+
): Record<string, unknown> | null {
|
|
607
|
+
if (!argsPreview) return null;
|
|
570
608
|
try {
|
|
571
|
-
const parsed = JSON.parse(argsPreview);
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const val = (parsed as Record<string, unknown>)[field];
|
|
576
|
-
if (typeof val === "string" && val.length > 0) return val;
|
|
577
|
-
}
|
|
578
|
-
return null;
|
|
609
|
+
const parsed: unknown = JSON.parse(argsPreview);
|
|
610
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
611
|
+
? (parsed as Record<string, unknown>)
|
|
612
|
+
: null;
|
|
579
613
|
} catch {
|
|
580
614
|
return null;
|
|
581
615
|
}
|
|
582
616
|
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* The preview-parsed args of a gated tool call with the proposed write content
|
|
620
|
+
* taken from the row's authoritative `args` — and only that.
|
|
621
|
+
*
|
|
622
|
+
* A tool row carries two projections of its arguments: `args`, the full
|
|
623
|
+
* arguments, and `args_preview`, the runner's sanitized summary (secrets
|
|
624
|
+
* redacted, oversized values left out, paths and commands kept whole). The
|
|
625
|
+
* summary is the right thing to *show* for every field but one: the content a
|
|
626
|
+
* write or edit proposes, which is exactly the value the summary cannot carry
|
|
627
|
+
* and exactly what the user is asked to approve. So the gate renders the
|
|
628
|
+
* preview's fields and overlays the content from `args`, under the field
|
|
629
|
+
* `args` carries it in. Nothing else crosses over — a field the runner
|
|
630
|
+
* redacted in the preview stays redacted here.
|
|
631
|
+
*
|
|
632
|
+
* Returns `previewArgs` itself when `args` carries no content (nothing to
|
|
633
|
+
* overlay) so memoized consumers keep their reference.
|
|
634
|
+
*/
|
|
635
|
+
export function withAuthoritativeWriteContent(
|
|
636
|
+
previewArgs: Record<string, unknown>,
|
|
637
|
+
args: Record<string, unknown> | undefined,
|
|
638
|
+
): Record<string, unknown> {
|
|
639
|
+
const content = args ? findWriteContent(args) : null;
|
|
640
|
+
if (!content) return previewArgs;
|
|
641
|
+
return { ...previewArgs, [content.field]: content.value };
|
|
642
|
+
}
|
|
@@ -54,7 +54,9 @@ export interface OrgSwitcherProps {
|
|
|
54
54
|
*
|
|
55
55
|
* Designed for sidebar placement — the trigger uses `sidebar-*` design
|
|
56
56
|
* tokens. The portaled dropdown and dialog use standard `popover-*` /
|
|
57
|
-
* main-area tokens
|
|
57
|
+
* main-area tokens, because portaled content renders outside the sidebar's
|
|
58
|
+
* DOM tree (the theme-token reference in the repository's
|
|
59
|
+
* sdk-console-architecture skill).
|
|
58
60
|
*
|
|
59
61
|
* Must be rendered inside an {@link OrgProvider}.
|
|
60
62
|
*
|
|
@@ -34,10 +34,11 @@ export interface UseCanCreateAgentShareReturn {
|
|
|
34
34
|
* resource id — an Organization's id equals its slug
|
|
35
35
|
* (ApiResourceMetadata.id).
|
|
36
36
|
*
|
|
37
|
-
* On the open-source edition
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* On the open-source edition the answer follows the server's posture. A
|
|
38
|
+
* server without sign-in has the permissive single-team authorizer and
|
|
39
|
+
* answers allowed (decision 011 D4). A server with sign-in enforces the
|
|
40
|
+
* same organization roles as the cloud, so `can_create_agent_share` is
|
|
41
|
+
* the admin's and a member sees no share affordance.
|
|
41
42
|
*
|
|
42
43
|
* Pass `null` for `agent` while it loads — `allowed` stays `false` so
|
|
43
44
|
* no affordance flashes before the gate can be evaluated.
|
package/src/user/UserMenu.tsx
CHANGED
|
@@ -90,7 +90,9 @@ export interface UserMenuProps {
|
|
|
90
90
|
*
|
|
91
91
|
* Designed for sidebar placement — the trigger uses `sidebar-*` design
|
|
92
92
|
* tokens. The portaled dropdown uses standard `popover-*` / main-area
|
|
93
|
-
* tokens
|
|
93
|
+
* tokens, because portaled content renders outside the sidebar's DOM tree
|
|
94
|
+
* (the theme-token reference in the repository's sdk-console-architecture
|
|
95
|
+
* skill).
|
|
94
96
|
*
|
|
95
97
|
* Framework-agnostic: all actions are expressed as callback props
|
|
96
98
|
* (DD-004). The consumer bridges to their routing, auth, and theme
|