@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
@@ -1,11 +1,22 @@
1
1
  "use client";
2
2
 
3
+ import { useId } from "react";
3
4
  import type { McpServerUsageInput, ResourceRef } from "@stigmer/sdk";
4
- import { cn } from "@stigmer/theme";
5
+ import { Copy, FileJson, FileText } from "lucide-react";
5
6
  import type { HarnessOption } from "../../models/harness.js";
6
7
  import { HARNESS_META } from "../../models/harness.js";
8
+ import { Switch } from "../../switch/Switch.js";
7
9
  import type { ExecutionTargetOption } from "../execution-target.js";
8
10
  import type { UseSessionVariablesReturn } from "../../execution/useSessionVariables.js";
11
+ import { useExportTranscript } from "../useExportTranscript.js";
12
+ import {
13
+ FACET_ROW_BUTTON,
14
+ FacetEmptyHint,
15
+ FacetKeyValueRow,
16
+ FacetRemoveButton,
17
+ FacetRow,
18
+ FacetSection,
19
+ } from "./primitives.js";
9
20
 
10
21
  /** Interactive mutation callbacks for config items in SetupTab. */
11
22
  export interface SetupTabMutationCallbacks {
@@ -17,6 +28,24 @@ export interface SetupTabMutationCallbacks {
17
28
  readonly onRemoveSkill?: (ref: ResourceRef) => void;
18
29
  }
19
30
 
31
+ /**
32
+ * Wiring for the Run Config section's auto-approve switch — the session-level
33
+ * arming control (stigmer/stigmer#816 rework: config lives in Config, not the
34
+ * composer). Session-scoped: an explicit flip beats the account's
35
+ * `default_auto_approve` preference and the host's `approvalDefaults` for
36
+ * THIS conversation only, and is never persisted anywhere.
37
+ *
38
+ * Presence renders the switch; absence keeps the section read-only. Consumers
39
+ * must only wire it on surfaces whose viewer may submit approvals — never
40
+ * observer or guest audiences (the approval-submission withhold).
41
+ */
42
+ export interface SetupTabAutoApprove {
43
+ /** Whether auto-approve is currently armed for this conversation. */
44
+ readonly armed: boolean;
45
+ /** Called with the next value when the user flips the switch. */
46
+ readonly onChange: (armed: boolean) => void;
47
+ }
48
+
20
49
  /** Props for {@link SetupTab}. */
21
50
  export interface SetupTabProps {
22
51
  readonly agentRef: ResourceRef | null;
@@ -27,6 +56,23 @@ export interface SetupTabProps {
27
56
  readonly harness: HarnessOption;
28
57
  readonly executionTarget: ExecutionTargetOption | undefined;
29
58
  readonly modelId: string | undefined;
59
+ /**
60
+ * Session-level auto-approve switch in the Run Config section. When
61
+ * provided, the switch renders and is interactive at all times — including
62
+ * mid-run, where flipping it ON releases the in-flight execution's pending
63
+ * gates (the flow's standing responder). When absent (read-only audiences,
64
+ * demo fixtures), the section shows no approval control at all.
65
+ */
66
+ readonly autoApprove?: SetupTabAutoApprove;
67
+ /**
68
+ * The session whose transcript the Session section exports. When provided,
69
+ * the facet renders Copy / Download Markdown / Download JSON actions
70
+ * (stigmer/stigmer#814's export, relocated from the header). Omit on the
71
+ * launcher (no session exists yet) and in inert fixtures. Deliberately not
72
+ * audience-gated: the export is `can_view`-scoped — it serializes exactly
73
+ * what the viewer already shows — so observers keep it.
74
+ */
75
+ readonly sessionId?: string | null;
30
76
  /**
31
77
  * Interactive mutation callbacks. When provided, items render remove
32
78
  * buttons. When absent, sections are read-only (DD-011).
@@ -43,15 +89,18 @@ export interface SetupTabProps {
43
89
  }
44
90
 
45
91
  /**
46
- * Persistent session configuration panel (Configure tab) — shows agent,
47
- * MCP servers, skills, run config, session variables, and the host's
92
+ * Persistent session configuration panel (Config facet) — shows run config
93
+ * (harness, model, target, and the session-level auto-approve switch), agent,
94
+ * MCP servers, skills, session variables, transcript export, and the host's
48
95
  * access management control (via `accessSlot`).
49
96
  *
50
- * Workspace management has moved to the dedicated Workspace tab.
97
+ * Rendered in the session panel's shared facet vocabulary (see
98
+ * `./primitives.tsx`): dense rows, quiet right-aligned metadata, and
99
+ * hover/focus-revealed actions — the Artifacts facet's idiom.
51
100
  *
52
101
  * When mutation callbacks are provided via `mutations`, items render
53
- * inline remove/reconfigure affordances. When callbacks are absent,
54
- * sections render read-only (backward compatible, DD-011).
102
+ * inline remove affordances. When callbacks are absent, sections render
103
+ * read-only (backward compatible, DD-011).
55
104
  *
56
105
  * All visual properties flow through `--stgm-*` tokens (DD-005).
57
106
  */
@@ -64,6 +113,8 @@ export function SetupTab({
64
113
  harness,
65
114
  executionTarget,
66
115
  modelId,
116
+ autoApprove,
117
+ sessionId,
67
118
  mutations,
68
119
  accessSlot,
69
120
  }: SetupTabProps) {
@@ -75,6 +126,7 @@ export function SetupTab({
75
126
  harness={harness}
76
127
  executionTarget={executionTarget}
77
128
  modelId={modelId}
129
+ autoApprove={autoApprove}
78
130
  />
79
131
 
80
132
  <AgentSection
@@ -97,53 +149,13 @@ export function SetupTab({
97
149
  <SessionVarsSection entries={sessionVariables.entries} />
98
150
  )}
99
151
 
152
+ {sessionId && <TranscriptSection sessionId={sessionId} />}
153
+
100
154
  {accessSlot && <section>{accessSlot}</section>}
101
155
  </div>
102
156
  );
103
157
  }
104
158
 
105
- // ---------------------------------------------------------------------------
106
- // Shared section primitives
107
- // ---------------------------------------------------------------------------
108
-
109
- function SectionHeading({ children }: { children: React.ReactNode }) {
110
- return (
111
- <h3 className="stg:text-[0.65rem] stg:font-semibold stg:uppercase stg:tracking-wider stg:text-muted-foreground">
112
- {children}
113
- </h3>
114
- );
115
- }
116
-
117
- function EmptyHint({ children }: { children: React.ReactNode }) {
118
- return (
119
- <p className="stg:text-xs stg:text-muted-foreground/70">{children}</p>
120
- );
121
- }
122
-
123
- function ItemPill({ children, className: cls }: { children: React.ReactNode; className?: string }) {
124
- return (
125
- <span className={cn(
126
- "stg:inline-flex stg:items-center stg:gap-1.5 stg:rounded-md stg:bg-muted-subtle stg:px-2 stg:py-1 stg:text-xs stg:text-foreground",
127
- cls,
128
- )}>
129
- {children}
130
- </span>
131
- );
132
- }
133
-
134
- function RemoveButton({ onClick, label }: { onClick: () => void; label: string }) {
135
- return (
136
- <button
137
- type="button"
138
- onClick={onClick}
139
- className="stg:shrink-0 stg:text-muted-foreground stg:hover:text-destructive stg:transition-colors"
140
- aria-label={label}
141
- >
142
- <XIcon />
143
- </button>
144
- );
145
- }
146
-
147
159
  // ---------------------------------------------------------------------------
148
160
  // Run config
149
161
  // ---------------------------------------------------------------------------
@@ -152,33 +164,38 @@ function RunConfigSection({
152
164
  harness,
153
165
  executionTarget,
154
166
  modelId,
167
+ autoApprove,
155
168
  }: {
156
169
  harness: HarnessOption;
157
170
  executionTarget: ExecutionTargetOption | undefined;
158
171
  modelId: string | undefined;
172
+ autoApprove: SetupTabAutoApprove | undefined;
159
173
  }) {
174
+ const autoApproveLabelId = useId();
175
+
160
176
  return (
161
- <section className="stg:flex stg:flex-col stg:gap-1.5">
162
- <SectionHeading>Run Config</SectionHeading>
163
- <div className="stg:flex stg:flex-wrap stg:gap-1.5">
164
- <ItemPill>
165
- <span className="stg:text-muted-foreground">Harness</span>
166
- {HARNESS_META[harness]?.label ?? harness}
167
- </ItemPill>
168
- {modelId && (
169
- <ItemPill>
170
- <span className="stg:text-muted-foreground">Model</span>
171
- {modelId}
172
- </ItemPill>
173
- )}
174
- {executionTarget && (
175
- <ItemPill>
176
- <span className="stg:text-muted-foreground">Target</span>
177
- {executionTarget === "local" ? "Local" : "Cloud"}
178
- </ItemPill>
179
- )}
180
- </div>
181
- </section>
177
+ <FacetSection heading="Run Config">
178
+ <FacetKeyValueRow label="Harness">
179
+ {HARNESS_META[harness]?.label ?? harness}
180
+ </FacetKeyValueRow>
181
+ {modelId && (
182
+ <FacetKeyValueRow label="Model">{modelId}</FacetKeyValueRow>
183
+ )}
184
+ {executionTarget && (
185
+ <FacetKeyValueRow label="Target">
186
+ {executionTarget === "local" ? "Local" : "Cloud"}
187
+ </FacetKeyValueRow>
188
+ )}
189
+ {autoApprove && (
190
+ <FacetKeyValueRow label="Auto-approve" labelId={autoApproveLabelId}>
191
+ <Switch
192
+ checked={autoApprove.armed}
193
+ onCheckedChange={autoApprove.onChange}
194
+ aria-labelledby={autoApproveLabelId}
195
+ />
196
+ </FacetKeyValueRow>
197
+ )}
198
+ </FacetSection>
182
199
  );
183
200
  }
184
201
 
@@ -196,26 +213,25 @@ function AgentSection({
196
213
  onRemove?: () => void;
197
214
  }) {
198
215
  return (
199
- <section className="stg:flex stg:flex-col stg:gap-1.5">
200
- <SectionHeading>Agent</SectionHeading>
216
+ <FacetSection heading="Agent">
201
217
  {agentRef ? (
202
- <div className="stg:flex stg:items-center stg:gap-1.5">
203
- <ItemPill>
204
- {agentRef.slug}
205
- {onRemove && !isDefaultAgent && (
206
- <RemoveButton onClick={onRemove} label={`Remove agent ${agentRef.slug}`} />
207
- )}
208
- </ItemPill>
209
- {isDefaultAgent && (
210
- <span className="stg:rounded stg:bg-primary/10 stg:px-1.5 stg:py-0.5 stg:text-[0.6rem] stg:font-medium stg:text-primary">
211
- default
212
- </span>
213
- )}
214
- </div>
218
+ <FacetRow
219
+ meta={isDefaultAgent ? "default" : undefined}
220
+ actions={
221
+ onRemove && !isDefaultAgent ? (
222
+ <FacetRemoveButton
223
+ onClick={onRemove}
224
+ label={`Remove agent ${agentRef.slug}`}
225
+ />
226
+ ) : undefined
227
+ }
228
+ >
229
+ <span className="stg:truncate">{agentRef.slug}</span>
230
+ </FacetRow>
215
231
  ) : (
216
- <EmptyHint>No agent selected — using platform default.</EmptyHint>
232
+ <FacetEmptyHint>No agent selected — using platform default.</FacetEmptyHint>
217
233
  )}
218
- </section>
234
+ </FacetSection>
219
235
  );
220
236
  }
221
237
 
@@ -231,40 +247,36 @@ function McpSection({
231
247
  onRemove?: (ref: ResourceRef) => void;
232
248
  }) {
233
249
  return (
234
- <section className="stg:flex stg:flex-col stg:gap-1.5">
235
- <SectionHeading>
236
- MCP Servers
237
- {mcpServerUsages.length > 0 && (
238
- <span className="stg:ml-1 stg:text-muted-foreground/60">({mcpServerUsages.length})</span>
239
- )}
240
- </SectionHeading>
250
+ <FacetSection heading="MCP Servers" count={mcpServerUsages.length}>
241
251
  {mcpServerUsages.length > 0 ? (
242
- <div className="stg:flex stg:flex-col stg:gap-1">
243
- {mcpServerUsages.map((usage) => {
244
- const slug = usage.mcpServerRef.slug;
245
- const enabledCount = usage.enabledTools?.length;
246
- return (
247
- <ItemPill key={`${usage.mcpServerRef.org}/${slug}`}>
248
- <span>{slug}</span>
249
- {enabledCount != null && enabledCount > 0 && (
250
- <span className="stg:text-muted-foreground">
251
- {enabledCount} tool{enabledCount !== 1 ? "s" : ""}
252
- </span>
253
- )}
254
- {onRemove && (
255
- <RemoveButton
252
+ mcpServerUsages.map((usage) => {
253
+ const slug = usage.mcpServerRef.slug;
254
+ const enabledCount = usage.enabledTools?.length;
255
+ return (
256
+ <FacetRow
257
+ key={`${usage.mcpServerRef.org}/${slug}`}
258
+ meta={
259
+ enabledCount != null && enabledCount > 0
260
+ ? `${enabledCount} tool${enabledCount !== 1 ? "s" : ""}`
261
+ : undefined
262
+ }
263
+ actions={
264
+ onRemove ? (
265
+ <FacetRemoveButton
256
266
  onClick={() => onRemove(usage.mcpServerRef)}
257
267
  label={`Remove MCP server ${slug}`}
258
268
  />
259
- )}
260
- </ItemPill>
261
- );
262
- })}
263
- </div>
269
+ ) : undefined
270
+ }
271
+ >
272
+ <span className="stg:truncate">{slug}</span>
273
+ </FacetRow>
274
+ );
275
+ })
264
276
  ) : (
265
- <EmptyHint>No MCP servers attached.</EmptyHint>
277
+ <FacetEmptyHint>No MCP servers attached.</FacetEmptyHint>
266
278
  )}
267
- </section>
279
+ </FacetSection>
268
280
  );
269
281
  }
270
282
 
@@ -280,31 +292,27 @@ function SkillsSection({
280
292
  onRemove?: (ref: ResourceRef) => void;
281
293
  }) {
282
294
  return (
283
- <section className="stg:flex stg:flex-col stg:gap-1.5">
284
- <SectionHeading>
285
- Skills
286
- {skillRefs.length > 0 && (
287
- <span className="stg:ml-1 stg:text-muted-foreground/60">({skillRefs.length})</span>
288
- )}
289
- </SectionHeading>
295
+ <FacetSection heading="Skills" count={skillRefs.length}>
290
296
  {skillRefs.length > 0 ? (
291
- <div className="stg:flex stg:flex-wrap stg:gap-1.5">
292
- {skillRefs.map((ref) => (
293
- <ItemPill key={`${ref.org}/${ref.slug}`}>
294
- {ref.slug}
295
- {onRemove && (
296
- <RemoveButton
297
+ skillRefs.map((ref) => (
298
+ <FacetRow
299
+ key={`${ref.org}/${ref.slug}`}
300
+ actions={
301
+ onRemove ? (
302
+ <FacetRemoveButton
297
303
  onClick={() => onRemove(ref)}
298
304
  label={`Remove skill ${ref.slug}`}
299
305
  />
300
- )}
301
- </ItemPill>
302
- ))}
303
- </div>
306
+ ) : undefined
307
+ }
308
+ >
309
+ <span className="stg:truncate">{ref.slug}</span>
310
+ </FacetRow>
311
+ ))
304
312
  ) : (
305
- <EmptyHint>No skills attached.</EmptyHint>
313
+ <FacetEmptyHint>No skills attached.</FacetEmptyHint>
306
314
  )}
307
- </section>
315
+ </FacetSection>
308
316
  );
309
317
  }
310
318
 
@@ -318,37 +326,65 @@ function SessionVarsSection({
318
326
  entries: UseSessionVariablesReturn["entries"];
319
327
  }) {
320
328
  return (
321
- <section className="stg:flex stg:flex-col stg:gap-1.5">
322
- <SectionHeading>
323
- Session Variables
324
- <span className="stg:ml-1 stg:font-normal stg:normal-case stg:tracking-normal stg:text-muted-foreground/60">
325
- (next message only)
326
- </span>
327
- </SectionHeading>
328
- <div className="stg:flex stg:flex-col stg:gap-1">
329
- {entries.map((entry) => (
330
- <ItemPill key={entry.id}>
331
- <span className="stg:font-medium">{entry.key || "(unnamed)"}</span>
332
- {entry.isSecret ? (
333
- <span className="stg:text-muted-foreground">********</span>
329
+ <FacetSection heading="Session Variables" annotation="next message only">
330
+ {entries.map((entry) => (
331
+ <FacetRow
332
+ key={entry.id}
333
+ meta={
334
+ entry.isSecret ? (
335
+ "********"
334
336
  ) : (
335
- <span className="stg:max-w-[140px] stg:truncate stg:text-muted-foreground">{entry.value}</span>
336
- )}
337
- </ItemPill>
338
- ))}
339
- </div>
340
- </section>
337
+ <span className="stg:inline-block stg:max-w-[140px] stg:truncate stg:align-bottom">
338
+ {entry.value}
339
+ </span>
340
+ )
341
+ }
342
+ >
343
+ <span className="stg:truncate stg:font-medium">
344
+ {entry.key || "(unnamed)"}
345
+ </span>
346
+ </FacetRow>
347
+ ))}
348
+ </FacetSection>
341
349
  );
342
350
  }
343
351
 
344
352
  // ---------------------------------------------------------------------------
345
- // Inline SVG icons (SDK independence no lucide dependency)
353
+ // Transcript export (stigmer/stigmer#814, relocated from the viewer header)
346
354
  // ---------------------------------------------------------------------------
347
355
 
348
- function XIcon() {
356
+ function TranscriptSection({ sessionId }: { sessionId: string }) {
357
+ const exporter = useExportTranscript(sessionId);
358
+
349
359
  return (
350
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
351
- <path d="M4 4L10 10M10 4L4 10" />
352
- </svg>
360
+ <FacetSection heading="Transcript">
361
+ <button
362
+ type="button"
363
+ onClick={() => void exporter.copyMarkdown()}
364
+ disabled={exporter.isExporting}
365
+ className={FACET_ROW_BUTTON}
366
+ >
367
+ <Copy className="stg:size-3.5 stg:shrink-0" aria-hidden="true" />
368
+ Copy transcript
369
+ </button>
370
+ <button
371
+ type="button"
372
+ onClick={() => void exporter.downloadMarkdown()}
373
+ disabled={exporter.isExporting}
374
+ className={FACET_ROW_BUTTON}
375
+ >
376
+ <FileText className="stg:size-3.5 stg:shrink-0" aria-hidden="true" />
377
+ Download Markdown
378
+ </button>
379
+ <button
380
+ type="button"
381
+ onClick={() => void exporter.downloadJson()}
382
+ disabled={exporter.isExporting}
383
+ className={FACET_ROW_BUTTON}
384
+ >
385
+ <FileJson className="stg:size-3.5 stg:shrink-0" aria-hidden="true" />
386
+ Download JSON
387
+ </button>
388
+ </FacetSection>
353
389
  );
354
390
  }
@@ -4,6 +4,7 @@ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexe
4
4
  import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
5
5
  import { useSessionUsage, type ExecutionUsageEntry } from "../useSessionUsage.js";
6
6
  import { UsageWidget, formatCost } from "../../execution/UsageWidget.js";
7
+ import { FacetSection } from "./primitives.js";
7
8
 
8
9
  export interface UsageTabProps {
9
10
  readonly executions: readonly AgentExecution[];
@@ -21,7 +22,8 @@ export interface UsageTabProps {
21
22
  * left in control). Requested-vs-billed divergence is a platform alarm
22
23
  * (`stigmer.billing.service_tier.mismatch`), not a per-row UI concern.
23
24
  *
24
- * Shows an empty state when no usage data is available.
25
+ * Rendered in the session panel's shared facet vocabulary (see
26
+ * `./primitives.tsx`). Shows an empty state when no usage data is available.
25
27
  */
26
28
  export function UsageTab({ executions }: UsageTabProps) {
27
29
  const usage = useSessionUsage(executions);
@@ -37,7 +39,7 @@ export function UsageTab({ executions }: UsageTabProps) {
37
39
  }
38
40
 
39
41
  return (
40
- <div className="stg:flex stg:flex-col stg:gap-4">
42
+ <div className="stg:flex stg:flex-col stg:gap-5">
41
43
  <UsageWidget executions={executions} />
42
44
  {usage.executionBreakdown.length > 0 && (
43
45
  <ExecutionModelList
@@ -61,43 +63,41 @@ function ExecutionModelList({
61
63
  readonly executions: readonly AgentExecution[];
62
64
  }) {
63
65
  return (
64
- <div
65
- className="stg:flex stg:flex-col stg:gap-1"
66
- role="list"
67
- aria-label="Per-execution model and tier"
68
- >
69
- <div className="stg:text-xs stg:font-medium stg:text-foreground">
70
- Models per run
66
+ <FacetSection heading="Models per run">
67
+ <div role="list" aria-label="Per-execution model and tier" className="stg:flex stg:flex-col">
68
+ {entries.map((entry, index) => {
69
+ const requestedTier = requestedTierLabel(executions, entry.executionId);
70
+ return (
71
+ <div
72
+ key={entry.executionId}
73
+ className="stg:flex stg:items-baseline stg:justify-between stg:gap-2 stg:px-2 stg:py-1 stg:text-xs"
74
+ role="listitem"
75
+ >
76
+ <span className="stg:min-w-0 stg:truncate stg:text-foreground">
77
+ <span className="stg:tabular-nums stg:text-muted-foreground">
78
+ #{index + 1}
79
+ </span>{" "}
80
+ {entry.resolvedModel || "—"}
81
+ {requestedTier && <RowBadge>{requestedTier} requested</RowBadge>}
82
+ {entry.isEstimated && <RowBadge>Estimated</RowBadge>}
83
+ </span>
84
+ <span className="stg:shrink-0 stg:tabular-nums stg:text-[0.65rem] stg:text-muted-foreground">
85
+ {formatCost(entry.billableCostUsd)}
86
+ </span>
87
+ </div>
88
+ );
89
+ })}
71
90
  </div>
72
- {entries.map((entry, index) => {
73
- const requestedTier = requestedTierLabel(executions, entry.executionId);
74
- return (
75
- <div
76
- key={entry.executionId}
77
- className="stg:flex stg:items-baseline stg:justify-between stg:gap-2 stg:text-xs stg:text-muted-foreground"
78
- role="listitem"
79
- >
80
- <span className="stg:truncate">
81
- <span className="stg:tabular-nums">#{index + 1}</span>{" "}
82
- {entry.resolvedModel || "—"}
83
- {requestedTier && (
84
- <span className="stg:ml-1 stg:rounded stg:bg-muted stg:px-1 stg:py-0.5 stg:text-[10px] stg:font-medium stg:leading-none">
85
- {requestedTier} requested
86
- </span>
87
- )}
88
- {entry.isEstimated && (
89
- <span className="stg:ml-1 stg:rounded stg:bg-muted stg:px-1 stg:py-0.5 stg:text-[10px] stg:font-medium stg:leading-none">
90
- Estimated
91
- </span>
92
- )}
93
- </span>
94
- <span className="stg:shrink-0 stg:tabular-nums">
95
- {formatCost(entry.billableCostUsd)}
96
- </span>
97
- </div>
98
- );
99
- })}
100
- </div>
91
+ </FacetSection>
92
+ );
93
+ }
94
+
95
+ /** Quiet inline qualifier chip for a provenance row. */
96
+ function RowBadge({ children }: { readonly children: React.ReactNode }) {
97
+ return (
98
+ <span className="stg:ml-1 stg:rounded stg:bg-muted stg:px-1 stg:py-0.5 stg:text-[0.6rem] stg:font-medium stg:leading-none stg:text-muted-foreground">
99
+ {children}
100
+ </span>
101
101
  );
102
102
  }
103
103
 
@@ -11,4 +11,5 @@ export { SetupTab } from "./SetupTab.js";
11
11
  export type {
12
12
  SetupTabProps,
13
13
  SetupTabMutationCallbacks,
14
+ SetupTabAutoApprove,
14
15
  } from "./SetupTab.js";