@stigmer/react 3.12.7 → 3.12.9

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 (196) hide show
  1. package/README.md +1 -3
  2. package/composer/ComposerToolbar.d.ts +1 -19
  3. package/composer/ComposerToolbar.d.ts.map +1 -1
  4. package/composer/ComposerToolbar.js +2 -7
  5. package/composer/ComposerToolbar.js.map +1 -1
  6. package/composer/SessionComposer.d.ts +0 -10
  7. package/composer/SessionComposer.d.ts.map +1 -1
  8. package/composer/SessionComposer.js +2 -2
  9. package/composer/SessionComposer.js.map +1 -1
  10. package/composer/index.d.ts +0 -1
  11. package/composer/index.d.ts.map +1 -1
  12. package/composer/index.js.map +1 -1
  13. package/conversation/ConversationTimelineView.d.ts +15 -1
  14. package/conversation/ConversationTimelineView.d.ts.map +1 -1
  15. package/conversation/ConversationTimelineView.js +3 -2
  16. package/conversation/ConversationTimelineView.js.map +1 -1
  17. package/conversation/ConversationsWorkbench.d.ts +14 -1
  18. package/conversation/ConversationsWorkbench.d.ts.map +1 -1
  19. package/conversation/ConversationsWorkbench.js +11 -2
  20. package/conversation/ConversationsWorkbench.js.map +1 -1
  21. package/execution/MessageThread.d.ts +33 -2
  22. package/execution/MessageThread.d.ts.map +1 -1
  23. package/execution/MessageThread.js +44 -6
  24. package/execution/MessageThread.js.map +1 -1
  25. package/execution/RecalledMemoriesCard.d.ts +47 -0
  26. package/execution/RecalledMemoriesCard.d.ts.map +1 -0
  27. package/execution/RecalledMemoriesCard.js +58 -0
  28. package/execution/RecalledMemoriesCard.js.map +1 -0
  29. package/execution/ResultView.d.ts.map +1 -1
  30. package/execution/ResultView.js +6 -0
  31. package/execution/ResultView.js.map +1 -1
  32. package/execution/ToolCallItem.d.ts.map +1 -1
  33. package/execution/ToolCallItem.js +13 -2
  34. package/execution/ToolCallItem.js.map +1 -1
  35. package/execution/index.d.ts +2 -0
  36. package/execution/index.d.ts.map +1 -1
  37. package/execution/index.js +1 -0
  38. package/execution/index.js.map +1 -1
  39. package/execution/tool-categories.d.ts +1 -1
  40. package/execution/tool-categories.d.ts.map +1 -1
  41. package/execution/tool-categories.js +6 -0
  42. package/execution/tool-categories.js.map +1 -1
  43. package/identity-account/AccountPreferencesPanel.d.ts +2 -1
  44. package/identity-account/AccountPreferencesPanel.d.ts.map +1 -1
  45. package/identity-account/AccountPreferencesPanel.js +41 -3
  46. package/identity-account/AccountPreferencesPanel.js.map +1 -1
  47. package/identity-account/useAccountExecutionDefaults.d.ts +7 -0
  48. package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -1
  49. package/identity-account/useAccountExecutionDefaults.js +5 -2
  50. package/identity-account/useAccountExecutionDefaults.js.map +1 -1
  51. package/index.d.ts +6 -6
  52. package/index.d.ts.map +1 -1
  53. package/index.js +2 -2
  54. package/index.js.map +1 -1
  55. package/internal/MemoryEnabledRow.d.ts +37 -0
  56. package/internal/MemoryEnabledRow.d.ts.map +1 -0
  57. package/internal/MemoryEnabledRow.js +25 -0
  58. package/internal/MemoryEnabledRow.js.map +1 -0
  59. package/internal/VirtualizedThread.d.ts +3 -1
  60. package/internal/VirtualizedThread.d.ts.map +1 -1
  61. package/internal/VirtualizedThread.js +3 -1
  62. package/internal/VirtualizedThread.js.map +1 -1
  63. package/internal/useAutoScroll.d.ts +18 -0
  64. package/internal/useAutoScroll.d.ts.map +1 -1
  65. package/internal/useAutoScroll.js +50 -0
  66. package/internal/useAutoScroll.js.map +1 -1
  67. package/memory/MemoryProposalCard.d.ts +35 -0
  68. package/memory/MemoryProposalCard.d.ts.map +1 -0
  69. package/memory/MemoryProposalCard.js +73 -0
  70. package/memory/MemoryProposalCard.js.map +1 -0
  71. package/memory/index.d.ts +2 -0
  72. package/memory/index.d.ts.map +1 -1
  73. package/memory/index.js +2 -0
  74. package/memory/index.js.map +1 -1
  75. package/memory/useMemory.d.ts +39 -0
  76. package/memory/useMemory.d.ts.map +1 -0
  77. package/memory/useMemory.js +43 -0
  78. package/memory/useMemory.js.map +1 -0
  79. package/organization/OrgPreferencesPanel.d.ts.map +1 -1
  80. package/organization/OrgPreferencesPanel.js +36 -4
  81. package/organization/OrgPreferencesPanel.js.map +1 -1
  82. package/package.json +4 -4
  83. package/session/AutoApproveIndicator.d.ts +18 -0
  84. package/session/AutoApproveIndicator.d.ts.map +1 -0
  85. package/session/AutoApproveIndicator.js +23 -0
  86. package/session/AutoApproveIndicator.js.map +1 -0
  87. package/session/NewSessionViewer.d.ts.map +1 -1
  88. package/session/NewSessionViewer.js +10 -9
  89. package/session/NewSessionViewer.js.map +1 -1
  90. package/session/SessionViewer.d.ts +21 -9
  91. package/session/SessionViewer.d.ts.map +1 -1
  92. package/session/SessionViewer.js +31 -21
  93. package/session/SessionViewer.js.map +1 -1
  94. package/session/facets/SetupTab.d.ts +43 -6
  95. package/session/facets/SetupTab.d.ts.map +1 -1
  96. package/session/facets/SetupTab.js +32 -36
  97. package/session/facets/SetupTab.js.map +1 -1
  98. package/session/facets/UsageTab.d.ts +2 -1
  99. package/session/facets/UsageTab.d.ts.map +1 -1
  100. package/session/facets/UsageTab.js +11 -5
  101. package/session/facets/UsageTab.js.map +1 -1
  102. package/session/facets/index.d.ts +1 -1
  103. package/session/facets/index.d.ts.map +1 -1
  104. package/session/facets/primitives.d.ts +58 -0
  105. package/session/facets/primitives.d.ts.map +1 -0
  106. package/session/facets/primitives.js +59 -0
  107. package/session/facets/primitives.js.map +1 -0
  108. package/session/index.d.ts +1 -1
  109. package/session/index.d.ts.map +1 -1
  110. package/session/index.js.map +1 -1
  111. package/session/useNewSessionFlow.d.ts +9 -4
  112. package/session/useNewSessionFlow.d.ts.map +1 -1
  113. package/session/useNewSessionFlow.js +20 -9
  114. package/session/useNewSessionFlow.js.map +1 -1
  115. package/session/useSessionPageFlow.d.ts +28 -10
  116. package/session/useSessionPageFlow.d.ts.map +1 -1
  117. package/session/useSessionPageFlow.js +16 -10
  118. package/session/useSessionPageFlow.js.map +1 -1
  119. package/settings/MemorySection.d.ts.map +1 -1
  120. package/settings/MemorySection.js +1 -1
  121. package/settings/MemorySection.js.map +1 -1
  122. package/src/attachment/__tests__/prepare-image.test.ts +12 -6
  123. package/src/composer/ComposerToolbar.tsx +0 -57
  124. package/src/composer/SessionComposer.tsx +1 -13
  125. package/src/composer/index.ts +0 -1
  126. package/src/conversation/ConversationTimelineView.tsx +17 -1
  127. package/src/conversation/ConversationsWorkbench.tsx +25 -0
  128. package/src/conversation/__tests__/ConversationTimelineView.layout.test.tsx +37 -1
  129. package/src/execution/MessageThread.tsx +75 -3
  130. package/src/execution/RecalledMemoriesCard.tsx +164 -0
  131. package/src/execution/ResultView.tsx +6 -0
  132. package/src/execution/ToolCallItem.tsx +18 -1
  133. package/src/execution/__tests__/RecalledMemoriesCard.test.tsx +123 -0
  134. package/src/execution/__tests__/message-thread-recalled-memories.test.tsx +216 -0
  135. package/src/execution/__tests__/message-thread-scroll-on-send.test.tsx +141 -0
  136. package/src/execution/__tests__/message-thread-slots.test.tsx +36 -0
  137. package/src/execution/index.ts +2 -0
  138. package/src/execution/tool-categories.ts +7 -0
  139. package/src/identity-account/AccountPreferencesPanel.tsx +89 -2
  140. package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +112 -0
  141. package/src/identity-account/__tests__/useAccountExecutionDefaults.test.tsx +34 -1
  142. package/src/identity-account/useAccountExecutionDefaults.ts +12 -2
  143. package/src/index.ts +8 -1
  144. package/src/internal/MemoryEnabledRow.tsx +92 -0
  145. package/src/internal/VirtualizedThread.tsx +5 -0
  146. package/src/internal/__tests__/useAutoScroll.layout.test.tsx +105 -4
  147. package/src/internal/__tests__/useAutoScroll.test.tsx +41 -1
  148. package/src/internal/useAutoScroll.ts +54 -0
  149. package/src/memory/MemoryProposalCard.tsx +181 -0
  150. package/src/memory/__tests__/MemoryProposalCard.test.tsx +236 -0
  151. package/src/memory/__tests__/memoryHooks.test.tsx +63 -0
  152. package/src/memory/index.ts +5 -0
  153. package/src/memory/useMemory.ts +80 -0
  154. package/src/organization/OrgPreferencesPanel.tsx +52 -3
  155. package/src/organization/__tests__/OrgPreferencesPanel.test.tsx +65 -0
  156. package/src/session/AutoApproveIndicator.tsx +45 -0
  157. package/src/session/NewSessionViewer.tsx +18 -15
  158. package/src/session/SessionViewer.tsx +70 -32
  159. package/src/session/__tests__/NewSessionViewer.test.tsx +32 -0
  160. package/src/session/__tests__/SessionViewer-autoApprove.test.tsx +244 -0
  161. package/src/session/__tests__/SessionViewer-transcriptExport.test.tsx +60 -22
  162. package/src/session/__tests__/useNewSessionFlow.test.tsx +56 -0
  163. package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +87 -0
  164. package/src/session/facets/SetupTab.tsx +193 -157
  165. package/src/session/facets/UsageTab.tsx +38 -38
  166. package/src/session/facets/index.ts +1 -0
  167. package/src/session/facets/primitives.tsx +169 -0
  168. package/src/session/index.ts +1 -0
  169. package/src/session/useNewSessionFlow.ts +30 -15
  170. package/src/session/useSessionPageFlow.ts +44 -20
  171. package/src/settings/MemorySection.tsx +4 -2
  172. package/src/switch/Switch.tsx +4 -0
  173. package/src/workflow/__tests__/WorkflowExecutionViewer.approvals.test.tsx +26 -7
  174. package/src/workflow/__tests__/task-presentation.test.ts +49 -12
  175. package/src/workflow/__tests__/useWorkflowCanvas.dirty.test.tsx +7 -2
  176. package/src/workflow/__tests__/useWorkflowCanvas.drag.test.tsx +7 -2
  177. package/src/workflow/__tests__/workflow-thread-scroll-on-send.test.tsx +145 -0
  178. package/src/workflow/thread/WorkflowTaskThread.tsx +59 -32
  179. package/src/workflow/thread/task-presentation.ts +52 -7
  180. package/src/workspace/WorkspaceSurface.tsx +26 -3
  181. package/styles.css +1 -1
  182. package/switch/Switch.d.ts +3 -1
  183. package/switch/Switch.d.ts.map +1 -1
  184. package/switch/Switch.js +2 -2
  185. package/switch/Switch.js.map +1 -1
  186. package/workflow/thread/WorkflowTaskThread.d.ts +13 -0
  187. package/workflow/thread/WorkflowTaskThread.d.ts.map +1 -1
  188. package/workflow/thread/WorkflowTaskThread.js +33 -25
  189. package/workflow/thread/WorkflowTaskThread.js.map +1 -1
  190. package/workflow/thread/task-presentation.d.ts.map +1 -1
  191. package/workflow/thread/task-presentation.js +50 -7
  192. package/workflow/thread/task-presentation.js.map +1 -1
  193. package/workspace/WorkspaceSurface.js +11 -2
  194. package/workspace/WorkspaceSurface.js.map +1 -1
  195. package/src/composer/__tests__/ComposerToolbar-autoApprove.test.tsx +0 -80
  196. package/src/session/__tests__/SessionViewer-autoApproveToggle.test.tsx +0 -178
