@stigmer/react 3.2.2 → 3.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/agent/AgentDetailView.d.ts.map +1 -1
  2. package/agent/AgentDetailView.js +6 -3
  3. package/agent/AgentDetailView.js.map +1 -1
  4. package/execution/index.d.ts +1 -2
  5. package/execution/index.d.ts.map +1 -1
  6. package/execution/index.js +0 -1
  7. package/execution/index.js.map +1 -1
  8. package/execution/useLiveAgentExecution.d.ts +17 -1
  9. package/execution/useLiveAgentExecution.d.ts.map +1 -1
  10. package/execution/useLiveAgentExecution.js +19 -4
  11. package/execution/useLiveAgentExecution.js.map +1 -1
  12. package/index.d.ts +5 -5
  13. package/index.d.ts.map +1 -1
  14. package/index.js +2 -2
  15. package/index.js.map +1 -1
  16. package/internal/useInViewport.d.ts +24 -0
  17. package/internal/useInViewport.d.ts.map +1 -0
  18. package/internal/useInViewport.js +49 -0
  19. package/internal/useInViewport.js.map +1 -0
  20. package/library/VisibilitySelector.d.ts +22 -6
  21. package/library/VisibilitySelector.d.ts.map +1 -1
  22. package/library/VisibilitySelector.js +12 -4
  23. package/library/VisibilitySelector.js.map +1 -1
  24. package/library/index.d.ts +1 -1
  25. package/library/index.d.ts.map +1 -1
  26. package/library/index.js.map +1 -1
  27. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  28. package/mcp-server/McpServerDetailView.js +4 -3
  29. package/mcp-server/McpServerDetailView.js.map +1 -1
  30. package/package.json +4 -4
  31. package/session/SessionViewer.js +8 -1
  32. package/session/SessionViewer.js.map +1 -1
  33. package/session/audience.d.ts +6 -1
  34. package/session/audience.d.ts.map +1 -1
  35. package/skill/SkillDetailView.d.ts.map +1 -1
  36. package/skill/SkillDetailView.js +4 -3
  37. package/skill/SkillDetailView.js.map +1 -1
  38. package/src/access/__tests__/useManageAccess.test.tsx +104 -0
  39. package/src/agent/AgentDetailView.tsx +9 -3
  40. package/src/execution/__tests__/useLiveAgentExecution.test.tsx +125 -0
  41. package/src/execution/index.ts +4 -7
  42. package/src/execution/useLiveAgentExecution.ts +41 -3
  43. package/src/index.ts +4 -5
  44. package/src/internal/__tests__/useInViewport.test.tsx +115 -0
  45. package/src/internal/useInViewport.ts +63 -0
  46. package/src/library/VisibilitySelector.tsx +50 -9
  47. package/src/library/__tests__/VisibilitySelector.test.tsx +38 -0
  48. package/src/library/index.ts +4 -1
  49. package/src/mcp-server/McpServerDetailView.tsx +7 -3
  50. package/src/session/SessionViewer.tsx +7 -1
  51. package/src/session/__tests__/audienceWiring.test.tsx +31 -1
  52. package/src/session/audience.ts +6 -1
  53. package/src/skill/SkillDetailView.tsx +7 -3
  54. package/src/workflow/WorkflowAgentCallTranscript.tsx +279 -0
  55. package/src/workflow/WorkflowDetailView.tsx +7 -3
  56. package/src/workflow/WorkflowExecutionViewer.tsx +20 -77
  57. package/src/workflow/WorkflowTaskApprovalSummary.tsx +45 -8
  58. package/src/workflow/__tests__/WorkflowAgentCallTranscript.liveregion.test.tsx +124 -0
  59. package/src/workflow/__tests__/{WorkflowAgentExecutionDocument.test.tsx → WorkflowAgentCallTranscript.test.tsx} +146 -121
  60. package/src/workflow/__tests__/WorkflowExecutionViewer.approvals.test.tsx +81 -220
  61. package/src/workflow/__tests__/WorkflowExecutionViewer.reconciliation.test.tsx +17 -16
  62. package/src/workflow/__tests__/WorkflowTaskApprovalSummary.test.tsx +52 -2
  63. package/src/workflow/__tests__/WorkflowTaskThread.test.tsx +168 -185
  64. package/src/workflow/__tests__/agent-call-live-experience.test.ts +4 -4
  65. package/src/workflow/__tests__/task-approval.test.ts +119 -0
  66. package/src/workflow/__tests__/task-presentation.test.ts +35 -1
  67. package/src/workflow/__tests__/useWorkflowExecutionPanel.test.ts +0 -57
  68. package/src/workflow/index.ts +6 -3
  69. package/src/workflow/task-detail/index.ts +3 -0
  70. package/src/workflow/task-detail/task-approval.ts +95 -1
  71. package/src/workflow/thread/WorkflowTaskThread.tsx +96 -252
  72. package/src/workflow/thread/task-presentation.ts +8 -2
  73. package/src/workflow/useWorkflowExecutionPanel.ts +0 -47
  74. package/workflow/WorkflowAgentCallTranscript.d.ts +87 -0
  75. package/workflow/WorkflowAgentCallTranscript.d.ts.map +1 -0
  76. package/workflow/WorkflowAgentCallTranscript.js +98 -0
  77. package/workflow/WorkflowAgentCallTranscript.js.map +1 -0
  78. package/workflow/WorkflowDetailView.d.ts.map +1 -1
  79. package/workflow/WorkflowDetailView.js +4 -3
  80. package/workflow/WorkflowDetailView.js.map +1 -1
  81. package/workflow/WorkflowExecutionViewer.d.ts.map +1 -1
  82. package/workflow/WorkflowExecutionViewer.js +23 -52
  83. package/workflow/WorkflowExecutionViewer.js.map +1 -1
  84. package/workflow/WorkflowTaskApprovalSummary.d.ts +1 -1
  85. package/workflow/WorkflowTaskApprovalSummary.d.ts.map +1 -1
  86. package/workflow/WorkflowTaskApprovalSummary.js +18 -1
  87. package/workflow/WorkflowTaskApprovalSummary.js.map +1 -1
  88. package/workflow/index.d.ts +2 -2
  89. package/workflow/index.d.ts.map +1 -1
  90. package/workflow/index.js +2 -2
  91. package/workflow/index.js.map +1 -1
  92. package/workflow/task-detail/index.d.ts +1 -1
  93. package/workflow/task-detail/index.d.ts.map +1 -1
  94. package/workflow/task-detail/index.js +1 -1
  95. package/workflow/task-detail/index.js.map +1 -1
  96. package/workflow/task-detail/task-approval.d.ts +44 -1
  97. package/workflow/task-detail/task-approval.d.ts.map +1 -1
  98. package/workflow/task-detail/task-approval.js +47 -0
  99. package/workflow/task-detail/task-approval.js.map +1 -1
  100. package/workflow/thread/WorkflowTaskThread.d.ts +17 -25
  101. package/workflow/thread/WorkflowTaskThread.d.ts.map +1 -1
  102. package/workflow/thread/WorkflowTaskThread.js +60 -105
  103. package/workflow/thread/WorkflowTaskThread.js.map +1 -1
  104. package/workflow/thread/task-presentation.d.ts.map +1 -1
  105. package/workflow/thread/task-presentation.js +7 -2
  106. package/workflow/thread/task-presentation.js.map +1 -1
  107. package/workflow/useWorkflowExecutionPanel.d.ts +0 -12
  108. package/workflow/useWorkflowExecutionPanel.d.ts.map +1 -1
  109. package/workflow/useWorkflowExecutionPanel.js +0 -12
  110. package/workflow/useWorkflowExecutionPanel.js.map +1 -1
  111. package/execution/agent-execution-document.d.ts +0 -31
  112. package/execution/agent-execution-document.d.ts.map +0 -1
  113. package/execution/agent-execution-document.js +0 -48
  114. package/execution/agent-execution-document.js.map +0 -1
  115. package/src/execution/__tests__/agent-execution-document.test.ts +0 -45
  116. package/src/execution/agent-execution-document.ts +0 -54
  117. package/src/workflow/WorkflowAgentExecutionDocument.tsx +0 -283
  118. package/workflow/WorkflowAgentExecutionDocument.d.ts +0 -79
  119. package/workflow/WorkflowAgentExecutionDocument.d.ts.map +0 -1
  120. package/workflow/WorkflowAgentExecutionDocument.js +0 -87
  121. package/workflow/WorkflowAgentExecutionDocument.js.map +0 -1
