@open-mercato/ui 0.6.7 → 0.6.8-canary.320.1.bf889f49e4

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 (229) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +4 -2
  3. package/agentic/standalone-guide.md +1 -1
  4. package/build.mjs +2 -54
  5. package/dist/ai/AiChat.js +73 -18
  6. package/dist/ai/AiChat.js.map +2 -2
  7. package/dist/ai/AiChatSessions.js +11 -2
  8. package/dist/ai/AiChatSessions.js.map +2 -2
  9. package/dist/ai/LoopDisabledBanner.js +1 -1
  10. package/dist/ai/LoopDisabledBanner.js.map +2 -2
  11. package/dist/ai/parts/ConfirmationCard.js +4 -4
  12. package/dist/ai/parts/ConfirmationCard.js.map +2 -2
  13. package/dist/ai/parts/FieldDiffCard.js +2 -2
  14. package/dist/ai/parts/FieldDiffCard.js.map +2 -2
  15. package/dist/ai/parts/MutationResultCard.js +3 -3
  16. package/dist/ai/parts/MutationResultCard.js.map +2 -2
  17. package/dist/ai/parts/useAiPendingActionPolling.js +12 -2
  18. package/dist/ai/parts/useAiPendingActionPolling.js.map +2 -2
  19. package/dist/ai/ui-parts/pending-phase3-placeholder.js +1 -1
  20. package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +2 -2
  21. package/dist/backend/AppShell.js +43 -4
  22. package/dist/backend/AppShell.js.map +2 -2
  23. package/dist/backend/AuthSessionGuard.js +14 -0
  24. package/dist/backend/AuthSessionGuard.js.map +2 -2
  25. package/dist/backend/BackendChromeProvider.js +6 -1
  26. package/dist/backend/BackendChromeProvider.js.map +2 -2
  27. package/dist/backend/CrudForm.js +6 -4
  28. package/dist/backend/CrudForm.js.map +2 -2
  29. package/dist/backend/DataTable.js +20 -18
  30. package/dist/backend/DataTable.js.map +2 -2
  31. package/dist/backend/JsonBuilder.js +3 -3
  32. package/dist/backend/JsonBuilder.js.map +2 -2
  33. package/dist/backend/NextStepCallout.js +4 -4
  34. package/dist/backend/NextStepCallout.js.map +1 -1
  35. package/dist/backend/PerspectiveSidebar.js +1 -1
  36. package/dist/backend/PerspectiveSidebar.js.map +2 -2
  37. package/dist/backend/ProfileDropdown.js +2 -1
  38. package/dist/backend/ProfileDropdown.js.map +2 -2
  39. package/dist/backend/RowActions.js +2 -2
  40. package/dist/backend/RowActions.js.map +2 -2
  41. package/dist/backend/confirm-dialog/ConfirmDialog.js +13 -1
  42. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
  43. package/dist/backend/crud/useGroupOrder.js +14 -21
  44. package/dist/backend/crud/useGroupOrder.js.map +2 -2
  45. package/dist/backend/custom-fields/FieldDefinitionsManager.js +1 -1
  46. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +2 -2
  47. package/dist/backend/dashboard/DashboardScreen.js +22 -8
  48. package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
  49. package/dist/backend/detail/AddressTiles.js +4 -5
  50. package/dist/backend/detail/AddressTiles.js.map +2 -2
  51. package/dist/backend/detail/AttachmentDeleteDialog.js +1 -1
  52. package/dist/backend/detail/AttachmentDeleteDialog.js.map +2 -2
  53. package/dist/backend/detail/AttachmentMetadataDialog.js.map +1 -1
  54. package/dist/backend/detail/AttachmentsSection.js +1 -1
  55. package/dist/backend/detail/AttachmentsSection.js.map +2 -2
  56. package/dist/backend/detail/NotesSection.js +1 -1
  57. package/dist/backend/detail/NotesSection.js.map +2 -2
  58. package/dist/backend/detail/TagsSection.js +1 -1
  59. package/dist/backend/detail/TagsSection.js.map +2 -2
  60. package/dist/backend/filters/AdvancedFilterPanel.js +1 -1
  61. package/dist/backend/filters/AdvancedFilterPanel.js.map +2 -2
  62. package/dist/backend/forms/FormHeader.js +1 -2
  63. package/dist/backend/forms/FormHeader.js.map +2 -2
  64. package/dist/backend/icons/lucideRegistry.generated.js +1 -25
  65. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  66. package/dist/backend/icons/lucideRegistry.js +3 -3
  67. package/dist/backend/icons/lucideRegistry.js.map +2 -2
  68. package/dist/backend/icons/lucideRegistryRuntime.js +28 -0
  69. package/dist/backend/icons/lucideRegistryRuntime.js.map +7 -0
  70. package/dist/backend/injection/ComponentOverrideProvider.js +7 -1
  71. package/dist/backend/injection/ComponentOverrideProvider.js.map +2 -2
  72. package/dist/backend/injection/spotIds.js +23 -18
  73. package/dist/backend/injection/spotIds.js.map +2 -2
  74. package/dist/backend/injection/useRegisteredComponent.js +3 -2
  75. package/dist/backend/injection/useRegisteredComponent.js.map +2 -2
  76. package/dist/backend/inputs/ComboboxInput.js.map +1 -1
  77. package/dist/backend/messages/EmailThreadsPanel.js +1 -1
  78. package/dist/backend/messages/EmailThreadsPanel.js.map +2 -2
  79. package/dist/backend/perspectiveState.js +34 -0
  80. package/dist/backend/perspectiveState.js.map +7 -0
  81. package/dist/backend/schedule/ScheduleAgenda.js +3 -3
  82. package/dist/backend/schedule/ScheduleAgenda.js.map +1 -1
  83. package/dist/backend/schedule/ScheduleGrid.js +3 -3
  84. package/dist/backend/schedule/ScheduleGrid.js.map +1 -1
  85. package/dist/backend/schedule/ScheduleToolbar.js +87 -70
  86. package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
  87. package/dist/backend/sidebar/SidebarCustomizationEditor.js +30 -2
  88. package/dist/backend/sidebar/SidebarCustomizationEditor.js.map +2 -2
  89. package/dist/backend/sidebar/customization-helpers.js +30 -0
  90. package/dist/backend/sidebar/customization-helpers.js.map +2 -2
  91. package/dist/backend/utils/apiCall.js +21 -1
  92. package/dist/backend/utils/apiCall.js.map +2 -2
  93. package/dist/backend/utils/customFieldDefs.js.map +2 -2
  94. package/dist/backend/utils/customFieldForms.js +10 -9
  95. package/dist/backend/utils/customFieldForms.js.map +2 -2
  96. package/dist/backend/utils/fetchAllPages.js +57 -0
  97. package/dist/backend/utils/fetchAllPages.js.map +7 -0
  98. package/dist/backend/utils/nav.js +5 -2
  99. package/dist/backend/utils/nav.js.map +2 -2
  100. package/dist/backend/version-history/VersionHistoryPanel.js +1 -1
  101. package/dist/backend/version-history/VersionHistoryPanel.js.map +2 -2
  102. package/dist/frontend/LanguageSwitcher.js +2 -1
  103. package/dist/frontend/LanguageSwitcher.js.map +2 -2
  104. package/dist/portal/PortalContext.js +1 -1
  105. package/dist/portal/PortalContext.js.map +2 -2
  106. package/dist/portal/hooks/portalBridgeStatus.js +25 -0
  107. package/dist/portal/hooks/portalBridgeStatus.js.map +7 -0
  108. package/dist/portal/hooks/usePortalEventBridge.js +6 -0
  109. package/dist/portal/hooks/usePortalEventBridge.js.map +2 -2
  110. package/dist/portal/hooks/usePortalNotifications.js +42 -4
  111. package/dist/portal/hooks/usePortalNotifications.js.map +2 -2
  112. package/dist/portal/utils/nav.js +2 -3
  113. package/dist/portal/utils/nav.js.map +2 -2
  114. package/dist/primitives/ErrorNotice.js +1 -1
  115. package/dist/primitives/ErrorNotice.js.map +2 -2
  116. package/dist/primitives/Notice.js +1 -1
  117. package/dist/primitives/Notice.js.map +1 -1
  118. package/dist/primitives/button.js +9 -4
  119. package/dist/primitives/button.js.map +2 -2
  120. package/dist/theme/ThemeProvider.js +1 -1
  121. package/dist/theme/ThemeProvider.js.map +2 -2
  122. package/dist/theme/index.js +3 -0
  123. package/dist/theme/index.js.map +2 -2
  124. package/dist/theme/theme-init-script.js +17 -0
  125. package/dist/theme/theme-init-script.js.map +7 -0
  126. package/figma/alert.figma.tsx +35 -0
  127. package/figma/badge.figma.tsx +33 -0
  128. package/figma/button.figma.tsx +33 -0
  129. package/figma/checkbox.figma.tsx +28 -0
  130. package/figma/drawer.figma.tsx +40 -0
  131. package/figma/input-variants.figma.tsx +58 -0
  132. package/figma/radio.figma.tsx +16 -0
  133. package/figma/select.figma.tsx +24 -0
  134. package/figma/status-badge.figma.tsx +24 -0
  135. package/figma/switch.figma.tsx +28 -0
  136. package/figma/tabs.figma.tsx +22 -0
  137. package/figma/tag.figma.tsx +27 -0
  138. package/figma.config.json +6 -0
  139. package/jest.config.cjs +20 -10
  140. package/package.json +21 -18
  141. package/scripts/lucideRegistrySource.cjs +133 -0
  142. package/src/ai/AiChat.tsx +111 -34
  143. package/src/ai/AiChatSessions.tsx +16 -4
  144. package/src/ai/LoopDisabledBanner.tsx +1 -1
  145. package/src/ai/__tests__/AiChat.test.tsx +222 -2
  146. package/src/ai/__tests__/AiChatSessions.test.tsx +62 -0
  147. package/src/ai/parts/ConfirmationCard.tsx +4 -4
  148. package/src/ai/parts/FieldDiffCard.tsx +2 -2
  149. package/src/ai/parts/MutationResultCard.tsx +3 -3
  150. package/src/ai/parts/__tests__/useAiPendingActionPolling.test.tsx +61 -0
  151. package/src/ai/parts/useAiPendingActionPolling.ts +21 -6
  152. package/src/ai/ui-parts/pending-phase3-placeholder.tsx +2 -2
  153. package/src/backend/AppShell.tsx +55 -4
  154. package/src/backend/AuthSessionGuard.tsx +29 -0
  155. package/src/backend/BackendChromeProvider.tsx +19 -1
  156. package/src/backend/CrudForm.tsx +18 -4
  157. package/src/backend/DataTable.tsx +36 -19
  158. package/src/backend/JsonBuilder.tsx +3 -3
  159. package/src/backend/NextStepCallout.tsx +4 -4
  160. package/src/backend/PerspectiveSidebar.tsx +1 -1
  161. package/src/backend/ProfileDropdown.tsx +1 -0
  162. package/src/backend/RowActions.tsx +2 -2
  163. package/src/backend/__tests__/AppShell.test.tsx +65 -7
  164. package/src/backend/__tests__/AuthSessionGuard.test.tsx +52 -0
  165. package/src/backend/__tests__/BackendChromeProvider.test.tsx +98 -2
  166. package/src/backend/__tests__/CrudForm.fieldsetBinding.test.tsx +158 -0
  167. package/src/backend/__tests__/CrudForm.groupInjectionColumn.test.tsx +98 -0
  168. package/src/backend/__tests__/DataTable.metaHiddenLateColumns.test.tsx +105 -0
  169. package/src/backend/__tests__/DataTable.perspectiveStorage.test.ts +62 -1
  170. package/src/backend/__tests__/nav-utils.test.ts +139 -0
  171. package/src/backend/confirm-dialog/ConfirmDialog.tsx +16 -1
  172. package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +68 -1
  173. package/src/backend/crud/__tests__/useGroupOrder.test.ts +39 -1
  174. package/src/backend/crud/useGroupOrder.ts +30 -25
  175. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +1 -1
  176. package/src/backend/dashboard/DashboardScreen.tsx +29 -8
  177. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +92 -1
  178. package/src/backend/detail/AddressTiles.tsx +3 -6
  179. package/src/backend/detail/AttachmentDeleteDialog.tsx +1 -1
  180. package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
  181. package/src/backend/detail/AttachmentsSection.tsx +1 -1
  182. package/src/backend/detail/NotesSection.tsx +1 -1
  183. package/src/backend/detail/TagsSection.tsx +1 -1
  184. package/src/backend/detail/__tests__/AttachmentAssignmentsEditor.test.tsx +21 -0
  185. package/src/backend/filters/AdvancedFilterPanel.tsx +1 -1
  186. package/src/backend/forms/FormHeader.tsx +1 -2
  187. package/src/backend/icons/__tests__/lucideRegistryGenerator.test.ts +240 -0
  188. package/src/backend/icons/lucideRegistry.generated.tsx +3 -37
  189. package/src/backend/icons/lucideRegistry.ts +2 -2
  190. package/src/backend/icons/lucideRegistryRuntime.tsx +36 -0
  191. package/src/backend/injection/ComponentOverrideProvider.tsx +11 -3
  192. package/src/backend/injection/__tests__/spotIds.extension-points.test.ts +38 -0
  193. package/src/backend/injection/spotIds.ts +23 -18
  194. package/src/backend/injection/useRegisteredComponent.tsx +3 -2
  195. package/src/backend/inputs/ComboboxInput.tsx +1 -1
  196. package/src/backend/messages/EmailThreadsPanel.tsx +1 -1
  197. package/src/backend/perspectiveState.ts +44 -0
  198. package/src/backend/schedule/ScheduleAgenda.tsx +3 -3
  199. package/src/backend/schedule/ScheduleGrid.tsx +3 -3
  200. package/src/backend/schedule/ScheduleToolbar.tsx +89 -68
  201. package/src/backend/sidebar/SidebarCustomizationEditor.tsx +42 -3
  202. package/src/backend/sidebar/__tests__/group-visibility.test.ts +131 -0
  203. package/src/backend/sidebar/customization-helpers.ts +47 -0
  204. package/src/backend/utils/__tests__/apiCall.test.ts +61 -0
  205. package/src/backend/utils/__tests__/customFieldForms.optionsPaging.test.ts +95 -0
  206. package/src/backend/utils/__tests__/fetchAllPages.test.ts +103 -0
  207. package/src/backend/utils/apiCall.ts +29 -1
  208. package/src/backend/utils/customFieldDefs.ts +1 -0
  209. package/src/backend/utils/customFieldForms.ts +15 -11
  210. package/src/backend/utils/fetchAllPages.ts +93 -0
  211. package/src/backend/utils/nav.ts +22 -2
  212. package/src/backend/version-history/VersionHistoryPanel.tsx +1 -1
  213. package/src/frontend/LanguageSwitcher.tsx +1 -0
  214. package/src/portal/PortalContext.tsx +1 -1
  215. package/src/portal/hooks/__tests__/usePortalEventBridge.test.tsx +66 -0
  216. package/src/portal/hooks/__tests__/usePortalNotifications.test.tsx +155 -0
  217. package/src/portal/hooks/portalBridgeStatus.ts +29 -0
  218. package/src/portal/hooks/usePortalEventBridge.ts +6 -0
  219. package/src/portal/hooks/usePortalNotifications.ts +55 -12
  220. package/src/portal/utils/__tests__/nav.test.ts +2 -2
  221. package/src/portal/utils/nav.ts +3 -4
  222. package/src/primitives/ErrorNotice.tsx +1 -1
  223. package/src/primitives/Notice.tsx +5 -5
  224. package/src/primitives/__tests__/button.test.tsx +70 -0
  225. package/src/primitives/button.tsx +10 -4
  226. package/src/theme/ThemeProvider.tsx +1 -2
  227. package/src/theme/__tests__/theme-init-script.test.ts +70 -0
  228. package/src/theme/index.ts +1 -0
  229. package/src/theme/theme-init-script.ts +23 -0
