@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
@@ -0,0 +1,270 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import {
8
+ ResourceIconPicker,
9
+ type ResourceIconPickerLabels,
10
+ } from "./ResourceIconPicker.js";
11
+ import type { ResourceIconImage, ResourceIconValue } from "./types.js";
12
+
13
+ vi.mock("./catalog.js", () => ({ loadTablerCatalog: async () => [] }));
14
+ vi.mock("./emoji-catalog.js", () => ({
15
+ EMOJI_CATEGORIES: [],
16
+ loadEmojiCatalog: async () => [],
17
+ }));
18
+
19
+ const labels: ResourceIconPickerLabels = {
20
+ trigger: "Choose icon",
21
+ iconsTab: "Icons",
22
+ emojiTab: "Emoji",
23
+ uploadTab: "Upload",
24
+ search: "Search",
25
+ noResults: "No results",
26
+ recents: "Recent",
27
+ colors: "Colors",
28
+ defaultColor: "Default",
29
+ remove: "Remove",
30
+ upload: "Choose image",
31
+ uploading: "Uploading",
32
+ saveError: "Could not save icon.",
33
+ uploadFailed: "Could not upload icon image. Try again.",
34
+ uploadTooLarge: "Icon image must be 5 MiB or smaller.",
35
+ uploadUnsupportedType: "Only PNG, JPEG, WebP, or SVG images can be uploaded.",
36
+ loadError: "Could not load uploaded icons.",
37
+ retry: "Retry",
38
+ };
39
+ const previousIcon: ResourceIconValue = {
40
+ version: 1,
41
+ kind: "emoji",
42
+ emoji: "🌿",
43
+ };
44
+ const uploadedIcon: ResourceIconImage = {
45
+ version: 1,
46
+ kind: "image",
47
+ authority: "url",
48
+ assetId: "/uploaded.svg",
49
+ };
50
+
51
+ describe("ResourceIconPicker uploads", () => {
52
+ let container: HTMLDivElement;
53
+ let root: Root;
54
+
55
+ beforeEach(() => {
56
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
57
+ vi.stubGlobal(
58
+ "ResizeObserver",
59
+ class {
60
+ observe() {}
61
+ unobserve() {}
62
+ disconnect() {}
63
+ },
64
+ );
65
+ container = document.createElement("div");
66
+ document.body.append(container);
67
+ root = createRoot(container);
68
+ });
69
+
70
+ afterEach(() => {
71
+ act(() => root.unmount());
72
+ container.remove();
73
+ vi.unstubAllGlobals();
74
+ });
75
+
76
+ async function renderPicker(
77
+ onUpload: (file: File) => Promise<ResourceIconImage>,
78
+ onValueChange = vi.fn(),
79
+ formatUploadError?: (error: unknown) => string,
80
+ ) {
81
+ await act(async () => {
82
+ root.render(
83
+ <ResourceIconPicker
84
+ value={previousIcon}
85
+ onValueChange={onValueChange}
86
+ onUpload={onUpload}
87
+ formatUploadError={formatUploadError}
88
+ labels={labels}
89
+ open
90
+ portalled={false}
91
+ />,
92
+ );
93
+ });
94
+ const uploadTab = Array.from(container.querySelectorAll("[role=tab]")).find(
95
+ (tab) => tab.textContent === labels.uploadTab,
96
+ );
97
+ expect(uploadTab).toBeDefined();
98
+ await act(async () => {
99
+ uploadTab!.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
100
+ uploadTab!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
101
+ });
102
+ }
103
+
104
+ async function choose(file: File) {
105
+ const input =
106
+ container.querySelector<HTMLInputElement>('input[type="file"]')!;
107
+ Object.defineProperty(input, "files", {
108
+ configurable: true,
109
+ value: [file],
110
+ });
111
+ await act(async () => {
112
+ input.dispatchEvent(new Event("change", { bubbles: true }));
113
+ });
114
+ }
115
+
116
+ async function drop(file: File) {
117
+ const target = Array.from(container.querySelectorAll("button")).find(
118
+ (button) => button.textContent?.includes(labels.upload),
119
+ )!;
120
+ const event = new Event("drop", { bubbles: true, cancelable: true });
121
+ Object.defineProperty(event, "dataTransfer", { value: { files: [file] } });
122
+ await act(async () => target.dispatchEvent(event));
123
+ }
124
+
125
+ function alertText() {
126
+ return container.querySelector('[role="alert"]')?.textContent;
127
+ }
128
+
129
+ it("rejects unsupported types from input and drop without replacing the icon", async () => {
130
+ const onUpload = vi.fn().mockResolvedValue(uploadedIcon);
131
+ const onValueChange = vi.fn();
132
+ await renderPicker(onUpload, onValueChange);
133
+
134
+ await choose(new File(["gif"], "animation.gif", { type: "image/gif" }));
135
+ expect(alertText()).toBe(labels.uploadUnsupportedType);
136
+
137
+ await drop(new File(["text"], "notes.txt", { type: "text/plain" }));
138
+ expect(alertText()).toBe(labels.uploadUnsupportedType);
139
+ expect(onUpload).not.toHaveBeenCalled();
140
+ expect(onValueChange).not.toHaveBeenCalled();
141
+ });
142
+
143
+ it("shows the size limit for a supported file from either path", async () => {
144
+ const onUpload = vi.fn().mockResolvedValue(uploadedIcon);
145
+ await renderPicker(onUpload);
146
+
147
+ const large = new File([new Uint8Array(5 * 1024 * 1024 + 1)], "large.png", {
148
+ type: "image/png",
149
+ });
150
+ await choose(large);
151
+ expect(alertText()).toBe(labels.uploadTooLarge);
152
+ await drop(large);
153
+ expect(alertText()).toBe(labels.uploadTooLarge);
154
+ await drop(
155
+ new File([new Uint8Array(5 * 1024 * 1024 + 1)], "large.txt", {
156
+ type: "text/plain",
157
+ }),
158
+ );
159
+ expect(alertText()).toBe(labels.uploadTooLarge);
160
+ expect(onUpload).not.toHaveBeenCalled();
161
+ });
162
+
163
+ it("keeps a mapped failure visible and clears it after a successful retry", async () => {
164
+ const onUpload = vi
165
+ .fn()
166
+ .mockRejectedValueOnce(new Error("private server detail"))
167
+ .mockResolvedValueOnce(uploadedIcon);
168
+ const onValueChange = vi.fn();
169
+ await renderPicker(
170
+ onUpload,
171
+ onValueChange,
172
+ () => "Storage is unavailable.",
173
+ );
174
+
175
+ await choose(new File(["first"], "first.png", { type: "image/png" }));
176
+ expect(alertText()).toBe("Storage is unavailable.");
177
+ expect(container.textContent).not.toContain("private server detail");
178
+ expect(onValueChange).not.toHaveBeenCalled();
179
+
180
+ await drop(new File(["<svg />"], "retry.SVG"));
181
+ expect(onUpload).toHaveBeenCalledTimes(2);
182
+ expect(onUpload.mock.calls[1]?.[0].type).toBe("image/svg+xml");
183
+ expect(onValueChange).toHaveBeenCalledWith(uploadedIcon);
184
+ expect(alertText()).toBeUndefined();
185
+ });
186
+
187
+ it("uses the localized upload failure when the caller has no safe detail", async () => {
188
+ const onUpload = vi
189
+ .fn()
190
+ .mockRejectedValue(new Error("private server detail"));
191
+ await renderPicker(onUpload);
192
+
193
+ await choose(new File(["first"], "first.webp", { type: "image/webp" }));
194
+ expect(alertText()).toBe(labels.uploadFailed);
195
+ expect(container.textContent).not.toContain("private server detail");
196
+ });
197
+
198
+ it("makes an unavailable upload library visible and retryable", async () => {
199
+ const onRetry = vi.fn();
200
+ await act(async () => {
201
+ root.render(
202
+ <ResourceIconPicker
203
+ value={previousIcon}
204
+ onValueChange={vi.fn()}
205
+ onUpload={vi.fn().mockResolvedValue(uploadedIcon)}
206
+ uploadedImagesError
207
+ onUploadedImagesRetry={onRetry}
208
+ labels={labels}
209
+ open
210
+ portalled={false}
211
+ />,
212
+ );
213
+ });
214
+ const uploadTab = Array.from(container.querySelectorAll("[role=tab]")).find(
215
+ (tab) => tab.textContent === labels.uploadTab,
216
+ );
217
+ await act(async () => {
218
+ uploadTab!.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
219
+ uploadTab!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
220
+ });
221
+
222
+ expect(alertText()).toContain(labels.loadError);
223
+ const retryButton = Array.from(container.querySelectorAll("button")).find(
224
+ (button) => button.textContent === labels.retry,
225
+ );
226
+ await act(async () => retryButton!.click());
227
+ expect(onRetry).toHaveBeenCalledOnce();
228
+ });
229
+
230
+ it("does not offer private recent images outside the current library", async () => {
231
+ const personal: ResourceIconImage = {
232
+ version: 1,
233
+ kind: "image",
234
+ authority: "private-icon",
235
+ assetId: "personal-asset",
236
+ alt: "Personal logo",
237
+ };
238
+ const workspace: ResourceIconImage = {
239
+ ...personal,
240
+ assetId: "workspace-asset",
241
+ alt: "Workspace logo",
242
+ };
243
+ await act(async () => {
244
+ root.render(
245
+ <ResourceIconPicker
246
+ value={previousIcon}
247
+ onValueChange={vi.fn()}
248
+ onUpload={vi.fn()}
249
+ uploadedImages={[workspace]}
250
+ recentValues={[personal, workspace]}
251
+ labels={labels}
252
+ open
253
+ portalled={false}
254
+ />,
255
+ );
256
+ });
257
+ const uploadTab = Array.from(container.querySelectorAll("[role=tab]")).find(
258
+ (tab) => tab.textContent === labels.uploadTab,
259
+ );
260
+ await act(async () => {
261
+ uploadTab!.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
262
+ uploadTab!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
263
+ });
264
+
265
+ expect(
266
+ container.querySelector('[aria-label="Workspace logo"]'),
267
+ ).not.toBeNull();
268
+ expect(container.querySelector('[aria-label="Personal logo"]')).toBeNull();
269
+ });
270
+ });
@@ -51,6 +51,32 @@ const colors: Array<ResourceIconColor | undefined> = [
51
51
  const columns = 7;
52
52
  const rowHeight = 36;
53
53
  const MAX_ICON_UPLOAD_BYTES = 5 * 1024 * 1024;
54
+ const ICON_UPLOAD_MIME_TYPES = new Set([
55
+ "image/png",
56
+ "image/jpeg",
57
+ "image/webp",
58
+ "image/svg+xml",
59
+ ]);
60
+ const ICON_UPLOAD_EXTENSION_TYPES: Record<string, string> = {
61
+ png: "image/png",
62
+ jpg: "image/jpeg",
63
+ jpeg: "image/jpeg",
64
+ webp: "image/webp",
65
+ svg: "image/svg+xml",
66
+ };
67
+
68
+ function supportedIconUpload(file: File): File | null {
69
+ const mimeType = file.type.split(";")[0]?.trim().toLowerCase() ?? "";
70
+ if (ICON_UPLOAD_MIME_TYPES.has(mimeType)) return file;
71
+ if (mimeType && mimeType !== "application/octet-stream") return null;
72
+ const extension = file.name.split(".").pop()?.toLowerCase() ?? "";
73
+ const inferredType = ICON_UPLOAD_EXTENSION_TYPES[extension];
74
+ if (!inferredType) return null;
75
+ return new File([file], file.name, {
76
+ type: inferredType,
77
+ lastModified: file.lastModified,
78
+ });
79
+ }
54
80
 
55
81
  export interface ResourceIconPickerLabels {
56
82
  trigger: string;
@@ -69,7 +95,9 @@ export interface ResourceIconPickerLabels {
69
95
  saveError?: string;
70
96
  retry?: string;
71
97
  uploadHint?: string;
98
+ uploadFailed?: string;
72
99
  uploadTooLarge?: string;
100
+ uploadUnsupportedType?: string;
73
101
  allCategories?: string;
74
102
  colorNames?: Partial<Record<ResourceIconColor, string>>;
75
103
  categoryNames?: Record<string, string>;
@@ -83,8 +111,11 @@ export interface ResourceIconPickerProps {
83
111
  recentValues?: readonly ResourceIconValue[];
84
112
  onRecentsChange?: (recents: ResourceIconValue[]) => void;
85
113
  uploadedImages?: readonly ResourceIconImage[];
114
+ uploadedImagesError?: boolean;
115
+ onUploadedImagesRetry?: () => void;
86
116
  onUpload?: (file: File) => Promise<ResourceIconImage>;
87
117
  onUploadError?: (error: unknown) => void;
118
+ formatUploadError?: (error: unknown) => string;
88
119
  resolveImageUrl?: (
89
120
  image: Extract<ResourceIconValue, { kind: "image" }>,
90
121
  ) => string | undefined;
@@ -323,8 +354,11 @@ export function ResourceIconPicker({
323
354
  recentValues,
324
355
  onRecentsChange,
325
356
  uploadedImages = [],
357
+ uploadedImagesError = false,
358
+ onUploadedImagesRetry,
326
359
  onUpload,
327
360
  onUploadError,
361
+ formatUploadError,
328
362
  resolveImageUrl,
329
363
  disabled,
330
364
  className,
@@ -475,13 +509,22 @@ export function ResourceIconPicker({
475
509
  if (fileInput.current) fileInput.current.value = "";
476
510
  return;
477
511
  }
512
+ const supportedFile = supportedIconUpload(file);
513
+ if (!supportedFile) {
514
+ setSaveFailed(false);
515
+ setUploadError(labels.uploadUnsupportedType ?? labels.saveError);
516
+ if (fileInput.current) fileInput.current.value = "";
517
+ return;
518
+ }
478
519
  setUploading(true);
479
520
  setSaveFailed(false);
480
521
  setUploadError(undefined);
481
522
  try {
482
- await select(await onUpload(file));
523
+ await select(await onUpload(supportedFile));
483
524
  } catch (error) {
484
- setSaveFailed(true);
525
+ setUploadError(
526
+ formatUploadError?.(error) || labels.uploadFailed || labels.saveError,
527
+ );
485
528
  onUploadError?.(error);
486
529
  } finally {
487
530
  setUploading(false);
@@ -524,7 +567,14 @@ export function ResourceIconPicker({
524
567
  const uploadChoices = [
525
568
  ...uploadedImages,
526
569
  ...effectiveRecents.filter(
527
- (entry): entry is ResourceIconImage => entry.kind === "image",
570
+ (entry): entry is ResourceIconImage =>
571
+ entry.kind === "image" &&
572
+ (entry.authority !== "private-icon" ||
573
+ uploadedImages.some(
574
+ (uploaded) =>
575
+ uploaded.authority === entry.authority &&
576
+ uploaded.assetId === entry.assetId,
577
+ )),
528
578
  ),
529
579
  ].filter(
530
580
  (entry, index, all) =>
@@ -720,7 +770,7 @@ export function ResourceIconPicker({
720
770
  <input
721
771
  ref={fileInput}
722
772
  type="file"
723
- accept="image/png,image/jpeg,image/webp,image/svg+xml"
773
+ accept="image/png,image/jpeg,image/webp,image/svg+xml,.png,.jpg,.jpeg,.webp,.svg"
724
774
  className="hidden"
725
775
  disabled={uploading}
726
776
  onChange={(event) => {
@@ -728,6 +778,24 @@ export function ResourceIconPicker({
728
778
  if (file) void upload(file);
729
779
  }}
730
780
  />
781
+ {uploadedImagesError && (
782
+ <div
783
+ role="alert"
784
+ className="flex items-center justify-between gap-2 text-sm text-destructive"
785
+ >
786
+ <span>{labels.loadError}</span>
787
+ {onUploadedImagesRetry && (
788
+ <Button
789
+ type="button"
790
+ variant="outline"
791
+ size="sm"
792
+ onClick={onUploadedImagesRetry}
793
+ >
794
+ {labels.retry}
795
+ </Button>
796
+ )}
797
+ </div>
798
+ )}
731
799
  {uploadChoices.length > 0 && (
732
800
  <div className="grid grid-cols-7 gap-1">
733
801
  {uploadChoices.map((entry) => (