@agent-native/toolkit 0.198.3 → 0.198.4

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 (144) hide show
  1. package/dist/app/agentkit/react/components.d.ts +4 -1
  2. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/components.js +12 -4
  4. package/dist/app/agentkit/react/components.js.map +1 -1
  5. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  6. package/dist/app/chat/AgentKitAssistantChat.js +43 -21
  7. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  8. package/dist/app/chat/AgentSidebar.d.ts.map +1 -1
  9. package/dist/app/chat/AgentSidebar.js +2 -0
  10. package/dist/app/chat/AgentSidebar.js.map +1 -1
  11. package/dist/app/chat/FileStorageSetupPopover.js +2 -2
  12. package/dist/app/chat/FileStorageSetupPopover.js.map +1 -1
  13. package/dist/app/chat/MultiTabAssistantChat.js +1 -1
  14. package/dist/app/chat/agentkit-chat/connections.d.ts +1 -4
  15. package/dist/app/chat/agentkit-chat/connections.d.ts.map +1 -1
  16. package/dist/app/chat/agentkit-chat/connections.js +1 -26
  17. package/dist/app/chat/agentkit-chat/connections.js.map +1 -1
  18. package/dist/app/chat/agentkit-chat/history.d.ts +4 -1
  19. package/dist/app/chat/agentkit-chat/history.d.ts.map +1 -1
  20. package/dist/app/chat/agentkit-chat/history.js +17 -5
  21. package/dist/app/chat/agentkit-chat/history.js.map +1 -1
  22. package/dist/app/chat/chat/history-types.d.ts +1 -1
  23. package/dist/app/chat/chat/history-types.d.ts.map +1 -1
  24. package/dist/app/chat/chat-first/copy.d.ts +1 -2
  25. package/dist/app/chat/chat-first/copy.d.ts.map +1 -1
  26. package/dist/app/chat/chat-first/copy.js +1 -121
  27. package/dist/app/chat/chat-first/copy.js.map +1 -1
  28. package/dist/app/chat/chat-first-copy.d.ts +3 -0
  29. package/dist/app/chat/chat-first-copy.d.ts.map +1 -0
  30. package/dist/app/chat/chat-first-copy.js +122 -0
  31. package/dist/app/chat/chat-first-copy.js.map +1 -0
  32. package/dist/app/feature-flags/FeatureFlagsPanel.d.ts.map +1 -1
  33. package/dist/app/feature-flags/FeatureFlagsPanel.js +1 -0
  34. package/dist/app/feature-flags/FeatureFlagsPanel.js.map +1 -1
  35. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  36. package/dist/app/i18n/catalogs/ar-SA.js +2 -1
  37. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  38. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  39. package/dist/app/i18n/catalogs/de-DE.js +2 -1
  40. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  41. package/dist/app/i18n/catalogs/en-US.d.ts +2 -1
  42. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  43. package/dist/app/i18n/catalogs/en-US.js +2 -1
  44. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  45. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  46. package/dist/app/i18n/catalogs/es-ES.js +2 -1
  47. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  48. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  49. package/dist/app/i18n/catalogs/fr-FR.js +2 -1
  50. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  51. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/hi-IN.js +2 -1
  53. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  54. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  55. package/dist/app/i18n/catalogs/ja-JP.js +2 -1
  56. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  57. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  58. package/dist/app/i18n/catalogs/ko-KR.js +2 -1
  59. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  60. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  61. package/dist/app/i18n/catalogs/pt-BR.js +2 -1
  62. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  63. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  64. package/dist/app/i18n/catalogs/zh-CN.js +2 -1
  65. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  66. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  67. package/dist/app/i18n/catalogs/zh-TW.js +2 -1
  68. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  69. package/dist/app/labs/LabsSettings.d.ts.map +1 -1
  70. package/dist/app/labs/LabsSettings.js +7 -3
  71. package/dist/app/labs/LabsSettings.js.map +1 -1
  72. package/dist/app/org/OrgGeneralSection.d.ts.map +1 -1
  73. package/dist/app/org/OrgGeneralSection.js +21 -18
  74. package/dist/app/org/OrgGeneralSection.js.map +1 -1
  75. package/dist/app/org/OrgSwitcher.d.ts.map +1 -1
  76. package/dist/app/org/OrgSwitcher.js +3 -2
  77. package/dist/app/org/OrgSwitcher.js.map +1 -1
  78. package/dist/app/org/workspace-app-links.d.ts +1 -0
  79. package/dist/app/org/workspace-app-links.d.ts.map +1 -1
  80. package/dist/app/org/workspace-app-links.js +3 -0
  81. package/dist/app/org/workspace-app-links.js.map +1 -1
  82. package/dist/app/resources/McpAgentKitConnectionRequest.d.ts +7 -2
  83. package/dist/app/resources/McpAgentKitConnectionRequest.d.ts.map +1 -1
  84. package/dist/app/resources/McpAgentKitConnectionRequest.js +132 -13
  85. package/dist/app/resources/McpAgentKitConnectionRequest.js.map +1 -1
  86. package/dist/icons/ResourceIconPicker.d.ts +6 -1
  87. package/dist/icons/ResourceIconPicker.d.ts.map +1 -1
  88. package/dist/icons/ResourceIconPicker.js +45 -6
  89. package/dist/icons/ResourceIconPicker.js.map +1 -1
  90. package/package.json +6 -4
  91. package/src/app/agentkit/react/components.spec.tsx +3 -3
  92. package/src/app/agentkit/react/components.tsx +22 -4
  93. package/src/app/agentkit/react/lifecycle.spec.tsx +42 -0
  94. package/src/app/chat/AgentKitAssistantChat.spec.tsx +114 -11
  95. package/src/app/chat/AgentKitAssistantChat.tsx +78 -17
  96. package/src/app/chat/AgentPanel.header.spec.ts +59 -22
  97. package/src/app/chat/AgentSidebar.tsx +2 -0
  98. package/src/app/chat/FileStorageSetupPopover.tsx +2 -2
  99. package/src/app/chat/MultiTabAssistantChat.messageCount.spec.tsx +3 -1
  100. package/src/app/chat/MultiTabAssistantChat.spec.tsx +8 -4
  101. package/src/app/chat/MultiTabAssistantChat.tsx +1 -1
  102. package/src/app/chat/agentkit-chat/action-widget.spec.tsx +5 -17
  103. package/src/app/chat/agentkit-chat/connections.spec.tsx +137 -0
  104. package/src/app/chat/agentkit-chat/connections.ts +2 -37
  105. package/src/app/chat/agentkit-chat/history.tsx +23 -8
  106. package/src/app/chat/chat/history-types.ts +1 -1
  107. package/src/app/chat/chat/tool-call-display.spec.tsx +2 -2
  108. package/src/app/chat/chat/widgets/record-change-widget.spec.tsx +20 -29
  109. package/src/app/chat/chat-first/copy.ts +1 -133
  110. package/src/app/chat/chat-first-copy.ts +133 -0
  111. package/src/app/chat/composer/use-agentkit-capabilities.hardening.spec.tsx +3 -2
  112. package/src/app/feature-flags/FeatureFlagsPanel.tsx +1 -0
  113. package/src/app/i18n/catalogs/ar-SA.ts +2 -1
  114. package/src/app/i18n/catalogs/de-DE.ts +3 -1
  115. package/src/app/i18n/catalogs/en-US.ts +2 -1
  116. package/src/app/i18n/catalogs/es-ES.ts +2 -1
  117. package/src/app/i18n/catalogs/fr-FR.ts +3 -1
  118. package/src/app/i18n/catalogs/hi-IN.ts +2 -1
  119. package/src/app/i18n/catalogs/ja-JP.ts +2 -1
  120. package/src/app/i18n/catalogs/ko-KR.ts +2 -1
  121. package/src/app/i18n/catalogs/pt-BR.ts +3 -1
  122. package/src/app/i18n/catalogs/zh-CN.ts +2 -1
  123. package/src/app/i18n/catalogs/zh-TW.ts +2 -1
  124. package/src/app/integrations/IntegrationDetailPage.spec.tsx +1 -1
  125. package/src/app/labs/LabsSettings.tsx +46 -10
  126. package/src/app/observability/ThumbsFeedback.spec.tsx +15 -0
  127. package/src/app/onboarding/FirstRunOnboarding.spec.tsx +4 -0
  128. package/src/app/org/OrgGeneralSection.tsx +32 -22
  129. package/src/app/org/OrgSwitcher.tsx +3 -2
  130. package/src/app/org/workspace-app-links.spec.ts +4 -0
  131. package/src/app/org/workspace-app-links.ts +8 -1
  132. package/src/app/resources/McpAgentKitConnectionRequest.spec.tsx +371 -2
  133. package/src/app/resources/McpAgentKitConnectionRequest.tsx +171 -11
  134. package/src/app/settings/SettingsTabsPage.spec.tsx +4 -2
  135. package/src/app/settings/shell/SettingsShell.spec.tsx +14 -8
  136. package/src/app/settings/shell/pages/app-group-pages.spec.tsx +35 -5
  137. package/src/app/settings/shell/pages/resource-pages.spec.tsx +3 -0
  138. package/src/app/settings/usage/UsagePage.spec.tsx +3 -3
  139. package/src/app/shared/lazy-chunk-error-boundary.spec.tsx +2 -0
  140. package/src/app-shell/sidebar-overflow.spec.tsx +4 -1
  141. package/src/design-tweaks/visual-style-controls.spec.tsx +38 -0
  142. package/src/editor/useCollabReconcile.concurrent.spec.ts +2 -1
  143. package/src/icons/ResourceIconPicker.spec.tsx +270 -0
  144. package/src/icons/ResourceIconPicker.tsx +72 -4