@@ -0,0 +1,145 @@
1
+ // Scroll-on-send wiring pins for WorkflowTaskThread (stigmer-cloud#267):
2
+ // the workflow surface's send-analog is a HITL decision submission —
3
+ // submitting pins the thread (default-ON, opt-out via
4
+ // `scrollOnSend={false}`) BEFORE delegating, so the unblocked run's
5
+ // continuation lands in view. The real scroll mechanics are pinned in
6
+ // `internal/__tests__/useAutoScroll.layout.test.tsx`; this file pins the
7
+ // hitl-wrapper wiring through a spied `jumpToLatest`.
8
+
9
+ import { describe, it, expect, vi, afterEach } from "vitest";
10
+ import { render, screen, fireEvent, cleanup, within } from "@testing-library/react";
11
+ import { create } from "@bufbuild/protobuf";
12
+ import { WorkflowTaskKind } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
13
+ import { ApprovalRequestedPayloadSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/event_pb";
14
+ import type { DerivedTaskState } from "../../internal/store/workflow-execution-event-store";
15
+
16
+ const jumpToLatestSpy = vi.fn();
17
+
18
+ // Replaces only the scroll machine (browser observers happy-dom lacks) with
19
+ // a spied jumpToLatest — the wrapper under test calls it directly.
20
+ vi.mock("../../internal/useAutoScroll", async (importOriginal) => {
21
+ const original =
22
+ await importOriginal<typeof import("../../internal/useAutoScroll")>();
23
+ return {
24
+ ...original,
25
+ useAutoScroll: () => ({
26
+ scrollRef: { current: null },
27
+ sentinelRef: { current: null },
28
+ contentRef: () => {},
29
+ isFollowing: true,
30
+ jumpToLatest: jumpToLatestSpy,
31
+ }),
32
+ };
33
+ });
34
+
35
+ import {
36
+ WorkflowTaskThread,
37
+ type WorkflowThreadHitl,
38
+ } from "../thread/WorkflowTaskThread";
39
+
40
+ afterEach(() => {
41
+ cleanup();
42
+ jumpToLatestSpy.mockClear();
43
+ });
44
+
45
+ function makeHitl(): WorkflowThreadHitl {
46
+ return {
47
+ submitApproval: vi.fn(),
48
+ approvalSubmittingToolCallIds: new Set<string>(),
49
+ approvalErrorsByToolCallId: new Map<string, Error>(),
50
+ submitTaskApproval: vi.fn(),
51
+ taskApprovalSubmittingTaskNames: new Set<string>(),
52
+ taskApprovalErrorsByTaskName: new Map<string, Error>(),
53
+ submitFileDecision: vi.fn(),
54
+ fileDecisionSubmittingKeys: new Set<string>(),
55
+ fileDecisionErrorsByKey: new Map<string, Error>(),
56
+ };
57
+ }
58
+
59
+ /** A gating human_input task carrying its captured request payload. */
60
+ function gatedHumanInput(taskName: string): DerivedTaskState {
61
+ return {
62
+ taskName,
63
+ taskKind: WorkflowTaskKind.human_input,
64
+ status: "waiting_approval",
65
+ durationMs: 0,
66
+ costMicros: 0n,
67
+ tokensUsed: 0n,
68
+ attemptNumber: 1,
69
+ error: "",
70
+ childExecutionId: "",
71
+ agentSlug: "",
72
+ currentToolName: "",
73
+ messagesCount: 0,
74
+ toolCallsCount: 0,
75
+ inputSummary: null,
76
+ outputSummary: null,
77
+ approvalRequest: create(ApprovalRequestedPayloadSchema, {
78
+ prompt: "Ship the release?",
79
+ outcomes: [
80
+ { name: "ship", label: "Ship It" },
81
+ { name: "hold", label: "Hold" },
82
+ ],
83
+ }),
84
+ approvalResolution: null,
85
+ } as DerivedTaskState;
86
+ }
87
+
88
+ function statesOf(...states: DerivedTaskState[]): ReadonlyMap<string, DerivedTaskState> {
89
+ return new Map(states.map((s) => [s.taskName, s]));
90
+ }
91
+
92
+ function submitShipIt(): void {
93
+ const form = screen.getByRole("form", {
94
+ name: "Approval decision for review-gate",
95
+ });
96
+ fireEvent.click(within(form).getByRole("button", { name: "Ship It" }));
97
+ }
98
+
99
+ describe("WorkflowTaskThread — scroll-on-send (stigmer-cloud#267)", () => {
100
+ it("pins the thread when a HITL decision is submitted, then delegates to the workflow-level submit (default-on)", () => {
101
+ const hitl = makeHitl();
102
+ render(
103
+ <WorkflowTaskThread
104
+ taskStates={statesOf(gatedHumanInput("review-gate"))}
105
+ totalTasks={1}
106
+ isRunning
107
+ hitl={hitl}
108
+ />,
109
+ );
110
+ expect(jumpToLatestSpy).not.toHaveBeenCalled();
111
+
112
+ submitShipIt();
113
+
114
+ expect(jumpToLatestSpy).toHaveBeenCalledTimes(1);
115
+ expect(hitl.submitTaskApproval).toHaveBeenCalledWith(
116
+ "review-gate",
117
+ "ship",
118
+ undefined,
119
+ undefined,
120
+ );
121
+ });
122
+
123
+ it("never pins with scrollOnSend={false} — the decision still routes through unchanged", () => {
124
+ const hitl = makeHitl();
125
+ render(
126
+ <WorkflowTaskThread
127
+ taskStates={statesOf(gatedHumanInput("review-gate"))}
128
+ totalTasks={1}
129
+ isRunning
130
+ hitl={hitl}
131
+ scrollOnSend={false}
132
+ />,
133
+ );
134
+
135
+ submitShipIt();
136
+
137
+ expect(jumpToLatestSpy).not.toHaveBeenCalled();
138
+ expect(hitl.submitTaskApproval).toHaveBeenCalledWith(
139
+ "review-gate",
140
+ "ship",
141
+ undefined,
142
+ undefined,
143
+ );
144
+ });
145
+ });
@@ -92,6 +92,19 @@ export interface WorkflowTaskThreadProps {
92
92
  * summaries already on the items.
93
93
  */
94
94
  readonly taskSnapshotsByName?: ReadonlyMap<string, WorkflowTask>;
95
+ /**
96
+ * Scroll to the latest content when the reader submits a HITL decision
97
+ * from a scrolled-up position (stigmer-cloud#267) — the workflow surface's
98
+ * send-analog: approving a gate, deciding a file review, or answering a
99
+ * task-level human_input gate re-engages follow mode, so the run's
100
+ * continuation lands in view. Incoming task activity is unaffected — it
101
+ * still never moves a scrolled-up reader. Default `true` on all three SDK
102
+ * thread surfaces at once (the ratified DD-011 divergence — cross-surface
103
+ * consistency is the point); set `false` to keep today's behavior.
104
+ *
105
+ * @default true
106
+ */
107
+ readonly scrollOnSend?: boolean;
95
108
  /** Additional CSS class names for the root container. */
96
109
  readonly className?: string;
97
110
  }
@@ -135,12 +148,38 @@ export const WorkflowTaskThread = memo(function WorkflowTaskThread({
135
148
  onNavigateToAgentExecution,
136
149
  hitl,
137
150
  taskSnapshotsByName,
151
+ scrollOnSend = true,
138
152
  className,
139
153
  }: WorkflowTaskThreadProps) {
140
154
  const { items, progress } = useWorkflowThreadItems(taskStates, totalTasks);
141
155
  const { scrollRef, sentinelRef, contentRef, isFollowing, jumpToLatest } =
142
156
  useAutoScroll();
143
157
 
158
+ // Scroll-on-send (stigmer-cloud#267): submitting a decision pins the
159
+ // thread before delegating, so the unblocked run's continuation lands in
160
+ // view. The wrapper's identity moves with the bundle's — deliberate: the
161
+ // bundle re-materializes exactly when its gate state flips (see the
162
+ // render-site note below), and a stale wrapper would leak stale state
163
+ // fields. `jumpToLatest` is referentially stable (useAutoScroll pins it).
164
+ const threadHitl = useMemo<WorkflowThreadHitl | undefined>(() => {
165
+ if (!hitl || !scrollOnSend) return hitl;
166
+ return {
167
+ ...hitl,
168
+ submitApproval: (...args: Parameters<WorkflowThreadHitl["submitApproval"]>) => {
169
+ jumpToLatest();
170
+ return hitl.submitApproval(...args);
171
+ },
172
+ submitTaskApproval: (...args: Parameters<WorkflowThreadHitl["submitTaskApproval"]>) => {
173
+ jumpToLatest();
174
+ return hitl.submitTaskApproval(...args);
175
+ },
176
+ submitFileDecision: (...args: Parameters<WorkflowThreadHitl["submitFileDecision"]>) => {
177
+ jumpToLatest();
178
+ return hitl.submitFileDecision(...args);
179
+ },
180
+ };
181
+ }, [hitl, scrollOnSend, jumpToLatest]);
182
+
144
183
  return (
145
184
  <div className={cn("stg:relative stg:flex stg:h-full stg:min-h-0 stg:flex-col", className)}>
146
185
  <ThreadProgressHeader progress={progress} isRunning={isRunning} />
@@ -164,7 +203,7 @@ export const WorkflowTaskThread = memo(function WorkflowTaskThread({
164
203
  // would re-render the whole column per spinner tick. Scoped
165
204
  // here, non-gating cards keep `undefined === undefined` and
166
205
  // their memo bails hold.
167
- hitl={item.status === "waiting_approval" ? hitl : undefined}
206
+ hitl={item.status === "waiting_approval" ? threadHitl : undefined}
168
207
  snapshot={taskSnapshotsByName?.get(item.taskName)}
169
208
  />
170
209
  ))
@@ -412,6 +451,10 @@ const ThreadTaskCard = memo(function ThreadTaskCard({
412
451
  </pre>
413
452
  </BoundedContent>
414
453
  )}
454
+ {/* Carries the card's (wrapped) bundle: a child-gate decision
455
+ inside this transcript is a send too, and the gating card is
456
+ the thread's active tail — pinning the outer thread keeps
457
+ the transcript's continuation in view as it streams. */}
415
458
  <WorkflowAgentCallTranscript
416
459
  childExecutionId={item.childExecutionId}
417
460
  agentSlug={item.agentSlug || undefined}
@@ -596,10 +639,11 @@ function ThreadTaskDetail({
596
639
  readonly inputIO: TaskDetailIO | null;
597
640
  readonly outputIO: TaskDetailIO | null;
598
641
  }) {
599
- const rows: Array<[string, string]> = [["Status", statusLabel(item.status)]];
600
- if (item.durationMs > 0) {
601
- rows.push(["Duration", formatMetaChips({ durationMs: item.durationMs }) ?? ""]);
602
- }
642
+ // No Status/Duration rows (R6-6): the card header is the single source
643
+ // for both — the status glyph and the duration meta chip. The detail
644
+ // body carries only what the header cannot: attempt count, usage, the
645
+ // agent slug.
646
+ const rows: Array<[string, string]> = [];
603
647
  if (item.attemptNumber > 1) rows.push(["Attempt", String(item.attemptNumber)]);
604
648
  const costChip = formatMetaChips({
605
649
  costMicros: item.costMicros,
@@ -612,14 +656,16 @@ function ThreadTaskDetail({
612
656
 
613
657
  return (
614
658
  <div className="stg:flex stg:flex-col stg:gap-2">
615
- <dl className="stg:grid stg:grid-cols-[auto_1fr] stg:gap-x-4 stg:gap-y-1 stg:text-xs">
616
- {rows.map(([label, value]) => (
617
- <div key={label} className="stg:contents">
618
- <dt className="stg:text-muted-foreground">{label}</dt>
619
- <dd className="stg:text-foreground">{value}</dd>
620
- </div>
621
- ))}
622
- </dl>
659
+ {rows.length > 0 && (
660
+ <dl className="stg:grid stg:grid-cols-[auto_1fr] stg:gap-x-4 stg:gap-y-1 stg:text-xs">
661
+ {rows.map(([label, value]) => (
662
+ <div key={label} className="stg:contents">
663
+ <dt className="stg:text-muted-foreground">{label}</dt>
664
+ <dd className="stg:text-foreground">{value}</dd>
665
+ </div>
666
+ ))}
667
+ </dl>
668
+ )}
623
669
 
624
670
  {item.error && (
625
671
  <BoundedContent>
@@ -637,25 +683,6 @@ function ThreadTaskDetail({
637
683
  );
638
684
  }
639
685
 
640
- function statusLabel(status: WorkflowThreadItem["status"]): string {
641
- switch (status) {
642
- case "waiting_approval":
643
- return "Waiting for approval";
644
- case "retrying":
645
- return "Retrying";
646
- case "running":
647
- return "Running";
648
- case "completed":
649
- return "Completed";
650
- case "failed":
651
- return "Failed";
652
- case "skipped":
653
- return "Skipped";
654
- case "pending":
655
- return "Pending";
656
- }
657
- }
658
-
659
686
  // ---------------------------------------------------------------------------
660
687
  // Status glyph — the shared thread-card set, colored by status token (T05)
661
688
  // ---------------------------------------------------------------------------
@@ -124,9 +124,18 @@ export function getTaskPresenter(
124
124
  * the output-envelope follow-up standardizes them (DD-T04-3) — stays a
125
125
  * clean one-line row with zero cost. `set_vars` earns its place from live
126
126
  * data: the runner writes the seeded variables to task output (T05, R2-5).
127
+ * `try_catch` earns it the same way: its output is the try (or catch.do)
128
+ * block's settled result — the `transform` class. `raise_error` earns it
129
+ * from the OTHER `showBody` arm: it always fails, and the preview body
130
+ * renders the full failure detail without a click (R6-5).
127
131
  *
128
- * Only genuinely body-less kinds (control flow, `wait`/`listen`, and the
129
- * snapshot fallback's `unspecified`) stay compact summary rows.
132
+ * Only genuinely body-less kinds stay compact summary rows: `wait`/
133
+ * `listen`, the snapshot fallback's `unspecified`, and the remaining
134
+ * control flow — including `switch_case`, DELIBERATELY (an R6-5
135
+ * refinement): its entire settled content is the one-word flow directive,
136
+ * carried by its always-visible preview line below; a body would only
137
+ * re-render the raw `{__flow_directive__}` marker, failing the "does the
138
+ * body carry content the one-line row cannot?" rule.
130
139
  */
131
140
  const PREVIEW_KINDS: ReadonlySet<WorkflowTaskKind> = new Set([
132
141
  WorkflowTaskKind.transform,
@@ -142,6 +151,8 @@ const PREVIEW_KINDS: ReadonlySet<WorkflowTaskKind> = new Set([
142
151
  WorkflowTaskKind.grpc_call,
143
152
  WorkflowTaskKind.activity_call,
144
153
  WorkflowTaskKind.run_workflow,
154
+ WorkflowTaskKind.try_catch,
155
+ WorkflowTaskKind.raise_error,
145
156
  ]);
146
157
 
147
158
  /** Default disclosure mode for a task kind. */
@@ -218,12 +229,17 @@ function defaultPreviewLine(state: DerivedTaskState): string | null {
218
229
  return waitLine(state);
219
230
  case WorkflowTaskKind.listen:
220
231
  return listenLine(state);
232
+ case WorkflowTaskKind.switch_case:
233
+ return switchCaseLine(state.outputSummary);
234
+ case WorkflowTaskKind.try_catch:
235
+ return tryCatchLine(state);
221
236
  default:
222
- // Control flow (branch-taken / fork progress need graph topology the
223
- // thread does not have — deferred), raise_error (the failure
224
- // precedence already shows the message), the invocation kinds
225
- // (pending the backend output-envelope follow-up), and the snapshot
226
- // fallback's `unspecified` all stay status-only.
237
+ // Remaining control flow (for_each / fork progress need graph
238
+ // topology the thread does not have — deferred), raise_error (the
239
+ // failure precedence already shows the message; its preview body
240
+ // carries the full detail), the invocation kinds (pending the
241
+ // backend output-envelope follow-up), and the snapshot fallback's
242
+ // `unspecified` all stay status-only.
227
243
  return null;
228
244
  }
229
245
  }
@@ -354,6 +370,35 @@ function listenLine(state: DerivedTaskState): string | null {
354
370
  return null;
355
371
  }
356
372
 
373
+ /**
374
+ * `→ approved-path` / `→ exit` — the flow directive the switch settled on
375
+ * (R6-5). The switch executor's only output is the `__flow_directive__`
376
+ * marker carrying the matched case's `then` (a target task name, or the
377
+ * `continue`/`end`/`exit` terminals), and `do-executor` writes raw task
378
+ * output to the event summary — so the branch taken is already on the
379
+ * wire. No case matched means no output: status-only, honestly.
380
+ */
381
+ function switchCaseLine(output: JsonObject | null): string | null {
382
+ const directive = asString(output?.["__flow_directive__"]);
383
+ return directive ? `\u2192 ${directive}` : null;
384
+ }
385
+
386
+ /**
387
+ * `recovered after 2 attempts` / snippet of the settled block result
388
+ * (R6-5). The try task's own `task_retrying` events drive
389
+ * `attemptNumber`, so a completion above attempt 1 IS a catch-retry
390
+ * recovery. A catch.do-path recovery (no retry configured) is NOT
391
+ * distinguishable from plain success in the emitted data — deliberately
392
+ * unmarked: adding a marker to task output would leak presentation into
393
+ * workflow dataflow (`processTaskOutput` consumes it downstream).
394
+ */
395
+ function tryCatchLine(state: DerivedTaskState): string | null {
396
+ if (state.status === "completed" && state.attemptNumber > 1) {
397
+ return `recovered after ${state.attemptNumber} attempts`;
398
+ }
399
+ return state.outputSummary ? valueSnippet(state.outputSummary) : null;
400
+ }
401
+
357
402
  // ---------------------------------------------------------------------------
358
403
  // Struct-reading helpers (defensive by construction)
359
404
  // ---------------------------------------------------------------------------
@@ -695,8 +695,16 @@ function EditorArea({
695
695
  <div className="stg:flex stg:h-full stg:min-h-0 stg:flex-col">
696
696
  {/* Collapse control + tab strip. The far right is kept clear (pr-24):
697
697
  the session viewer floats its top-right controls (host
698
- `headerActions` and the panel chip) over this region. */}
699
- <div className="stg:flex stg:shrink-0 stg:items-stretch stg:border-b stg:border-border stg:pr-24">
698
+ `headerActions` and the panel chip) over this region. Fixed to the
699
+ shared HEADER_ROW_HEIGHT so this row's bottom border stays flush
700
+ with the sidebar facet header's — and so an empty tab strip (no
701
+ open editors) cannot collapse the row. */}
702
+ <div
703
+ className={cn(
704
+ HEADER_ROW_HEIGHT,
705
+ "stg:flex stg:shrink-0 stg:items-stretch stg:border-b stg:border-border stg:pr-24",
706
+ )}
707
+ >
700
708
  <Tooltip>
701
709
  <TooltipTrigger
702
710
  render={
@@ -843,9 +851,24 @@ function Breadcrumbs({
843
851
  // Shared sidebar chrome
844
852
  // ---------------------------------------------------------------------------
845
853
 
854
+ /**
855
+ * One fixed height for every header row across the surface — the sidebar
856
+ * facet header AND the editor area's tab-strip row — so their bottom borders
857
+ * form a single continuous line across the panel. Fixed rather than
858
+ * padding-derived: the two rows have different content (micro-caps title vs
859
+ * text-xs tabs, sometimes no tabs at all), so padding math can never keep
860
+ * them aligned.
861
+ */
862
+ const HEADER_ROW_HEIGHT = "stg:h-8";
863
+
846
864
  function SidebarHeader({ title }: { readonly title: string }) {
847
865
  return (
848
- <div className="stg:shrink-0 stg:border-b stg:border-border stg:px-3 stg:py-2 stg:text-[0.65rem] stg:font-semibold stg:uppercase stg:tracking-wide stg:text-muted-foreground">
866
+ <div
867
+ className={cn(
868
+ HEADER_ROW_HEIGHT,
869
+ "stg:flex stg:shrink-0 stg:items-center stg:border-b stg:border-border stg:px-3 stg:text-[0.65rem] stg:font-semibold stg:uppercase stg:tracking-wide stg:text-muted-foreground",
870
+ )}
871
+ >
849
872
  {title}
850
873
  </div>
851
874
  );