@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
@@ -253,4 +253,42 @@ describe("VisibilityBadge", () => {
253
253
  render(<VisibilityBadge visibility={ApiResourceVisibility.visibility_platform} />);
254
254
  expect(screen.getByText("Platform")).toBeTruthy();
255
255
  });
256
+
257
+ it("is non-interactive without onClick — a plain badge, not a button", () => {
258
+ render(<VisibilityBadge visibility={ApiResourceVisibility.visibility_org} />);
259
+ expect(screen.getByText("Organization")).toBeTruthy();
260
+ expect(screen.queryByRole("button")).toBeNull();
261
+ });
262
+
263
+ it("renders as an accessible Manage-access button when onClick is provided", () => {
264
+ const onClick = vi.fn();
265
+ render(
266
+ <VisibilityBadge
267
+ visibility={ApiResourceVisibility.visibility_org}
268
+ onClick={onClick}
269
+ />,
270
+ );
271
+
272
+ const button = screen.getByRole("button", {
273
+ name: /Organization visibility — manage access/i,
274
+ });
275
+ fireEvent.click(button);
276
+ expect(onClick).toHaveBeenCalledTimes(1);
277
+ });
278
+
279
+ it("is keyboard-activatable when clickable (native button semantics)", () => {
280
+ const onClick = vi.fn();
281
+ render(
282
+ <VisibilityBadge
283
+ visibility={ApiResourceVisibility.visibility_private}
284
+ onClick={onClick}
285
+ />,
286
+ );
287
+
288
+ const button = screen.getByRole("button", { name: /manage access/i });
289
+ // A native <button type="button"> fires click on Enter/Space; assert the
290
+ // element is the real thing rather than a div with a handler.
291
+ expect(button.tagName).toBe("BUTTON");
292
+ expect(button.getAttribute("type")).toBe("button");
293
+ });
256
294
  });
@@ -76,7 +76,10 @@ export { ImportResourceDialog } from "./ImportResourceDialog.js";
76
76
  export type { ImportResourceDialogProps } from "./ImportResourceDialog.js";
77
77
 
78
78
  export { VisibilitySelector, VisibilityBadge } from "./VisibilitySelector.js";
79
- export type { VisibilitySelectorProps } from "./VisibilitySelector.js";
79
+ export type {
80
+ VisibilitySelectorProps,
81
+ VisibilityBadgeProps,
82
+ } from "./VisibilitySelector.js";
80
83
 