@@ -1,5 +1,18 @@
1
+ import type { AgentConnectionRequest } from "@agent-native/agentkit/protocol";
1
2
  import {
2
- consumeMcpConnectionResume,
3
+ agentNativePath,
4
+ appBasePath,
5
+ } from "@agent-native/core/client/api-path";
6
+ import { useT } from "@agent-native/core/client/i18n";
7
+ import {
8
+ getWorkspaceConnectionProvider,
9
+ workspaceProviderOAuthUrl,
10
+ } from "@agent-native/core/client/integrations";
11
+ import { openOAuthPopup } from "@agent-native/core/client/oauth-popup";
12
+ import {
13
+ addMcpConnectionCompleteListener,
14
+ clearMcpConnectionResume,
15
+ getPendingMcpConnectionResume,
3
16
  saveMcpConnectionResume,
4
17
  type McpConnectionResumeRequest,
5
18
  } from "@agent-native/core/client/resources/mcp-connection-resume";
@@ -7,8 +20,14 @@ import {
7
20
  getDefaultMcpIntegrations,
8
21
  navigateToMcpOAuthStart,
9
22
  } from "@agent-native/core/client/resources/mcp-integration-catalog";
10
- import { useEffect, useMemo, useRef, type ReactNode } from "react";
23
+ import { IconAlertCircle } from "@tabler/icons-react";
24
+ import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
11
25
 