@@ -1,10 +1,11 @@
1
- // Composition test for the execution-level HITL surfaces (S10/T06): gating
2
- // task cards carry their child gates inline in the default Thread view
3
- // (through the shared session ApprovalCard via WorkflowApprovalList) since
4
- // T06 the card is the ONLY decision surface (the Inspect drill-down is gone)
5
- // and every decision routes through the single useWorkflowExecutionActions
6
- // instance. Data hooks and heavy leaves (React Flow graph) are mocked
7
- // exactly as in the reconciliation harness; this file proves the seams.
1
+ // Composition test for the execution-level HITL surfaces (S10/T06/T07):
2
+ // gating AGENT_CALL cards carry the child's INLINE TRANSCRIPT as their
3
+ // decision surface — the child's gates decide inside it (the transcript's
4
+ // own suite covers the ApprovalCard rendering and workflow-RPC routing)
5
+ // and the viewer's single useWorkflowExecutionActions instance is what
6
+ // reaches every card. Data hooks and heavy leaves (React Flow graph, the
7
+ // streaming transcript) are mocked exactly as in the reconciliation
8
+ // harness; this file proves the seams.
8
9
  //
9
10
  // REALISTIC FIXTURES (a hard S10 requirement): the gated task states are
10
11
  // produced by the REAL store derivation from REAL event shapes — an