@@ -1,3 +1,3 @@
1
1
  Generated lucide registry with 145 icons -> /home/runner/work/open-mercato/open-mercato/packages/ui/src/backend/icons/lucideRegistry.generated.tsx
2
- [build:ui] found 393 entry points
2
+ [build:ui] found 398 entry points
3
3
  [build:ui] built successfully
package/AGENTS.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # UI Package - Agent Guidelines
2
2
 
3
- UI usage patterns based on customers, sales, and staff modules. Use these defaults when building new UI in `packages/ui` or consuming from other modules.
3
+ UI defaults for `packages/ui` and its consumers.
4
4
 
5
- > **DS reference:** [`.ai/ds-rules.md`](../../.ai/ds-rules.md) — color tokens, typography, spacing, decision trees. **Component reference (variants/sizes/props/examples/MUST rules):** [`.ai/ui-components.md`](../../.ai/ui-components.md). **Backend component families (charts, filters, detail sections, schedule, messages, page scaffolding):** [`.ai/ui-backend-components.md`](../../.ai/ui-backend-components.md) — check it before building any of these from scratch.
5
+ > References: [DS rules](../../.ai/ds-rules.md), [component contracts](../../.ai/ui-components.md), and [backend component families](../../.ai/ui-backend-components.md).
6
6
 