26
+ import { AgentConnectionRequestCard } from "../agentkit/react/components.js";
27
+ import {
28
+ dispatchIntegrationsHref,
29
+ useOrgSwitcherAppLinks,
30
+ } from "../org/workspace-app-links.js";
12
31
  import { McpConnectionSuggestion } from "./McpConnectionSuggestion.js";
13
32
 
14
33
  export interface McpAgentKitConnectionTarget {
@@ -20,6 +39,10 @@ export interface McpAgentKitConnectionTarget {
20
39
  export interface McpAgentKitConnectionRequestCardProps {
21
40
  provider: string;
22
41
  detail?: string;
42
+ reason?: AgentConnectionRequest["reason"];
43
+ status?: AgentConnectionRequest["status"];
44
+ appId?: string;
45
+ source?: AgentConnectionRequest["source"];
23
46
  target: McpAgentKitConnectionTarget;
24
47
  onConnected: () => void | Promise<void>;
25
48
  onDeclined: () => void | Promise<void>;
@@ -29,18 +52,35 @@ export interface McpAgentKitConnectionRequestCardProps {
29
52
  export function McpAgentKitConnectionRequestCard({
30
53
  provider,
31
54
  detail,
55
+ reason,
56
+ status = "requested",
57
+ appId,
58
+ source,
32
59
  target,
33
60
  onConnected,
34
61
  onDeclined,
35
62
  fallback = null,
36
63
  }: McpAgentKitConnectionRequestCardProps) {
37
64
  const settledRef = useRef(false);
65
+ const [workspaceSetupOpened, setWorkspaceSetupOpened] = useState(false);
38
66
  const integrations = useMemo(() => getDefaultMcpIntegrations(), []);
39
67
  const integration = integrations.find(
40
68
  (candidate) =>
41
69
  candidate.id.toLowerCase() === provider.trim().toLowerCase() ||
42
70
  candidate.provider.toLowerCase() === provider.trim().toLowerCase(),
43
71
  );
72
+ const workspaceProvider =
73
+ source?.kind === "workspace_connection" && source.id === provider
74
+ ? getWorkspaceConnectionProvider(source.id)
75
+ : null;
76
+ const needsWorkspaceSetup =
77
+ source?.kind === "workspace_connection" &&
78
+ source.id === provider &&
79
+ (reason === "grant" || !workspaceProvider?.oauth);
80
+ const { apps: workspaceApps } = useOrgSwitcherAppLinks(needsWorkspaceSetup);
81
+ useEffect(() => {
82
+ if (status === "failed") setWorkspaceSetupOpened(false);
83
+ }, [status]);
44
84
  const settle = async (callback: () => void | Promise<void>) => {
45
85
  if (settledRef.current) return;
46
86
  settledRef.current = true;
@@ -51,6 +91,86 @@ export function McpAgentKitConnectionRequestCard({
51
91
  throw error;
52
92
  }
53
93
  };
94
+ if (source?.kind === "workspace_connection") {
95
+ if (!workspaceProvider || !appId) return fallback;
96
+ const request: AgentConnectionRequest = {
97
+ id: target.requestId,
98
+ provider,
99
+ reason: reason ?? "connect",
100
+ status,
101
+ appId,
102
+ detail,
103
+ source,
104
+ };
105
+ if (reason === "grant" || !workspaceProvider.oauth) {
106
+ return (
107
+ <AgentConnectionRequestCard
108
+ request={request}
109
+ runId={target.runId}
110
+ providerLabel={source.label}
111
+ retry={workspaceSetupOpened}
112
+ onConnect={() => {
113
+ if (workspaceSetupOpened) return onConnected();
114
+ const setupWindow = window.open("", "_blank");
115
+ if (!setupWindow) return false;
116
+ try {
117
+ setupWindow.opener = null;
118
+ setupWindow.location.assign(
119
+ dispatchIntegrationsHref(workspaceApps),
120
+ );
121
+ } catch {
122
+ setupWindow.close();
123
+ return false;
124
+ }
125
+ setWorkspaceSetupOpened(true);
126
+ }}
127
+ />
128
+ );
129
+ }
130
+ return (
131
+ <AgentConnectionRequestCard
132
+ request={request}
133
+ runId={target.runId}
134
+ providerLabel={source.label}
135
+ onConnect={() => {
136
+ const popup = openOAuthPopup({ features: "width=640,height=760" });
137
+ if (!popup) return false;
138
+ const completionId = crypto.randomUUID();
139
+ const message = detail ?? `Continue after connecting ${provider}.`;
140
+ if (!saveMcpConnectionResume(message, target, completionId)) {
141
+ popup.close();
142
+ return false;
143
+ }
144
+ const returnUrl = new URL(
145
+ agentNativePath("/_agent-native/oauth/popup"),
146
+ window.location.href,
147
+ );
148
+ returnUrl.searchParams.set("complete", "workspace-connection");
149
+ returnUrl.searchParams.set("resume", completionId);
150
+ const basePath = appBasePath();
151
+ const hasBasePath =
152
+ basePath && returnUrl.pathname.startsWith(basePath + "/");
153
+ const returnPath =
154
+ (hasBasePath
155
+ ? returnUrl.pathname.slice(basePath.length)
156
+ : returnUrl.pathname) + returnUrl.search;
157
+ try {
158
+ popup.location.assign(
159
+ workspaceProviderOAuthUrl(source.id, {
160
+ appId,
161
+ scope: "user",
162
+ returnPath,
163
+ }),
164
+ );
165
+ } catch (error) {
166
+ clearMcpConnectionResume(completionId);
167
+ popup.close();
168
+ throw error;
169
+ }
170
+ }}
171
+ />
172
+ );
173
+ }
54
174
  if (!integration) return fallback;
55
175
  return (
56
176
  <McpConnectionSuggestion
@@ -87,14 +207,54 @@ export function McpAgentKitConnectionResume({
87
207
  onResume,
88
208
  onMessageResume,
89
209
  }: McpAgentKitConnectionResumeProps) {
210
+ const t = useT();
211
+ const onResumeRef = useRef(onResume);
212
+ const onMessageResumeRef = useRef(onMessageResume);
213
+ const processingRef = useRef(false);
214
+ const [failed, setFailed] = useState(false);
215
+ onResumeRef.current = onResume;
216
+ onMessageResumeRef.current = onMessageResume;
217
+
90
218
  useEffect(() => {
91
- const pending: McpConnectionResumeRequest | null =
92
- consumeMcpConnectionResume();
93
- if (pending?.agentKit) {
94
- void Promise.resolve(onResume(pending.agentKit, pending)).catch(() => {});
95
- } else if (pending) {
96
- void Promise.resolve(onMessageResume?.(pending)).catch(() => {});
97
- }
98
- }, [onMessageResume, onResume]);
99
- return null;
219
+ const resumePending = async () => {
220
+ if (processingRef.current) return;
221
+ const pending = getPendingMcpConnectionResume();
222
+ if (!pending) return;
223
+ processingRef.current = true;
224
+ try {
225
+ if (pending.agentKit) {
226
+ try {
227
+ await onResumeRef.current(pending.agentKit, pending);
228
+ } catch (error) {
229
+ if (
230
+ !(error instanceof Error) ||
231
+ !error.message.startsWith("Unknown AgentKit run:") ||
232
+ !onMessageResumeRef.current
233
+ ) {
234
+ throw error;
235
+ }
236
+ await onMessageResumeRef.current(pending);
237
+ }
238
+ } else {
239
+ if (!onMessageResumeRef.current) return;
240
+ await onMessageResumeRef.current(pending);
241
+ }
242
+ clearMcpConnectionResume(pending);
243
+ setFailed(false);
244
+ } finally {
245
+ processingRef.current = false;
246
+ }
247
+ };
248
+ const resume = () => {
249
+ void resumePending().catch(() => setFailed(true));
250
+ };
251
+ resume();
252
+ return addMcpConnectionCompleteListener(resume);
253
+ }, []);
254
+ return failed ? (
255
+ <div className="agentkit-command-error" role="alert">
256
+ <IconAlertCircle aria-hidden="true" className="agentkit-icon" />
257
+ <span>{t("agentChat.connection.failed")}</span>
258
+ </div>
259
+ ) : null;
100
260
  }
@@ -518,6 +518,7 @@ describe("SettingsTabsPage", () => {
518
518
  });
519
519
 
520
520
  it("renders What's new markdown in the legacy settings fallback", async () => {
521
+ await import("../changelog/Changelog.js");
521
522
  act(() => {
522
523
  root.render(
523
524
  <SettingsTabsPage
@@ -607,11 +608,11 @@ describe("SettingsTabsPage", () => {
607
608
  ]);
608
609
  });
609
610
 
610
- it("keeps shell-only app groups out of the tabbed page", () => {
611
+ it("keeps shell-only app groups out of the tabbed page with legacy General content", () => {
611
612
  act(() => {
612
613
  root.render(
613
614
  <SettingsTabsPage
614
- general={<div>General content</div>}
615
+ general={<div>Legacy General content</div>}
615
616
  generalGroups={<div>App groups</div>}
616
617
  notifications={<div>Email settings</div>}
617
618
  notificationsLabel="Notifications"
@@ -644,6 +645,7 @@ describe("SettingsTabsPage", () => {
644
645
  ]);
645
646
  // `generalGroups` may call useSettingsShell(), which throws here.
646
647
  expect(container.textContent).not.toContain("App groups");
648
+ expect(container.textContent).toContain("Legacy General content");
647
649
  });
648
650
 
649
651
  it("visually separates app, agent, and workspace tabs", () => {
@@ -35,10 +35,14 @@ const appState = vi.hoisted(() => ({
35
35
  write: vi.fn(async (_key: string, value: unknown) => value),
36
36
  remove: vi.fn(async () => undefined),
37
37
  }));
38
- vi.mock("@agent-native/core/client/application-state", () => ({
39
- writeClientAppState: appState.write,
40
- deleteClientAppState: appState.remove,
41
- }));
38
+ vi.mock(
39
+ "@agent-native/core/client/application-state",
40
+ async (importOriginal) => ({
41
+ ...(await importOriginal<Record<string, unknown>>()),
42
+ writeClientAppState: appState.write,
43
+ deleteClientAppState: appState.remove,
44
+ }),
45
+ );
42
46
 
43
47
  import { AgentNativeI18nProvider } from "@agent-native/core/client/i18n";
44
48
 
@@ -728,10 +732,12 @@ describe("SettingsShell", () => {
728
732
  // the router is still committing the previous navigation.
729
733
  const [, setTick] = useState(0);
730
734
  React.useEffect(() => {
731
- // Several ticks land inside each 60ms loader, so the shell re-renders
732
- // mid-navigation; a tick shorter than one render keeps act() from ever
733
- // going idle on a slow runner.
734
- const timer = setInterval(() => setTick((tick) => tick + 1), 25);
735
+ // Re-render during the 60ms loader without keeping act() busy forever.
736
+ let ticks = 0;
737
+ const timer = setInterval(() => {
738
+ setTick((tick) => tick + 1);
739
+ if (++ticks === 5) clearInterval(timer);
740
+ }, 10);
735
741
  return () => clearInterval(timer);
736
742
  }, []);
737
743
  const current = useLocation();
@@ -1,5 +1,6 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
+ import type { LabStates } from "@agent-native/core/client/labs/use-lab";
3
4
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
5
  import React, { act, Suspense } from "react";
5
6
  import { createRoot, type Root } from "react-dom/client";
@@ -32,17 +33,17 @@ vi.mock("@agent-native/core/client/app-model-default", () => ({
32
33
  }));
33
34
 
34
35
  const labsActions = vi.hoisted(() => ({
35
- query: { data: undefined as Record<string, boolean> | undefined },
36
+ query: { data: undefined as LabStates | undefined },
36
37
  isError: false,
37
38
  mutate: vi.fn(),
38
39
  }));
39
40
  vi.mock("@agent-native/core/client/hooks", () => ({
40
- useChangeVersions: () => 0,
41
41
  useDemoModeStatus: () => ({
42
42
  enabled: false,
43
43
  forced: false,
44
44
  isLoading: false,
45
45
  }),
46
+ useChangeVersions: () => 0,
46
47
  }));
47
48
  vi.mock("@agent-native/core/client/use-action", () => ({
48
49
  useActionQuery: () => ({
@@ -422,9 +423,13 @@ describe("app group pages", () => {
422
423
 
423
424
  it("hides labs the server didn't register once it answers", async () => {
424
425
  labsActions.query.data = {
425
- "chatgpt-subscription": false,
426
- "clips.wisprflow": false,
427
- "clips.meetings": true,
426
+ "chatgpt-subscription": {
427
+ enabled: false,
428
+ source: "default",
429
+ mixed: false,
430
+ },
431
+ "clips.wisprflow": { enabled: false, source: "choice", mixed: false },
432
+ "clips.meetings": { enabled: true, source: "choice", mixed: false },
428
433
  };
429
434
  await renderPage(LabsSettingsPage, { input: { labs } });
430
435
  expect(switchFor("Retired lab")).toBeNull();
@@ -436,6 +441,31 @@ describe("app group pages", () => {
436
441
  ).toBe("true");
437
442
  expect(switchFor("Voice dictation")?.disabled).toBe(false);
438
443
  });
444
+
445
+ it("offers explicit choices for inherited mixed settings", async () => {
446
+ labsActions.query.data = {
447
+ "clips.meetings": {
448
+ enabled: false,
449
+ source: "legacy",
450
+ mixed: true,
451
+ legacyValues: { capture: false, recovery: true },
452
+ },
453
+ };
454
+ await renderPage(LabsSettingsPage, { input: { labs } });
455
+ expect(switchFor("Meetings and transcription")).toBeNull();
456
+ const on = container.querySelector<HTMLButtonElement>(
457
+ '[aria-label="Meetings and transcription: On"]',
458
+ );
459
+ expect(on?.disabled).toBe(false);
460
+ act(() => on?.click());
461
+ expect(labsActions.mutate).toHaveBeenCalledWith(
462
+ { key: "clips.meetings", enabled: true },
463
+ expect.any(Object),
464
+ );
465
+ expect(
466
+ switchFor("Meetings and transcription")?.getAttribute("aria-checked"),
467
+ ).toBe("true");
468
+ });
439
469
  });
440
470
 
441
471
  describe("Automations", () => {
@@ -10,6 +10,9 @@ const orgState = vi.hoisted(() => ({
10
10
  value: { orgId: "org-1", orgName: "Acme", role: "member" as string },
11
11
  }));
12
12
 
13
+ vi.mock("@agent-native/core/client/org/hooks", () => ({
14
+ useOrg: () => ({ data: orgState.value, isLoading: false }),
15
+ }));
13
16
  vi.mock("@agent-native/core/client/org/hooks", () => ({
14
17
  useOrg: () => ({ data: orgState.value, isLoading: false }),
15
18
  }));
@@ -54,9 +54,9 @@ vi.mock("@agent-native/core/client/hooks", () => ({
54
54
  }),
55
55
  }));
56
56
 
57
- vi.mock("@agent-native/core/client/use-action", () => ({
58
- useActionQuery: () => ({ data: null, isError: false, isLoading: false }),
59
- }));
57
+ vi.mock("@agent-native/core/client/use-action", async () =>
58
+ vi.importMock("@agent-native/core/client/hooks"),
59
+ );
60
60
 
61
61
  vi.mock("@agent-native/core/client/i18n", () => ({
62
62
  useT:
@@ -12,6 +12,7 @@ vi.mock("@agent-native/core/client/route-chunk-recovery", () => ({
12
12
 
13
13
  import { AgentNativeI18nProvider } from "@agent-native/core/client/i18n";
14
14
 
15
+ import { createToolkitI18nCatalog } from "../i18n.js";
15
16
  import { LazyChunkErrorBoundary } from "./LazyChunkErrorBoundary.js";
16
17
  import { LazyChunkRetryFallback } from "./LazyChunkRetryFallback.js";
17
18
 
@@ -42,6 +43,7 @@ describe("LazyChunkErrorBoundary", () => {
42
43
  await act(async () => {
43
44
  root.render(
44
45
  <AgentNativeI18nProvider
46
+ catalog={createToolkitI18nCatalog({ messages: {} })}
45
47
  initialLocale="en-US"
46
48
  initialPreference="en-US"
47
49
  persistPreference={false}
@@ -32,7 +32,10 @@ describe("AppSidebar overflow affordances", () => {
32
32
  root = createRoot(container);
33
33
  });
34
34
 
35
- afterEach(() => {
35
+ afterEach(async () => {
36
+ await act(async () => {
37
+ await new Promise((resolve) => setTimeout(resolve, 20));
38
+ });
36
39
  act(() => root.unmount());
37
40
  container.remove();
38
41
  vi.unstubAllGlobals();
@@ -378,6 +378,44 @@ describe("visual style controls", () => {
378
378
  expect(input.getAttribute("data-design-history-hotkeys")).toBe("true");
379
379
  });
380
380
 
381
+ it("commits a zero pixel value with leading zeros", () => {
382
+ const onChange = vi.fn();
383
+ act(() => {
384
+ root.render(
385
+ <VisualScrubInput
386
+ label="Border width"
387
+ value={1}
388
+ min={0}
389
+ unit="px"
390
+ onChange={onChange}
391
+ />,
392
+ );
393
+ });
394
+
395
+ const input = container.querySelector<HTMLInputElement>("input")!;
396
+ act(() => input.focus());
397
+ act(() => {
398
+ const setter = Object.getOwnPropertyDescriptor(
399
+ HTMLInputElement.prototype,
400
+ "value",
401
+ )!.set!;
402
+ setter.call(input, "00px");
403
+ input.dispatchEvent(new Event("input", { bubbles: true }));
404
+ });
405
+ act(() => {
406
+ input.dispatchEvent(
407
+ new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
408
+ );
409
+ });
410
+
411
+ expect(onChange).toHaveBeenCalledWith(
412
+ 0,
413
+ expect.objectContaining({ source: "commit", expression: "00px" }),
414
+ );
415
+ expect(onChange).toHaveBeenCalledTimes(1);
416
+ expect(input.value).toBe("0px");
417
+ });
418
+
381
419
  it("releases focus on Enter when the consumer opts in", () => {
382
420
  const onChange = vi.fn();
383
421
  act(() => {
@@ -1786,6 +1786,7 @@ describe("useCollabReconcile — concurrent edit / lost-update guards", () => {
1786
1786
  });
1787
1787
 
1788
1788
  it("still clears a stale collab value when there is no local emission", async () => {
1789
+ vi.useFakeTimers();
1789
1790
  const { captured, Harness } = makeCollabSeedHarness("stale persisted body");
1790
1791
  act(() =>
1791
1792
  root.render(
@@ -1799,7 +1800,7 @@ describe("useCollabReconcile — concurrent edit / lost-update guards", () => {
1799
1800
  ),
1800
1801
  );
1801
1802
  expect(getEditorMarkdown(captured.editor!)).toBe("stale persisted body");
1802
- await act(async () => new Promise((resolve) => setTimeout(resolve, 30)));
1803
+ await act(async () => vi.advanceTimersByTimeAsync(30));
1803
1804
 
1804
1805
  expect(getEditorMarkdown(captured.editor!)).toBe("canonical SQL body");
1805
1806
  expect(captured.emitted).toEqual([]);