@stigmer/react 3.12.7 → 3.12.8

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 (137) hide show
  1. package/composer/ComposerToolbar.d.ts +1 -19
  2. package/composer/ComposerToolbar.d.ts.map +1 -1
  3. package/composer/ComposerToolbar.js +2 -7
  4. package/composer/ComposerToolbar.js.map +1 -1
  5. package/composer/SessionComposer.d.ts +0 -10
  6. package/composer/SessionComposer.d.ts.map +1 -1
  7. package/composer/SessionComposer.js +2 -2
  8. package/composer/SessionComposer.js.map +1 -1
  9. package/composer/index.d.ts +0 -1
  10. package/composer/index.d.ts.map +1 -1
  11. package/composer/index.js.map +1 -1
  12. package/execution/ResultView.d.ts.map +1 -1
  13. package/execution/ResultView.js +6 -0
  14. package/execution/ResultView.js.map +1 -1
  15. package/execution/ToolCallItem.d.ts.map +1 -1
  16. package/execution/ToolCallItem.js +13 -2
  17. package/execution/ToolCallItem.js.map +1 -1
  18. package/execution/tool-categories.d.ts +1 -1
  19. package/execution/tool-categories.d.ts.map +1 -1
  20. package/execution/tool-categories.js +6 -0
  21. package/execution/tool-categories.js.map +1 -1
  22. package/identity-account/AccountPreferencesPanel.d.ts +2 -1
  23. package/identity-account/AccountPreferencesPanel.d.ts.map +1 -1
  24. package/identity-account/AccountPreferencesPanel.js +41 -3
  25. package/identity-account/AccountPreferencesPanel.js.map +1 -1
  26. package/identity-account/useAccountExecutionDefaults.d.ts +7 -0
  27. package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -1
  28. package/identity-account/useAccountExecutionDefaults.js +5 -2
  29. package/identity-account/useAccountExecutionDefaults.js.map +1 -1
  30. package/index.d.ts +4 -4
  31. package/index.d.ts.map +1 -1
  32. package/index.js +1 -1
  33. package/index.js.map +1 -1
  34. package/internal/MemoryEnabledRow.d.ts +37 -0
  35. package/internal/MemoryEnabledRow.d.ts.map +1 -0
  36. package/internal/MemoryEnabledRow.js +25 -0
  37. package/internal/MemoryEnabledRow.js.map +1 -0
  38. package/memory/MemoryProposalCard.d.ts +35 -0
  39. package/memory/MemoryProposalCard.d.ts.map +1 -0
  40. package/memory/MemoryProposalCard.js +73 -0
  41. package/memory/MemoryProposalCard.js.map +1 -0
  42. package/memory/index.d.ts +2 -0
  43. package/memory/index.d.ts.map +1 -1
  44. package/memory/index.js +2 -0
  45. package/memory/index.js.map +1 -1
  46. package/memory/useMemory.d.ts +39 -0
  47. package/memory/useMemory.d.ts.map +1 -0
  48. package/memory/useMemory.js +43 -0
  49. package/memory/useMemory.js.map +1 -0
  50. package/organization/OrgPreferencesPanel.d.ts.map +1 -1
  51. package/organization/OrgPreferencesPanel.js +36 -4
  52. package/organization/OrgPreferencesPanel.js.map +1 -1
  53. package/package.json +4 -4
  54. package/session/AutoApproveIndicator.d.ts +18 -0
  55. package/session/AutoApproveIndicator.d.ts.map +1 -0
  56. package/session/AutoApproveIndicator.js +23 -0
  57. package/session/AutoApproveIndicator.js.map +1 -0
  58. package/session/NewSessionViewer.d.ts.map +1 -1
  59. package/session/NewSessionViewer.js +10 -9
  60. package/session/NewSessionViewer.js.map +1 -1
  61. package/session/SessionViewer.d.ts +21 -9
  62. package/session/SessionViewer.d.ts.map +1 -1
  63. package/session/SessionViewer.js +31 -21
  64. package/session/SessionViewer.js.map +1 -1
  65. package/session/facets/SetupTab.d.ts +43 -6
  66. package/session/facets/SetupTab.d.ts.map +1 -1
  67. package/session/facets/SetupTab.js +32 -36
  68. package/session/facets/SetupTab.js.map +1 -1
  69. package/session/facets/UsageTab.d.ts +2 -1
  70. package/session/facets/UsageTab.d.ts.map +1 -1
  71. package/session/facets/UsageTab.js +11 -5
  72. package/session/facets/UsageTab.js.map +1 -1
  73. package/session/facets/index.d.ts +1 -1
  74. package/session/facets/index.d.ts.map +1 -1
  75. package/session/facets/primitives.d.ts +58 -0
  76. package/session/facets/primitives.d.ts.map +1 -0
  77. package/session/facets/primitives.js +59 -0
  78. package/session/facets/primitives.js.map +1 -0
  79. package/session/index.d.ts +1 -1
  80. package/session/index.d.ts.map +1 -1
  81. package/session/index.js.map +1 -1
  82. package/session/useNewSessionFlow.d.ts +9 -4
  83. package/session/useNewSessionFlow.d.ts.map +1 -1
  84. package/session/useNewSessionFlow.js +20 -9
  85. package/session/useNewSessionFlow.js.map +1 -1
  86. package/session/useSessionPageFlow.d.ts +28 -10
  87. package/session/useSessionPageFlow.d.ts.map +1 -1
  88. package/session/useSessionPageFlow.js +16 -10
  89. package/session/useSessionPageFlow.js.map +1 -1
  90. package/src/attachment/__tests__/prepare-image.test.ts +12 -6
  91. package/src/composer/ComposerToolbar.tsx +0 -57
  92. package/src/composer/SessionComposer.tsx +1 -13
  93. package/src/composer/index.ts +0 -1
  94. package/src/execution/ResultView.tsx +6 -0
  95. package/src/execution/ToolCallItem.tsx +18 -1
  96. package/src/execution/tool-categories.ts +7 -0
  97. package/src/identity-account/AccountPreferencesPanel.tsx +89 -2
  98. package/src/identity-account/__tests__/AccountPreferencesPanel.test.tsx +112 -0
  99. package/src/identity-account/__tests__/useAccountExecutionDefaults.test.tsx +34 -1
  100. package/src/identity-account/useAccountExecutionDefaults.ts +12 -2
  101. package/src/index.ts +5 -1
  102. package/src/internal/MemoryEnabledRow.tsx +92 -0
  103. package/src/memory/MemoryProposalCard.tsx +181 -0
  104. package/src/memory/__tests__/MemoryProposalCard.test.tsx +236 -0
  105. package/src/memory/__tests__/memoryHooks.test.tsx +63 -0
  106. package/src/memory/index.ts +5 -0
  107. package/src/memory/useMemory.ts +80 -0
  108. package/src/organization/OrgPreferencesPanel.tsx +52 -3
  109. package/src/organization/__tests__/OrgPreferencesPanel.test.tsx +65 -0
  110. package/src/session/AutoApproveIndicator.tsx +45 -0
  111. package/src/session/NewSessionViewer.tsx +18 -15
  112. package/src/session/SessionViewer.tsx +70 -32
  113. package/src/session/__tests__/NewSessionViewer.test.tsx +32 -0
  114. package/src/session/__tests__/SessionViewer-autoApprove.test.tsx +244 -0
  115. package/src/session/__tests__/SessionViewer-transcriptExport.test.tsx +60 -22
  116. package/src/session/__tests__/useNewSessionFlow.test.tsx +56 -0
  117. package/src/session/__tests__/useSessionPageFlow.autoApprove.test.tsx +87 -0
  118. package/src/session/facets/SetupTab.tsx +193 -157
  119. package/src/session/facets/UsageTab.tsx +38 -38
  120. package/src/session/facets/index.ts +1 -0
  121. package/src/session/facets/primitives.tsx +169 -0
  122. package/src/session/index.ts +1 -0
  123. package/src/session/useNewSessionFlow.ts +30 -15
  124. package/src/session/useSessionPageFlow.ts +44 -20
  125. package/src/switch/Switch.tsx +4 -0
  126. package/src/workflow/__tests__/useWorkflowCanvas.dirty.test.tsx +7 -2
  127. package/src/workflow/__tests__/useWorkflowCanvas.drag.test.tsx +7 -2
  128. package/src/workspace/WorkspaceSurface.tsx +26 -3
  129. package/styles.css +1 -1
  130. package/switch/Switch.d.ts +3 -1
  131. package/switch/Switch.d.ts.map +1 -1
  132. package/switch/Switch.js +2 -2
  133. package/switch/Switch.js.map +1 -1
  134. package/workspace/WorkspaceSurface.js +11 -2
  135. package/workspace/WorkspaceSurface.js.map +1 -1
  136. package/src/composer/__tests__/ComposerToolbar-autoApprove.test.tsx +0 -80
  137. package/src/session/__tests__/SessionViewer-autoApproveToggle.test.tsx +0 -178