7
7
  ## Always
8
8
 
@@ -14,6 +14,7 @@ UI usage patterns based on customers, sales, and staff modules. Use these defaul
14
14
  - Use i18n keys and `useT()` for user-facing copy.
15
15
  - Keep UMES spot IDs, replacement handles, field/group IDs, and portal page metadata stable.
16
16
  - Follow `.ai/ds-rules.md` and `.ai/ui-components.md` for tokens, primitives, and component contracts.
17
+ - Keep documented primitive additions/renames and their `figma/*.figma.tsx` mappings in the same PR; run `yarn ds:code-connect:check`.
17
18
 
18
19
  ## Ask First
19
20
 
@@ -36,6 +37,7 @@ UI usage patterns based on customers, sales, and staff modules. Use these defaul
36
37
  ```bash
37
38
  yarn workspace @open-mercato/ui test
38
39
  yarn workspace @open-mercato/ui build
40
+ yarn ds:code-connect:check
39
41
  yarn i18n:check
40
42
  ```
41
43
 
@@ -189,7 +189,7 @@ import {
189
189
  | `usePortalAppEvent(pattern, handler, deps?)` | Listen for portal SSE events by glob pattern (e.g., `'sales.order.*'`) |
190
190
  | `usePortalEventBridge()` | Establish singleton SSE connection — mount once in shell/layout |
191
191
  | `usePortalInjectedMenuItems(surfaceId)` | Load feature-gated menu items for portal nav surfaces: `{ items, isLoading }` |
192
- | `usePortalNotifications()` | Poll portal notifications: `{ notifications, unreadCount, hasNew, isLoading, refresh, markAsRead, dismiss, markAllRead }` |
192
+ | `usePortalNotifications()` | Load portal notifications with SSE-first refresh and an 8-second fallback until the portal event bridge reports healthy: `{ notifications, unreadCount, hasNew, isLoading, refresh, markAsRead, dismiss, markAllRead }` |
193
193
  | `usePortalDashboardWidgets(spotId)` | Load UI injection widgets (with `Widget` component) for a portal spot: `{ widgets, isLoading, error }` |
194
194
 
195
195
  ### Portal Components
package/build.mjs CHANGED
@@ -3,6 +3,7 @@ import { readFileSync, writeFileSync } from 'node:fs'
3
3
  import { dirname, join } from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { buildPackage } from '../../scripts/build-package.mjs'
6
+ import { buildLucideRegistrySource } from './scripts/lucideRegistrySource.cjs'
6
7
 
7
8
  const packageDir = dirname(fileURLToPath(import.meta.url))
8
9
 
@@ -88,61 +89,8 @@ async function generateLucideRegistry() {
88
89
  }
89
90
  }
