@agent-native/toolkit 0.209.1 → 0.209.2-nightly-20261015213030

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 (112) hide show
  1. package/agent-native.eject.json +3 -0
  2. package/dist/app/chat/AgentPanel.d.ts.map +1 -1
  3. package/dist/app/chat/AgentPanel.js +8 -3
  4. package/dist/app/chat/AgentPanel.js.map +1 -1
  5. package/dist/app/chat/AgentSidebar.d.ts.map +1 -1
  6. package/dist/app/chat/AgentSidebar.js +2 -0
  7. package/dist/app/chat/AgentSidebar.js.map +1 -1
  8. package/dist/app/chat/FileStorageSetupPopover.d.ts.map +1 -1
  9. package/dist/app/chat/FileStorageSetupPopover.js +11 -2
  10. package/dist/app/chat/FileStorageSetupPopover.js.map +1 -1
  11. package/dist/app/chat/agent-chat-presence.d.ts +3 -0
  12. package/dist/app/chat/agent-chat-presence.d.ts.map +1 -0
  13. package/dist/app/chat/agent-chat-presence.js +24 -0
  14. package/dist/app/chat/agent-chat-presence.js.map +1 -0
  15. package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
  16. package/dist/app/chat/chat/run-recovery.js +15 -3
  17. package/dist/app/chat/chat/run-recovery.js.map +1 -1
  18. package/dist/app/feedback/ReportProblemButton.d.ts +6 -0
  19. package/dist/app/feedback/ReportProblemButton.d.ts.map +1 -0
  20. package/dist/app/feedback/ReportProblemButton.js +17 -0
  21. package/dist/app/feedback/ReportProblemButton.js.map +1 -0
  22. package/dist/app/feedback/ReportProblemDialog.d.ts +6 -0
  23. package/dist/app/feedback/ReportProblemDialog.d.ts.map +1 -0
  24. package/dist/app/feedback/ReportProblemDialog.js +90 -0
  25. package/dist/app/feedback/ReportProblemDialog.js.map +1 -0
  26. package/dist/app/feedback/index.d.ts +1 -0
  27. package/dist/app/feedback/index.d.ts.map +1 -1
  28. package/dist/app/feedback/index.js +1 -0
  29. package/dist/app/feedback/index.js.map +1 -1
  30. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  31. package/dist/app/i18n/catalogs/ar-SA.js +13 -0
  32. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  33. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  34. package/dist/app/i18n/catalogs/de-DE.js +9 -0
  35. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  36. package/dist/app/i18n/catalogs/en-US.d.ts +9 -0
  37. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  38. package/dist/app/i18n/catalogs/en-US.js +9 -0
  39. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  40. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  41. package/dist/app/i18n/catalogs/es-ES.js +10 -0
  42. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  43. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  44. package/dist/app/i18n/catalogs/fr-FR.js +10 -0
  45. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  46. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  47. package/dist/app/i18n/catalogs/hi-IN.js +9 -0
  48. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  49. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  50. package/dist/app/i18n/catalogs/ja-JP.js +8 -0
  51. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  52. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  53. package/dist/app/i18n/catalogs/ko-KR.js +8 -0
  54. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  55. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  56. package/dist/app/i18n/catalogs/pt-BR.js +10 -0
  57. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  58. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  59. package/dist/app/i18n/catalogs/zh-CN.js +8 -0
  60. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  61. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  62. package/dist/app/i18n/catalogs/zh-TW.js +8 -0
  63. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  64. package/dist/app/settings/BuilderConnectPopover.d.ts.map +1 -1
  65. package/dist/app/settings/BuilderConnectPopover.js +13 -7
  66. package/dist/app/settings/BuilderConnectPopover.js.map +1 -1
  67. package/dist/app/shared/AppSidebar.d.ts.map +1 -1
  68. package/dist/app/shared/AppSidebar.js +11 -3
  69. package/dist/app/shared/AppSidebar.js.map +1 -1
  70. package/dist/app/shared/CommandMenu.d.ts.map +1 -1
  71. package/dist/app/shared/CommandMenu.js +21 -4
  72. package/dist/app/shared/CommandMenu.js.map +1 -1
  73. package/dist/design-tweaks/scrub-input.d.ts +6 -0
  74. package/dist/design-tweaks/scrub-input.d.ts.map +1 -1
  75. package/dist/design-tweaks/scrub-input.js +12 -0
  76. package/dist/design-tweaks/scrub-input.js.map +1 -1
  77. package/dist/design-tweaks/visual-style-controls.d.ts +1 -1
  78. package/dist/design-tweaks/visual-style-controls.d.ts.map +1 -1
  79. package/dist/design-tweaks/visual-style-controls.js +1 -1
  80. package/dist/design-tweaks/visual-style-controls.js.map +1 -1
  81. package/package.json +4 -4
  82. package/src/app/chat/AgentPanel.header.spec.ts +1 -1
  83. package/src/app/chat/AgentPanel.tsx +23 -0
  84. package/src/app/chat/AgentSidebar.tsx +2 -0
  85. package/src/app/chat/FileStorageSetupPopover.spec.tsx +66 -5
  86. package/src/app/chat/FileStorageSetupPopover.tsx +15 -6
  87. package/src/app/chat/agent-chat-presence.ts +30 -0
  88. package/src/app/chat/chat/run-recovery.spec.tsx +60 -0
  89. package/src/app/chat/chat/run-recovery.tsx +28 -2
  90. package/src/app/feedback/ReportProblemButton.tsx +59 -0
  91. package/src/app/feedback/ReportProblemDialog.spec.tsx +190 -0
  92. package/src/app/feedback/ReportProblemDialog.tsx +212 -0
  93. package/src/app/feedback/index.ts +4 -0
  94. package/src/app/i18n/catalogs/ar-SA.ts +20 -0
  95. package/src/app/i18n/catalogs/de-DE.ts +12 -0
  96. package/src/app/i18n/catalogs/en-US.ts +12 -0
  97. package/src/app/i18n/catalogs/es-ES.ts +14 -0
  98. package/src/app/i18n/catalogs/fr-FR.ts +14 -0
  99. package/src/app/i18n/catalogs/hi-IN.ts +12 -0
  100. package/src/app/i18n/catalogs/ja-JP.ts +10 -0
  101. package/src/app/i18n/catalogs/ko-KR.ts +10 -0
  102. package/src/app/i18n/catalogs/pt-BR.ts +14 -0
  103. package/src/app/i18n/catalogs/zh-CN.ts +9 -0
  104. package/src/app/i18n/catalogs/zh-TW.ts +10 -0
  105. package/src/app/settings/BuilderConnectPopover.spec.tsx +19 -14
  106. package/src/app/settings/BuilderConnectPopover.tsx +80 -64
  107. package/src/app/shared/AppSidebar.spec.tsx +22 -1
  108. package/src/app/shared/AppSidebar.tsx +24 -3
  109. package/src/app/shared/CommandMenu.tsx +41 -1
  110. package/src/design-tweaks/scrub-input.tsx +19 -0
  111. package/src/design-tweaks/visual-style-controls.spec.tsx +31 -0
  112. package/src/design-tweaks/visual-style-controls.tsx +2 -0