@@ -0,0 +1,169 @@
1
+ "use client";
2
+
3
+ // Shared visual vocabulary for the session-panel facets (Config / Changes /
4
+ // Artifacts / Usage): one dense, VS Code-style row-and-section language so the
5
+ // facets read as one surface instead of three eras of styling. The Artifacts
6
+ // facet's `ArtifactRowView` set the idiom — compact rows, quiet metadata,
7
+ // hover/focus-revealed actions — and these primitives carry it to the
8
+ // non-file-list facets. All visual properties flow through `--stgm-*`
9
+ // tokens (DD-005).
10
+
11
+ import { cn } from "@stigmer/theme";
12
+
13
+ /**
14
+ * A titled facet section: uppercase micro-heading over flush content.
15
+ * `count` renders the "(N)" suffix the retired inspector used; `annotation`
16
+ * renders a normal-case, muted trailing note (e.g. "next message only").
17
+ */
18
+ export function FacetSection({
19
+ heading,
20
+ count,
21
+ annotation,
22
+ children,
23
+ }: {
24
+ readonly heading: string;
25
+ readonly count?: number;
26
+ readonly annotation?: string;
27
+ readonly children: React.ReactNode;
28
+ }) {
29
+ return (
30
+ <section className="stg:flex stg:flex-col stg:gap-1">
31
+ <h3 className="stg:px-2 stg:text-[0.65rem] stg:font-semibold stg:uppercase stg:tracking-wider stg:text-muted-foreground">
32
+ {heading}
33
+ {count != null && count > 0 && (
34
+ <span className="stg:ml-1 stg:text-muted-foreground-faint">({count})</span>
35
+ )}
36
+ {annotation && (
37
+ <span className="stg:ml-1 stg:font-normal stg:normal-case stg:tracking-normal stg:text-muted-foreground-faint">
38
+ ({annotation})
39
+ </span>
40
+ )}
41
+ </h3>
42
+ {children}
43
+ </section>
44
+ );
45
+ }
46
+
47
+ /**
48
+ * One dense, non-interactive facet row: leading content, optional
49
+ * right-aligned metadata, and hover/focus-revealed trailing actions
50
+ * (the `group` the actions' reveal keys on). Interactive rows (whole-row
51
+ * buttons) build on {@link FACET_ROW_BUTTON} instead.
52
+ */
53
+ export function FacetRow({
54
+ children,
55
+ meta,
56
+ actions,
57
+ className,
58
+ }: {
59
+ readonly children: React.ReactNode;
60
+ /** Right-aligned quiet metadata (counts, sizes, values). */
61
+ readonly meta?: React.ReactNode;
62
+ /** Hover/focus-revealed trailing controls — siblings, never nested. */
63
+ readonly actions?: React.ReactNode;
64
+ readonly className?: string;
65
+ }) {
66
+ return (
67
+ <div
68
+ className={cn(
69
+ "stg:group stg:flex stg:items-center stg:gap-2 stg:px-2 stg:py-1 stg:text-xs",
70
+ className,
71
+ )}
72
+ >
73
+ <span className="stg:flex stg:min-w-0 stg:flex-1 stg:items-center stg:gap-2 stg:text-foreground">
74
+ {children}
75
+ </span>
76
+ {meta && (
77
+ <span className="stg:shrink-0 stg:text-[0.65rem] stg:text-muted-foreground">
78
+ {meta}
79
+ </span>
80
+ )}
81
+ {actions}
82
+ </div>
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Class string for a whole-row interactive facet action (the
88
+ * `ArtifactRowView` open-button idiom): dense row metrics plus hover and
89
+ * focus-visible treatments. A string constant (not a component) so callers
90
+ * keep their own element type and extra classes.
91
+ */
92
+ export const FACET_ROW_BUTTON = cn(
93
+ "stg:flex stg:w-full stg:min-w-0 stg:items-center stg:gap-2 stg:px-2 stg:py-1 stg:text-left stg:text-xs stg:text-muted-foreground stg:transition-colors",
94
+ "stg:hover:bg-muted stg:hover:text-foreground",
95
+ "stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-inset stg:focus-visible:ring-ring",
96
+ "stg:disabled:pointer-events-none stg:disabled:opacity-50",
97
+ );
98
+
99
+ /**
100
+ * Hover/focus-revealed remove control for a {@link FacetRow} — the
101
+ * `ArtifactRowView` download-button reveal pattern applied to removal.
102
+ * Always reachable by keyboard (focus reveals it without hover).
103
+ */
104
+ export function FacetRemoveButton({
105
+ onClick,
106
+ label,
107
+ }: {
108
+ readonly onClick: () => void;
109
+ readonly label: string;
110
+ }) {
111
+ return (
112
+ <button
113
+ type="button"
114
+ onClick={onClick}
115
+ aria-label={label}
116
+ className={cn(
117
+ "stg:flex stg:shrink-0 stg:items-center stg:text-muted-foreground stg:opacity-0 stg:transition-opacity",
118
+ "stg:group-hover:opacity-100 stg:focus-visible:opacity-100",
119
+ "stg:hover:text-destructive",
120
+ "stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-inset stg:focus-visible:ring-ring",
121
+ )}
122
+ >
123
+ <FacetXIcon />
124
+ </button>
125
+ );
126
+ }
127
+
128
+ /** Muted empty-state line for a facet section with nothing to list. */
129
+ export function FacetEmptyHint({ children }: { readonly children: React.ReactNode }) {
130
+ return (
131
+ <p className="stg:px-2 stg:text-xs stg:text-muted-foreground-faint">{children}</p>
132
+ );
133
+ }
134
+
135
+ /**
136
+ * A key→value config row ("Harness Native"): muted label left, value
137
+ * right — the dense inverse of a definition list, sized like every other
138
+ * facet row. `children` is the value; pass a control (e.g. a Switch) for
139
+ * interactive config rows.
140
+ */
141
+ export function FacetKeyValueRow({
142
+ label,
143
+ labelId,
144
+ children,
145
+ }: {
146
+ readonly label: string;
147
+ /** Optional id for the label span — point a control's `aria-labelledby` here. */
148
+ readonly labelId?: string;
149
+ readonly children: React.ReactNode;
150
+ }) {
151
+ return (
152
+ <div className="stg:flex stg:items-center stg:justify-between stg:gap-2 stg:px-2 stg:py-1 stg:text-xs">
153
+ <span id={labelId} className="stg:shrink-0 stg:text-muted-foreground">
154
+ {label}
155
+ </span>
156
+ <span className="stg:flex stg:min-w-0 stg:items-center stg:gap-1.5 stg:truncate stg:text-foreground">
157
+ {children}
158
+ </span>
159
+ </div>
160
+ );
161
+ }
162
+
163
+ function FacetXIcon() {
164
+ return (
165
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
166
+ <path d="M4 4L10 10M10 4L4 10" />
167
+ </svg>
168
+ );
169
+ }
@@ -170,6 +170,7 @@ export { SetupTab } from "./facets/index.js";
170
170
  export type {
171
171
  SetupTabProps,
172
172
  SetupTabMutationCallbacks,
173
+ SetupTabAutoApprove,
173
174
  } from "./facets/index.js";
174
175
 
175
176
  // Session utilities (re-exported from @stigmer/sdk)
@@ -116,6 +116,9 @@ export interface UseNewSessionFlowOptions {
116
116
  * - model: stored per-harness choice > the account's model for the
117
117
  * active harness (validated against the registry; a stale model
118
118
  * silently falls through) > the harness default.
119
+ * - autoApprove: explicit in-session flip > the account's
120
+ * `default_auto_approve` > the host's `approvalDefaults` (derived,
121
+ * so a late-arriving value applies without a touched-ref effect).
119
122
  *
120
123
  * Seeded values are never persisted to localStorage — the account
121
124
  * preference keeps applying until the user decides. Ignored for the
@@ -221,10 +224,12 @@ export interface UseNewSessionFlowReturn {
221
224
  /**
222
225
  * Pre-arm "auto-approve tool calls" for the session this surface will
223
226
  * create (stigmer/stigmer#816, the walk-away scenario). Seeded from the
224
- * host's `StigmerProvider` `approvalDefaults` (#302, guests excluded);
225
- * the user's toggle wins from then on. Carried into the bootstrap create
226
- * as `spec.auto_approve_all` the whole-run server-side bypass so the
227
- * first run stays covered even if the user closes the tab.
227
+ * account's `default_auto_approve` preference and the host's
228
+ * `StigmerProvider` `approvalDefaults` (#302) guests inherit neither;
229
+ * the user's explicit flip (the Config facet switch) wins from then on.
230
+ * Carried into the bootstrap create as `spec.auto_approve_all` the
231
+ * whole-run server-side bypass — so the first run stays covered even if
232
+ * the user closes the tab.
228
233
  */
229
234
  readonly autoApproveAll: boolean;
230
235
  /** Set the user's explicit auto-approve choice for the created session. */
@@ -323,18 +328,28 @@ export function useNewSessionFlow(
323
328
  const contextTarget = useExecutionTarget();
324
329
  const executionTarget = options.executionTarget ?? contextTarget;
325
330
  const adapter = useRunnerAdapter();
326
- // Auto-approve for the session about to be created (#816): the host
327
- // approval default (#302) seeds the INITIAL state only; the user's
328
- // composer toggle wins from then on. Guests never inherit the default
329
- // and never see the toggle — a share-link visitor is not the operator
330
- // the host's trust judgment covers (the GUEST_HARNESS
331
- // fixed-platform-policy reasoning). Carried into the bootstrap create as
332
- // spec.auto_approve_all, the whole-run server-side bypass the created
333
- // run stays covered even if this tab dies before its first gate.
331
+ // Auto-approve for the session about to be created (#816): derived like
332
+ // useSessionPageFlow's the user's explicit flip wins, otherwise the
333
+ // truth is computed from its seeds:
334
+ //
335
+ // effective = userChoice ?? (accountSeed || hostSeed)
336
+ //
337
+ // Derived rather than useState-initialized because the account preference
338
+ // (default_auto_approve) resolves async with whoAmI a mount-time
339
+ // initializer would silently miss it (the harness seed's late-arrival
340
+ // problem, solved here by derivation instead of a touched-ref effect).
341
+ // Guests inherit neither seed — a share-link visitor is not the operator
342
+ // either trust judgment covers (the GUEST_HARNESS fixed-platform-policy
343
+ // reasoning). Carried into the bootstrap create as spec.auto_approve_all,
344
+ // the whole-run server-side bypass — the created run stays covered even
345
+ // if this tab dies before its first gate.
334
346
  const approvalDefaults = useApprovalDefaults();
335
- const [autoApproveAll, setAutoApproveAll] = useState(
336
- () => !isGuest && (approvalDefaults?.autoApproveAll ?? false),
337
- );
347
+ const [autoApproveChoice, setAutoApproveAll] = useState<boolean | null>(null);
348
+ const autoApproveAll =
349
+ autoApproveChoice ??
350
+ (!isGuest &&
351
+ ((accountDefaults?.autoApprove ?? false) ||
352
+ (approvalDefaults?.autoApproveAll ?? false)));
338
353
 
339
354
  const [harness, setHarnessRaw] = useState<HarnessOption>(() => {
340
355
  // Guests get the fixed platform policy (see GUEST_HARNESS) and never
@@ -22,6 +22,7 @@ import { toSessionUpdateInput } from "@stigmer/sdk";
22
22
  import type { SessionAudience } from "./audience.js";
23
23
  import { isChannelOriginSession } from "./channelOrigin.js";
24
24
  import { assertValidRunConfig, type SessionRunConfig } from "./run-config.js";
25
+ import type { AccountExecutionDefaults } from "../identity-account/useAccountExecutionDefaults.js";
25
26
 
26
27
  /**
27
28
  * Well-known Daytona sandbox workspace root. Used as the SDK safety-net
@@ -69,6 +70,18 @@ export interface UseSessionPageFlowOptions {
69
70
  * config). See {@link SessionRunConfig}.
70
71
  */
71
72
  readonly runConfig?: SessionRunConfig;
73
+ /**
74
+ * The user's account-level execution defaults, typically from
75
+ * `useAccountExecutionDefaults()`. On the session page only
76
+ * `autoApprove` applies — it seeds the session-scoped auto-approve
77
+ * state so a user whose `default_auto_approve` preference is ON gets
78
+ * armed follow-ups on every session, not just newly created ones. An
79
+ * explicit in-session flip always wins for this session; the seed may
80
+ * arrive after mount (whoAmI resolves async) and still applies, being
81
+ * part of the derivation rather than an initializer. Ignored for the
82
+ * `"guest"` audience (guests inherit no identity-derived preference).
83
+ */
84
+ readonly accountDefaults?: AccountExecutionDefaults;
72
85
  }
73
86
 
74
87
  /** Return value of {@link useSessionPageFlow}. */
@@ -146,28 +159,33 @@ export interface UseSessionPageFlowReturn {
146
159
  * `false` by default. `true` when any of its sources arms it — the user's
147
160
  * explicit choice always winning:
148
161
  *
149
- * 1. the user's composer toggle ({@link setAutoApproveAll});
162
+ * 1. the user's Config facet switch ({@link setAutoApproveAll});
150
163
  * 2. "Approve & don't ask again" at an approval gate (see
151
164
  * {@link submitApproval});
152
- * 3. the host's app-wide `StigmerProvider` `approvalDefaults` (#302,
165
+ * 3. the account's `default_auto_approve` preference
166
+ * (`UseSessionPageFlowOptions.accountDefaults`, guests excluded);
167
+ * 4. the host's app-wide `StigmerProvider` `approvalDefaults` (#302,
153
168
  * guests excluded);
154
- * 4. the active in-flight execution having been created with
169
+ * 5. the active in-flight execution having been created with
155
170
  * `spec.auto_approve_all` (e.g. armed on the new-session surface), so
156
171
  * the state survives the launcher → session-page handoff.
157
172
  *
158
173
  * While `true`, follow-ups carry `auto_approve_all` ({@link handleSubmit})
159
174
  * and gates appearing in the in-flight run are auto-released (never for
160
- * guest/observer/channel-origin surfaces). Held in memory only — reset on
161
- * reload back to the host default, never persisted server-side.
175
+ * guest/observer/channel-origin surfaces). The explicit per-session flip is
176
+ * held in memory only — reset on reload back to the seeds; the account
177
+ * preference is the one persisted default, declared by the user in
178
+ * Account Preferences.
162
179
  */
163
180
  readonly autoApproveAll: boolean;
164
181
  /**
165
182
  * Set the user's explicit session-scoped auto-approve choice. Wired to the
166
- * composer's always-visible toggle; an explicit `false` wins over the host
167
- * default and over an armed in-flight run for everything the client
168
- * controls (follow-up carry, gate auto-release) — an already-armed run's
169
- * server-side bypass cannot be revoked mid-run, exactly as with today's
170
- * gate-time "Approve & don't ask again".
183
+ * Config facet's switch; an explicit `false` wins over both seeds and over
184
+ * an armed in-flight run for everything the client controls (follow-up
185
+ * carry, gate auto-release) — an already-armed run's server-side bypass
186
+ * cannot be revoked mid-run, exactly as with today's gate-time "Approve &
187
+ * don't ask again". Never written back to the account preference: the
188
+ * flip is scoped to THIS conversation.
171
189
  */
172
190
  readonly setAutoApproveAll: (value: boolean) => void;
173
191
 
@@ -340,27 +358,33 @@ export function useSessionPageFlow(
340
358
  // the user's explicit in-session decision wins, otherwise the truth is
341
359
  // computed from its sources — never an effect-synced copy:
342
360
  //
343
- // effective = userChoice ?? (hostSeed || runArmed)
361
+ // effective = userChoice ?? (accountSeed || hostSeed || runArmed)
344
362
  //
345
- // - `userChoice`: the composer toggle, or the gate-time "Approve & don't
346
- // ask again" (which has always escalated the session preference).
363
+ // - `userChoice`: the Config facet's switch, or the gate-time "Approve &
364
+ // don't ask again" (which has always escalated the session preference).
365
+ // - `accountSeed`: the account's default_auto_approve preference — the
366
+ // user's own persisted walk-away default. Resolves async with whoAmI;
367
+ // being part of the derivation (not an initializer), a late arrival
368
+ // still applies unless the user has already flipped this session.
347
369
  // - `hostSeed`: the host's provider-level approvalDefaults (#302) — an
348
- // app-level trust judgment. Guests never inherit it (a share-link
349
- // visitor is not the operator the host's trust judgment covers).
370
+ // app-level trust judgment. Guests inherit neither seed (a share-link
371
+ // visitor is not the operator either trust judgment covers).
350
372
  // - `runArmed`: the ACTIVE execution was created with
351
- // spec.auto_approve_all, so the toggle reflects an armed in-flight run —
373
+ // spec.auto_approve_all, so the switch reflects an armed in-flight run —
352
374
  // this is what carries the state across the new-session → session-page
353
375
  // handoff. Deliberately derived from the active execution ONLY, never
354
376
  // from history: deriving from past executions would silently survive a
355
- // reload and change the reset-on-reload consent contract below.
377
+ // reload independent of the seeds above.
356
378
  //
357
- // Lives only in memory for the life of this page — reset on reload
358
- // re-applies the host default, never a user choice.
379
+ // The explicit choice lives only in memory for the life of this page —
380
+ // reset on reload re-applies the seeds (the account preference now being
381
+ // the persisted one the user actually declared), never a per-session flip.
359
382
  const approvalDefaults = useApprovalDefaults();
360
383
  const [autoApproveChoice, setAutoApproveChoice] = useState<boolean | null>(null);
384
+ const accountSeed = !isGuest && (options.accountDefaults?.autoApprove ?? false);
361
385
  const hostSeed = !isGuest && (approvalDefaults?.autoApproveAll ?? false);
362
386
  const runArmed = conv.activeStreamExecution?.spec?.autoApproveAll === true;
363
- const autoApproveAll = autoApproveChoice ?? (hostSeed || runArmed);
387
+ const autoApproveAll = autoApproveChoice ?? (accountSeed || hostSeed || runArmed);
364
388
  const setAutoApproveAll = useCallback((value: boolean) => {
365
389
  setAutoApproveChoice(value);
366
390
  }, []);
@@ -17,6 +17,8 @@ export interface SwitchProps {
17
17
  readonly "aria-label"?: string;
18
18
  /** ID of the element that labels this switch. */
19
19
  readonly "aria-labelledby"?: string;
20
+ /** ID of the element that describes this switch (helper copy). */
21
+ readonly "aria-describedby"?: string;
20
22
  /** Element ID, for association with an external `<label htmlFor>`. */
21
23
  readonly id?: string;
22
24
  /** Additional CSS classes for the root button. */
@@ -54,6 +56,7 @@ export function Switch({
54
56
  disabled,
55
57
  "aria-label": ariaLabel,
56
58
  "aria-labelledby": ariaLabelledby,
59
+ "aria-describedby": ariaDescribedby,
57
60
  id,
58
61
  className,
59
62
  }: SwitchProps) {
@@ -65,6 +68,7 @@ export function Switch({
65
68
  aria-checked={checked}
66
69
  aria-label={ariaLabel}
67
70
  aria-labelledby={ariaLabelledby}
71
+ aria-describedby={ariaDescribedby}
68
72
  disabled={disabled}
69
73
  onClick={() => onCheckedChange(!checked)}
70
74
  className={cn(
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
2
2
  import { renderHook, act } from "@testing-library/react";
3
3
  import type { ReactNode } from "react";
4
4
  import { ReactFlowProvider } from "@xyflow/react";
5
- import type { Node } from "@xyflow/react";
5
+ import type { Node, OnNodeDrag } from "@xyflow/react";
6
6
  import { useWorkflowCanvas } from "../useWorkflowCanvas";
7
7
 
8
8
  // Regression tests for oss#609: isDirty was a model-reference comparison
@@ -54,7 +54,12 @@ function renderCanvas() {
54
54
 
55
55
  type CanvasResult = ReturnType<typeof renderCanvas>["result"];
56
56
 
57
- const DRAG_EVENT = {} as React.MouseEvent;
57
+ // The drag fixture derives its type from the handler itself, so it tracks
58
+ // whatever event type the installed @xyflow/react declares (the pinned
59
+ // 12.10.2 takes the React synthetic event; newer majors take the native
60
+ // event) — a dependency bump can never strand this cast again (#821's
61
+ // hand-written native-MouseEvent cast did not compile under npm ci).
62
+ const DRAG_EVENT = {} as Parameters<OnNodeDrag>[0];
58
63
 
59
64
  /** Drives one full drag gesture the way React Flow produces it. */
60
65
  function drag(result: CanvasResult, id: string, to: { x: number; y: number }) {
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
2
2
  import { renderHook, act } from "@testing-library/react";
3
3
  import type { ReactNode } from "react";
4
4
  import { ReactFlowProvider } from "@xyflow/react";
5
- import type { Node } from "@xyflow/react";
5
+ import type { Node, OnNodeDrag } from "@xyflow/react";
6
6
  import { useWorkflowCanvas } from "../useWorkflowCanvas";
7
7
 
8
8
  // Regression tests for oss#602: node drags updated only React Flow's copy
@@ -51,7 +51,12 @@ function renderCanvas() {
51
51
 
52
52
  type CanvasResult = ReturnType<typeof renderCanvas>["result"];
53
53
 
54
- const DRAG_EVENT = {} as React.MouseEvent;
54
+ // The drag fixture derives its type from the handler itself, so it tracks
55
+ // whatever event type the installed @xyflow/react declares (the pinned
56
+ // 12.10.2 takes the React synthetic event; newer majors take the native
57
+ // event) — a dependency bump can never strand this cast again (#821's
58
+ // hand-written native-MouseEvent cast did not compile under npm ci).
59
+ const DRAG_EVENT = {} as Parameters<OnNodeDrag>[0];
55
60
 
56
61
  function canvasPosition(result: CanvasResult, id: string): { x: number; y: number } {
57
62
  const node = result.current.nodes.find((n) => n.id === id);
@@ -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
  );