@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
@@ -27,15 +27,7 @@ import {
27
27
  FILE_CHANGE_DOCUMENT_ENTRY_ID,
28
28
  fileChangeTabPath,
29
29
  } from "../execution/file-change-document.js";
30
- import {
31
- AGENT_EXECUTION_DOCUMENT_ENTRY_ID,
32
- parseAgentExecutionTabPath,
33
- } from "../execution/agent-execution-document.js";
34
30
  import { FileChangeDiff } from "../execution/FileChangesView.js";
35
- import {
36
- WorkflowAgentExecutionDocument,
37
- type WorkflowAgentExecutionHitl,
38
- } from "./WorkflowAgentExecutionDocument.js";
39
31
  import {
40
32
  WorkspaceSurface,
41
33
  type SurfaceVirtualDocument,
@@ -281,14 +273,15 @@ export const WorkflowExecutionViewer = memo(function WorkflowExecutionViewer({
281
273
  onSuccess: refetchExecution,
282
274
  });
283
275
 
284
- // The child-gate HITL wiring — ONE bundle from the single actions
285
- // instance, narrowed to the six child-gate fields, consumed by the
286
- // in-place transcript documents (S5, as `WorkflowAgentExecutionHitl`), so
287
- // a gate's spinner/error is pixel-identical on every surface. Deps are
288
- // the individual fields (DD-010): the bundle's ref must survive unrelated
289
- // churn on `actions` (a lifecycle action's isSubmitting flip), so
290
- // consumers re-render only when a gate's own in-flight/error state moves.
291
- const childGateHitl = useMemo<WorkflowAgentExecutionHitl>(
276
+ // The thread's HITL bundle (T06/T07) — ONE bundle from the single actions
277
+ // instance, covering all three gate kinds: the child-gate fields (which
278
+ // the thread hands to gating agent-call cards' inline transcripts, as the
279
+ // `WorkflowAgentExecutionHitl` subset) plus the task-level (human_input)
280
+ // approval wiring. Deps are the individual fields (DD-010): the bundle's
281
+ // ref must survive unrelated churn on `actions` (a lifecycle action's
282
+ // isSubmitting flip), and the thread scopes it to gating cards, so a
283
+ // decision's in-flight flip re-renders the gating card only.
284
+ const threadHitl = useMemo<WorkflowThreadHitl>(
292
285
  () => ({
293
286
  submitApproval: actions.submitApproval,
294
287
  approvalSubmittingToolCallIds: actions.approvalSubmittingToolCallIds,
@@ -296,6 +289,9 @@ export const WorkflowExecutionViewer = memo(function WorkflowExecutionViewer({
296
289
  submitFileDecision: actions.submitFileDecision,
297
290
  fileDecisionSubmittingKeys: actions.fileDecisionSubmittingKeys,
298
291
  fileDecisionErrorsByKey: actions.fileDecisionErrorsByKey,
292
+ submitTaskApproval: actions.submitTaskApproval,
293
+ taskApprovalSubmittingTaskNames: actions.taskApprovalSubmittingTaskNames,
294
+ taskApprovalErrorsByTaskName: actions.taskApprovalErrorsByTaskName,
299
295
  }),
300
296
  [
301
297
  actions.submitApproval,
@@ -304,24 +300,6 @@ export const WorkflowExecutionViewer = memo(function WorkflowExecutionViewer({
304
300
  actions.submitFileDecision,
305
301
  actions.fileDecisionSubmittingKeys,
306
302
  actions.fileDecisionErrorsByKey,
307
- ],
308
- );
309
-
310
- // The thread's HITL bundle (T06): the child-gate fields PLUS the
311
- // task-level (human_input) approval wiring — since the gating card is
312
- // the only decision surface, the thread carries all three gate kinds.
313
- // A separate memo from `childGateHitl` so a human_input decision's
314
- // in-flight flip re-renders the gating card, never the open transcript
315
- // documents (DD-010).
316
- const threadHitl = useMemo<WorkflowThreadHitl>(
317
- () => ({
318
- ...childGateHitl,
319
- submitTaskApproval: actions.submitTaskApproval,
320
- taskApprovalSubmittingTaskNames: actions.taskApprovalSubmittingTaskNames,
321
- taskApprovalErrorsByTaskName: actions.taskApprovalErrorsByTaskName,
322
- }),
323
- [
324
- childGateHitl,
325
303
  actions.submitTaskApproval,
326
304
  actions.taskApprovalSubmittingTaskNames,
327
305
  actions.taskApprovalErrorsByTaskName,
@@ -586,13 +564,12 @@ export const WorkflowExecutionViewer = memo(function WorkflowExecutionViewer({
586
564
  taskStates={effectiveTaskStates}
587
565
  totalTasks={effectiveTotalTasks}
588
566
  isRunning={isRunning}
589
- onOpenAgentExecution={panel.openAgentExecution}
590
- // In-card HITL (S10/T06): all three gate kinds decide on the
591
- // gating card, backed by the snapshot gate lists (kept fresh
592
- // by the approval boundary's refetch above).
567
+ onNavigateToAgentExecution={onNavigateToAgentExecution}
568
+ // In-card HITL (S10/T06/T07): all three gate kinds decide on
569
+ // the gating card human_input on the card itself, child tool
570
+ // approvals and file reviews inside the card's inline child
571
+ // transcript (which streams them from the child directly).
593
572
  hitl={threadHitl}
594
- pendingApprovals={execution?.status?.pendingApprovals}
595
- pendingFileReviews={execution?.status?.pendingFileReviews}
596
573
  // Full per-task I/O for the card bodies (T04) — an O(1)
597
574
  // per-card snapshot lookup.
598
575
  taskSnapshotsByName={taskSnapshotsByName}
@@ -632,9 +609,7 @@ export const WorkflowExecutionViewer = memo(function WorkflowExecutionViewer({
632
609
  fileChangesState={fileChangesState}
633
610
  costSummary={costSummary}
634
611
  taskStates={effectiveTaskStates}
635
- onNavigateToAgentExecution={onNavigateToAgentExecution}
636
612
  onApplyFix={onNavigateToWorkflowEditor ? handleApplyFix : undefined}
637
- transcriptHitl={childGateHitl}
638
613
  />
639
614
  ) : null
640
615
  }
@@ -654,8 +629,9 @@ export const WorkflowExecutionViewer = memo(function WorkflowExecutionViewer({
654
629
  * open-editor group (keeping that subscription out of the streaming owner),
655
630
  * assembles the rail facets, and resolves open virtual-document tabs back to
656
631
  * their records (artifact tabs → `Artifact`, file-change tabs → the current
657
- * net `FileChange`, transcript tabs → their child id, the diagnosis tab →
658
- * `WorkflowRepairCard`).
632
+ * net `FileChange`, the diagnosis tab → `WorkflowRepairCard`). Child
633
+ * transcripts are NOT a document family here — since T07 they render inline
634
+ * in the thread's agent-call cards.
659
635
  */
660
636
  function ExecutionWorkspacePanel({
661
637
  panel,
@@ -665,9 +641,7 @@ function ExecutionWorkspacePanel({
665
641
  fileChangesState,
666
642
  costSummary,
667
643
  taskStates,
668
- onNavigateToAgentExecution,
669
644
  onApplyFix,
670
- transcriptHitl,
671
645
  }: {
672
646
  readonly panel: WorkflowExecutionPanelController;
673
647
  readonly executionId: string;
@@ -676,11 +650,8 @@ function ExecutionWorkspacePanel({
676
650
  readonly fileChangesState: UseWorkflowExecutionFileChangesReturn;
677
651
  readonly costSummary: DerivedCostSummary;
678
652
  readonly taskStates: ReadonlyMap<string, DerivedTaskState>;
679
- readonly onNavigateToAgentExecution?: (agentExecutionId: string) => void;
680
653
  /** "Apply Fix" from the diagnosis document — host-routed (DD-004). */
681
654
  readonly onApplyFix?: (yaml: string) => void;
682
- /** Workflow-level HITL wiring for open transcript documents (S5). */
683
- readonly transcriptHitl: WorkflowAgentExecutionHitl;
684
655
  }) {
685
656
  const { editors, activeFile } = useWorkspaceEditors(panel.editorsStore);
686
657
 
@@ -732,7 +703,6 @@ function ExecutionWorkspacePanel({
732
703
  (editor) =>
733
704
  editor.entryId === ARTIFACT_DOCUMENT_ENTRY_ID ||
734
705
  editor.entryId === FILE_CHANGE_DOCUMENT_ENTRY_ID ||
735
- editor.entryId === AGENT_EXECUTION_DOCUMENT_ENTRY_ID ||
736
706
  editor.entryId === DIAGNOSIS_DOCUMENT_ENTRY_ID,
737
707
  )
738
708
  .map((editor) => {
@@ -761,30 +731,6 @@ function ExecutionWorkspacePanel({
761
731
  ),
762
732
  };
763
733
  }
764
- if (editor.entryId === AGENT_EXECUTION_DOCUMENT_ENTRY_ID) {
765
- // The tab path CARRIES the child id (no lookup map — unlike the
766
- // artifact/change families, a transcript needs only its id to
767
- // fetch/stream itself). The suffix is the AGENT_CALL task name;
768
- // its live state provides the agent slug for the header.
769
- const childExecutionId = parseAgentExecutionTabPath(editor.path);
770
- const taskName = editor.path.slice(childExecutionId.length + 1);
771
- return {
772
- entryId: AGENT_EXECUTION_DOCUMENT_ENTRY_ID,
773
- path: editor.path,
774
- // Keyed by tab path so the mounted fetch/stream survives
775
- // unrelated editors churn while this tab stays active.
776
- content: (
777
- <WorkflowAgentExecutionDocument
778
- key={editor.path}
779
- childExecutionId={childExecutionId}
780
- taskName={taskName}
781
- agentSlug={taskStates.get(taskName)?.agentSlug || undefined}
782
- onNavigateToAgentExecution={onNavigateToAgentExecution}
783
- hitl={transcriptHitl}
784
- />
785
- ),
786
- };
787
- }
788
734
  if (editor.entryId === FILE_CHANGE_DOCUMENT_ENTRY_ID) {
789
735
  const change = fileChangeByTabPath.get(editor.path);
790
736
  return {
@@ -814,9 +760,6 @@ function ExecutionWorkspacePanel({
814
760
  editors,
815
761
  artifactByTabPath,
816
762
  fileChangeByTabPath,
817
- taskStates,
818
- onNavigateToAgentExecution,
819
- transcriptHitl,
820
763
  executionId,
821
764
  org,
822
765
  onApplyFix,
@@ -7,7 +7,11 @@ import { cn } from "@stigmer/theme";
7
7
  import { MARKDOWN_COMPONENTS, REMARK_PLUGINS } from "../internal/markdown-components.js";
8
8
  import { formatDuration, formatTimestamp } from "./format-utils.js";
9
9
  import type { TaskOutcome } from "./WorkflowTaskApprovalCard.js";
10
- import type { TaskDetailApprovalDecision } from "./task-detail/task-approval.js";
10
+ import {
11
+ deriveTaskReviewer,
12
+ type TaskDetailApprovalDecision,
13
+ type TaskReviewerView,
14
+ } from "./task-detail/task-approval.js";
11
15
 
12
16
  /** Props for {@link WorkflowTaskApprovalSummary}. */
13
17
  export interface WorkflowTaskApprovalSummaryProps {
@@ -82,6 +86,11 @@ export const WorkflowTaskApprovalSummary = memo(function WorkflowTaskApprovalSum
82
86
 
83
87
  const formEntries = useMemo(() => extractFormEntries(decision?.formData ?? null), [decision]);
84
88
 
89
+ const reviewer = useMemo(
90
+ () => (decision ? deriveTaskReviewer(decision) : null),
91
+ [decision],
92
+ );
93
+
85
94
  return (
86
95
  <div
87
96
  role="group"
@@ -116,20 +125,16 @@ export const WorkflowTaskApprovalSummary = memo(function WorkflowTaskApprovalSum
116
125
  {/* Reviewer + timing */}
117
126
  {!isFinalizing && (
118
127
  <div className="mt-1.5 flex flex-wrap items-center gap-x-1.5 text-[11px] text-muted-foreground">
119
- {decision!.reviewer && (
120
- <span>
121
- by <span className="font-medium text-foreground">{decision!.reviewer}</span>
122
- </span>
123
- )}
128
+ {reviewer && <ReviewerChip reviewer={reviewer} />}
124
129
  {decision!.respondedAt && (
125
130
  <>
126
- {decision!.reviewer && <Dot />}
131
+ {reviewer && <Dot />}
127
132
  <span className="tabular-nums">{formatTimestamp(decision!.respondedAt)}</span>
128
133
  </>
129
134
  )}
130
135
  {decision!.waitDurationMs > 0 && (
131
136
  <>
132
- {(decision!.reviewer || decision!.respondedAt) && <Dot />}
137
+ {(reviewer || decision!.respondedAt) && <Dot />}
133
138
  <span className="tabular-nums">waited {formatDuration(decision!.waitDurationMs)}</span>
134
139
  </>
135
140
  )}
@@ -178,6 +183,38 @@ export const WorkflowTaskApprovalSummary = memo(function WorkflowTaskApprovalSum
178
183
  );
179
184
  });
180
185
 
186
+ // ---------------------------------------------------------------------------
187
+ // Reviewer chip
188
+ // ---------------------------------------------------------------------------
189
+
190
+ /**
191
+ * `by <avatar> Ada Lovelace` — the reviewer's display identity, following
192
+ * the platform's actor pattern (see `WorkflowVersionTimeline`). The email
193
+ * rides as a native tooltip when it is not already the visible label. A
194
+ * label that fell through to the raw identity (legacy records) renders
195
+ * de-emphasized — internal IDs are never presented as if they were names.
196
+ */
197
+ function ReviewerChip({ reviewer }: { readonly reviewer: TaskReviewerView }) {
198
+ return (
199
+ <span className="flex items-center gap-1">
200
+ by{" "}
201
+ {reviewer.avatar && (
202
+ <img src={reviewer.avatar} alt="" className="size-3.5 rounded-full" />
203
+ )}
204
+ <span
205
+ className={cn(
206
+ reviewer.isRawId
207
+ ? "font-mono text-[10px] text-muted-foreground"
208
+ : "font-medium text-foreground",
209
+ )}
210
+ title={reviewer.email || undefined}
211
+ >
212
+ {reviewer.label}
213
+ </span>
214
+ </span>
215
+ );
216
+ }
217
+
181
218
  // ---------------------------------------------------------------------------
182
219
  // Tone (visual treatment of the chosen outcome)
183
220
  // ---------------------------------------------------------------------------
@@ -0,0 +1,124 @@
1
+ // The T07 stacked-live-region check: several inline transcripts mount
2
+ // several REAL MessageThreads, each carrying its own `role="log"
3
+ // aria-live="polite"` scroller, under the viewer's one task-state
4
+ // announcer. This pins the a11y-relevant facts that make the stack
5
+ // acceptable rather than noisy:
6
+ //
7
+ // - each live region is scoped to ITS transcript (announcements name their
8
+ // own child's content, never a sibling's);
9
+ // - `polite` regions only announce on CONTENT CHANGE — a settled child's
10
+ // transcript never mutates, so it is permanently silent;
11
+ // - the viewport gate (useInViewport → useLiveAgentExecution's `live`)
12
+ // pauses off-screen streams, so at most the on-screen running children
13
+ // mutate concurrently.
14
+ //
15
+ // If a live round ever proves this noisy in real screen readers, the
16
+ // planned fallback is a backward-compatible `ariaLive` opt-out prop on
17
+ // MessageThread (DD-011) — not committed speculatively.
18
+
19
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
20
+ import { render, cleanup, within, screen } from "@testing-library/react";
21
+ import { create } from "@bufbuild/protobuf";
22
+ import {
23
+ AgentExecutionSchema,
24
+ AgentExecutionStatusSchema,
25
+ type AgentExecution,
26
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
27
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
28
+ import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
29
+ import {
30
+ ExecutionPhase,
31
+ MessageType,
32
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
33
+
34
+ vi.mock("../../execution/useLiveAgentExecution", () => ({
35
+ useLiveAgentExecution: vi.fn(),
36
+ }));
37
+
38
+ import { useLiveAgentExecution } from "../../execution/useLiveAgentExecution";
39
+ import { WorkflowAgentCallTranscript } from "../WorkflowAgentCallTranscript";
40
+
41
+ const mockUseLiveAgentExecution = vi.mocked(useLiveAgentExecution);
42
+
43
+ function executionWithMessage(id: string, text: string): AgentExecution {
44
+ const exec = create(AgentExecutionSchema);
45
+ exec.metadata = create(ApiResourceMetadataSchema, { id });
46
+ exec.status = create(AgentExecutionStatusSchema, {
47
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
48
+ messages: [
49
+ create(AgentMessageSchema, {
50
+ type: MessageType.MESSAGE_AI,
51
+ content: text,
52
+ }),
53
+ ],
54
+ });
55
+ return exec;
56
+ }
57
+
58
+ beforeEach(() => {
59
+ vi.clearAllMocks();
60
+ // MessageThread's auto-scroll + the transcript's viewport gate both need
61
+ // observers happy-dom lacks.
62
+ vi.stubGlobal(
63
+ "IntersectionObserver",
64
+ vi.fn(() => ({
65
+ observe: vi.fn(),
66
+ unobserve: vi.fn(),
67
+ disconnect: vi.fn(),
68
+ takeRecords: vi.fn(() => []),
69
+ })),
70
+ );
71
+ vi.stubGlobal(
72
+ "ResizeObserver",
73
+ vi.fn(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() })),
74
+ );
75
+ });
76
+
77
+ afterEach(() => {
78
+ cleanup();
79
+ vi.restoreAllMocks();
80
+ });
81
+
82
+ describe("WorkflowAgentCallTranscript — stacked live regions (T07 a11y)", () => {
83
+ it("scopes each transcript's polite log region to its own child's content, under the viewer-style announcer", () => {
84
+ mockUseLiveAgentExecution.mockImplementation((id) => ({
85
+ execution: executionWithMessage(id!, `report from ${id}`),
86
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
87
+ isLoading: false,
88
+ isStreaming: false,
89
+ isReconnecting: false,
90
+ error: null,
91
+ reconnect: vi.fn(),
92
+ }));
93
+
94
+ const { container } = render(
95
+ <div>
96
+ {/* The viewer's single always-visible task-state announcer. */}
97
+ <div role="log" aria-live="polite" data-testid="viewer-announcer" />
98
+ <WorkflowAgentCallTranscript childExecutionId="aex_a" agentSlug="alpha" />
99
+ <WorkflowAgentCallTranscript childExecutionId="aex_b" agentSlug="beta" />
100
+ </div>,
101
+ );
102
+
103
+ // Three polite regions coexist: the announcer + one per transcript.
104
+ const regions = Array.from(
105
+ container.querySelectorAll('[aria-live="polite"]'),
106
+ );
107
+ expect(regions).toHaveLength(3);
108
+ for (const region of regions) {
109
+ expect(region.getAttribute("aria-live")).toBe("polite");
110
+ }
111
+
112
+ // Scoping: each transcript's log carries ONLY its own child's content —
113
+ // an announcement can never attribute one agent's words to another.
114
+ const transcriptA = screen.getByRole("group", {
115
+ name: "Transcript of agent alpha",
116
+ });
117
+ const transcriptB = screen.getByRole("group", {
118
+ name: "Transcript of agent beta",
119
+ });
120
+ expect(within(transcriptA).getByText("report from aex_a")).toBeTruthy();
121
+ expect(within(transcriptA).queryByText("report from aex_b")).toBeNull();
122
+ expect(within(transcriptB).getByText("report from aex_b")).toBeTruthy();
123
+ });
124
+ });