@@ -92,45 +92,51 @@ export function BuilderConnectChoicePanel({
92
92
  defaultValue: "Activate free credits",
93
93
  })}
94
94
  </h2>
95
- <p
96
- id="builder-connect-popover-description"
97
- className="text-xs leading-5 text-muted-foreground"
98
- role={flow.connecting ? "status" : undefined}
99
- aria-live={flow.connecting ? "polite" : undefined}
100
- >
101
- {flow.connecting
102
- ? t("agentChat.onboarding.builderProvisioningDescription", {
103
- defaultValue:
104
- "Creating your Builder.io account and activating free credits.",
105
- })
106
- : canProvisionAccount
107
- ? t("agentChat.onboarding.builderActivationDescription", {
95
+ {!flow.accountExists ? (
96
+ <p
97
+ id="builder-connect-popover-description"
98
+ className="text-xs leading-5 text-muted-foreground"
99
+ role={flow.connecting ? "status" : undefined}
100
+ aria-live={flow.connecting ? "polite" : undefined}
101
+ >
102
+ {flow.connecting
103
+ ? t("agentChat.onboarding.builderProvisioningDescription", {
108
104
  defaultValue:
109
- "Create a new account to activate free credits, or continue with an existing account.",
105
+ "Creating your Builder.io account and activating free credits.",
110
106
  })
111
- : statusReadFailed
112
- ? t("agentChat.onboarding.builderActivationCheckFailed", {
107
+ : canProvisionAccount
108
+ ? t("agentChat.onboarding.builderActivationDescription", {
113
109
  defaultValue:
114
- "Couldn't check whether this app can create new accounts.",
110
+ "Create a new account to activate free credits, or continue with an existing account.",
115
111
  })
116
- : flow.statusCheckEnabled === false
117
- ? t("agentChat.onboarding.builderActivationUnavailableInMode", {
112
+ : statusReadFailed
113
+ ? t("agentChat.onboarding.builderActivationCheckFailed", {
118
114
  defaultValue:
119
- "Create a Builder.io account or sign in to connect.",
115
+ "Couldn't check whether this app can create new accounts.",
120
116
  })
121
- : flow.statusResolved === false
122
- ? t("agentChat.onboarding.builderActivationChecking", {
123
- defaultValue:
124
- "Checking whether new account activation is available.",
125
- })
126
- : t("agentChat.onboarding.builderActivationUnavailable", {
127
- defaultValue:
128
- "Create a Builder.io account or sign in to connect.",
129
- })}
130
- </p>
117
+ : flow.statusCheckEnabled === false
118
+ ? t(
119
+ "agentChat.onboarding.builderActivationUnavailableInMode",
120
+ {
121
+ defaultValue:
122
+ "Create a Builder.io account or sign in to connect.",
123
+ },
124
+ )
125
+ : flow.statusResolved === false
126
+ ? t("agentChat.onboarding.builderActivationChecking", {
127
+ defaultValue:
128
+ "Checking whether new account activation is available.",
129
+ })
130
+ : t("agentChat.onboarding.builderActivationUnavailable", {
131
+ defaultValue:
132
+ "Create a Builder.io account or sign in to connect.",
133
+ })}
134
+ </p>
135
+ ) : null}
131
136
  {flow.accountExists ? (
132
137
  <div
133
138
  role="alert"
139
+ id="builder-connect-popover-account-exists"
134
140
  className="rounded-md bg-muted px-3 py-2 text-xs leading-5"
135
141
  >
136
142
  <p className="font-medium text-foreground">
@@ -177,7 +183,7 @@ export function BuilderConnectChoicePanel({
177
183
  </div>
178
184
  ) : null}
179
185
  <BuilderConnectIncludedServices appId={appId} />
180
- {canProvisionAccount ? (
186
+ {canProvisionAccount && !flow.accountExists ? (
181
187
  <div className="flex flex-col gap-2">
182
188
  <Button
183
189
  type="button"
@@ -214,45 +220,55 @@ export function BuilderConnectChoicePanel({
214
220
  type="button"
215
221
  data-testid={primaryTestId}
216
222
  className="w-full"
217
- aria-describedby="builder-connect-popover-description"
223
+ aria-describedby={
224
+ flow.accountExists
225
+ ? "builder-connect-popover-account-exists"
226
+ : "builder-connect-popover-description"
227
+ }
218
228
  onClick={onExistingAccount}
219
229
  disabled={flow.connecting}
220
230
  >
221
231
  {flow.connecting ? <Spinner aria-hidden /> : null}
222
- {t("agentChat.onboarding.builderConnect", {
223
- defaultValue: "Connect Builder.io",
224
- })}
232
+ {flow.accountExists
233
+ ? t("agentChat.onboarding.builderExistingAccount", {
234
+ defaultValue: "I have a Builder.io account",
235
+ })
236
+ : t("agentChat.onboarding.builderConnect", {
237
+ defaultValue: "Connect Builder.io",
238
+ })}
225
239
  </Button>
226
240
  )}
227
- <p className="text-[11px] leading-4 text-muted-foreground">
228
- {t("agentChat.onboarding.builderConsentPrefix", {
229
- defaultValue: "By creating a Builder.io account, you agree to our",
230
- })}{" "}
231
- <a
232
- href="https://www.builder.io/legal/terms"
233
- target="_blank"
234
- rel="noreferrer"
235
- className="underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
236
- >
237
- {t("agentChat.onboarding.builderTerms", {
238
- defaultValue: "Terms of Service",
239
- })}
240
- </a>{" "}
241
- {t("agentChat.onboarding.builderConsentAnd", {
242
- defaultValue: "and",
243
- })}{" "}
244
- <a
245
- href="https://www.builder.io/legal/privacy"
246
- target="_blank"
247
- rel="noreferrer"
248
- className="underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
249
- >
250
- {t("agentChat.onboarding.builderPrivacy", {
251
- defaultValue: "Privacy Policy",
252
- })}
253
- </a>
254
- .
255
- </p>
241
+ {canProvisionAccount && !flow.accountExists ? (
242
+ <p className="text-[11px] leading-4 text-muted-foreground">
243
+ {t("agentChat.onboarding.builderConsentPrefix", {
244
+ defaultValue: "By creating a Builder.io account, you agree to our",
245
+ })}{" "}
246
+ <a
247
+ href="https://www.builder.io/legal/terms"
248
+ target="_blank"
249
+ rel="noreferrer"
250
+ className="underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
251
+ >
252
+ {t("agentChat.onboarding.builderTerms", {
253
+ defaultValue: "Terms of Service",
254
+ })}
255
+ </a>{" "}
256
+ {t("agentChat.onboarding.builderConsentAnd", {
257
+ defaultValue: "and",
258
+ })}{" "}
259
+ <a
260
+ href="https://www.builder.io/legal/privacy"
261
+ target="_blank"
262
+ rel="noreferrer"
263
+ className="underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
264
+ >
265
+ {t("agentChat.onboarding.builderPrivacy", {
266
+ defaultValue: "Privacy Policy",
267
+ })}
268
+ </a>
269
+ .
270
+ </p>
271
+ ) : null}
256
272
  </div>
257
273
  );
258
274
  }
@@ -6,7 +6,7 @@ import {
6
6
  TooltipProvider,
7
7
  TooltipTrigger,
8
8
  } from "@agent-native/toolkit/ui/tooltip";
9
- import { act } from "react";
9
+ import { act, type ReactNode } from "react";
10
10
  import { createRoot, type Root } from "react-dom/client";
11
11
  import { MemoryRouter, useLocation } from "react-router";
12
12
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -61,6 +61,7 @@ describe("AppSidebar (router links)", () => {
61
61
  "Slides",
62
62
  "Decks",
63
63
  "Settings",
64
+ "Report a problem",
64
65
  "Expand sidebar",
65
66
  ]);
66
67
 
@@ -74,6 +75,26 @@ describe("AppSidebar (router links)", () => {
74
75
  }
75
76
  });
76
77
 
78
+ it("offers Report a problem beside the feedback row and hides it with feedback", () => {
79
+ const renderSidebar = (feedback: ReactNode) => (
80
+ <MemoryRouter>
81
+ <AppSidebar
82
+ showBadge={false}
83
+ brandName="Slides"
84
+ items={[{ label: "Decks", to: "/home" }]}
85
+ feedback={feedback}
86
+ />
87
+ </MemoryRouter>
88
+ );
89
+
90
+ act(() => root.render(renderSidebar(<span>Send feedback</span>)));
91
+ expect(container.textContent).toContain("Send feedback");
92
+ expect(container.textContent).toContain("Report a problem");
93
+
94
+ act(() => root.render(renderSidebar(null)));
95
+ expect(container.textContent).not.toContain("Report a problem");
96
+ });
97
+
77
98
  it("keeps rail and settings-gear tooltip links mounted across rerenders", () => {
78
99
  const renderLinks = (revision: number) => (
79
100
  <MemoryRouter>
@@ -9,10 +9,12 @@ import {
9
9
  type AppSidebarHeaderProps as ToolkitAppSidebarHeaderProps,
10
10
  type AppSidebarFooterProps as ToolkitAppSidebarFooterProps,
11
11
  } from "@agent-native/toolkit/app-shell";
12
- import { forwardRef } from "react";
12
+ import { cn } from "@agent-native/toolkit/utils";
13
+ import { forwardRef, type ReactNode } from "react";
13
14
  import { useHref, useLinkClickHandler } from "react-router";
14
15
 
15
16
  import { FeedbackButton } from "../feedback/FeedbackButton.js";
17
+ import { ReportProblemButton } from "../feedback/ReportProblemButton.js";
16
18
  import { AgentNativeIcon } from "./AgentNativeIcon.js";
17
19
  import { EnvironmentBadge } from "./EnvironmentBadge.js";
18
20
 
@@ -99,6 +101,25 @@ export const AppSidebarHeader = forwardRef<
99
101
  );
100
102
  AppSidebarHeader.displayName = "AppSidebarHeader";
101
103
 
104
+ // Every app's feedback row also offers Report a problem; an app that hides feedback hides both.
105
+ function withReportProblem(
106
+ feedback: ReactNode,
107
+ collapsed: boolean | undefined,
108
+ ): ReactNode {
109
+ if (!feedback) return feedback;
110
+ return (
111
+ <div
112
+ className={cn(
113
+ "flex flex-col",
114
+ collapsed ? "items-center gap-1" : "gap-0.5",
115
+ )}
116
+ >
117
+ {feedback}
118
+ <ReportProblemButton variant={collapsed ? "icon" : "sidebar"} />
119
+ </div>
120
+ );
121
+ }
122
+
102
123
  export interface AppSidebarFooterProps extends ToolkitAppSidebarFooterProps {}
103
124
 
104
125
  export const AppSidebarFooter = forwardRef<
@@ -118,7 +139,7 @@ export const AppSidebarFooter = forwardRef<
118
139
  <ToolkitAppSidebarFooter
119
140
  ref={ref}
120
141
  collapsed={collapsed}
121
- feedback={resolvedFeedback}
142
+ feedback={withReportProblem(resolvedFeedback, collapsed)}
122
143
  {...props}
123
144
  />
124
145
  );
@@ -178,7 +199,7 @@ export const AppSidebar = forwardRef<HTMLElement, AppSidebarProps>(
178
199
  badge={resolvedBadge}
179
200
  linkComponent={resolvedLinkComponent}
180
201
  {...props}
181
- feedback={resolvedFeedback}
202
+ feedback={withReportProblem(resolvedFeedback, props.collapsed)}
182
203
  />
183
204
  );
184
205
  },
@@ -38,6 +38,7 @@ import {
38
38
  IconExternalLink,
39
39
  IconInfoCircle,
40
40
  IconMessage,
41
+ IconMessageReport,
41
42
  IconHistory,
42
43
  IconLogout,
43
44
  IconSettings,
@@ -54,6 +55,7 @@ import React, {
54
55
  type ReactNode,
55
56
  } from "react";
56
57
 
58
+ import { ReportProblemDialog } from "../feedback/ReportProblemDialog.js";
57
59
  import { AboutAgentNativeDialog } from "./AboutAgentNativeDialog.js";
58
60
  import { LazyChunkErrorBoundary } from "./LazyChunkErrorBoundary.js";
59
61
 
@@ -356,6 +358,7 @@ export function CommandMenu({
356
358
 
357
359
  const [changelogOpen, setChangelogOpen] = useState(false);
358
360
  const [aboutOpen, setAboutOpen] = useState(false);
361
+ const [reportProblemOpen, setReportProblemOpen] = useState(false);
359
362
  const hasChangelog =
360
363
  typeof changelog === "string" && changelog.trim().length > 0;
361
364
  const showAbout = showAboutProp ?? hasChangelog;
@@ -374,6 +377,11 @@ export function CommandMenu({
374
377
  setTimeout(() => setAboutOpen(true), 50);
375
378
  }, [onOpenChange]);
376
379
 
380
+ const openReportProblem = useCallback(() => {
381
+ onOpenChange(false);
382
+ setTimeout(() => setReportProblemOpen(true), 50);
383
+ }, [onOpenChange]);
384
+
377
385
  useEffect(() => {
378
386
  if (!open) {
379
387
  setSearch("");
@@ -451,6 +459,13 @@ export function CommandMenu({
451
459
  .toLowerCase()
452
460
  .includes(search.toLowerCase());
453
461
  const showAboutRow = showAbout && aboutRowMatches;
462
+ const reportProblemLabel = t("agentChat.reportProblem.menuItem");
463
+ const showReportProblemRow =
464
+ !search ||
465
+ [reportProblemLabel, "bug", "issue", "problem", "report"]
466
+ .join(" ")
467
+ .toLowerCase()
468
+ .includes(search.toLowerCase());
454
469
  const settingsLabel = t("settingsShortcut.command");
455
470
  const showSettingsRow =
456
471
  !search ||
@@ -600,12 +615,31 @@ export function CommandMenu({
600
615
  </>
601
616
  )}
602
617
 
603
- {showSignOutRow && (
618
+ {showReportProblemRow && (
604
619
  <>
605
620
  {(hasResults ||
606
621
  showSettingsRow ||
607
622
  showChangelogRow ||
608
623
  showAboutRow) && <CommandSeparator />}
624
+ <div className="p-1">
625
+ <CommandItemPrimitive
626
+ className="cursor-pointer gap-2 py-2"
627
+ onSelect={openReportProblem}
628
+ >
629
+ <IconMessageReport className="h-4 w-4 text-muted-foreground" />
630
+ <span>{reportProblemLabel}</span>
631
+ </CommandItemPrimitive>
632
+ </div>
633
+ </>
634
+ )}
635
+
636
+ {showSignOutRow && (
637
+ <>
638
+ {(hasResults ||
639
+ showSettingsRow ||
640
+ showChangelogRow ||
641
+ showAboutRow ||
642
+ showReportProblemRow) && <CommandSeparator />}
609
643
  <div className="p-1">
610
644
  <CommandItemPrimitive
611
645
  className="cursor-pointer gap-2 py-2"
@@ -624,6 +658,7 @@ export function CommandMenu({
624
658
  showSettingsRow ||
625
659
  showChangelogRow ||
626
660
  showAboutRow ||
661
+ showReportProblemRow ||
627
662
  showSignOutRow ||
628
663
  Boolean(results)) && <CommandSeparator />}
629
664
  <div className="p-1">
@@ -761,6 +796,11 @@ export function CommandMenu({
761
796
  {showAbout && (
762
797
  <AboutAgentNativeDialog open={aboutOpen} onOpenChange={setAboutOpen} />
763
798
  )}
799
+
800
+ <ReportProblemDialog
801
+ open={reportProblemOpen}
802
+ onOpenChange={setReportProblemOpen}
803
+ />
764
804
  </>
765
805
  );
766
806
  }
@@ -91,6 +91,17 @@ interface PendingScrubTextCommit {
91
91
  }
92
92
 
93
93
  const activeScrubCancelCallbacks = new Set<() => void>();
94
+ const mountedScrubInputs = new Set<() => ScrubInputDisplay>();
95
+
96
+ export interface ScrubInputDisplay {
97
+ label: string;
98
+ shown: string;
99
+ }
100
+
101
+ /** What each mounted scrub input is showing right now. */
102
+ export function readVisualScrubInputs(): ScrubInputDisplay[] {
103
+ return [...mountedScrubInputs].map((read) => read());
104
+ }
94
105
 
95
106
  export function cancelActiveScrubInputs() {
96
107
  for (const cancel of [...activeScrubCancelCallbacks]) cancel();
@@ -181,6 +192,14 @@ export function VisualScrubInput({
181
192
  [],
182
193
  );
183
194
 
195
+ useEffect(() => {
196
+ const read = () => ({ label: ariaLabel ?? label, shown: draftRef.current });
197
+ mountedScrubInputs.add(read);
198
+ return () => {
199
+ mountedScrubInputs.delete(read);
200
+ };
201
+ }, [ariaLabel, label]);
202
+
184
203
  useEffect(() => {
185
204
  if (mixed) {
186
205
  pendingCommitRef.current = null;
@@ -11,6 +11,7 @@ import {
11
11
  VisualSegmentedControl,
12
12
  parseScrubExpression,
13
13
  parseScrubRelativeExpression,
14
+ readVisualScrubInputs,
14
15
  } from "./visual-style-controls.js";
15
16
 
16
17
  describe("visual style controls", () => {
@@ -109,6 +110,36 @@ describe("visual style controls", () => {
109
110
  },
110
111
  );
111
112
 
113
+ it("reads what each mounted scrub input shows until it unmounts", () => {
114
+ act(() => {
115
+ root.render(
116
+ <>
117
+ <VisualScrubInput
118
+ label="Opacity"
119
+ value={91}
120
+ unit="%"
121
+ onChange={vi.fn()}
122
+ />
123
+ <VisualScrubInput
124
+ label="Opacity"
125
+ value={40}
126
+ unit="%"
127
+ onChange={vi.fn()}
128
+ />
129
+ </>,
130
+ );
131
+ });
132
+
133
+ expect(readVisualScrubInputs()).toEqual([
134
+ { label: "Opacity", shown: "91%" },
135
+ { label: "Opacity", shown: "40%" },
136
+ ]);
137
+
138
+ act(() => root.render(<></>));
139
+
140
+ expect(readVisualScrubInputs()).toEqual([]);
141
+ });
142
+
112
143
  it("starts a base-rate scrub when Option-drag begins on the numeric input", () => {
113
144
  const onChange = vi.fn();
114
145
  act(() => {
@@ -21,9 +21,11 @@ import { cn } from "../utils.js";
21
21
  export {
22
22
  cancelActiveScrubInputs,
23
23
  VisualScrubInput,
24
+ readVisualScrubInputs,
24
25
  resolvePendingScrubCommit,
25
26
  type PendingScrubCommit,
26
27
  type ScrubInputChangeMeta,
28
+ type ScrubInputDisplay,
27
29
  type ScrubInputProps,
28
30
  } from "./scrub-input.js";
29
31
  export {