@@ -12,20 +13,11 @@
12
13
  // (D-T02-14). S9's hand-crafted `waiting_approval` states masked that
13
14
  // production never emitted them for child gates; deriving through the store
14
15
  // keeps these tests honest about the production path.
15
- //
16
- // GUARDRAIL (S5 rationale): the entire render runs WITHOUT a StigmerProvider.
17
- // Any component reaching for a client hook (the child's agentExecution.*
18
- // submit path) would throw — so a passing render plus the workflow actions
19
- // spy receiving the decision proves gates route through the WORKFLOW-level
20
- // RPC only. (WorkflowFileReviewList streams its child itself and therefore
21
- // NEEDS the provider — it is stubbed at the module seam here; its own suite
22
- // covers the real child-derived rendering.)
23
16
 
24
17
  import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
25
- import { render, screen, fireEvent, cleanup, within } from "@testing-library/react";
18
+ import { render, screen, cleanup, within } from "@testing-library/react";
26
19
  import { create } from "@bufbuild/protobuf";
27
20
  import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
28
- import type { WorkflowPendingApproval, WorkflowPendingFileReview } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
29
21
  import {
30
22
  ExecutionPhase,
31
23
  WorkflowTaskStatus,
@@ -39,10 +31,7 @@ import {
39
31
  } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/event_pb";
40
32
  import type { WorkflowExecutionEvent } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/event_pb";
41
33
  import { WorkflowTaskKind } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
42
- import {
43
- ApprovalAction,
44
- ExecutionPhase as AgentExecutionPhase,
45
- } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
34
+ import { ExecutionPhase as AgentExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
46
35
  import type { DerivedCostSummary } from "../../internal/store/workflow-execution-event-store";
47
36
  import { WorkflowExecutionEventStore } from "../../internal/store/workflow-execution-event-store";
48
37
  import { useWorkflowExecution } from "../useWorkflowExecution";
@@ -50,7 +39,7 @@ import { useWorkflowExecutionEventStream } from "../useWorkflowExecutionEventStr
50
39
  import { useWorkflowExecutionArtifacts } from "../useWorkflowExecutionArtifacts";
51
40
  import { useWorkflowExecutionFileChanges } from "../useWorkflowExecutionFileChanges";
52
41
  import { useWorkflowExecutionActions } from "../useWorkflowExecutionActions";
53
- import type { WorkflowFileDecisionSubmit } from "../WorkflowFileReviewList";
42
+ import type { WorkflowAgentCallTranscriptProps } from "../WorkflowAgentCallTranscript";
54
43
  import { WorkflowExecutionViewer } from "../WorkflowExecutionViewer";
55
44
 
56
45
  vi.mock("../useWorkflowExecution", () => ({
@@ -76,43 +65,29 @@ vi.mock("../execution-comparison/ExecutionComparisonPicker", () => ({
76
65
  ExecutionComparisonPicker: () => null,
77
66
  }));
78
67
 
79
- // Streams its child (needs the provider this guardrail render deliberately
80
- // omits) stubbed to prove the seam: both the gating card and the Inspect
81
- // Approval tab hand it the task-scoped references and the workflow-level
82
- // decision submit.
83
- vi.mock("../WorkflowFileReviewList", () => ({
84
- WorkflowFileReviewList: ({
85
- pendingFileReviews,
86
- onSubmitFileDecision,
87
- }: {
88
- pendingFileReviews: readonly WorkflowPendingFileReview[];
89
- onSubmitFileDecision: WorkflowFileDecisionSubmit;
90
- }) => (
91
- <div data-testid="file-review-list-stub">
92
- {pendingFileReviews.map((ref) => (
93
- <button
94
- key={ref.childAgentExecutionId}
95
- type="button"
96
- onClick={() =>
97
- onSubmitFileDecision(
98
- ref.childAgentExecutionId,
99
- ref.changeSetId[0] ?? "",
100
- 1 as never,
101
- )
102
- }
103
- >
104
- decide-files-{ref.childAgentExecutionId}
105
- </button>
106
- ))}
107
- </div>
68
+ // Fetches and streams its child (its own suite covers that, including the
69
+ // ApprovalCard rendering and the workflow-RPC routing guardrail) stubbed
70
+ // to a probe recording the seam under test here: which child each card
71
+ // bound, and whether the viewer's hitl bundle reached it.
72
+ vi.mock("../WorkflowAgentCallTranscript", () => ({
73
+ WorkflowAgentCallTranscript: vi.fn(
74
+ ({ childExecutionId, hitl }: WorkflowAgentCallTranscriptProps) => (
75
+ <div
76
+ data-testid="transcript-probe"
77
+ data-child-id={childExecutionId}
78
+ data-interactive={hitl ? "true" : "false"}
79
+ />
80
+ ),
108
81
  ),
109
82
  }));
83
+ import { WorkflowAgentCallTranscript } from "../WorkflowAgentCallTranscript";
110
84
 
111
85
  const mockedUseWorkflowExecution = vi.mocked(useWorkflowExecution);
112
86
  const mockedUseEventStream = vi.mocked(useWorkflowExecutionEventStream);
113
87
  const mockedUseArtifacts = vi.mocked(useWorkflowExecutionArtifacts);
114
88
  const mockedUseFileChanges = vi.mocked(useWorkflowExecutionFileChanges);
115
89
  const mockedUseActions = vi.mocked(useWorkflowExecutionActions);
90
+ const mockedTranscript = vi.mocked(WorkflowAgentCallTranscript);
116
91
 
117
92
  const COST_SUMMARY: DerivedCostSummary = {
118
93
  costConsumedMicros: 0n,
@@ -192,16 +167,8 @@ function buildGatedStore(): WorkflowExecutionEventStore {
192
167
  return store;
193
168
  }
194
169
 
195
- /**
196
- * The parent snapshot both gated children surfaced onto (via the
197
- * call-agent orchestrator's UpdateWorkflowTaskApprovalStatus write). Task
198
- * snapshots carry `agent_execution_id` metadata — the inspector's
199
- * event-less path for resolving the AGENT_CALL child.
200
- */
201
- function makeExecutionWithGates(overrides?: {
202
- pendingApprovals?: unknown[];
203
- pendingFileReviews?: unknown[];
204
- }) {
170
+ /** The parent snapshot both gated children surfaced onto. */
171
+ function makeExecutionWithGates() {
205
172
  return create(WorkflowExecutionSchema, {
206
173
  metadata: { id: "wex_1", name: "nightly-report" },
207
174
  spec: { workflowId: "wf_1" },
@@ -222,52 +189,25 @@ function makeExecutionWithGates(overrides?: {
222
189
  metadata: { agent_execution_id: "agx_child_b" },
223
190
  },
224
191
  ],
225
- pendingApprovals: (overrides?.pendingApprovals ?? [
226
- {
227
- childAgentExecutionId: "agx_child_a",
228
- approval: {
229
- toolCallId: "tc_delete",
230
- toolName: "delete_file",
231
- argsPreview: '{"path":"/tmp/x"}',
232
- },
233
- },
234
- {
235
- childAgentExecutionId: "agx_child_b",
236
- approval: {
237
- toolCallId: "tc_shell",
238
- toolName: "shell",
239
- argsPreview: '{"command":"npm test"}',
240
- },
241
- },
242
- ]) as WorkflowPendingApproval[],
243
- pendingFileReviews: (overrides?.pendingFileReviews ??
244
- []) as WorkflowPendingFileReview[],
245
192
  },
246
193
  });
247
194
  }
248
195
 
249
- function mockActions(overrides?: {
250
- submitApproval?: ReturnType<typeof vi.fn>;
251
- submitFileDecision?: ReturnType<typeof vi.fn>;
252
- approvalSubmittingToolCallIds?: ReadonlySet<string>;
253
- approvalErrorsByToolCallId?: ReadonlyMap<string, Error>;
254
- }) {
196
+ function mockActions() {
255
197
  return {
256
198
  cancel: vi.fn(),
257
199
  terminate: vi.fn(),
258
200
  pause: vi.fn(),
259
201
  resume: vi.fn(),
260
202
  recover: vi.fn(),
261
- submitApproval: overrides?.submitApproval ?? vi.fn(),
203
+ submitApproval: vi.fn(),
262
204
  submitTaskApproval: vi.fn(),
263
- submitFileDecision: overrides?.submitFileDecision ?? vi.fn(),
205
+ submitFileDecision: vi.fn(),
264
206
  isSubmitting: false,
265
207
  error: null,
266
208
  clearError: vi.fn(),
267
- approvalSubmittingToolCallIds:
268
- overrides?.approvalSubmittingToolCallIds ?? new Set<string>(),
269
- approvalErrorsByToolCallId:
270
- overrides?.approvalErrorsByToolCallId ?? new Map<string, Error>(),
209
+ approvalSubmittingToolCallIds: new Set<string>(),
210
+ approvalErrorsByToolCallId: new Map<string, Error>(),
271
211
  taskApprovalSubmittingTaskNames: new Set<string>(),
272
212
  taskApprovalErrorsByTaskName: new Map<string, Error>(),
273
213
  fileDecisionSubmittingKeys: new Set<string>(),
@@ -275,13 +215,10 @@ function mockActions(overrides?: {
275
215
  } as unknown as ReturnType<typeof useWorkflowExecutionActions>;
276
216
  }
277
217
 
278
- function arrange(
279
- actions = mockActions(),
280
- executionOverrides?: Parameters<typeof makeExecutionWithGates>[0],
281
- ) {
218
+ function arrange(actions = mockActions()) {
282
219
  const store = buildGatedStore();
283
220
  mockedUseWorkflowExecution.mockReturnValue({
284
- execution: makeExecutionWithGates(executionOverrides),
221
+ execution: makeExecutionWithGates(),
285
222
  isLoading: false,
286
223
  error: null,
287
224
  refetch: vi.fn(),
@@ -325,149 +262,73 @@ function cardRootOf(taskName: string): HTMLElement {
325
262
  return root;
326
263
  }
327
264
 
328
- describe("WorkflowExecutionViewer in-thread HITL (S10/T06)", () => {
265
+ describe("WorkflowExecutionViewer in-thread HITL (S10/T06/T07)", () => {
329
266
  beforeEach(() => {
330
267
  vi.clearAllMocks();
331
268
  localStorage.clear();
332
269
  });
333
270
  afterEach(cleanup);
334
271
 
335
- it("each gating card carries its own child's 4-action gate inline; the boundary never opens the panel (T06)", () => {
272
+ it("each gating card carries its own child's INTERACTIVE transcript inline; the boundary never opens the panel", () => {
336
273
  arrange();
337
274
  render(<WorkflowExecutionViewer executionId="wex_1" />);
338
275
 
339
- // Both tasks crossed into waiting_approval at mount. The panel stays
340
- // closed the cards ARE the (only) decision surface, and selection
341
- // died with the Inspect drill-down.
276
+ // Both tasks crossed into waiting_approval at mount (derived by the
277
+ // REAL store from real agent_call_progress events — D-T02-14). The
278
+ // panel stays closed — the cards ARE the (only) decision surface, and
279
+ // selection died with the Inspect drill-down.
342
280
  expect(screen.queryByRole("tab", { name: /Approval/ })).toBeNull();
343
281
  expect(document.querySelector("[aria-pressed]")).toBeNull();
344
282
 
345
- // Per-task scoping ON the cards: each renders exactly its child's gate,
346
- // with the full decision model of the shared session card — visible
347
- // without expanding anything (D-T02-12).
348
- const cardA = within(cardRootOf("call-helper-a"));
349
- const cardB = within(cardRootOf("call-helper-b"));
350
- const deleteCard = cardA.getByRole("alert", {
351
- name: "Approval required for delete_file",
352
- });
353
- expect(
354
- within(deleteCard).getByRole("button", { name: "Approve all file deletions" }),
355
- ).toBeTruthy();
356
- expect(cardA.queryByRole("alert", { name: "Approval required for shell" })).toBeNull();
357
-
358
- const shellCard = cardB.getByRole("alert", {
359
- name: "Approval required for shell",
360
- });
361
- expect(within(shellCard).getByRole("button", { name: "Approve" })).toBeTruthy();
362
- expect(within(shellCard).getByRole("button", { name: "Skip" })).toBeTruthy();
363
- expect(within(shellCard).getByRole("button", { name: "Reject" })).toBeTruthy();
364
- expect(
365
- within(shellCard).getByRole("button", { name: "Approve all shell commands" }),
366
- ).toBeTruthy();
367
- // The rich preview the retired thin card never had.
368
- expect(shellCard.textContent).toContain("npm test");
369
- });
370
-
371
- it("routes every in-card decision through the workflow-level actions instance with the gate's toolCallId", () => {
372
- const submitApproval = vi.fn();
373
- arrange(mockActions({ submitApproval }));
374
- render(<WorkflowExecutionViewer executionId="wex_1" />);
375
-
376
- fireEvent.click(
377
- within(cardRootOf("call-helper-b")).getByRole("button", {
378
- name: "Approve all shell commands",
379
- }),
283
+ // Per-card scoping: each card mounts ITS child's transcript, made
284
+ // interactive by the gate (the hitl bundle reached it) the child's
285
+ // ApprovalCards render inside it (the transcript's own suite).
286
+ const probeIn = (taskName: string) =>
287
+ within(cardRootOf(taskName)).getByTestId("transcript-probe");
288
+ expect(probeIn("call-helper-a").getAttribute("data-child-id")).toBe(
289
+ "agx_child_a",
380
290
  );
381
- fireEvent.click(
382
- within(cardRootOf("call-helper-a")).getByRole("button", { name: "Reject" }),
291
+ expect(probeIn("call-helper-a").getAttribute("data-interactive")).toBe(
292
+ "true",
293
+ );
294
+ expect(probeIn("call-helper-b").getAttribute("data-child-id")).toBe(
295
+ "agx_child_b",
296
+ );
297
+ expect(probeIn("call-helper-b").getAttribute("data-interactive")).toBe(
298
+ "true",
383
299
  );
384
-
385
- expect(submitApproval).toHaveBeenNthCalledWith(1, "tc_shell", ApprovalAction.APPROVE_ALL, undefined);
386
- expect(submitApproval).toHaveBeenNthCalledWith(2, "tc_delete", ApprovalAction.REJECT, undefined);
387
300
  });
388
301
 
389
- it("each gate renders on exactly ONE surfaceits own card (no Inspect duplicate exists, T06)", () => {
390
- arrange();
302
+ it("hands each transcript the viewer's SINGLE actions instancedecisions route through the workflow-level RPCs (S5 guardrail)", () => {
303
+ const actions = arrange();
391
304
  render(<WorkflowExecutionViewer executionId="wex_1" />);
392
305
 
393
- expect(
394
- screen.getAllByRole("alert", { name: "Approval required for delete_file" }),
395
- ).toHaveLength(1);
396
- expect(
397
- screen.getAllByRole("alert", { name: "Approval required for shell" }),
398
- ).toHaveLength(1);
399
- expect(screen.queryByRole("tab", { name: /Approval/ })).toBeNull();
306
+ // Identity, not equivalence: the transcript submits through the SAME
307
+ // workflow-level handlers every other surface uses, so in-flight and
308
+ // error state can never fork — and never the child's agentExecution.*
309
+ // path (checked exhaustively in WorkflowAgentCallTranscript.test.tsx).
310
+ for (const call of mockedTranscript.mock.calls) {
311
+ const hitl = call[0].hitl!;
312
+ expect(hitl.submitApproval).toBe(actions.submitApproval);
313
+ expect(hitl.submitFileDecision).toBe(actions.submitFileDecision);
314
+ expect(hitl.approvalSubmittingToolCallIds).toBe(
315
+ actions.approvalSubmittingToolCallIds,
316
+ );
317
+ expect(hitl.approvalErrorsByToolCallId).toBe(
318
+ actions.approvalErrorsByToolCallId,
319
+ );
320
+ }
400
321
  });
401
322
 
402
- it("keeps in-flight and error state per gate: one gate's failure never leaks to its sibling or the banner", () => {
403
- // Both gates on ONE child so one card carries both — the tightest
404
- // adjacency for leak detection.
405
- arrange(
406
- mockActions({
407
- approvalSubmittingToolCallIds: new Set(["tc_shell"]),
408
- approvalErrorsByToolCallId: new Map([
409
- ["tc_delete", new Error("gate already resolved")],
410
- ]),
411
- }),
412
- {
413
- pendingApprovals: [
414
- {
415
- childAgentExecutionId: "agx_child_b",
416
- approval: {
417
- toolCallId: "tc_delete",
418
- toolName: "delete_file",
419
- argsPreview: '{"path":"/tmp/x"}',
420
- },
421
- },
422
- {
423
- childAgentExecutionId: "agx_child_b",
424
- approval: {
425
- toolCallId: "tc_shell",
426
- toolName: "shell",
427
- argsPreview: '{"command":"npm test"}',
428
- },
429
- },
430
- ],
431
- },
432
- );
433
- const { container } = render(<WorkflowExecutionViewer executionId="wex_1" />);
434
-
435
- // In-flight: only the shell gate's actions are disabled.
436
- const shellCard = screen.getByRole("alert", { name: "Approval required for shell" });
437
- const deleteCard = screen.getByRole("alert", { name: "Approval required for delete_file" });
438
- expect(
439
- (within(shellCard).getByRole("button", { name: "Approve" }) as HTMLButtonElement).disabled,
440
- ).toBe(true);
441
- expect(
442
- (within(deleteCard).getByRole("button", { name: "Approve" }) as HTMLButtonElement).disabled,
443
- ).toBe(false);
444
-
445
- // Error: in-card on the failing gate only — never the lifecycle banner
446
- // (actions.error is null, so no banner exists at all).
447
- const errors = container.querySelectorAll('[data-cursor-target="approval-error"]');
448
- expect(errors).toHaveLength(1);
449
- expect(deleteCard.contains(errors[0]!)).toBe(true);
450
- expect(screen.queryByRole("button", { name: "Dismiss" })).toBeNull();
451
- });
452
-
453
- it("file reviews render on their own card, child-scoped, wired to the workflow-level submit", () => {
454
- const submitFileDecision = vi.fn();
455
- arrange(mockActions({ submitFileDecision }), {
456
- pendingApprovals: [],
457
- pendingFileReviews: [
458
- { childAgentExecutionId: "agx_child_a", changeSetId: ["cs_1"] },
459
- { childAgentExecutionId: "agx_child_b", changeSetId: ["cs_2"] },
460
- ],
461
- });
323
+ it("renders each child's transcript on exactly ONE surface its own card (no document tab, no Inspect duplicate)", () => {
324
+ arrange();
462
325
  render(<WorkflowExecutionViewer executionId="wex_1" />);
463
326
 
464
- // Each gating card hands ITS child's reference to the (stubbed) list.
465
- const cardB = within(cardRootOf("call-helper-b"));
466
- expect(cardB.getByText("decide-files-agx_child_b")).toBeTruthy();
467
- expect(cardB.queryByText("decide-files-agx_child_a")).toBeNull();
468
-
469
- // Decisions forward to the single actions instance (workflow RPC).
470
- fireEvent.click(cardB.getByText("decide-files-agx_child_b"));
471
- expect(submitFileDecision).toHaveBeenCalledWith("agx_child_b", "cs_2", 1);
327
+ const probes = screen.getAllByTestId("transcript-probe");
328
+ expect(probes).toHaveLength(2);
329
+ expect(
330
+ new Set(probes.map((p) => p.getAttribute("data-child-id"))).size,
331
+ ).toBe(2);
332
+ expect(screen.queryByRole("button", { name: "Open transcript" })).toBeNull();
472
333
  });
473
334
  });
@@ -79,19 +79,19 @@ vi.mock("../execution-comparison/ExecutionComparisonPicker", () => ({
79
79
  ExecutionComparisonPicker: () => null,
80
80
  }));
81
81
 
82
- // The child transcript document fetches and streams (its own suite covers
83
- // that); here it only proves the thread's "Open transcript" reaches the
84
- // panel's editor area with the right identity.
85
- vi.mock("../WorkflowAgentExecutionDocument", () => ({
86
- WorkflowAgentExecutionDocument: ({
82
+ // The inline child transcript fetches and streams (its own suite covers
83
+ // that); here it only proves the thread mounts it in the AGENT_CALL card
84
+ // with the right child identity (T07).
85
+ vi.mock("../WorkflowAgentCallTranscript", () => ({
86
+ WorkflowAgentCallTranscript: ({
87
87
  childExecutionId,
88
- taskName,
88
+ agentSlug,
89
89
  }: {
90
90
  childExecutionId: string;
91
- taskName: string;
91
+ agentSlug?: string;
92
92
  }) => (
93
- <div data-testid="agent-doc-stub">
94
- {childExecutionId}:{taskName}
93
+ <div data-testid="agent-transcript-stub">
94
+ {childExecutionId}:{agentSlug ?? ""}
95
95
  </div>
96
96
  ),
97
97
  }));
@@ -437,7 +437,7 @@ describe("WorkflowExecutionViewer (center-column Thread|Graph toggle, S9)", () =
437
437
  ).toHaveBeenCalledWith("review-gate", "ship", undefined, undefined);
438
438
  });
439
439
 
440
- it("an AGENT_CALL card's Open transcript opens the S4 document in the panel", () => {
440
+ it("an AGENT_CALL card renders the child's inline transcript in the thread — no document tab, no launcher (T07)", () => {
441
441
  // Add a settled agent-call task carrying its child execution id. The
442
442
  // stream stage must not be "complete" with zero events, or the viewer's
443
443
  // snapshot fallback would replace these derived states (its documented
@@ -464,13 +464,14 @@ describe("WorkflowExecutionViewer (center-column Thread|Graph toggle, S9)", () =
464
464
  } as unknown as ReturnType<typeof useWorkflowExecutionEventStream>);
465
465
  renderViewer();
466
466
 
467
- // Preview-kind cards (agent_call) keep the transcript affordance in
468
- // their always-visible bodyno expand gesture required (T04).
469
- fireEvent.click(screen.getByRole("button", { name: "Open transcript" }));
470
-
471
- expect(screen.getByTestId("agent-doc-stub").textContent).toBe(
472
- "aex_child_1:call-writer",
467
+ // The transcript is IN the card body, bound to the right child, with
468
+ // no launcher gesture in between the card is the single home.
469
+ expect(screen.getByTestId("agent-transcript-stub").textContent).toBe(
470
+ "aex_child_1:blog-writer",
473
471
  );
472
+ expect(
473
+ screen.queryByRole("button", { name: "Open transcript" }),
474
+ ).toBeNull();
474
475
  });
475
476
 
476
477
  it("keeps exactly one aria-live announcer across both center views", () => {
@@ -10,6 +10,7 @@ function makeDecision(
10
10
  return {
11
11
  outcome: "approve",
12
12
  reviewer: "alice",
13
+ reviewerActor: null,
13
14
  respondedAt: "2026-06-02T07:40:08Z",
14
15
  comment: "",
15
16
  formData: null,
@@ -57,11 +58,60 @@ describe("WorkflowTaskApprovalSummary", () => {
57
58
  });
58
59
 
59
60
  describe("decision metadata", () => {
60
- it("renders the reviewer", () => {
61
+ it("renders the reviewer's display name with avatar and email tooltip when enriched", () => {
62
+ render(
63
+ <WorkflowTaskApprovalSummary
64
+ {...baseProps}
65
+ decision={makeDecision({
66
+ reviewer: "ida_01abc",
67
+ reviewerActor: {
68
+ id: "ida_01abc",
69
+ displayName: "Ada Lovelace",
70
+ email: "ada@example.com",
71
+ avatar: "https://example.com/ada.png",
72
+ },
73
+ })}
74
+ />,
75
+ );
76
+ const name = screen.getByText("Ada Lovelace");
77
+ expect(name.getAttribute("title")).toBe("ada@example.com");
78
+ expect(document.querySelector('img[src="https://example.com/ada.png"]')).toBeTruthy();
79
+ // The raw identity is never shown when a display identity exists.
80
+ expect(screen.queryByText("ida_01abc")).toBeNull();
81
+ });
82
+
83
+ it("falls back to the email when the actor has no display name", () => {
84
+ render(
85
+ <WorkflowTaskApprovalSummary
86
+ {...baseProps}
87
+ decision={makeDecision({
88
+ reviewer: "ida_01abc",
89
+ reviewerActor: {
90
+ id: "ida_01abc",
91
+ displayName: "",
92
+ email: "ada@example.com",
93
+ avatar: "",
94
+ },
95
+ })}
96
+ />,
97
+ );
98
+ expect(screen.getByText("ada@example.com")).toBeTruthy();
99
+ expect(screen.queryByText("ida_01abc")).toBeNull();
100
+ });
101
+
102
+ it("renders a de-emphasized raw id for legacy unenriched records", () => {
61
103
  render(
62
104
  <WorkflowTaskApprovalSummary {...baseProps} decision={makeDecision({ reviewer: "bob" })} />,
63
105
  );
64
- expect(screen.getByText("bob")).toBeTruthy();
106
+ const raw = screen.getByText("bob");
107
+ expect(raw.className).toContain("font-mono");
108
+ });
109
+
110
+ it("omits the 'by' segment entirely for unattributed decisions", () => {
111
+ render(
112
+ <WorkflowTaskApprovalSummary {...baseProps} decision={makeDecision({ reviewer: "" })} />,
113
+ );
114
+ expect(screen.queryByText(/^by/)).toBeNull();
65
115
  });
66
116
 
67
117
  it("renders the wait duration when present", () => {