90
91
 
91
- resolved.sort((a, b) => a.kebab.localeCompare(b.kebab))
92
- const uniqueExports = [...new Set(resolved.map((r) => r.exportName))].sort((a, b) => a.localeCompare(b))
93
-
94
- const importSection = uniqueExports.length
95
- ? `import {\n${uniqueExports.map((n) => ` ${n},`).join('\n')}\n} from 'lucide-react'\n`
96
- : ``
97
-
98
- const registryEntries = resolved.map((r) => ` '${r.kebab}': ${r.exportName},`).join('\n')
99
-
100
92
  const outPath = join(packageDir, 'src/backend/icons/lucideRegistry.generated.tsx')
101
- const file = `/* eslint-disable */
102
- // AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.
103
- // Generated from discovered icon string usages across the repo.
104
-
105
- import type * as React from 'react'
106
- import type { LucideIcon } from 'lucide-react'
107
- ${importSection}
108
- export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
109
- ${registryEntries ? `${registryEntries}\n` : ''}}
110
-
111
- function normalizeKebabIconName(input: string): string {
112
- const trimmed = input.trim()
113
- if (!trimmed) return ''
114
- const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
115
- if (!withoutPrefix) return ''
116
- if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
117
- return withoutPrefix
118
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
119
- .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
120
- .toLowerCase()
121
- }
122
- return withoutPrefix
123
- .replace(/[_\\s]+/g, '-')
124
- .replace(/-+/g, '-')
125
- .toLowerCase()
126
- }
127
-
128
- export function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {
129
- if (!name) return null
130
- const normalized = normalizeKebabIconName(name)
131
- if (!normalized) return null
132
- return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null
133
- }
134
-
135
- export function resolveRegisteredLucideIconNode(
136
- name: string | undefined,
137
- className: string
138
- ): React.ReactNode | null {
139
- const Icon = resolveRegisteredLucideIcon(name)
140
- if (!Icon) return null
141
- return <Icon className={className} />
142
- }
143
- `
144
-
145
- writeFileSync(outPath, file)
93
+ writeFileSync(outPath, buildLucideRegistrySource(resolved))
146
94
  console.log(`Generated lucide registry with ${resolved.length} icons -> ${outPath}`)