81
84
  export {
82
85
  blueprintVisibilityLevels,
@@ -407,10 +407,14 @@ export function McpServerDetailView({
407
407
  updatedAt: specAudit?.updatedAt ? timestampDate(specAudit.updatedAt) : null,
408
408
  };
409
409
 
410
- // Inline visibility is read-only (at-a-glance); editing lives in the
411
- // Manage access dialog, the single writer for both access axes.
410
+ // Inline visibility is at-a-glance AND a shortcut into the Manage access
411
+ // dialog the single writer for both access axes. The chip is navigation
412
+ // only; it stays a static badge for users who cannot view access.
412
413
  const visibilityControl = meta ? (
413
- <VisibilityBadge visibility={meta.visibility} />
414
+ <VisibilityBadge
415
+ visibility={meta.visibility}
416
+ onClick={access.action ? access.open : undefined}
417
+ />
414
418
  ) : undefined;
415
419
 
416
420
  const mergedActions = access.action
@@ -817,7 +817,13 @@ const ConversationColumn = memo(function ConversationColumn({
817
817
  // a plan read-only in the panel remains — reading is the point.
818
818
  onRetrySend={isObserver ? undefined : conv.retryLastSend}
819
819
  onRetryExecution={isObserver ? undefined : onRetryExecution}
820
- onApprovalSubmit={isObserver ? undefined : flow.submitApproval}
820
+ // Approval mechanics are an OPERATOR surface, never a guest's
821
+ // (DD-014): the HITL gate protects the org's tools, and an anonymous
822
+ // visitor is not its trustee. Guest executions run unattended
823
+ // (gated tools auto-skip server-side), so nothing is ever pending on
824
+ // a new execution — withholding the callback is the belt-and-braces
825
+ // for pre-existing sessions and embedders composing the SDK directly.
826
+ onApprovalSubmit={isObserver || isGuest ? undefined : flow.submitApproval}
821
827
  submittingApprovalIds={conv.submittingApprovalIds}
822
828
  approvalErrors={conv.approvalErrors}
823
829
  showFileReviewRecords
@@ -35,8 +35,12 @@ vi.mock("../facets/SetupTab", () => ({
35
35
  },
36
36
  }));
37
37
 
38
+ const threadProps: CapturedProps[] = [];
38
39
  vi.mock("../../execution/MessageThread", () => ({
39
- MessageThread: () => <div data-testid="thread-probe" />,
40
+ MessageThread: (props: CapturedProps) => {
41
+ threadProps.push(props);
42
+ return <div data-testid="thread-probe" />;
43
+ },
40
44
  }));
41
45
 
42
46
  // ---------------------------------------------------------------------------
@@ -93,6 +97,7 @@ const stubConv = {
93
97
  isSending: false,
94
98
  sendError: null,
95
99
  clearSendError: vi.fn(),
100
+ retryLastSend: vi.fn(),
96
101
  pendingUserMessage: null,
97
102
  workspaceEntries: [],
98
103
  mcpServerUsages: [],
@@ -159,6 +164,11 @@ function lastComposerProps(): CapturedProps {
159
164
  return composerProps.at(-1)!;
160
165
  }
161
166
 
167
+ function lastThreadProps(): CapturedProps {
168
+ expect(threadProps.length).toBeGreaterThan(0);
169
+ return threadProps.at(-1)!;
170
+ }
171
+
162
172
  /**
163
173
  * Reach the Config facet through the unified panel: expand it via the
164
174
  * top-right chip, pick Config in the rail, and return the props the (mocked)
@@ -174,6 +184,7 @@ function openedConfigFacet(): { mutations?: unknown } {
174
184
  beforeEach(() => {
175
185
  composerProps.length = 0;
176
186
  setupTabProps.length = 0;
187
+ threadProps.length = 0;
177
188
  stubSessionPageFlow.submitError = null;
178
189
  });
179
190
 
@@ -375,6 +386,25 @@ describe("SessionViewer — audience wiring", () => {
375
386
  );
376
387
  });
377
388
 
389
+ it("guest: approval mechanics are withheld from the thread (DD-014)", () => {
390
+ // The HITL gate protects the ORG's tools; an anonymous visitor is not
391
+ // its trustee. Guest executions run unattended server-side, so nothing
392
+ // is ever pending on a new execution — withholding the callback is the
393
+ // belt-and-braces for pre-existing sessions and direct SDK embedders.
394
+ render(<SessionViewer sessionId="ses_1" org="acme" audience="guest" />);
395
+
396
+ const props = lastThreadProps();
397
+ expect(props.onApprovalSubmit).toBeUndefined();
398
+ // Chat interaction survives — a guest owns their own conversation.
399
+ expect(props.onRetrySend).toBeDefined();
400
+ });
401
+
402
+ it("integrator: approvals stay wired on the thread", () => {
403
+ render(<SessionViewer sessionId="ses_1" org="acme" />);
404
+
405
+ expect(lastThreadProps().onApprovalSubmit).toBeDefined();
406
+ });
407
+
378
408
  it("forwards getRuntimeEnv to the flow", () => {
379
409
  const getRuntimeEnv = vi.fn();
380
410
  render(
@@ -19,7 +19,12 @@
19
19
  * behavioral, not just cosmetic — the organisms also skip the
20
20
  * org-level reads a guest principal cannot make (default-agent
21
21
  * resolution, session→agent derivation, personal environments) and
22
- * never fall back to the org's default agent.
22
+ * never fall back to the org's default agent. Approval mechanics are
23
+ * also withheld (DD-014): the HITL gate protects the ORG's tools and
24
+ * an anonymous visitor is not its trustee — guest executions run in
25
+ * unattended approval mode server-side (gated tools auto-skip and the
26
+ * agent explains in plain language), so tool-approval vocabulary never
27
+ * reaches a guest.
23
28
  * - `"observer"` — a read-only transcript for someone watching a
24
29
  * conversation they cannot participate in, e.g. a channel connector
25
30
  * or org admin reviewing a Slack/WhatsApp conversation the channel
@@ -254,10 +254,14 @@ export function SkillDetailView({
254
254
  statusLabel: status ? skillStateLabel(status.state) : undefined,
255
255
  };
256
256
 
257
- // Inline visibility is read-only (at-a-glance); editing lives in the
258
- // Manage access dialog, the single writer for both access axes.
257
+ // Inline visibility is at-a-glance AND a shortcut into the Manage access
258
+ // dialog the single writer for both access axes. The chip is navigation
259
+ // only; it stays a static badge for users who cannot view access.
259
260
  const visibilityControl = meta ? (
260
- <VisibilityBadge visibility={meta.visibility} />
261
+ <VisibilityBadge
262
+ visibility={meta.visibility}
263
+ onClick={access.action ? access.open : undefined}
264
+ />
261
265
  ) : undefined;
262
266
 
263
267
  const mergedActions = access.action
@@ -0,0 +1,279 @@
1
+ "use client";
2
+
3
+ // The inline child-transcript body of an AGENT_CALL task card (T07): the
4
+ // card IS the session experience — live while running, full history when
5
+ // settled — replacing the S4 "Open transcript" document tab.
6
+
7
+ import { memo, useCallback, useMemo } from "react";
8
+ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
9
+ import {
10
+ FileChangeSetStatus,
11
+ type FileDecisionAction,
12
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
13
+ import type { FileChangeSet } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
14
+ import { displayFileChangeSets } from "@stigmer/sdk";
15
+ import { cn } from "@stigmer/theme";
16
+ import { useLiveAgentExecution } from "../execution/useLiveAgentExecution.js";
17
+ import { MessageThread } from "../execution/MessageThread.js";
18
+ import { ThreadSkeleton } from "../execution/ThreadSkeleton.js";
19
+ import { FileReviewDock } from "../execution/FileReviewDock.js";
20
+ import type { FileDecisionOptions } from "../execution/useFileReview.js";
21
+ import { isTerminalPhase } from "../execution/execution-phases.js";
22
+ import { useInViewport } from "../internal/useInViewport.js";
23
+ import type { UseWorkflowExecutionActionsReturn } from "./useWorkflowExecutionActions.js";
24
+
25
+ /** Stable empty list so the thread's memoized rows keep identity (DD-010). */
26
+ const EMPTY_EXECUTIONS: readonly AgentExecution[] = [];
27
+
28
+ /**
29
+ * The HITL wiring for an inline child transcript: the slice of
30
+ * {@link UseWorkflowExecutionActionsReturn} needed to submit child-gate
31
+ * decisions at the WORKFLOW level.
32
+ *
33
+ * A `Pick` (not a new shape) so the bundle can never drift from the actions
34
+ * hook — the viewer builds it from its single `useWorkflowExecutionActions`
35
+ * instance, so a gate's in-flight spinner or failure is identical wherever
36
+ * that gate is shown.
37
+ */
38
+ export type WorkflowAgentExecutionHitl = Pick<
39
+ UseWorkflowExecutionActionsReturn,
40
+ | "submitApproval"
41
+ | "approvalSubmittingToolCallIds"
42
+ | "approvalErrorsByToolCallId"
43
+ | "submitFileDecision"
44
+ | "fileDecisionSubmittingKeys"
45
+ | "fileDecisionErrorsByKey"
46
+ >;
47
+
48
+ /** Props for {@link WorkflowAgentCallTranscript}. */
49
+ export interface WorkflowAgentCallTranscriptProps {
50
+ /** ID of the child AgentExecution whose transcript to render. */
51
+ readonly childExecutionId: string;
52
+ /** Slug of the agent the task called, when known (labels the region). */
53
+ readonly agentSlug?: string;
54
+ /**
55
+ * Workflow-level HITL handlers. When provided, the transcript is
56
+ * interactive: tool-approval gates render their decision cards inline on
57
+ * the gated rows, and pending file reviews dock at the bottom. When
58
+ * omitted, the transcript is read-only — gates show status only
59
+ * (backward compatible, DD-011).
60
+ */
61
+ readonly hitl?: WorkflowAgentExecutionHitl;
62
+ /**
63
+ * Open the child execution as a standalone page — the deep-dive escape
64
+ * hatch now that the card is the transcript's single home. Host-routed
65
+ * (DD-004).
66
+ */
67
+ readonly onNavigateToAgentExecution?: (agentExecutionId: string) => void;
68
+ /** Additional CSS classes for the root element. */
69
+ readonly className?: string;
70
+ }
71
+
72
+ /**
73
+ * A child AgentExecution's full transcript rendered inline in the
74
+ * AGENT_CALL task card — the session-grade `MessageThread` (tool calls,
75
+ * sub-agents, plan cards, and — with {@link
76
+ * WorkflowAgentCallTranscriptProps.hitl | hitl} — in-place HITL), streaming
77
+ * live while the child runs and serving the full history once settled.
78
+ *
79
+ * BOUNDED, NOT FULL-HEIGHT. The transcript caps at a generous viewport
80
+ * fraction and owns its own scroll + auto-follow inside that box
81
+ * (`MessageThread`'s scroll machine is reused unchanged). Once a transcript
82
+ * hits its cap it stops growing, so the card height stabilizes and the
83
+ * OUTER thread's auto-follow stops chasing it — the bound fixes both the
84
+ * inner and the outer scroll ergonomics at once.
85
+ *
86
+ * VIEWPORT-GATED STREAMING. Every mounted card fetches its (DD-014-cached)
87
+ * snapshot so the body renders immediately, but the live subscription opens
88
+ * only while the card is on-screen (`useInViewport` → the hook's `live`
89
+ * gate). Off-screen running children pause their stream and keep the
90
+ * last-streamed snapshot visible (never a rewind); scrolling back — or
91
+ * toggling back from the CSS-hidden Graph view — re-attaches in place. This
92
+ * preserves the bounded-concurrency intent of the S4 single-active-tab
93
+ * design (DD-LIVE-006) for fan-out workflows: N concurrent children cost at
94
+ * most the streams the user can actually see.
95
+ *
96
+ * MINIMAL CHROME. The card header already owns the task's identity and
97
+ * metrics (the `agentCallLine` preview: slug, current tool, message/tool
98
+ * counts, plus duration/cost chips and the running/settled status glyph).
99
+ * The transcript adds only what the header cannot express: a
100
+ * `Reconnecting…` notice for the CHILD stream and the "Open standalone"
101
+ * pop-out. Body = purely the conversation.
102
+ *
103
+ * HITL ROUTING. Child gates are decided through the WORKFLOW-level RPCs
104
+ * (`WorkflowExecution.submitApproval` / `submitFileDecision`), never the
105
+ * child's own `agentExecution.*` submit path. The two paths are
106
+ * server-equivalent (the workflow RPC forwards to the child), but they
107
+ * differ in authorization: the workflow RPC checks `can_edit` on the
108
+ * workflow execution — the resource the operator owns — while the child
109
+ * path checks the runner-spawned AgentExecution, which the operator may
110
+ * not (the S5 rationale).
111
+ *
112
+ * FILE-REVIEW DOCK. Pending (AWAITING_REVIEW) sets derive from the CHILD's
113
+ * own live stream — the freshest source this component already holds — not
114
+ * from the parent's `status.pending_file_reviews`, which is fetched once
115
+ * and may lag a mid-run gate. Docked below the thread so a decision the
116
+ * child is blocked on can never scroll out of view; settled sets render as
117
+ * read-only records in-thread (`showFileReviewRecords`) — the two surfaces
118
+ * partition by the same pending test `MessageThread` uses.
119
+ *
120
+ * All visual properties flow through `--stgm-*` tokens (DD-005).
121
+ */
122
+ export const WorkflowAgentCallTranscript = memo(
123
+ function WorkflowAgentCallTranscript({
124
+ childExecutionId,
125
+ agentSlug,
126
+ hitl,
127
+ onNavigateToAgentExecution,
128
+ className,
129
+ }: WorkflowAgentCallTranscriptProps) {
130
+ const { ref: viewportRef, isVisible } = useInViewport();
131
+ const {
132
+ execution,
133
+ phase,
134
+ isLoading,
135
+ isReconnecting,
136
+ error,
137
+ reconnect,
138
+ } = useLiveAgentExecution(childExecutionId, { live: isVisible });
139
+
140
+ // Pending file reviews from the child's own (streamed) status — see the
141
+ // component doc for why the parent's pending_file_reviews is not the
142
+ // source. Terminal executions never dock: their AWAITING_REVIEW sets
143
+ // are settled history and render as in-thread records instead.
144
+ const terminal = isTerminalPhase(phase);
145
+ const pendingReviewSets = useMemo<readonly FileChangeSet[]>(() => {
146
+ if (terminal) return [];
147
+ return displayFileChangeSets(execution?.status).filter(
148
+ (cs) =>
149
+ cs.status === FileChangeSetStatus.AWAITING_REVIEW &&
150
+ cs.changes.length > 0,
151
+ );
152
+ }, [execution?.status, terminal]);
153
+
154
+ // Bind the child id into the dock's card-level submit signature. Deps
155
+ // are the specific method (DD-010), so the callback survives unrelated
156
+ // in-flight churn on the actions instance.
157
+ const submitFileDecision = hitl?.submitFileDecision;
158
+ const handleFileDecision = useCallback(
159
+ (changeSetId: string, action: FileDecisionAction, options?: FileDecisionOptions) => {
160
+ submitFileDecision?.(childExecutionId, changeSetId, action, options);
161
+ },
162
+ [submitFileDecision, childExecutionId],
163
+ );
164
+
165
+ const showBar = isReconnecting || !!onNavigateToAgentExecution;
166
+
167
+ return (
168
+ <div
169
+ ref={viewportRef}
170
+ role="group"
171
+ aria-label={
172
+ agentSlug ? `Transcript of agent ${agentSlug}` : "Agent transcript"
173
+ }
174
+ className={cn("flex max-h-[60vh] min-w-0 flex-col", className)}
175
+ >
176
+ {/* Only what the card header cannot express: the child stream's
177
+ reconnecting state and the standalone pop-out. */}
178
+ {showBar && (
179
+ <div className="flex min-w-0 shrink-0 items-center gap-3 pb-1.5">
180
+ {isReconnecting && (
181
+ <span className="text-xs text-muted-foreground">Reconnecting…</span>
182
+ )}
183
+ {onNavigateToAgentExecution && (
184
+ <button
185
+ type="button"
186
+ onClick={() => onNavigateToAgentExecution(childExecutionId)}
187
+ className="ml-auto inline-flex shrink-0 items-center gap-1.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm"
188
+ >
189
+ <PopOutIcon />
190
+ Open standalone
191
+ </button>
192
+ )}
193
+ </div>
194
+ )}
195
+
196
+ {/* The thread owns the scrolling inside the bounded box (its
197
+ auto-scroll machine + jump-to-latest need the scroll container);
198
+ min-h-0 lets the flex slot shrink to the cap. */}
199
+ <div className="min-h-0 min-w-0 flex-1">
200
+ {execution ? (
201
+ <MessageThread
202
+ executions={EMPTY_EXECUTIONS}
203
+ activeStreamExecution={execution}
204
+ onApprovalSubmit={hitl?.submitApproval}
205
+ submittingApprovalIds={hitl?.approvalSubmittingToolCallIds}
206
+ approvalErrors={hitl?.approvalErrorsByToolCallId}
207
+ showFileReviewRecords={hitl != null}
208
+ className="h-full"
209
+ />
210
+ ) : isLoading ? (
211
+ <ThreadSkeleton className="py-2" />
212
+ ) : error ? (
213
+ <div
214
+ role="alert"
215
+ className="flex flex-col items-start gap-2 py-2"
216
+ >
217
+ <p className="text-xs text-destructive">{error.message}</p>
218
+ <button
219
+ type="button"
220
+ onClick={reconnect}
221
+ className="rounded border border-border px-3 py-1.5 text-xs font-medium text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
222
+ >
223
+ Retry
224
+ </button>
225
+ </div>
226
+ ) : (
227
+ <div role="status" className="flex flex-col gap-1 py-2">
228
+ <p className="text-xs font-medium text-foreground">
229
+ This agent execution is no longer available.
230
+ </p>
231
+ <p className="text-xs text-muted-foreground">
232
+ It may have been removed, or the workflow run predates
233
+ transcript retention.
234
+ </p>
235
+ </div>
236
+ )}
237
+ </div>
238
+
239
+ {/* Pending file reviews dock below the thread, inside the bounded
240
+ box — the decision the child is blocked on can never scroll out
241
+ of view. Same shape as the session's composer dock. */}
242
+ {hitl && pendingReviewSets.length > 0 && (
243
+ <FileReviewDock
244
+ changeSets={pendingReviewSets}
245
+ onSubmit={handleFileDecision}
246
+ submittingDecisionKeys={hitl.fileDecisionSubmittingKeys}
247
+ decisionErrors={hitl.fileDecisionErrorsByKey}
248
+ className="shrink-0"
249
+ />
250
+ )}
251
+ </div>
252
+ );
253
+ },
254
+ );
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Inline SVG icons (SDK independence — no lucide dependency)
258
+ // ---------------------------------------------------------------------------
259
+
260
+ function PopOutIcon() {
261
+ return (
262
+ <svg
263
+ width="12"
264
+ height="12"
265
+ viewBox="0 0 12 12"
266
+ fill="none"
267
+ stroke="currentColor"
268
+ strokeWidth="1.3"
269
+ strokeLinecap="round"
270
+ strokeLinejoin="round"
271
+ className="shrink-0"
272
+ aria-hidden="true"
273
+ >
274
+ <path d="M10.5 4.5V1.5H7.5" />
275
+ <path d="M10.5 1.5L6 6" />
276
+ <path d="M8.5 7v3a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h3" />
277
+ </svg>
278
+ );
279
+ }
@@ -289,10 +289,14 @@ export function WorkflowDetailView({
289
289
  <ValidationIndicator state={validationState} />
290
290
  ) : undefined;
291
291
 
292
- // Inline visibility is read-only (at-a-glance); editing lives in the
293
- // Manage access dialog, the single writer for both access axes.
292
+ // Inline visibility is at-a-glance AND a shortcut into the Manage access
293
+ // dialog the single writer for both access axes. The chip is navigation
294
+ // only; it stays a static badge for users who cannot view access.
294
295
  const visibilityControl = meta ? (
295
- <VisibilityBadge visibility={meta.visibility} />
296
+ <VisibilityBadge
297
+ visibility={meta.visibility}
298
+ onClick={access.action ? access.open : undefined}
299
+ />
296
300
  ) : undefined;
297
301
 
298
302
  const mergedActions = access.action