147
95
  }
148
96
 
package/dist/ai/AiChat.js CHANGED
@@ -72,35 +72,68 @@ import { createLogger } from "@open-mercato/shared/lib/logger";
72
72
  const logger = createLogger("ui").child({ component: "AiChat" });
73
73
  const PREVIEW_DATA_URL_MAX_BYTES = 2 * 1024 * 1024;
74
74
  const COMPACT_FOOTER_MAX_WIDTH = 640;
75
+ function readErrorStatus(error) {
76
+ if (typeof error !== "object" || error === null) return void 0;
77
+ const { status } = error;
78
+ return typeof status === "number" ? status : void 0;
79
+ }
80
+ function logModelsFailure(status, details) {
81
+ const message = "Failed to load AI agent models";
82
+ if (status === 401 || status === 403) {
83
+ logger.warn(message, details);
84
+ return;
85
+ }
86
+ logger.error(message, details);
87
+ }
75
88
  function useAgentModels(agent) {
76
89
  const [providers, setProviders] = React.useState([]);
77
90
  const [allowRuntimeOverride, setAllowRuntimeOverride] = React.useState(false);
78
91
  const [defaultLabel, setDefaultLabel] = React.useState(null);
79
- const [loaded, setLoaded] = React.useState(false);
92
+ const [status, setStatus] = React.useState("loading");
93
+ const [degraded, setDegraded] = React.useState(false);
80
94
  React.useEffect(() => {
95
+ let cancelled = false;
81
96
  const modelsUrl = `/api/ai_assistant/ai/agents/${encodeURIComponent(agent)}/models`;
82
- setLoaded(false);
97
+ setStatus("loading");
83
98
  setDefaultLabel(null);
84
- void apiCall(modelsUrl).then((result) => {
99
+ setDegraded(false);
100
+ apiCall(modelsUrl).then((result) => {
101
+ if (cancelled) return;
85
102
  if (!result.ok || !result.result) {
86
- setLoaded(true);
103
+ logModelsFailure(result.status, { agent, status: result.status });
104
+ setStatus("failed");
87
105
  return;
88
106
  }
89
107
  const effectiveAllowRuntimeOverride = result.result.allowRuntimeOverride ?? result.result.allowRuntimeModelOverride ?? false;
90
108
  setAllowRuntimeOverride(effectiveAllowRuntimeOverride);
91
109
  setProviders(result.result.providers);
110
+ if (result.result.degraded === true) {
111
+ setDegraded(true);
112
+ logger.warn("AI agent models response is degraded", {
113
+ agent,
114
+ degradedReason: result.result.degradedReason ?? null
115
+ });
116
+ }
92
117
  setDefaultLabel(
93
118
  result.result.defaultProviderName && result.result.defaultModelName ? `${result.result.defaultProviderName} / ${result.result.defaultModelName}` : result.result.defaultProviderId && result.result.defaultModelId ? `${result.result.defaultProviderId} / ${result.result.defaultModelId}` : null
94
119
  );
95
- setLoaded(true);
120
+ setStatus("ready");
121
+ }).catch((err) => {
122
+ if (cancelled) return;
123
+ logModelsFailure(readErrorStatus(err), { agent, err });
124
+ setStatus("failed");
96
125
  });
126
+ return () => {
127
+ cancelled = true;
128
+ };
97
129
  }, [agent]);
98
130
  return {
99
131
  providers,
100
132
  allowRuntimeOverride,
101
133
  allowRuntimeModelOverride: allowRuntimeOverride,
102
134
  defaultLabel,
103
- loaded
135
+ status,
136
+ degraded
104
137
  };
105
138
  }
106
139
  function isModelPickerValueAvailable(value, providers) {
@@ -122,14 +155,33 @@ async function readFileAsDataUrl(file) {
122
155
  }
123
156
  });
124
157
  }
125
- function mapErrorCodeToVariant(code) {
126
- if (!code) return "destructive";
158
+ function mapErrorCodeToStatus(code) {
159
+ if (!code) return "error";
127
160
  const warningCodes = /* @__PURE__ */ new Set([
128
161
  "tool_not_whitelisted",
129
162
  "tool_features_denied",
130
- "attachment_type_not_accepted"
163
+ "attachment_type_not_accepted",
164
+ // Content-safety rejections are soft: the user can rephrase and retry.
165
+ "moderation_blocked",
166
+ "moderation_unavailable"
131
167
  ]);
132
- return warningCodes.has(code) ? "warning" : "destructive";
168
+ return warningCodes.has(code) ? "warning" : "error";
169
+ }
170
+ function resolveChatErrorMessage(code, fallback, translate) {
171
+ switch (code) {
172
+ case "moderation_blocked":
173
+ return translate(
174
+ "ai_assistant.errors.moderationBlocked",
175
+ "Your message was blocked by the content safety filter. Please rephrase and try again."
176
+ );
177
+ case "moderation_unavailable":
178
+ return translate(
179
+ "ai_assistant.errors.moderationUnavailable",
180
+ "The content safety check is temporarily unavailable. Please try again in a moment."
181
+ );
182
+ default:
183
+ return fallback;
184
+ }
133
185
  }
134
186
  const MARKDOWN_TYPOGRAPHY_CLASS = cn(
135
187
  "[&_p]:my-1 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0",
@@ -835,13 +887,14 @@ function AiChat({
835
887
  allowRuntimeOverride,
836
888
  allowRuntimeModelOverride,
837
889
  defaultLabel: modelDefaultLabel,
838
- loaded: modelProvidersLoaded
890
+ status: modelProvidersStatus,
891
+ degraded: modelProvidersDegraded
839
892
  } = useAgentModels(agent);
840
893
  const [modelPickerValue, setModelPickerValue] = React.useState(
841
894
  () => readModelPickerValue(agent)
842
895
  );
843
896
  const effectiveModelPickerValue = React.useMemo(() => {
844
- if (!modelProvidersLoaded || !allowRuntimeOverride || modelProviders.length === 0) {
897
+ if (modelProvidersStatus !== "ready" || !allowRuntimeOverride || modelProviders.length === 0) {
845
898
  return null;
846
899
  }
847
900
  if (modelPickerValue && isModelPickerValueAvailable(modelPickerValue, modelProviders)) {
@@ -852,13 +905,14 @@ function AiChat({
852
905
  allowRuntimeOverride,
853
906
  modelPickerValue,
854
907
  modelProviders,
855
- modelProvidersLoaded
908
+ modelProvidersStatus
856
909
  ]);
857
910
  React.useEffect(() => {
858
911
  setModelPickerValue(readModelPickerValue(agent));
859
912
  }, [agent]);
860
913
  React.useEffect(() => {
861
- if (!modelProvidersLoaded) return;
914
+ if (modelProvidersStatus !== "ready") return;
915
+ if (modelProvidersDegraded) return;
862
916
  if (!allowRuntimeModelOverride || modelProviders.length === 0) {
863
917
  if (modelPickerValue !== null) {
864
918
  setModelPickerValue(null);
@@ -875,7 +929,8 @@ function AiChat({
875
929
  allowRuntimeModelOverride,
876
930
  modelPickerValue,
877
931
  modelProviders,
878
- modelProvidersLoaded
932
+ modelProvidersDegraded,
933
+ modelProvidersStatus
879
934
  ]);
880
935
  const handleModelPickerChange = React.useCallback(
881
936
  (value) => {
@@ -1092,7 +1147,7 @@ function AiChat({
1092
1147
  setTimeout(() => textareaRef.current?.focus(), 0);
1093
1148
  }, [chat, upload]);
1094
1149
  const resolvedPlaceholder = placeholder ?? t("ai_assistant.chat.composerPlaceholder", "Message the AI agent...");
1095
- const errorVariant = mapErrorCodeToVariant(chat.error?.code);
1150
+ const errorStatus = mapErrorCodeToStatus(chat.error?.code);
1096
1151
  return /* @__PURE__ */ jsxs(
1097
1152
  "section",
1098
1153
  {
@@ -1172,11 +1227,11 @@ function AiChat({
1172
1227
  ]
1173
1228
  }
1174
1229
  ),
1175
- chat.error ? /* @__PURE__ */ jsxs(Alert, { variant: errorVariant, "data-ai-chat-error": chat.error.code ?? "unknown", children: [
1230
+ chat.error ? /* @__PURE__ */ jsxs(Alert, { status: errorStatus, "data-ai-chat-error": chat.error.code ?? "unknown", children: [
1176
1231
  /* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.chat.errorTitle", "Agent dispatch failed") }),
1177
1232
  /* @__PURE__ */ jsxs(AlertDescription, { children: [
1178
1233
  chat.error.code ? /* @__PURE__ */ jsx("span", { className: "mr-2 font-mono text-xs", children: chat.error.code }) : null,
1179
- chat.error.message
1234
+ resolveChatErrorMessage(chat.error.code, chat.error.message, t)
1180
1235
  ] })
1181
1236
  ] }) : null,
1182
1237
  chat.isOwner === false ? /* @__PURE__ */ jsx(