@open-mercato/ui 0.7.1-develop.7123.1.25365e718d → 0.7.1-develop.7130.1.fef2396fd8
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/ai/AiAssistantLauncher.js +7 -1
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChatSessions.js +4 -1
- package/dist/ai/AiChatSessions.js.map +2 -2
- package/dist/ai/index.js +8 -0
- package/dist/ai/index.js.map +2 -2
- package/dist/ai/useAiAssistantAvailable.js +32 -0
- package/dist/ai/useAiAssistantAvailable.js.map +7 -0
- package/dist/backend/detail/addressFormat.js +18 -2
- package/dist/backend/detail/addressFormat.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +108 -92
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/package.json +3 -3
- package/src/ai/AiAssistantLauncher.tsx +26 -6
- package/src/ai/AiChatSessions.tsx +9 -1
- package/src/ai/__tests__/AiAssistantLauncher.test.tsx +67 -0
- package/src/ai/__tests__/AiChatSessions.test.tsx +77 -0
- package/src/ai/__tests__/useAiAssistantAvailable.test.tsx +96 -0
- package/src/ai/index.ts +5 -0
- package/src/ai/useAiAssistantAvailable.ts +50 -0
- package/src/backend/detail/__tests__/addressFormat.taxId.test.ts +33 -0
- package/src/backend/detail/addressFormat.tsx +59 -1
- package/src/backend/inputs/ComboboxInput.tsx +112 -79
- package/src/backend/inputs/__tests__/ComboboxInput.dialog.test.tsx +134 -0
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +95 -0
- package/src/primitives/__tests__/zindex-overlay.test.tsx +4 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -28,6 +28,7 @@ import { IconButton } from "../primitives/icon-button.js";
|
|
|
28
28
|
import { Kbd, KbdShortcut } from "../primitives/kbd.js";
|
|
29
29
|
import { useAiDock } from "./AiDock.js";
|
|
30
30
|
import { useAiChatSessions } from "./AiChatSessions.js";
|
|
31
|
+
import { useAiAssistantAvailable } from "./useAiAssistantAvailable.js";
|
|
31
32
|
import { ChatPaneTabs } from "./ChatPaneTabs.js";
|
|
32
33
|
import { ConversationShareButton } from "./ConversationShareButton.js";
|
|
33
34
|
import { AiIcon } from "./AiIcon.js";
|
|
@@ -86,7 +87,12 @@ function isTextEntryTarget(target) {
|
|
|
86
87
|
if (target.isContentEditable) return true;
|
|
87
88
|
return false;
|
|
88
89
|
}
|
|
89
|
-
function AiAssistantLauncher({
|
|
90
|
+
function AiAssistantLauncher(props) {
|
|
91
|
+
const aiAvailable = useAiAssistantAvailable();
|
|
92
|
+
if (!aiAvailable) return null;
|
|
93
|
+
return /* @__PURE__ */ jsx(AiAssistantLauncherContent, { ...props });
|
|
94
|
+
}
|
|
95
|
+
function AiAssistantLauncherContent({
|
|
90
96
|
variant: _variant = "topbar",
|
|
91
97
|
agentsEndpoint = DEFAULT_AGENTS_ENDPOINT,
|
|
92
98
|
healthEndpoint = DEFAULT_HEALTH_ENDPOINT,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ai/AiAssistantLauncher.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\n/**\n * Global AI assistant launcher.\n *\n * Reusable component that:\n * - Hides itself when the AI runtime is not configured (no provider key,\n * `/api/ai_assistant/health` returns non-OK, or `/api/ai_assistant/ai/agents`\n * returns zero accessible agents for the caller).\n * - Exposes a compact icon trigger styled for the topbar.\n * - Opens a Cmd-K-style searchable dialog listing every typed agent the\n * caller is allowed to launch \u2014 searchable by label, description, or id \u2014\n * so it scales to many assistants.\n * - On agent select, opens `<AiChat>` in a right-side sheet with empty\n * `pageContext` (the picker is intentionally page-agnostic; per-page\n * triggers continue to embed `<AiChat>` directly with their own context).\n * - Binds a global keyboard shortcut (default Cmd/Ctrl+L) that opens the\n * picker. Cmd+K stays reserved for global search; Cmd+J stays reserved\n * for the legacy OpenCode command palette. Browsers normally use\n * Cmd/Ctrl+L for \"focus address bar\"; we `preventDefault()` so the\n * launcher wins when an Open Mercato page has focus.\n */\n\nimport * as React from 'react'\nimport { usePathname, useRouter, useSearchParams } from 'next/navigation'\nimport {\n AlertTriangle,\n BadgeQuestionMark,\n Database,\n ExternalLink,\n FileQuestion,\n Lightbulb,\n Loader2,\n PanelRightOpen,\n Search,\n} from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { apiCall } from '../backend/utils/apiCall'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '../primitives/dialog'\nimport { Button } from '../primitives/button'\nimport { IconButton } from '../primitives/icon-button'\nimport { Kbd, KbdShortcut } from '../primitives/kbd'\nimport { useAiDock } from './AiDock'\nimport { useAiChatSessions } from './AiChatSessions'\nimport { ChatPaneTabs } from './ChatPaneTabs'\nimport { ConversationShareButton } from './ConversationShareButton'\nimport { AiIcon } from './AiIcon'\nimport type { AiChatContextItem, AiChatSuggestion } from './AiChat'\n\n// Lazy-load the chat surface so AppShell tests (and any other importers that\n// don't actually open the launcher) avoid pulling the AI SDK runtime \u2014 which\n// touches `TransformStream` and breaks under jsdom.\nconst LazyAiChat = React.lazy(async () => {\n const mod = await import('./AiChat')\n return { default: mod.AiChat }\n})\n\nexport interface AiAssistantLauncherAgent {\n id: string\n label: string\n description?: string | null\n moduleId?: string | null\n /**\n * `read-only` (default), `confirm-required`, or\n * `destructive-confirm-required`. Surfaced as a small badge in the picker\n * row so operators can see at a glance which assistants can write.\n */\n mutationPolicy?: string | null\n keywords?: string[]\n suggestions?: AiChatSuggestion[]\n}\n\nexport interface AiAssistantLauncherProps {\n /**\n * Trigger placement. `topbar` (default) = rounded-rectangle pill button\n * matching the global-search trigger (icon + \"AI\" label + \u2318L kbd hint).\n * `inline` is a back-compat alias and renders the same trigger.\n */\n variant?: 'topbar' | 'inline'\n /**\n * Optional override of the agents endpoint. Defaults to\n * `/api/ai_assistant/ai/agents` from the typed agent dispatcher.\n */\n agentsEndpoint?: string\n /**\n * Optional override of the health endpoint. Defaults to\n * `/api/ai_assistant/health`. The launcher hides itself when this returns\n * non-2xx OR a JSON body with `{ healthy: false }`.\n */\n healthEndpoint?: string\n /**\n * When true, skip the health check (useful in tests / hosts that already\n * gated visibility by feature). Defaults to false.\n */\n skipHealthCheck?: boolean\n /**\n * Disable the global keyboard shortcut binding (Cmd/Ctrl+L). Useful for\n * nested launchers in dialogs / portals where the host owns shortcut\n * handling.\n */\n disableGlobalShortcut?: boolean\n className?: string\n}\n\ninterface AgentsResponse {\n agents?: Array<{\n id?: string | null\n label?: string | null\n description?: string | null\n moduleId?: string | null\n mutationPolicy?: string | null\n keywords?: string[] | null\n suggestions?: Array<{\n label?: string | null\n prompt?: string | null\n }> | null\n }>\n aiConfigured?: boolean\n}\n\ninterface HealthResponse {\n healthy?: boolean\n status?: string | null\n}\n\nconst DEFAULT_AGENTS_ENDPOINT = '/api/ai_assistant/ai/agents'\nconst DEFAULT_HEALTH_ENDPOINT = '/api/ai_assistant/health'\nconst AI_ASSISTANT_DOCS_URL = 'https://docs.openmercato.com/framework/ai-assistant/overview'\nconst AI_ASSISTANT_SETTINGS_DOCS_URL = 'https://docs.openmercato.com/framework/ai-assistant/settings'\nexport const AI_ASSISTANT_LAUNCHER_OPEN_EVENT = 'om:open-ai-assistant-launcher'\n\nfunction isMutationCapable(policy: string | null | undefined): boolean {\n return policy === 'confirm-required' || policy === 'destructive-confirm-required'\n}\n\nfunction normalizeAgents(payload: AgentsResponse | null | undefined): AiAssistantLauncherAgent[] {\n if (!payload || !Array.isArray(payload.agents)) return []\n const result: AiAssistantLauncherAgent[] = []\n for (const raw of payload.agents) {\n if (!raw || typeof raw.id !== 'string' || raw.id.length === 0) continue\n if (typeof raw.label !== 'string' || raw.label.length === 0) continue\n result.push({\n id: raw.id,\n label: raw.label,\n description: typeof raw.description === 'string' ? raw.description : null,\n moduleId: typeof raw.moduleId === 'string' ? raw.moduleId : null,\n mutationPolicy:\n typeof raw.mutationPolicy === 'string' ? raw.mutationPolicy : null,\n keywords: Array.isArray(raw.keywords)\n ? raw.keywords.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : [],\n suggestions: Array.isArray(raw.suggestions)\n ? raw.suggestions\n .map((suggestion) => {\n if (!suggestion) return null\n if (typeof suggestion.label !== 'string' || typeof suggestion.prompt !== 'string') {\n return null\n }\n if (!suggestion.label || !suggestion.prompt) return null\n return { label: suggestion.label, prompt: suggestion.prompt }\n })\n .filter((suggestion): suggestion is AiChatSuggestion => suggestion !== null)\n : [],\n })\n }\n return result\n}\n\nfunction matchesQuery(agent: AiAssistantLauncherAgent, query: string): boolean {\n if (!query) return true\n const needle = query.trim().toLowerCase()\n if (!needle) return true\n if (agent.label.toLowerCase().includes(needle)) return true\n if (agent.id.toLowerCase().includes(needle)) return true\n if (agent.description && agent.description.toLowerCase().includes(needle)) return true\n if (agent.moduleId && agent.moduleId.toLowerCase().includes(needle)) return true\n if (agent.keywords && agent.keywords.some((keyword) => keyword.toLowerCase().includes(needle))) return true\n return false\n}\n\nfunction isTextEntryTarget(target: EventTarget | null): boolean {\n if (!target || !(target instanceof HTMLElement)) return false\n const tag = target.tagName\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true\n if (target.isContentEditable) return true\n return false\n}\n\nexport function AiAssistantLauncher({\n variant: _variant = 'topbar',\n agentsEndpoint = DEFAULT_AGENTS_ENDPOINT,\n healthEndpoint = DEFAULT_HEALTH_ENDPOINT,\n skipHealthCheck = false,\n disableGlobalShortcut = false,\n className,\n}: AiAssistantLauncherProps) {\n const t = useT()\n const dock = useAiDock()\n const router = useRouter()\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const deepLinkParam = searchParams.get('openAiConversation')\n const [healthy, setHealthy] = React.useState<boolean | null>(skipHealthCheck ? true : null)\n const [agents, setAgents] = React.useState<AiAssistantLauncherAgent[]>([])\n const [agentsLoaded, setAgentsLoaded] = React.useState(false)\n const [agentsError, setAgentsError] = React.useState<string | null>(null)\n // `aiConfigured: false` \u2192 no LLM provider key in env. Keep the launcher\n // visible when assistants exist so operators get a concrete setup prompt\n // instead of a disappearing control or a failing chat stream.\n const [aiConfigured, setAiConfigured] = React.useState<boolean | null>(null)\n const [pickerOpen, setPickerOpen] = React.useState(false)\n const [activeAgent, setActiveAgent] = React.useState<AiAssistantLauncherAgent | null>(null)\n const [chatOpen, setChatOpen] = React.useState(false)\n const [query, setQuery] = React.useState('')\n const [highlight, setHighlight] = React.useState(0)\n const [deepLinkConversationId, setDeepLinkConversationId] = React.useState<string | null>(null)\n // Tracks the last conversation ID whose deep-link fetch was dispatched, so we never\n // double-handle the same ID (guards against re-renders and URL staying unchanged).\n const deepLinkHandledRef = React.useRef<string | null>(null)\n\n // Reactively update deepLinkConversationId from the URL so client-side navigation\n // (e.g. clicking a notification while already on the app) is also handled \u2014 a\n // mount-only useEffect would miss those transitions since AppShell never remounts.\n React.useEffect(() => {\n if (deepLinkParam) setDeepLinkConversationId(deepLinkParam)\n }, [deepLinkParam])\n\n React.useEffect(() => {\n if (!deepLinkConversationId || !agentsLoaded || agents.length === 0) return\n if (deepLinkHandledRef.current === deepLinkConversationId) return\n deepLinkHandledRef.current = deepLinkConversationId\n let cancelled = false\n apiCall<{ conversation: { agentId: string } }>(\n `/api/ai_assistant/ai/conversations/${deepLinkConversationId}`,\n { headers: { 'x-om-forbidden-redirect': '0', 'x-om-unauthorized-redirect': '0' } },\n )\n .then((res) => {\n if (cancelled || !res.ok || !res.result) return\n const { agentId } = res.result.conversation\n const matched = agents.find((a) => a.id === agentId) ?? agents[0]\n if (!matched) return\n setActiveAgent(matched)\n setChatOpen(true)\n // Strip the deep-link param from the URL so future normal chat opens\n // don't accidentally inherit the shared conversation ID.\n router.replace(pathname, { scroll: false })\n })\n .catch(() => undefined)\n return () => {\n cancelled = true\n }\n }, [deepLinkConversationId, agentsLoaded, agents, router, pathname])\n\n // Health check \u2014 best-effort signal only. We do NOT gate the launcher\n // behind it any more: a flaky / slow / transiently-401 health endpoint on\n // page refresh used to leave the launcher permanently hidden because the\n // agents effect short-circuited on `healthy !== true`. Now `healthy` is\n // purely advisory; the launcher's visibility is driven by the agents\n // endpoint, which is the authoritative source \u2014 it returns zero agents\n // when AI is not configured anyway. Treat *any* non-explicit-false health\n // response (including network errors and unreachable endpoints) as\n // \"probably healthy\" so the agents fetch always runs.\n React.useEffect(() => {\n if (skipHealthCheck) return\n let cancelled = false\n apiCall<HealthResponse>(healthEndpoint, {\n credentials: 'same-origin',\n headers: { 'x-om-forbidden-redirect': '0', 'x-om-unauthorized-redirect': '0' },\n })\n .then((call) => {\n if (cancelled) return\n if (!call.ok) {\n // Don't hide the launcher on 4xx/5xx \u2014 fall back to \"unknown\"\n // (treated as healthy below) and let the agents endpoint decide.\n setHealthy(true)\n return\n }\n const body = call.result\n if (body && typeof body === 'object' && body.healthy === false) {\n setHealthy(false)\n return\n }\n setHealthy(true)\n })\n .catch(() => {\n if (cancelled) return\n // Network errors are treated as \"probably healthy\" too \u2014 the\n // agents endpoint is authoritative for visibility.\n setHealthy(true)\n })\n return () => {\n cancelled = true\n }\n }, [healthEndpoint, skipHealthCheck])\n\n // Agents \u2014 fetched on mount, independently of the health check. The\n // endpoint already filters by the caller's ACL features server-side, so\n // an empty response is the right signal to hide the launcher. Loading\n // these here (instead of behind `healthy === true`) makes the launcher\n // resilient to a flaky / slow / transiently-401 health endpoint that\n // would otherwise leave it permanently hidden after a page refresh.\n React.useEffect(() => {\n if (agentsLoaded) return\n let cancelled = false\n apiCall<AgentsResponse>(agentsEndpoint, {\n credentials: 'same-origin',\n headers: { 'x-om-forbidden-redirect': '0', 'x-om-unauthorized-redirect': '0' },\n })\n .then((call) => {\n if (cancelled) return\n if (!call.ok) {\n setAgents([])\n setAgentsError(`agents endpoint returned ${call.status}`)\n setAgentsLoaded(true)\n return\n }\n if (call.result) {\n setAgents(normalizeAgents(call.result))\n setAgentsError(null)\n if (typeof call.result.aiConfigured === 'boolean') {\n setAiConfigured(call.result.aiConfigured)\n }\n } else {\n setAgents([])\n setAgentsError('Empty agents response')\n }\n setAgentsLoaded(true)\n })\n .catch((error) => {\n if (cancelled) return\n setAgents([])\n setAgentsError(error instanceof Error ? error.message : String(error))\n setAgentsLoaded(true)\n })\n return () => {\n cancelled = true\n }\n }, [agentsEndpoint, agentsLoaded])\n\n const filteredAgents = React.useMemo(\n () => agents.filter((agent) => matchesQuery(agent, query)),\n [agents, query],\n )\n\n // Reset highlight when the filtered set changes; clamp to a valid index.\n React.useEffect(() => {\n if (filteredAgents.length === 0) {\n if (highlight !== 0) setHighlight(0)\n return\n }\n if (highlight >= filteredAgents.length) setHighlight(0)\n }, [filteredAgents, highlight])\n\n const openPicker = React.useCallback(() => {\n setQuery('')\n setHighlight(0)\n setPickerOpen(true)\n }, [])\n\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n const listener = () => openPicker()\n window.addEventListener(AI_ASSISTANT_LAUNCHER_OPEN_EVENT, listener)\n return () => window.removeEventListener(AI_ASSISTANT_LAUNCHER_OPEN_EVENT, listener)\n }, [openPicker])\n\n const handleSelectAgent = React.useCallback((agent: AiAssistantLauncherAgent) => {\n // Manual agent selection \u2192 discard any pending deep-link so the user gets\n // a fresh session instead of the shared conversation.\n setDeepLinkConversationId(null)\n if (dock.state.assistant?.agent === agent.id) {\n dock.dock(dock.state.assistant)\n setPickerOpen(false)\n setChatOpen(false)\n return\n }\n setActiveAgent(agent)\n setPickerOpen(false)\n setChatOpen(true)\n }, [dock])\n\n // Global Cmd/Ctrl+L \u2014 opens the picker. We bind on `keydown` at the\n // document level and ignore events from text-entry targets so it never\n // interferes with typing inside inputs/textarea. Browsers normally focus\n // the address bar on Cmd/Ctrl+L; `preventDefault()` reclaims the combo\n // when an Open Mercato page has focus.\n React.useEffect(() => {\n if (disableGlobalShortcut) return\n if (typeof window === 'undefined') return\n if (healthy !== true) return\n if (!agentsLoaded || agents.length === 0) return\n const listener = (event: KeyboardEvent) => {\n const isModifier = event.metaKey || event.ctrlKey\n if (!isModifier) return\n if (event.shiftKey || event.altKey) return\n if (event.key !== 'l' && event.key !== 'L') return\n if (isTextEntryTarget(event.target)) return\n event.preventDefault()\n openPicker()\n }\n window.addEventListener('keydown', listener)\n return () => window.removeEventListener('keydown', listener)\n }, [agents.length, agentsLoaded, disableGlobalShortcut, healthy, openPicker])\n\n const handlePickerKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (aiConfigured === false || filteredAgents.length === 0) {\n if (event.key === 'Escape') {\n setPickerOpen(false)\n }\n return\n }\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n setHighlight((current) => (current + 1) % filteredAgents.length)\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n setHighlight((current) => (current - 1 + filteredAgents.length) % filteredAgents.length)\n } else if (event.key === 'Enter') {\n event.preventDefault()\n const target = filteredAgents[highlight] ?? filteredAgents[0]\n if (target) handleSelectAgent(target)\n } else if (event.key === 'Escape') {\n setPickerOpen(false)\n }\n },\n [aiConfigured, filteredAgents, handleSelectAgent, highlight],\n )\n\n const triggerLabel = t('ai_assistant.launcher.triggerAriaLabel', 'Open AI assistant')\n const dialogTitle = t('ai_assistant.launcher.dialogTitle', 'AI assistants')\n const dialogDescription = t(\n 'ai_assistant.launcher.dialogDescription',\n 'Pick an assistant. Use \u2191/\u2193 to navigate, Enter to launch, Esc to close.',\n )\n const placeholder = t('ai_assistant.launcher.searchPlaceholder', 'Search assistants...')\n const emptyText = t('ai_assistant.launcher.empty', 'No assistants match your search.')\n const noneText = t(\n 'ai_assistant.launcher.none',\n 'No assistants are available for your account.',\n )\n const writesBadge = t('ai_assistant.launcher.writesBadge', 'Can write')\n\n const launcherSuggestions = React.useMemo<AiChatSuggestion[]>(\n () => {\n const generic: AiChatSuggestion[] = [\n {\n label: t('ai_assistant.launcher.welcome.suggestion1', 'What can you help me with?'),\n prompt: 'What can you help me with on this tenant?',\n icon: <BadgeQuestionMark className=\"size-4 text-foreground\" />,\n },\n {\n label: t('ai_assistant.launcher.welcome.suggestion2', 'Show what data you can access'),\n prompt: 'Describe the data you can read for this tenant \u2014 entities, fields, and limits.',\n icon: <Database className=\"size-4 text-foreground\" />,\n },\n {\n label: t('ai_assistant.launcher.welcome.suggestion3', 'Suggest things to try'),\n prompt:\n 'Suggest five concrete questions I could ask you that would surface useful insights for this tenant.',\n icon: <Lightbulb className=\"size-4 text-foreground\" />,\n },\n {\n label: t('ai_assistant.launcher.welcome.suggestion4', 'How do I use this assistant?'),\n prompt:\n 'Walk me through how to use this assistant: when to ask, what tools you call, and how confirmations work.',\n icon: <FileQuestion className=\"size-4 text-foreground\" />,\n },\n ]\n return [...(activeAgent?.suggestions ?? []), ...generic]\n },\n [t, activeAgent],\n )\n\n // The launcher is page-agnostic \u2014 it has no record-level context to pin.\n // Context chips render only when records are actually attached (selection,\n // file uploads, etc.); a chip showing just the agent's name is redundant\n // because the agent is already named in the dialog/dock header.\n const launcherContextItems = React.useMemo<AiChatContextItem[]>(() => [], [])\n\n // Hide the launcher entirely when no agents are accessible. If agents exist\n // but providers are not configured, still render the trigger and show a\n // setup prompt inside the picker.\n // Treat `healthy === false` (an explicit `{ healthy: false }` body) as a\n // hard veto so we still hide when the runtime explicitly opts out, but\n // unknown / pending health does NOT block the agents fetch result.\n const shouldRender =\n healthy !== false &&\n agentsLoaded &&\n agents.length > 0\n\n if (!shouldRender) return null\n\n const shortLabel = t('ai_assistant.launcher.triggerLabel', 'AI')\n\n return (\n <>\n {/* Desktop: rounded rectangle pill matching the global-search trigger\n (variant=ghost, size=sm, icon + label + \u2318L kbd hint). */}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={openPicker}\n className={cn('hidden sm:inline-flex items-center gap-2 text-foreground [&_svg]:text-foreground', className)}\n data-ai-launcher-trigger=\"\"\n aria-label={triggerLabel}\n title={triggerLabel}\n >\n <AiIcon className=\"size-4 text-foreground\" />\n <span>{shortLabel}</span>\n <span className=\"ml-2 rounded border px-1 text-xs text-muted-foreground\">\n \u2318L\n </span>\n </Button>\n {/* Mobile fallback: icon-only button \u2014 same pattern as global search. */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"sm:hidden text-foreground [&_svg]:text-foreground\"\n onClick={openPicker}\n aria-label={triggerLabel}\n data-ai-launcher-trigger-mobile=\"\"\n >\n <AiIcon className=\"size-4 text-foreground\" />\n </IconButton>\n <Dialog open={pickerOpen} onOpenChange={setPickerOpen}>\n <DialogContent\n className=\"sm:max-w-lg p-0 gap-0 overflow-hidden\"\n data-ai-launcher-picker=\"\"\n onKeyDown={handlePickerKeyDown}\n >\n <DialogHeader className=\"px-4 pt-4 pb-2\">\n <DialogTitle className=\"flex items-center gap-2 text-base\">\n <AiIcon className=\"size-4\" />\n {dialogTitle}\n </DialogTitle>\n <DialogDescription className=\"text-xs\">\n {dialogDescription}\n </DialogDescription>\n </DialogHeader>\n <div className=\"border-y border-border bg-muted/30 px-3 py-2\">\n <div className=\"relative\">\n <Search\n className=\"pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\"\n aria-hidden\n />\n <input\n autoFocus\n type=\"text\"\n value={query}\n onChange={(event) => {\n setQuery(event.target.value)\n setHighlight(0)\n }}\n placeholder={placeholder}\n className=\"w-full rounded-md border border-input bg-background px-8 py-1.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40\"\n data-ai-launcher-search-input=\"\"\n />\n </div>\n </div>\n <div\n className=\"max-h-80 overflow-y-auto py-1\"\n data-ai-launcher-list=\"\"\n role=\"listbox\"\n aria-label={dialogTitle}\n >\n {aiConfigured === false ? (\n <AiProviderSetupPanel t={t} />\n ) : filteredAgents.length === 0 ? (\n <div className=\"px-4 py-6 text-center text-xs text-muted-foreground\">\n {agents.length === 0 ? noneText : emptyText}\n </div>\n ) : (\n filteredAgents.map((agent, index) => {\n const isActive = index === highlight\n const writes = isMutationCapable(agent.mutationPolicy)\n return (\n <button\n key={agent.id}\n type=\"button\"\n role=\"option\"\n aria-selected={isActive}\n onMouseEnter={() => setHighlight(index)}\n onClick={() => handleSelectAgent(agent)}\n data-ai-launcher-agent-id={agent.id}\n data-active={isActive ? 'true' : 'false'}\n className={cn(\n 'flex w-full items-start gap-3 px-3 py-2 text-left text-sm transition-colors',\n isActive ? 'bg-accent text-accent-foreground' : 'hover:bg-accent/60',\n )}\n >\n <span className=\"mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-full bg-brand-violet/10\">\n <AiIcon className=\"size-3.5\" />\n </span>\n <span className=\"flex-1 min-w-0 space-y-0.5\">\n <span className=\"flex items-center gap-2\">\n <span className=\"truncate font-medium leading-tight\">\n {agent.label}\n </span>\n <span\n className=\"inline-flex items-center rounded-full border border-border bg-secondary px-1.5 py-0 text-[10px] font-medium uppercase tracking-wide text-secondary-foreground\"\n data-ai-beta-chip=\"\"\n >\n {t('ai_assistant.chat.betaChip', 'beta')}\n </span>\n {writes ? (\n <span\n className=\"inline-flex items-center rounded-full border border-border bg-secondary px-1.5 py-0 text-[10px] font-medium text-secondary-foreground\"\n data-ai-launcher-writes=\"\"\n >\n {writesBadge}\n </span>\n ) : null}\n </span>\n {agent.description ? (\n <span className=\"block truncate text-xs text-muted-foreground\">\n {agent.description}\n </span>\n ) : null}\n <span className=\"block truncate font-mono text-[10px] text-muted-foreground/80\">\n {agent.id}\n </span>\n </span>\n </button>\n )\n })\n )}\n </div>\n <div className=\"flex items-center justify-between gap-2 border-t border-border px-3 py-2 text-[11px] text-muted-foreground\">\n <span className=\"flex items-center gap-2\">\n <KbdShortcut keys={['\u2191', '\u2193']} />{' '}\n {t('ai_assistant.launcher.hint.navigate', 'Navigate')}\n <span className=\"mx-1 text-border\">\u00B7</span>\n <Kbd>Enter</Kbd> {t('ai_assistant.launcher.hint.launch', 'Launch')}\n <span className=\"mx-1 text-border\">\u00B7</span>\n <Kbd>Esc</Kbd> {t('ai_assistant.launcher.hint.close', 'Close')}\n </span>\n <span className=\"hidden sm:inline-flex items-center gap-1\">\n <KbdShortcut keys={['\u2318', 'L']} />\n </span>\n </div>\n {agentsError ? (\n <div\n className=\"border-t border-status-error-border bg-status-error-bg px-3 py-1.5 text-[11px] text-status-error-foreground\"\n data-ai-launcher-error=\"\"\n >\n <Loader2 className=\"mr-1 inline size-3 animate-spin\" aria-hidden />\n {agentsError}\n </div>\n ) : null}\n </DialogContent>\n </Dialog>\n <Dialog open={chatOpen} onOpenChange={(open) => {\n setChatOpen(open)\n if (!open) {\n setDeepLinkConversationId(null)\n deepLinkHandledRef.current = null\n }\n }}>\n <DialogContent\n className={cn(\n // Mobile: full-screen sheet (matches per-page assistant\n // triggers). Desktop (\u2265sm): right-anchored side sheet so the\n // chat doesn't appear randomly cropped or off-center.\n // The Dialog primitive applies a centering transform at the\n // sm breakpoint (`sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2\n // sm:-translate-y-1/2 sm:inset-auto`); each piece must be\n // overridden at the same breakpoint or the panel renders half\n // off the viewport on the left.\n 'top-0 left-0 right-0 bottom-0 translate-x-0 translate-y-0 max-w-none w-screen h-svh max-h-svh rounded-none',\n 'sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:translate-x-0 sm:translate-y-0',\n 'sm:max-w-xl sm:w-[36rem] sm:rounded-l-2xl sm:h-screen sm:max-h-screen',\n 'flex flex-col gap-3 p-4 z-[70]',\n )}\n data-ai-launcher-sheet=\"\"\n >\n <DialogHeader>\n <div className=\"flex items-center gap-3 pr-8\">\n {/* Dock button on the LEFT to avoid colliding with the\n Dialog primitive's auto-rendered X close button in the\n top-right corner. Desktop-only \u2014 the side dock panel\n itself is hidden on mobile. */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ai_assistant.launcher.sheet.dock', 'Dock to side')}\n title={t('ai_assistant.launcher.sheet.dock', 'Dock to side')}\n onClick={() => {\n if (!activeAgent) return\n dock.dock({\n agent: activeAgent.id,\n label: activeAgent.label,\n description:\n activeAgent.moduleId ??\n t('ai_assistant.launcher.dock.subtitle', 'AI assistant'),\n pageContext: {},\n placeholder: t(\n 'ai_assistant.launcher.composerPlaceholder',\n 'Ask anything\u2026',\n ),\n suggestions: launcherSuggestions,\n contextItems: launcherContextItems,\n welcomeTitle: activeAgent.label,\n welcomeDescription:\n activeAgent.description ??\n t(\n 'ai_assistant.launcher.welcome.fallback',\n 'How can I help?',\n ),\n })\n setChatOpen(false)\n }}\n data-ai-launcher-dock=\"\"\n className=\"hidden lg:inline-flex shrink-0\"\n >\n <PanelRightOpen className=\"size-4\" aria-hidden />\n </IconButton>\n <DialogTitle className=\"flex-1 min-w-0 flex items-center gap-2\">\n <AiIcon className=\"size-4 shrink-0\" />\n <span className=\"min-w-0 truncate\">{activeAgent?.label ?? dialogTitle}</span>\n <span\n className=\"inline-flex shrink-0 items-center rounded-full border border-border bg-secondary px-1.5 py-0 text-[10px] font-medium uppercase tracking-wide text-secondary-foreground\"\n data-ai-beta-chip=\"\"\n >\n {t('ai_assistant.chat.betaChip', 'beta')}\n </span>\n </DialogTitle>\n </div>\n {activeAgent?.description ? (\n <DialogDescription>{activeAgent.description}</DialogDescription>\n ) : null}\n </DialogHeader>\n {activeAgent ? (\n <LauncherChatBody\n activeAgent={activeAgent}\n suggestions={launcherSuggestions}\n contextItems={launcherContextItems}\n welcomeFallback={t(\n 'ai_assistant.launcher.welcome.fallback',\n 'How can I help?',\n )}\n placeholder={t(\n 'ai_assistant.launcher.composerPlaceholder',\n 'Ask anything\u2026',\n )}\n sharedConversationId={deepLinkConversationId ?? undefined}\n />\n ) : null}\n </DialogContent>\n </Dialog>\n </>\n )\n}\n\ninterface LauncherChatBodyProps {\n activeAgent: AiAssistantLauncherAgent\n suggestions: AiChatSuggestion[]\n contextItems: AiChatContextItem[]\n welcomeFallback: string\n placeholder: string\n /** When set, bypass session management and load this conversation directly (e.g. from a notification deep-link). */\n sharedConversationId?: string\n}\n\nfunction LauncherChatBody({\n activeAgent,\n suggestions,\n contextItems,\n welcomeFallback,\n placeholder,\n sharedConversationId,\n}: LauncherChatBodyProps) {\n const sessions = useAiChatSessions()\n\n // When a shared conversation is opened via deep-link, find its session from\n // the server-synced list so it shows as a named tab rather than being hidden.\n const sharedSession = React.useMemo(\n () =>\n sharedConversationId\n ? (sessions.state.sessions.find((s) => s.conversationId === sharedConversationId) ?? null)\n : null,\n [sharedConversationId, sessions.state.sessions],\n )\n\n const session = sessions.getActiveSession(activeAgent.id)\n\n React.useEffect(() => {\n if (sharedSession) {\n sessions.setActiveSession(sharedSession.id)\n return\n }\n if (sharedConversationId) return\n if (!session) sessions.ensureSession(activeAgent.id)\n }, [activeAgent.id, session, sessions, sharedConversationId, sharedSession])\n\n const conversationId = session?.conversationId ?? sharedConversationId\n const chatKey = session?.id ?? sharedConversationId\n\n return (\n <>\n <ChatPaneTabs agentId={activeAgent.id} className=\"border-b\" />\n <div className=\"min-h-0 flex-1\" data-ai-launcher-chat-container=\"\">\n {conversationId ? (\n <React.Suspense fallback={null}>\n <LazyAiChat\n key={chatKey}\n agent={activeAgent.id}\n conversationId={conversationId}\n pageContext={{}}\n className=\"h-full\"\n placeholder={placeholder}\n suggestions={suggestions}\n contextItems={contextItems}\n welcomeTitle={activeAgent.label}\n welcomeDescription={activeAgent.description ?? welcomeFallback}\n headerActions={<ConversationShareButton conversationId={conversationId} />}\n />\n </React.Suspense>\n ) : null}\n </div>\n </>\n )\n}\n\nexport default AiAssistantLauncher\n\ninterface AiProviderSetupPanelProps {\n t: ReturnType<typeof useT>\n}\n\nfunction AiProviderSetupPanel({ t }: AiProviderSetupPanelProps) {\n return (\n <div className=\"px-4 py-5\" data-ai-launcher-provider-setup=\"\">\n <div className=\"rounded-lg border border-status-warning-border bg-status-warning-bg p-4 text-status-warning-text\">\n <div className=\"flex items-start gap-3\">\n <span className=\"mt-0.5 inline-flex size-8 shrink-0 items-center justify-center rounded-full bg-background/70 text-status-warning-icon\">\n <AlertTriangle className=\"size-4\" aria-hidden />\n </span>\n <div className=\"min-w-0 space-y-3\">\n <div>\n <h3 className=\"text-sm font-semibold\">\n {t('ai_assistant.launcher.setup.title', 'Configure an AI provider to use assistants')}\n </h3>\n <p className=\"mt-1 text-xs leading-5 text-status-warning-text/90\">\n {t(\n 'ai_assistant.launcher.setup.body',\n 'AI assistants are installed, but no provider key is configured. Set OPENCODE_PROVIDER and one matching API key in your .env file, then restart the app.',\n )}\n </p>\n </div>\n <div className=\"rounded-md border border-status-warning-border/70 bg-background/80 p-3 font-mono text-[11px] leading-5 text-foreground\">\n <div>OPENCODE_PROVIDER=anthropic</div>\n <div>ANTHROPIC_API_KEY=...</div>\n <div className=\"mt-2 text-muted-foreground\"># or</div>\n <div>OPENCODE_PROVIDER=openai</div>\n <div>OPENAI_API_KEY=...</div>\n <div className=\"mt-2 text-muted-foreground\"># or</div>\n <div>OPENCODE_PROVIDER=google</div>\n <div>GOOGLE_GENERATIVE_AI_API_KEY=...</div>\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button asChild size=\"sm\" variant=\"outline\">\n <a href={AI_ASSISTANT_DOCS_URL} target=\"_blank\" rel=\"noreferrer\">\n {t('ai_assistant.launcher.setup.docs', 'AI assistant docs')}\n <ExternalLink className=\"ml-1 size-3\" aria-hidden />\n </a>\n </Button>\n <Button asChild size=\"sm\" variant=\"ghost\">\n <a href={AI_ASSISTANT_SETTINGS_DOCS_URL} target=\"_blank\" rel=\"noreferrer\">\n {t('ai_assistant.launcher.setup.settingsDocs', 'Provider settings')}\n <ExternalLink className=\"ml-1 size-3\" aria-hidden />\n </a>\n </Button>\n </div>\n </div>\n </div>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * Global AI assistant launcher.\n *\n * Reusable component that:\n * - Renders nothing unless the `ai_assistant` module is enabled AND the\n * caller holds `ai_assistant.view` (`useAiAssistantAvailable`). Both are\n * resolved from data the backend shell already holds, so an installation\n * without the module never reaches the endpoints below.\n * - Past that gate, hides itself when `/api/ai_assistant/ai/agents` returns\n * zero accessible agents for the caller \u2014 that endpoint is the only\n * authoritative visibility signal. `/api/ai_assistant/health` is advisory:\n * a non-OK response or a network error is deliberately treated as\n * \"probably healthy\" so a flaky endpoint cannot hide the launcher, and\n * only an explicit `{ healthy: false }` body vetoes rendering.\n * - Stays visible with a setup prompt when agents exist but no provider key\n * is configured (`aiConfigured: false`), rather than vanishing.\n * - Exposes a compact icon trigger styled for the topbar.\n * - Opens a Cmd-K-style searchable dialog listing every typed agent the\n * caller is allowed to launch \u2014 searchable by label, description, or id \u2014\n * so it scales to many assistants.\n * - On agent select, opens `<AiChat>` in a right-side sheet with empty\n * `pageContext` (the picker is intentionally page-agnostic; per-page\n * triggers continue to embed `<AiChat>` directly with their own context).\n * - Binds a global keyboard shortcut (default Cmd/Ctrl+L) that opens the\n * picker. Cmd+K stays reserved for global search; Cmd+J stays reserved\n * for the legacy OpenCode command palette. Browsers normally use\n * Cmd/Ctrl+L for \"focus address bar\"; we `preventDefault()` so the\n * launcher wins when an Open Mercato page has focus.\n */\n\nimport * as React from 'react'\nimport { usePathname, useRouter, useSearchParams } from 'next/navigation'\nimport {\n AlertTriangle,\n BadgeQuestionMark,\n Database,\n ExternalLink,\n FileQuestion,\n Lightbulb,\n Loader2,\n PanelRightOpen,\n Search,\n} from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { apiCall } from '../backend/utils/apiCall'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '../primitives/dialog'\nimport { Button } from '../primitives/button'\nimport { IconButton } from '../primitives/icon-button'\nimport { Kbd, KbdShortcut } from '../primitives/kbd'\nimport { useAiDock } from './AiDock'\nimport { useAiChatSessions } from './AiChatSessions'\nimport { useAiAssistantAvailable } from './useAiAssistantAvailable'\nimport { ChatPaneTabs } from './ChatPaneTabs'\nimport { ConversationShareButton } from './ConversationShareButton'\nimport { AiIcon } from './AiIcon'\nimport type { AiChatContextItem, AiChatSuggestion } from './AiChat'\n\n// Lazy-load the chat surface so AppShell tests (and any other importers that\n// don't actually open the launcher) avoid pulling the AI SDK runtime \u2014 which\n// touches `TransformStream` and breaks under jsdom.\nconst LazyAiChat = React.lazy(async () => {\n const mod = await import('./AiChat')\n return { default: mod.AiChat }\n})\n\nexport interface AiAssistantLauncherAgent {\n id: string\n label: string\n description?: string | null\n moduleId?: string | null\n /**\n * `read-only` (default), `confirm-required`, or\n * `destructive-confirm-required`. Surfaced as a small badge in the picker\n * row so operators can see at a glance which assistants can write.\n */\n mutationPolicy?: string | null\n keywords?: string[]\n suggestions?: AiChatSuggestion[]\n}\n\nexport interface AiAssistantLauncherProps {\n /**\n * Trigger placement. `topbar` (default) = rounded-rectangle pill button\n * matching the global-search trigger (icon + \"AI\" label + \u2318L kbd hint).\n * `inline` is a back-compat alias and renders the same trigger.\n */\n variant?: 'topbar' | 'inline'\n /**\n * Optional override of the agents endpoint. Defaults to\n * `/api/ai_assistant/ai/agents` from the typed agent dispatcher.\n */\n agentsEndpoint?: string\n /**\n * Optional override of the health endpoint. Defaults to\n * `/api/ai_assistant/health`. Advisory only: non-2xx responses and network\n * errors count as \"probably healthy\" so a flaky endpoint cannot hide the\n * launcher. Only an explicit `{ healthy: false }` body hides it.\n */\n healthEndpoint?: string\n /**\n * When true, skip the health check (useful in tests / hosts that already\n * gated visibility by feature). Defaults to false.\n */\n skipHealthCheck?: boolean\n /**\n * Disable the global keyboard shortcut binding (Cmd/Ctrl+L). Useful for\n * nested launchers in dialogs / portals where the host owns shortcut\n * handling.\n */\n disableGlobalShortcut?: boolean\n className?: string\n}\n\ninterface AgentsResponse {\n agents?: Array<{\n id?: string | null\n label?: string | null\n description?: string | null\n moduleId?: string | null\n mutationPolicy?: string | null\n keywords?: string[] | null\n suggestions?: Array<{\n label?: string | null\n prompt?: string | null\n }> | null\n }>\n aiConfigured?: boolean\n}\n\ninterface HealthResponse {\n healthy?: boolean\n status?: string | null\n}\n\nconst DEFAULT_AGENTS_ENDPOINT = '/api/ai_assistant/ai/agents'\nconst DEFAULT_HEALTH_ENDPOINT = '/api/ai_assistant/health'\nconst AI_ASSISTANT_DOCS_URL = 'https://docs.openmercato.com/framework/ai-assistant/overview'\nconst AI_ASSISTANT_SETTINGS_DOCS_URL = 'https://docs.openmercato.com/framework/ai-assistant/settings'\nexport const AI_ASSISTANT_LAUNCHER_OPEN_EVENT = 'om:open-ai-assistant-launcher'\n\nfunction isMutationCapable(policy: string | null | undefined): boolean {\n return policy === 'confirm-required' || policy === 'destructive-confirm-required'\n}\n\nfunction normalizeAgents(payload: AgentsResponse | null | undefined): AiAssistantLauncherAgent[] {\n if (!payload || !Array.isArray(payload.agents)) return []\n const result: AiAssistantLauncherAgent[] = []\n for (const raw of payload.agents) {\n if (!raw || typeof raw.id !== 'string' || raw.id.length === 0) continue\n if (typeof raw.label !== 'string' || raw.label.length === 0) continue\n result.push({\n id: raw.id,\n label: raw.label,\n description: typeof raw.description === 'string' ? raw.description : null,\n moduleId: typeof raw.moduleId === 'string' ? raw.moduleId : null,\n mutationPolicy:\n typeof raw.mutationPolicy === 'string' ? raw.mutationPolicy : null,\n keywords: Array.isArray(raw.keywords)\n ? raw.keywords.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : [],\n suggestions: Array.isArray(raw.suggestions)\n ? raw.suggestions\n .map((suggestion) => {\n if (!suggestion) return null\n if (typeof suggestion.label !== 'string' || typeof suggestion.prompt !== 'string') {\n return null\n }\n if (!suggestion.label || !suggestion.prompt) return null\n return { label: suggestion.label, prompt: suggestion.prompt }\n })\n .filter((suggestion): suggestion is AiChatSuggestion => suggestion !== null)\n : [],\n })\n }\n return result\n}\n\nfunction matchesQuery(agent: AiAssistantLauncherAgent, query: string): boolean {\n if (!query) return true\n const needle = query.trim().toLowerCase()\n if (!needle) return true\n if (agent.label.toLowerCase().includes(needle)) return true\n if (agent.id.toLowerCase().includes(needle)) return true\n if (agent.description && agent.description.toLowerCase().includes(needle)) return true\n if (agent.moduleId && agent.moduleId.toLowerCase().includes(needle)) return true\n if (agent.keywords && agent.keywords.some((keyword) => keyword.toLowerCase().includes(needle))) return true\n return false\n}\n\nfunction isTextEntryTarget(target: EventTarget | null): boolean {\n if (!target || !(target instanceof HTMLElement)) return false\n const tag = target.tagName\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true\n if (target.isContentEditable) return true\n return false\n}\n\nexport function AiAssistantLauncher(props: AiAssistantLauncherProps) {\n // The AI routes only exist when the `ai_assistant` module is enabled, and\n // they are gated on `ai_assistant.view`. Deciding this before mounting the\n // content keeps the health and agents effects from firing doomed reads.\n const aiAvailable = useAiAssistantAvailable()\n if (!aiAvailable) return null\n return <AiAssistantLauncherContent {...props} />\n}\n\nfunction AiAssistantLauncherContent({\n variant: _variant = 'topbar',\n agentsEndpoint = DEFAULT_AGENTS_ENDPOINT,\n healthEndpoint = DEFAULT_HEALTH_ENDPOINT,\n skipHealthCheck = false,\n disableGlobalShortcut = false,\n className,\n}: AiAssistantLauncherProps) {\n const t = useT()\n const dock = useAiDock()\n const router = useRouter()\n const pathname = usePathname()\n const searchParams = useSearchParams()\n const deepLinkParam = searchParams.get('openAiConversation')\n const [healthy, setHealthy] = React.useState<boolean | null>(skipHealthCheck ? true : null)\n const [agents, setAgents] = React.useState<AiAssistantLauncherAgent[]>([])\n const [agentsLoaded, setAgentsLoaded] = React.useState(false)\n const [agentsError, setAgentsError] = React.useState<string | null>(null)\n // `aiConfigured: false` \u2192 no LLM provider key in env. Keep the launcher\n // visible when assistants exist so operators get a concrete setup prompt\n // instead of a disappearing control or a failing chat stream.\n const [aiConfigured, setAiConfigured] = React.useState<boolean | null>(null)\n const [pickerOpen, setPickerOpen] = React.useState(false)\n const [activeAgent, setActiveAgent] = React.useState<AiAssistantLauncherAgent | null>(null)\n const [chatOpen, setChatOpen] = React.useState(false)\n const [query, setQuery] = React.useState('')\n const [highlight, setHighlight] = React.useState(0)\n const [deepLinkConversationId, setDeepLinkConversationId] = React.useState<string | null>(null)\n // Tracks the last conversation ID whose deep-link fetch was dispatched, so we never\n // double-handle the same ID (guards against re-renders and URL staying unchanged).\n const deepLinkHandledRef = React.useRef<string | null>(null)\n\n // Reactively update deepLinkConversationId from the URL so client-side navigation\n // (e.g. clicking a notification while already on the app) is also handled \u2014 a\n // mount-only useEffect would miss those transitions since AppShell never remounts.\n React.useEffect(() => {\n if (deepLinkParam) setDeepLinkConversationId(deepLinkParam)\n }, [deepLinkParam])\n\n React.useEffect(() => {\n if (!deepLinkConversationId || !agentsLoaded || agents.length === 0) return\n if (deepLinkHandledRef.current === deepLinkConversationId) return\n deepLinkHandledRef.current = deepLinkConversationId\n let cancelled = false\n apiCall<{ conversation: { agentId: string } }>(\n `/api/ai_assistant/ai/conversations/${deepLinkConversationId}`,\n { headers: { 'x-om-forbidden-redirect': '0', 'x-om-unauthorized-redirect': '0' } },\n )\n .then((res) => {\n if (cancelled || !res.ok || !res.result) return\n const { agentId } = res.result.conversation\n const matched = agents.find((a) => a.id === agentId) ?? agents[0]\n if (!matched) return\n setActiveAgent(matched)\n setChatOpen(true)\n // Strip the deep-link param from the URL so future normal chat opens\n // don't accidentally inherit the shared conversation ID.\n router.replace(pathname, { scroll: false })\n })\n .catch(() => undefined)\n return () => {\n cancelled = true\n }\n }, [deepLinkConversationId, agentsLoaded, agents, router, pathname])\n\n // Health check \u2014 best-effort signal only. We do NOT gate the launcher\n // behind it any more: a flaky / slow / transiently-401 health endpoint on\n // page refresh used to leave the launcher permanently hidden because the\n // agents effect short-circuited on `healthy !== true`. Now `healthy` is\n // purely advisory; the launcher's visibility is driven by the agents\n // endpoint, which is the authoritative source \u2014 it returns zero agents\n // when AI is not configured anyway. Treat *any* non-explicit-false health\n // response (including network errors and unreachable endpoints) as\n // \"probably healthy\" so the agents fetch always runs.\n React.useEffect(() => {\n if (skipHealthCheck) return\n let cancelled = false\n apiCall<HealthResponse>(healthEndpoint, {\n credentials: 'same-origin',\n headers: { 'x-om-forbidden-redirect': '0', 'x-om-unauthorized-redirect': '0' },\n })\n .then((call) => {\n if (cancelled) return\n if (!call.ok) {\n // Don't hide the launcher on 4xx/5xx \u2014 fall back to \"unknown\"\n // (treated as healthy below) and let the agents endpoint decide.\n setHealthy(true)\n return\n }\n const body = call.result\n if (body && typeof body === 'object' && body.healthy === false) {\n setHealthy(false)\n return\n }\n setHealthy(true)\n })\n .catch(() => {\n if (cancelled) return\n // Network errors are treated as \"probably healthy\" too \u2014 the\n // agents endpoint is authoritative for visibility.\n setHealthy(true)\n })\n return () => {\n cancelled = true\n }\n }, [healthEndpoint, skipHealthCheck])\n\n // Agents \u2014 fetched on mount, independently of the health check. The\n // endpoint already filters by the caller's ACL features server-side, so\n // an empty response is the right signal to hide the launcher. Loading\n // these here (instead of behind `healthy === true`) makes the launcher\n // resilient to a flaky / slow / transiently-401 health endpoint that\n // would otherwise leave it permanently hidden after a page refresh.\n React.useEffect(() => {\n if (agentsLoaded) return\n let cancelled = false\n apiCall<AgentsResponse>(agentsEndpoint, {\n credentials: 'same-origin',\n headers: { 'x-om-forbidden-redirect': '0', 'x-om-unauthorized-redirect': '0' },\n })\n .then((call) => {\n if (cancelled) return\n if (!call.ok) {\n setAgents([])\n setAgentsError(`agents endpoint returned ${call.status}`)\n setAgentsLoaded(true)\n return\n }\n if (call.result) {\n setAgents(normalizeAgents(call.result))\n setAgentsError(null)\n if (typeof call.result.aiConfigured === 'boolean') {\n setAiConfigured(call.result.aiConfigured)\n }\n } else {\n setAgents([])\n setAgentsError('Empty agents response')\n }\n setAgentsLoaded(true)\n })\n .catch((error) => {\n if (cancelled) return\n setAgents([])\n setAgentsError(error instanceof Error ? error.message : String(error))\n setAgentsLoaded(true)\n })\n return () => {\n cancelled = true\n }\n }, [agentsEndpoint, agentsLoaded])\n\n const filteredAgents = React.useMemo(\n () => agents.filter((agent) => matchesQuery(agent, query)),\n [agents, query],\n )\n\n // Reset highlight when the filtered set changes; clamp to a valid index.\n React.useEffect(() => {\n if (filteredAgents.length === 0) {\n if (highlight !== 0) setHighlight(0)\n return\n }\n if (highlight >= filteredAgents.length) setHighlight(0)\n }, [filteredAgents, highlight])\n\n const openPicker = React.useCallback(() => {\n setQuery('')\n setHighlight(0)\n setPickerOpen(true)\n }, [])\n\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n const listener = () => openPicker()\n window.addEventListener(AI_ASSISTANT_LAUNCHER_OPEN_EVENT, listener)\n return () => window.removeEventListener(AI_ASSISTANT_LAUNCHER_OPEN_EVENT, listener)\n }, [openPicker])\n\n const handleSelectAgent = React.useCallback((agent: AiAssistantLauncherAgent) => {\n // Manual agent selection \u2192 discard any pending deep-link so the user gets\n // a fresh session instead of the shared conversation.\n setDeepLinkConversationId(null)\n if (dock.state.assistant?.agent === agent.id) {\n dock.dock(dock.state.assistant)\n setPickerOpen(false)\n setChatOpen(false)\n return\n }\n setActiveAgent(agent)\n setPickerOpen(false)\n setChatOpen(true)\n }, [dock])\n\n // Global Cmd/Ctrl+L \u2014 opens the picker. We bind on `keydown` at the\n // document level and ignore events from text-entry targets so it never\n // interferes with typing inside inputs/textarea. Browsers normally focus\n // the address bar on Cmd/Ctrl+L; `preventDefault()` reclaims the combo\n // when an Open Mercato page has focus.\n React.useEffect(() => {\n if (disableGlobalShortcut) return\n if (typeof window === 'undefined') return\n if (healthy !== true) return\n if (!agentsLoaded || agents.length === 0) return\n const listener = (event: KeyboardEvent) => {\n const isModifier = event.metaKey || event.ctrlKey\n if (!isModifier) return\n if (event.shiftKey || event.altKey) return\n if (event.key !== 'l' && event.key !== 'L') return\n if (isTextEntryTarget(event.target)) return\n event.preventDefault()\n openPicker()\n }\n window.addEventListener('keydown', listener)\n return () => window.removeEventListener('keydown', listener)\n }, [agents.length, agentsLoaded, disableGlobalShortcut, healthy, openPicker])\n\n const handlePickerKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (aiConfigured === false || filteredAgents.length === 0) {\n if (event.key === 'Escape') {\n setPickerOpen(false)\n }\n return\n }\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n setHighlight((current) => (current + 1) % filteredAgents.length)\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n setHighlight((current) => (current - 1 + filteredAgents.length) % filteredAgents.length)\n } else if (event.key === 'Enter') {\n event.preventDefault()\n const target = filteredAgents[highlight] ?? filteredAgents[0]\n if (target) handleSelectAgent(target)\n } else if (event.key === 'Escape') {\n setPickerOpen(false)\n }\n },\n [aiConfigured, filteredAgents, handleSelectAgent, highlight],\n )\n\n const triggerLabel = t('ai_assistant.launcher.triggerAriaLabel', 'Open AI assistant')\n const dialogTitle = t('ai_assistant.launcher.dialogTitle', 'AI assistants')\n const dialogDescription = t(\n 'ai_assistant.launcher.dialogDescription',\n 'Pick an assistant. Use \u2191/\u2193 to navigate, Enter to launch, Esc to close.',\n )\n const placeholder = t('ai_assistant.launcher.searchPlaceholder', 'Search assistants...')\n const emptyText = t('ai_assistant.launcher.empty', 'No assistants match your search.')\n const noneText = t(\n 'ai_assistant.launcher.none',\n 'No assistants are available for your account.',\n )\n const writesBadge = t('ai_assistant.launcher.writesBadge', 'Can write')\n\n const launcherSuggestions = React.useMemo<AiChatSuggestion[]>(\n () => {\n const generic: AiChatSuggestion[] = [\n {\n label: t('ai_assistant.launcher.welcome.suggestion1', 'What can you help me with?'),\n prompt: 'What can you help me with on this tenant?',\n icon: <BadgeQuestionMark className=\"size-4 text-foreground\" />,\n },\n {\n label: t('ai_assistant.launcher.welcome.suggestion2', 'Show what data you can access'),\n prompt: 'Describe the data you can read for this tenant \u2014 entities, fields, and limits.',\n icon: <Database className=\"size-4 text-foreground\" />,\n },\n {\n label: t('ai_assistant.launcher.welcome.suggestion3', 'Suggest things to try'),\n prompt:\n 'Suggest five concrete questions I could ask you that would surface useful insights for this tenant.',\n icon: <Lightbulb className=\"size-4 text-foreground\" />,\n },\n {\n label: t('ai_assistant.launcher.welcome.suggestion4', 'How do I use this assistant?'),\n prompt:\n 'Walk me through how to use this assistant: when to ask, what tools you call, and how confirmations work.',\n icon: <FileQuestion className=\"size-4 text-foreground\" />,\n },\n ]\n return [...(activeAgent?.suggestions ?? []), ...generic]\n },\n [t, activeAgent],\n )\n\n // The launcher is page-agnostic \u2014 it has no record-level context to pin.\n // Context chips render only when records are actually attached (selection,\n // file uploads, etc.); a chip showing just the agent's name is redundant\n // because the agent is already named in the dialog/dock header.\n const launcherContextItems = React.useMemo<AiChatContextItem[]>(() => [], [])\n\n // Hide the launcher entirely when no agents are accessible. If agents exist\n // but providers are not configured, still render the trigger and show a\n // setup prompt inside the picker.\n // Treat `healthy === false` (an explicit `{ healthy: false }` body) as a\n // hard veto so we still hide when the runtime explicitly opts out, but\n // unknown / pending health does NOT block the agents fetch result.\n const shouldRender =\n healthy !== false &&\n agentsLoaded &&\n agents.length > 0\n\n if (!shouldRender) return null\n\n const shortLabel = t('ai_assistant.launcher.triggerLabel', 'AI')\n\n return (\n <>\n {/* Desktop: rounded rectangle pill matching the global-search trigger\n (variant=ghost, size=sm, icon + label + \u2318L kbd hint). */}\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={openPicker}\n className={cn('hidden sm:inline-flex items-center gap-2 text-foreground [&_svg]:text-foreground', className)}\n data-ai-launcher-trigger=\"\"\n aria-label={triggerLabel}\n title={triggerLabel}\n >\n <AiIcon className=\"size-4 text-foreground\" />\n <span>{shortLabel}</span>\n <span className=\"ml-2 rounded border px-1 text-xs text-muted-foreground\">\n \u2318L\n </span>\n </Button>\n {/* Mobile fallback: icon-only button \u2014 same pattern as global search. */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"sm:hidden text-foreground [&_svg]:text-foreground\"\n onClick={openPicker}\n aria-label={triggerLabel}\n data-ai-launcher-trigger-mobile=\"\"\n >\n <AiIcon className=\"size-4 text-foreground\" />\n </IconButton>\n <Dialog open={pickerOpen} onOpenChange={setPickerOpen}>\n <DialogContent\n className=\"sm:max-w-lg p-0 gap-0 overflow-hidden\"\n data-ai-launcher-picker=\"\"\n onKeyDown={handlePickerKeyDown}\n >\n <DialogHeader className=\"px-4 pt-4 pb-2\">\n <DialogTitle className=\"flex items-center gap-2 text-base\">\n <AiIcon className=\"size-4\" />\n {dialogTitle}\n </DialogTitle>\n <DialogDescription className=\"text-xs\">\n {dialogDescription}\n </DialogDescription>\n </DialogHeader>\n <div className=\"border-y border-border bg-muted/30 px-3 py-2\">\n <div className=\"relative\">\n <Search\n className=\"pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\"\n aria-hidden\n />\n <input\n autoFocus\n type=\"text\"\n value={query}\n onChange={(event) => {\n setQuery(event.target.value)\n setHighlight(0)\n }}\n placeholder={placeholder}\n className=\"w-full rounded-md border border-input bg-background px-8 py-1.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40\"\n data-ai-launcher-search-input=\"\"\n />\n </div>\n </div>\n <div\n className=\"max-h-80 overflow-y-auto py-1\"\n data-ai-launcher-list=\"\"\n role=\"listbox\"\n aria-label={dialogTitle}\n >\n {aiConfigured === false ? (\n <AiProviderSetupPanel t={t} />\n ) : filteredAgents.length === 0 ? (\n <div className=\"px-4 py-6 text-center text-xs text-muted-foreground\">\n {agents.length === 0 ? noneText : emptyText}\n </div>\n ) : (\n filteredAgents.map((agent, index) => {\n const isActive = index === highlight\n const writes = isMutationCapable(agent.mutationPolicy)\n return (\n <button\n key={agent.id}\n type=\"button\"\n role=\"option\"\n aria-selected={isActive}\n onMouseEnter={() => setHighlight(index)}\n onClick={() => handleSelectAgent(agent)}\n data-ai-launcher-agent-id={agent.id}\n data-active={isActive ? 'true' : 'false'}\n className={cn(\n 'flex w-full items-start gap-3 px-3 py-2 text-left text-sm transition-colors',\n isActive ? 'bg-accent text-accent-foreground' : 'hover:bg-accent/60',\n )}\n >\n <span className=\"mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-full bg-brand-violet/10\">\n <AiIcon className=\"size-3.5\" />\n </span>\n <span className=\"flex-1 min-w-0 space-y-0.5\">\n <span className=\"flex items-center gap-2\">\n <span className=\"truncate font-medium leading-tight\">\n {agent.label}\n </span>\n <span\n className=\"inline-flex items-center rounded-full border border-border bg-secondary px-1.5 py-0 text-[10px] font-medium uppercase tracking-wide text-secondary-foreground\"\n data-ai-beta-chip=\"\"\n >\n {t('ai_assistant.chat.betaChip', 'beta')}\n </span>\n {writes ? (\n <span\n className=\"inline-flex items-center rounded-full border border-border bg-secondary px-1.5 py-0 text-[10px] font-medium text-secondary-foreground\"\n data-ai-launcher-writes=\"\"\n >\n {writesBadge}\n </span>\n ) : null}\n </span>\n {agent.description ? (\n <span className=\"block truncate text-xs text-muted-foreground\">\n {agent.description}\n </span>\n ) : null}\n <span className=\"block truncate font-mono text-[10px] text-muted-foreground/80\">\n {agent.id}\n </span>\n </span>\n </button>\n )\n })\n )}\n </div>\n <div className=\"flex items-center justify-between gap-2 border-t border-border px-3 py-2 text-[11px] text-muted-foreground\">\n <span className=\"flex items-center gap-2\">\n <KbdShortcut keys={['\u2191', '\u2193']} />{' '}\n {t('ai_assistant.launcher.hint.navigate', 'Navigate')}\n <span className=\"mx-1 text-border\">\u00B7</span>\n <Kbd>Enter</Kbd> {t('ai_assistant.launcher.hint.launch', 'Launch')}\n <span className=\"mx-1 text-border\">\u00B7</span>\n <Kbd>Esc</Kbd> {t('ai_assistant.launcher.hint.close', 'Close')}\n </span>\n <span className=\"hidden sm:inline-flex items-center gap-1\">\n <KbdShortcut keys={['\u2318', 'L']} />\n </span>\n </div>\n {agentsError ? (\n <div\n className=\"border-t border-status-error-border bg-status-error-bg px-3 py-1.5 text-[11px] text-status-error-foreground\"\n data-ai-launcher-error=\"\"\n >\n <Loader2 className=\"mr-1 inline size-3 animate-spin\" aria-hidden />\n {agentsError}\n </div>\n ) : null}\n </DialogContent>\n </Dialog>\n <Dialog open={chatOpen} onOpenChange={(open) => {\n setChatOpen(open)\n if (!open) {\n setDeepLinkConversationId(null)\n deepLinkHandledRef.current = null\n }\n }}>\n <DialogContent\n className={cn(\n // Mobile: full-screen sheet (matches per-page assistant\n // triggers). Desktop (\u2265sm): right-anchored side sheet so the\n // chat doesn't appear randomly cropped or off-center.\n // The Dialog primitive applies a centering transform at the\n // sm breakpoint (`sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2\n // sm:-translate-y-1/2 sm:inset-auto`); each piece must be\n // overridden at the same breakpoint or the panel renders half\n // off the viewport on the left.\n 'top-0 left-0 right-0 bottom-0 translate-x-0 translate-y-0 max-w-none w-screen h-svh max-h-svh rounded-none',\n 'sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:translate-x-0 sm:translate-y-0',\n 'sm:max-w-xl sm:w-[36rem] sm:rounded-l-2xl sm:h-screen sm:max-h-screen',\n 'flex flex-col gap-3 p-4 z-[70]',\n )}\n data-ai-launcher-sheet=\"\"\n >\n <DialogHeader>\n <div className=\"flex items-center gap-3 pr-8\">\n {/* Dock button on the LEFT to avoid colliding with the\n Dialog primitive's auto-rendered X close button in the\n top-right corner. Desktop-only \u2014 the side dock panel\n itself is hidden on mobile. */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ai_assistant.launcher.sheet.dock', 'Dock to side')}\n title={t('ai_assistant.launcher.sheet.dock', 'Dock to side')}\n onClick={() => {\n if (!activeAgent) return\n dock.dock({\n agent: activeAgent.id,\n label: activeAgent.label,\n description:\n activeAgent.moduleId ??\n t('ai_assistant.launcher.dock.subtitle', 'AI assistant'),\n pageContext: {},\n placeholder: t(\n 'ai_assistant.launcher.composerPlaceholder',\n 'Ask anything\u2026',\n ),\n suggestions: launcherSuggestions,\n contextItems: launcherContextItems,\n welcomeTitle: activeAgent.label,\n welcomeDescription:\n activeAgent.description ??\n t(\n 'ai_assistant.launcher.welcome.fallback',\n 'How can I help?',\n ),\n })\n setChatOpen(false)\n }}\n data-ai-launcher-dock=\"\"\n className=\"hidden lg:inline-flex shrink-0\"\n >\n <PanelRightOpen className=\"size-4\" aria-hidden />\n </IconButton>\n <DialogTitle className=\"flex-1 min-w-0 flex items-center gap-2\">\n <AiIcon className=\"size-4 shrink-0\" />\n <span className=\"min-w-0 truncate\">{activeAgent?.label ?? dialogTitle}</span>\n <span\n className=\"inline-flex shrink-0 items-center rounded-full border border-border bg-secondary px-1.5 py-0 text-[10px] font-medium uppercase tracking-wide text-secondary-foreground\"\n data-ai-beta-chip=\"\"\n >\n {t('ai_assistant.chat.betaChip', 'beta')}\n </span>\n </DialogTitle>\n </div>\n {activeAgent?.description ? (\n <DialogDescription>{activeAgent.description}</DialogDescription>\n ) : null}\n </DialogHeader>\n {activeAgent ? (\n <LauncherChatBody\n activeAgent={activeAgent}\n suggestions={launcherSuggestions}\n contextItems={launcherContextItems}\n welcomeFallback={t(\n 'ai_assistant.launcher.welcome.fallback',\n 'How can I help?',\n )}\n placeholder={t(\n 'ai_assistant.launcher.composerPlaceholder',\n 'Ask anything\u2026',\n )}\n sharedConversationId={deepLinkConversationId ?? undefined}\n />\n ) : null}\n </DialogContent>\n </Dialog>\n </>\n )\n}\n\ninterface LauncherChatBodyProps {\n activeAgent: AiAssistantLauncherAgent\n suggestions: AiChatSuggestion[]\n contextItems: AiChatContextItem[]\n welcomeFallback: string\n placeholder: string\n /** When set, bypass session management and load this conversation directly (e.g. from a notification deep-link). */\n sharedConversationId?: string\n}\n\nfunction LauncherChatBody({\n activeAgent,\n suggestions,\n contextItems,\n welcomeFallback,\n placeholder,\n sharedConversationId,\n}: LauncherChatBodyProps) {\n const sessions = useAiChatSessions()\n\n // When a shared conversation is opened via deep-link, find its session from\n // the server-synced list so it shows as a named tab rather than being hidden.\n const sharedSession = React.useMemo(\n () =>\n sharedConversationId\n ? (sessions.state.sessions.find((s) => s.conversationId === sharedConversationId) ?? null)\n : null,\n [sharedConversationId, sessions.state.sessions],\n )\n\n const session = sessions.getActiveSession(activeAgent.id)\n\n React.useEffect(() => {\n if (sharedSession) {\n sessions.setActiveSession(sharedSession.id)\n return\n }\n if (sharedConversationId) return\n if (!session) sessions.ensureSession(activeAgent.id)\n }, [activeAgent.id, session, sessions, sharedConversationId, sharedSession])\n\n const conversationId = session?.conversationId ?? sharedConversationId\n const chatKey = session?.id ?? sharedConversationId\n\n return (\n <>\n <ChatPaneTabs agentId={activeAgent.id} className=\"border-b\" />\n <div className=\"min-h-0 flex-1\" data-ai-launcher-chat-container=\"\">\n {conversationId ? (\n <React.Suspense fallback={null}>\n <LazyAiChat\n key={chatKey}\n agent={activeAgent.id}\n conversationId={conversationId}\n pageContext={{}}\n className=\"h-full\"\n placeholder={placeholder}\n suggestions={suggestions}\n contextItems={contextItems}\n welcomeTitle={activeAgent.label}\n welcomeDescription={activeAgent.description ?? welcomeFallback}\n headerActions={<ConversationShareButton conversationId={conversationId} />}\n />\n </React.Suspense>\n ) : null}\n </div>\n </>\n )\n}\n\nexport default AiAssistantLauncher\n\ninterface AiProviderSetupPanelProps {\n t: ReturnType<typeof useT>\n}\n\nfunction AiProviderSetupPanel({ t }: AiProviderSetupPanelProps) {\n return (\n <div className=\"px-4 py-5\" data-ai-launcher-provider-setup=\"\">\n <div className=\"rounded-lg border border-status-warning-border bg-status-warning-bg p-4 text-status-warning-text\">\n <div className=\"flex items-start gap-3\">\n <span className=\"mt-0.5 inline-flex size-8 shrink-0 items-center justify-center rounded-full bg-background/70 text-status-warning-icon\">\n <AlertTriangle className=\"size-4\" aria-hidden />\n </span>\n <div className=\"min-w-0 space-y-3\">\n <div>\n <h3 className=\"text-sm font-semibold\">\n {t('ai_assistant.launcher.setup.title', 'Configure an AI provider to use assistants')}\n </h3>\n <p className=\"mt-1 text-xs leading-5 text-status-warning-text/90\">\n {t(\n 'ai_assistant.launcher.setup.body',\n 'AI assistants are installed, but no provider key is configured. Set OPENCODE_PROVIDER and one matching API key in your .env file, then restart the app.',\n )}\n </p>\n </div>\n <div className=\"rounded-md border border-status-warning-border/70 bg-background/80 p-3 font-mono text-[11px] leading-5 text-foreground\">\n <div>OPENCODE_PROVIDER=anthropic</div>\n <div>ANTHROPIC_API_KEY=...</div>\n <div className=\"mt-2 text-muted-foreground\"># or</div>\n <div>OPENCODE_PROVIDER=openai</div>\n <div>OPENAI_API_KEY=...</div>\n <div className=\"mt-2 text-muted-foreground\"># or</div>\n <div>OPENCODE_PROVIDER=google</div>\n <div>GOOGLE_GENERATIVE_AI_API_KEY=...</div>\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button asChild size=\"sm\" variant=\"outline\">\n <a href={AI_ASSISTANT_DOCS_URL} target=\"_blank\" rel=\"noreferrer\">\n {t('ai_assistant.launcher.setup.docs', 'AI assistant docs')}\n <ExternalLink className=\"ml-1 size-3\" aria-hidden />\n </a>\n </Button>\n <Button asChild size=\"sm\" variant=\"ghost\">\n <a href={AI_ASSISTANT_SETTINGS_DOCS_URL} target=\"_blank\" rel=\"noreferrer\">\n {t('ai_assistant.launcher.setup.settingsDocs', 'Provider settings')}\n <ExternalLink className=\"ml-1 size-3\" aria-hidden />\n </a>\n </Button>\n </div>\n </div>\n </div>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
listAiServerConversations,
|
|
13
13
|
updateAiServerConversation
|
|
14
14
|
} from "./conversation-store.js";
|
|
15
|
+
import { useAiAssistantAvailable } from "./useAiAssistantAvailable.js";
|
|
15
16
|
const logger = createLogger("ui").child({ component: "AiChatSessions" });
|
|
16
17
|
const LEGACY_STORAGE_KEY = "om-ai-chat-sessions-v1";
|
|
17
18
|
const STORAGE_KEY_PREFIX = "om-ai-chat-sessions-v1";
|
|
@@ -100,6 +101,7 @@ function mergeServerConversations(prev, conversations) {
|
|
|
100
101
|
return { sessions, activeByAgent };
|
|
101
102
|
}
|
|
102
103
|
function AiChatSessionsProvider({ children }) {
|
|
104
|
+
const aiAvailable = useAiAssistantAvailable();
|
|
103
105
|
const [storageKey, setStorageKey] = React.useState(() => {
|
|
104
106
|
const scope = getCurrentOrganizationScope();
|
|
105
107
|
return getScopedStorageKey(scope.tenantId, scope.organizationId);
|
|
@@ -117,6 +119,7 @@ function AiChatSessionsProvider({ children }) {
|
|
|
117
119
|
writePersisted(storageKey, state);
|
|
118
120
|
}, [storageKey, state]);
|
|
119
121
|
React.useEffect(() => {
|
|
122
|
+
if (!aiAvailable) return;
|
|
120
123
|
let cancelled = false;
|
|
121
124
|
listAiServerConversations({ limit: 100 }).then((conversations) => {
|
|
122
125
|
if (cancelled) return;
|
|
@@ -132,7 +135,7 @@ function AiChatSessionsProvider({ children }) {
|
|
|
132
135
|
return () => {
|
|
133
136
|
cancelled = true;
|
|
134
137
|
};
|
|
135
|
-
}, [storageKey]);
|
|
138
|
+
}, [aiAvailable, storageKey]);
|
|
136
139
|
const update = React.useCallback(
|
|
137
140
|
(mutator) => {
|
|
138
141
|
setState((prev) => mutator(prev));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ai/AiChatSessions.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\n/**\n * Multi-tab AI chat sessions.\n *\n * Each agent can have several concurrent conversation threads (sessions).\n * The provider mirrors session metadata from the server-side conversation\n * store. localStorage remains a backward-compatible cache so existing browser\n * sessions can be imported rather than orphaned, but the server is the source\n * of truth whenever it is reachable.\n *\n * Sessions are partitioned into:\n * - `open` \u2192 currently shown in the tab strip\n * - `closed` \u2192 hidden but kept for the history dropdown\n *\n * Closing a tab moves it to `closed`. Re-opening from history flips it back\n * to `open` and surfaces it as the active tab. Renaming is a single\n * `name` field; falling back to the formatted creation date when the user\n * never named the session keeps the picker scannable.\n */\n\nimport * as React from 'react'\nimport {\n getCurrentOrganizationScope,\n subscribeOrganizationScopeChanged,\n} from '@open-mercato/shared/lib/frontend/organizationEvents'\nimport { readVersionedPreference, writeVersionedPreference } from '@open-mercato/shared/lib/browser/versionedPreference'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n createAiServerConversation,\n listAiServerConversations,\n updateAiServerConversation,\n type AiServerConversation,\n} from './conversation-store'\n\nconst logger = createLogger('ui').child({ component: 'AiChatSessions' })\n\n/**\n * Legacy app-global storage key used before tenant/org scoping.\n *\n * Kept only as a documented constant for grep / debugging. No code path\n * reads or writes it any more \u2014 the legacy entry is intentionally\n * abandoned (not migrated) because the origin scope of any data stored\n * under this key is unknown; silently importing it into the wrong scope\n * is worse than empty state. `listAiServerConversations` repopulates\n * sessions from the authoritative server source on first load.\n */\nexport const LEGACY_STORAGE_KEY = 'om-ai-chat-sessions-v1'\nconst STORAGE_KEY_PREFIX = 'om-ai-chat-sessions-v1'\nconst HISTORY_LIMIT = 50\n\nfunction getScopedStorageKey(\n tenantId: string | null | undefined,\n organizationId: string | null | undefined,\n): string {\n return `${STORAGE_KEY_PREFIX}:${tenantId ?? 'no-tenant'}:${organizationId ?? 'no-org'}`\n}\n\nexport type AiChatSessionStatus = 'open' | 'closed'\n\nexport interface AiChatSession {\n id: string\n agentId: string\n conversationId: string\n name?: string\n createdAt: number\n lastUsedAt: number\n status: AiChatSessionStatus\n}\n\ninterface AiChatSessionsState {\n sessions: AiChatSession[]\n activeByAgent: Record<string, string>\n}\n\ninterface AiChatSessionsApi {\n state: AiChatSessionsState\n /** Returns open sessions for `agentId`, ordered by creation. */\n getOpenSessions: (agentId: string) => AiChatSession[]\n /** Returns closed sessions (history) for `agentId`, newest first. */\n getClosedSessions: (agentId: string, limit?: number) => AiChatSession[]\n /** Returns the active session for `agentId`, or null if none. */\n getActiveSession: (agentId: string) => AiChatSession | null\n /** Creates and activates a fresh open session for `agentId`. */\n createSession: (agentId: string) => AiChatSession\n /** Closes a session (moves to history). If it was active, picks another. */\n closeSession: (sessionId: string) => void\n /** Re-opens a closed session and activates it. */\n reopenSession: (sessionId: string) => void\n /** Activates an open session. */\n setActiveSession: (sessionId: string) => void\n /** Renames a session \u2014 empty / whitespace clears the custom name. */\n renameSession: (sessionId: string, name: string) => void\n /** Bumps `lastUsedAt` on a session (call when activity happens). */\n touchSession: (sessionId: string) => void\n /** Ensures `agentId` has at least one open session and returns its id. */\n ensureSession: (agentId: string) => AiChatSession\n}\n\nconst AiChatSessionsContext = React.createContext<AiChatSessionsApi | null>(null)\n\nfunction makeId(): string {\n const g = globalThis as unknown as { crypto?: { randomUUID?: () => string } }\n if (g.crypto && typeof g.crypto.randomUUID === 'function') {\n try {\n return g.crypto.randomUUID()\n } catch {\n /* fall through */\n }\n }\n const rand = () => Math.random().toString(16).slice(2, 10)\n return `${Date.now().toString(16)}-${rand()}-${rand()}`\n}\n\n// Versioned-envelope discriminator for the persisted sessions cache. Bump when\n// the stored shape changes incompatibly; legacy bare `{ sessions, activeByAgent }`\n// values are migrated forward on the next write. See\n// `@open-mercato/shared/lib/browser/versionedPreference`.\nconst AI_CHAT_SESSIONS_VERSION = 1\n\nfunction isPersistedSessionsShape(value: unknown): value is Partial<AiChatSessionsState> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nexport function readPersisted(storageKey: string): AiChatSessionsState {\n const parsed = readVersionedPreference<Partial<AiChatSessionsState> | null>(\n storageKey,\n AI_CHAT_SESSIONS_VERSION,\n (value): value is Partial<AiChatSessionsState> | null => isPersistedSessionsShape(value),\n null,\n { legacyIsValid: (value): value is Partial<AiChatSessionsState> | null => isPersistedSessionsShape(value) },\n )\n if (!parsed) return { sessions: [], activeByAgent: {} }\n const sessions = Array.isArray(parsed.sessions)\n ? (parsed.sessions as unknown[])\n .filter((entry): entry is AiChatSession => {\n if (!entry || typeof entry !== 'object') return false\n const value = entry as Record<string, unknown>\n return (\n typeof value.id === 'string' &&\n typeof value.agentId === 'string' &&\n typeof value.conversationId === 'string' &&\n typeof value.createdAt === 'number' &&\n typeof value.lastUsedAt === 'number' &&\n (value.status === 'open' || value.status === 'closed')\n )\n })\n .map((entry) => {\n const value = entry as unknown as Record<string, unknown>\n const candidate = value.name\n return {\n ...entry,\n name: typeof candidate === 'string' ? candidate : undefined,\n }\n })\n : []\n const activeByAgent =\n parsed.activeByAgent && typeof parsed.activeByAgent === 'object'\n ? Object.fromEntries(\n Object.entries(parsed.activeByAgent as Record<string, unknown>).filter(\n (entry): entry is [string, string] =>\n typeof entry[0] === 'string' && typeof entry[1] === 'string',\n ),\n )\n : {}\n return { sessions, activeByAgent }\n}\n\nexport function writePersisted(storageKey: string, state: AiChatSessionsState): void {\n writeVersionedPreference(storageKey, AI_CHAT_SESSIONS_VERSION, state)\n}\n\nfunction serverConversationToSession(\n conversation: AiServerConversation,\n existing?: AiChatSession,\n): AiChatSession {\n const createdAt = Date.parse(conversation.createdAt)\n const updatedAt = Date.parse(conversation.lastMessageAt ?? conversation.updatedAt)\n return {\n id: existing?.id ?? conversation.conversationId,\n agentId: conversation.agentId,\n conversationId: conversation.conversationId,\n name: conversation.title ?? existing?.name,\n createdAt: Number.isFinite(createdAt) ? createdAt : existing?.createdAt ?? Date.now(),\n lastUsedAt: Number.isFinite(updatedAt) ? updatedAt : existing?.lastUsedAt ?? Date.now(),\n status: conversation.status,\n }\n}\n\nfunction mergeServerConversations(\n prev: AiChatSessionsState,\n conversations: AiServerConversation[],\n): AiChatSessionsState {\n const byConversationId = new Map(prev.sessions.map((session) => [session.conversationId, session]))\n const serverSessions = conversations.map((conversation) =>\n serverConversationToSession(conversation, byConversationId.get(conversation.conversationId)),\n )\n const serverIds = new Set(serverSessions.map((session) => session.conversationId))\n const localOnly = prev.sessions.filter((session) => !serverIds.has(session.conversationId))\n const sessions = [...serverSessions, ...localOnly]\n const activeByAgent = { ...prev.activeByAgent }\n const agentIds = new Set(sessions.map((session) => session.agentId))\n for (const agentId of agentIds) {\n const activeId = activeByAgent[agentId]\n const active = sessions.find((session) => session.id === activeId)\n if (active && active.status === 'open') continue\n const nextOpen = sessions\n .filter((session) => session.agentId === agentId && session.status === 'open')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0]\n if (nextOpen) activeByAgent[agentId] = nextOpen.id\n else delete activeByAgent[agentId]\n }\n return { sessions, activeByAgent }\n}\n\nexport function AiChatSessionsProvider({ children }: { children: React.ReactNode }) {\n // Hydrate synchronously via a lazy initializer. The previous \"empty\n // state + post-mount load effect\" pattern had a window where the\n // persistence effect ran with the empty closure value (because the\n // hydrate effect's queued setState had not committed yet) and clobbered\n // localStorage with `[]` before the loaded state's re-render wrote it\n // back. The lazy initializer puts the loaded state into the very first\n // render, so the persistence effect always sees the real data.\n // `readPersisted` already short-circuits to an empty object on the\n // server (no `window`), so SSR stays consistent with the bare-bones\n // shell \u2014 actual session-dependent UI only renders after a user\n // interaction opens a chat surface.\n //\n // The storage key is scoped to the current tenant/organization so a\n // scope switch in the topbar does not surface another tenant's\n // sessions. `getCurrentOrganizationScope()` reads module-level state\n // populated by the topbar before the provider mounts; a `null` scope\n // (pre-resolution) lands in a harmless `no-tenant:no-org` bucket that\n // the scope-change subscription below immediately corrects when the\n // real scope arrives.\n const [storageKey, setStorageKey] = React.useState<string>(() => {\n const scope = getCurrentOrganizationScope()\n return getScopedStorageKey(scope.tenantId, scope.organizationId)\n })\n const [state, setState] = React.useState<AiChatSessionsState>(() => readPersisted(storageKey))\n\n React.useEffect(() => {\n return subscribeOrganizationScopeChanged((detail) => {\n const nextKey = getScopedStorageKey(detail.tenantId, detail.organizationId)\n if (nextKey === storageKey) return\n // React 18 batches both updates within the synchronous handler so\n // there is no intermediate render with a mismatched key/state pair.\n setStorageKey(nextKey)\n setState(readPersisted(nextKey))\n })\n }, [storageKey])\n\n React.useEffect(() => {\n writePersisted(storageKey, state)\n }, [storageKey, state])\n\n React.useEffect(() => {\n let cancelled = false\n listAiServerConversations({ limit: 100 })\n .then((conversations) => {\n if (cancelled) return\n if (!conversations) {\n logger.warn('Could not load server conversations; keeping the locally persisted sessions')\n return\n }\n setState((prev) => mergeServerConversations(prev, conversations))\n })\n .catch((err) => {\n if (cancelled) return\n logger.error('Failed to load server conversations', { err })\n })\n return () => {\n cancelled = true\n }\n }, [storageKey])\n\n const update = React.useCallback(\n (mutator: (prev: AiChatSessionsState) => AiChatSessionsState) => {\n setState((prev) => mutator(prev))\n },\n [],\n )\n\n const getOpenSessions = React.useCallback(\n (agentId: string) =>\n state.sessions\n .filter((s) => s.agentId === agentId && s.status === 'open')\n .sort((a, b) => a.createdAt - b.createdAt),\n [state.sessions],\n )\n\n const getClosedSessions = React.useCallback(\n (agentId: string, limit = HISTORY_LIMIT) =>\n state.sessions\n .filter((s) => s.agentId === agentId && s.status === 'closed')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)\n .slice(0, limit),\n [state.sessions],\n )\n\n const getActiveSession = React.useCallback(\n (agentId: string) => {\n const id = state.activeByAgent[agentId]\n if (!id) return null\n const session = state.sessions.find((s) => s.id === id)\n if (!session || session.status !== 'open') return null\n return session\n },\n [state.activeByAgent, state.sessions],\n )\n\n const createSession = React.useCallback((agentId: string): AiChatSession => {\n const session: AiChatSession = {\n id: makeId(),\n agentId,\n conversationId: makeId(),\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }\n void createAiServerConversation({\n agentId: session.agentId,\n conversationId: session.conversationId,\n })\n update((prev) => ({\n sessions: [...prev.sessions, session],\n activeByAgent: { ...prev.activeByAgent, [agentId]: session.id },\n }))\n return session\n }, [update])\n\n const closeSession = React.useCallback(\n (sessionId: string) => {\n const serverTarget = state.sessions.find((s) => s.id === sessionId)\n if (serverTarget) {\n void updateAiServerConversation(serverTarget.conversationId, { status: 'closed' })\n }\n update((prev) => {\n const target = prev.sessions.find((s) => s.id === sessionId)\n if (!target) return prev\n const sessions = prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, status: 'closed' as const, lastUsedAt: Date.now() } : s,\n )\n const activeByAgent = { ...prev.activeByAgent }\n if (activeByAgent[target.agentId] === sessionId) {\n // Pick the next open session for this agent (most-recent first).\n const fallback = sessions\n .filter((s) => s.agentId === target.agentId && s.status === 'open')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0]\n if (fallback) {\n activeByAgent[target.agentId] = fallback.id\n } else {\n delete activeByAgent[target.agentId]\n }\n }\n return { sessions, activeByAgent }\n })\n },\n [state.sessions, update],\n )\n\n const reopenSession = React.useCallback(\n (sessionId: string) => {\n const serverTarget = state.sessions.find((s) => s.id === sessionId)\n if (serverTarget) {\n void updateAiServerConversation(serverTarget.conversationId, { status: 'open' })\n }\n update((prev) => {\n const target = prev.sessions.find((s) => s.id === sessionId)\n if (!target) return prev\n const sessions = prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, status: 'open' as const, lastUsedAt: Date.now() } : s,\n )\n return {\n sessions,\n activeByAgent: { ...prev.activeByAgent, [target.agentId]: sessionId },\n }\n })\n },\n [state.sessions, update],\n )\n\n const setActiveSession = React.useCallback(\n (sessionId: string) => {\n update((prev) => {\n const target = prev.sessions.find((s) => s.id === sessionId)\n if (!target || target.status !== 'open') return prev\n if (prev.activeByAgent[target.agentId] === sessionId) return prev\n const sessions = prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, lastUsedAt: Date.now() } : s,\n )\n return {\n sessions,\n activeByAgent: { ...prev.activeByAgent, [target.agentId]: sessionId },\n }\n })\n },\n [update],\n )\n\n const renameSession = React.useCallback(\n (sessionId: string, name: string) => {\n const trimmed = name.trim()\n const serverTarget = state.sessions.find((s) => s.id === sessionId)\n if (serverTarget) {\n void updateAiServerConversation(serverTarget.conversationId, {\n title: trimmed.length > 0 ? trimmed : null,\n })\n }\n update((prev) => ({\n ...prev,\n sessions: prev.sessions.map((s) =>\n s.id === sessionId\n ? { ...s, name: trimmed.length > 0 ? trimmed : undefined }\n : s,\n ),\n }))\n },\n [state.sessions, update],\n )\n\n const touchSession = React.useCallback(\n (sessionId: string) => {\n update((prev) => ({\n ...prev,\n sessions: prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, lastUsedAt: Date.now() } : s,\n ),\n }))\n },\n [update],\n )\n\n const ensureSession = React.useCallback(\n (agentId: string): AiChatSession => {\n // Everything happens inside a single functional setState so we always\n // see the latest pending state (not a stale closure). React Strict\n // Mode double-invokes both effects AND setState updaters in dev,\n // which previously caused the auto-bootstrap path to mint two\n // sessions on first chat-pane open. The `mintedSession` closure\n // cache makes the updater itself idempotent across double-invokes\n // (one fresh id per `ensureSession` call, regardless of how many\n // times React replays the updater for purity testing); the\n // functional `prev` lookup handles the case where two queued\n // ensureSession calls resolve back-to-back \u2014 the second one sees\n // the first's pending append and short-circuits.\n let resolved: AiChatSession | null = null\n let mintedSession: AiChatSession | null = null\n setState((prev) => {\n const activeId = prev.activeByAgent[agentId]\n if (activeId) {\n const active = prev.sessions.find((s) => s.id === activeId)\n if (active && active.status === 'open') {\n resolved = active\n return prev\n }\n }\n const anyOpen = prev.sessions\n .filter((s) => s.agentId === agentId && s.status === 'open')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0]\n if (anyOpen) {\n resolved = anyOpen\n return {\n ...prev,\n activeByAgent: { ...prev.activeByAgent, [agentId]: anyOpen.id },\n }\n }\n if (!mintedSession) {\n mintedSession = {\n id: makeId(),\n agentId,\n conversationId: makeId(),\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }\n void createAiServerConversation({\n agentId: mintedSession.agentId,\n conversationId: mintedSession.conversationId,\n })\n }\n resolved = mintedSession\n return {\n sessions: [...prev.sessions, mintedSession],\n activeByAgent: { ...prev.activeByAgent, [agentId]: mintedSession.id },\n }\n })\n // `setState` returns synchronously after invoking the updater (twice\n // in Strict Mode dev), so `resolved` is guaranteed to be set here.\n return resolved as unknown as AiChatSession\n },\n [],\n )\n\n const api = React.useMemo<AiChatSessionsApi>(\n () => ({\n state,\n getOpenSessions,\n getClosedSessions,\n getActiveSession,\n createSession,\n closeSession,\n reopenSession,\n setActiveSession,\n renameSession,\n touchSession,\n ensureSession,\n }),\n [\n state,\n getOpenSessions,\n getClosedSessions,\n getActiveSession,\n createSession,\n closeSession,\n reopenSession,\n setActiveSession,\n renameSession,\n touchSession,\n ensureSession,\n ],\n )\n\n return <AiChatSessionsContext.Provider value={api}>{children}</AiChatSessionsContext.Provider>\n}\n\nexport function useAiChatSessions(): AiChatSessionsApi {\n const ctx = React.useContext(AiChatSessionsContext)\n if (ctx) return ctx\n // Fallback no-op API \u2014 keeps consumers safe when the provider is absent\n // (legacy code paths, isolated unit tests). Every method is a no-op so a\n // chat surface without the provider behaves like a single anonymous\n // session (the behavior shipped before the multi-tab work).\n return {\n state: { sessions: [], activeByAgent: {} },\n getOpenSessions: () => [],\n getClosedSessions: () => [],\n getActiveSession: () => null,\n createSession: (agentId) => ({\n id: 'noop',\n agentId,\n conversationId: 'noop',\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }),\n closeSession: () => {},\n reopenSession: () => {},\n setActiveSession: () => {},\n renameSession: () => {},\n touchSession: () => {},\n ensureSession: (agentId) => ({\n id: 'noop',\n agentId,\n conversationId: 'noop',\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }),\n }\n}\n\nexport function defaultSessionLabel(session: AiChatSession): string {\n if (session.name && session.name.trim().length > 0) return session.name.trim()\n const date = new Date(session.createdAt)\n if (Number.isNaN(date.getTime())) return 'Session'\n return date.toLocaleString(undefined, {\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n })\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * Multi-tab AI chat sessions.\n *\n * Each agent can have several concurrent conversation threads (sessions).\n * The provider mirrors session metadata from the server-side conversation\n * store. localStorage remains a backward-compatible cache so existing browser\n * sessions can be imported rather than orphaned, but the server is the source\n * of truth whenever it is reachable.\n *\n * Sessions are partitioned into:\n * - `open` \u2192 currently shown in the tab strip\n * - `closed` \u2192 hidden but kept for the history dropdown\n *\n * Closing a tab moves it to `closed`. Re-opening from history flips it back\n * to `open` and surfaces it as the active tab. Renaming is a single\n * `name` field; falling back to the formatted creation date when the user\n * never named the session keeps the picker scannable.\n */\n\nimport * as React from 'react'\nimport {\n getCurrentOrganizationScope,\n subscribeOrganizationScopeChanged,\n} from '@open-mercato/shared/lib/frontend/organizationEvents'\nimport { readVersionedPreference, writeVersionedPreference } from '@open-mercato/shared/lib/browser/versionedPreference'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n createAiServerConversation,\n listAiServerConversations,\n updateAiServerConversation,\n type AiServerConversation,\n} from './conversation-store'\nimport { useAiAssistantAvailable } from './useAiAssistantAvailable'\n\nconst logger = createLogger('ui').child({ component: 'AiChatSessions' })\n\n/**\n * Legacy app-global storage key used before tenant/org scoping.\n *\n * Kept only as a documented constant for grep / debugging. No code path\n * reads or writes it any more \u2014 the legacy entry is intentionally\n * abandoned (not migrated) because the origin scope of any data stored\n * under this key is unknown; silently importing it into the wrong scope\n * is worse than empty state. `listAiServerConversations` repopulates\n * sessions from the authoritative server source on first load.\n */\nexport const LEGACY_STORAGE_KEY = 'om-ai-chat-sessions-v1'\nconst STORAGE_KEY_PREFIX = 'om-ai-chat-sessions-v1'\nconst HISTORY_LIMIT = 50\n\nfunction getScopedStorageKey(\n tenantId: string | null | undefined,\n organizationId: string | null | undefined,\n): string {\n return `${STORAGE_KEY_PREFIX}:${tenantId ?? 'no-tenant'}:${organizationId ?? 'no-org'}`\n}\n\nexport type AiChatSessionStatus = 'open' | 'closed'\n\nexport interface AiChatSession {\n id: string\n agentId: string\n conversationId: string\n name?: string\n createdAt: number\n lastUsedAt: number\n status: AiChatSessionStatus\n}\n\ninterface AiChatSessionsState {\n sessions: AiChatSession[]\n activeByAgent: Record<string, string>\n}\n\ninterface AiChatSessionsApi {\n state: AiChatSessionsState\n /** Returns open sessions for `agentId`, ordered by creation. */\n getOpenSessions: (agentId: string) => AiChatSession[]\n /** Returns closed sessions (history) for `agentId`, newest first. */\n getClosedSessions: (agentId: string, limit?: number) => AiChatSession[]\n /** Returns the active session for `agentId`, or null if none. */\n getActiveSession: (agentId: string) => AiChatSession | null\n /** Creates and activates a fresh open session for `agentId`. */\n createSession: (agentId: string) => AiChatSession\n /** Closes a session (moves to history). If it was active, picks another. */\n closeSession: (sessionId: string) => void\n /** Re-opens a closed session and activates it. */\n reopenSession: (sessionId: string) => void\n /** Activates an open session. */\n setActiveSession: (sessionId: string) => void\n /** Renames a session \u2014 empty / whitespace clears the custom name. */\n renameSession: (sessionId: string, name: string) => void\n /** Bumps `lastUsedAt` on a session (call when activity happens). */\n touchSession: (sessionId: string) => void\n /** Ensures `agentId` has at least one open session and returns its id. */\n ensureSession: (agentId: string) => AiChatSession\n}\n\nconst AiChatSessionsContext = React.createContext<AiChatSessionsApi | null>(null)\n\nfunction makeId(): string {\n const g = globalThis as unknown as { crypto?: { randomUUID?: () => string } }\n if (g.crypto && typeof g.crypto.randomUUID === 'function') {\n try {\n return g.crypto.randomUUID()\n } catch {\n /* fall through */\n }\n }\n const rand = () => Math.random().toString(16).slice(2, 10)\n return `${Date.now().toString(16)}-${rand()}-${rand()}`\n}\n\n// Versioned-envelope discriminator for the persisted sessions cache. Bump when\n// the stored shape changes incompatibly; legacy bare `{ sessions, activeByAgent }`\n// values are migrated forward on the next write. See\n// `@open-mercato/shared/lib/browser/versionedPreference`.\nconst AI_CHAT_SESSIONS_VERSION = 1\n\nfunction isPersistedSessionsShape(value: unknown): value is Partial<AiChatSessionsState> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nexport function readPersisted(storageKey: string): AiChatSessionsState {\n const parsed = readVersionedPreference<Partial<AiChatSessionsState> | null>(\n storageKey,\n AI_CHAT_SESSIONS_VERSION,\n (value): value is Partial<AiChatSessionsState> | null => isPersistedSessionsShape(value),\n null,\n { legacyIsValid: (value): value is Partial<AiChatSessionsState> | null => isPersistedSessionsShape(value) },\n )\n if (!parsed) return { sessions: [], activeByAgent: {} }\n const sessions = Array.isArray(parsed.sessions)\n ? (parsed.sessions as unknown[])\n .filter((entry): entry is AiChatSession => {\n if (!entry || typeof entry !== 'object') return false\n const value = entry as Record<string, unknown>\n return (\n typeof value.id === 'string' &&\n typeof value.agentId === 'string' &&\n typeof value.conversationId === 'string' &&\n typeof value.createdAt === 'number' &&\n typeof value.lastUsedAt === 'number' &&\n (value.status === 'open' || value.status === 'closed')\n )\n })\n .map((entry) => {\n const value = entry as unknown as Record<string, unknown>\n const candidate = value.name\n return {\n ...entry,\n name: typeof candidate === 'string' ? candidate : undefined,\n }\n })\n : []\n const activeByAgent =\n parsed.activeByAgent && typeof parsed.activeByAgent === 'object'\n ? Object.fromEntries(\n Object.entries(parsed.activeByAgent as Record<string, unknown>).filter(\n (entry): entry is [string, string] =>\n typeof entry[0] === 'string' && typeof entry[1] === 'string',\n ),\n )\n : {}\n return { sessions, activeByAgent }\n}\n\nexport function writePersisted(storageKey: string, state: AiChatSessionsState): void {\n writeVersionedPreference(storageKey, AI_CHAT_SESSIONS_VERSION, state)\n}\n\nfunction serverConversationToSession(\n conversation: AiServerConversation,\n existing?: AiChatSession,\n): AiChatSession {\n const createdAt = Date.parse(conversation.createdAt)\n const updatedAt = Date.parse(conversation.lastMessageAt ?? conversation.updatedAt)\n return {\n id: existing?.id ?? conversation.conversationId,\n agentId: conversation.agentId,\n conversationId: conversation.conversationId,\n name: conversation.title ?? existing?.name,\n createdAt: Number.isFinite(createdAt) ? createdAt : existing?.createdAt ?? Date.now(),\n lastUsedAt: Number.isFinite(updatedAt) ? updatedAt : existing?.lastUsedAt ?? Date.now(),\n status: conversation.status,\n }\n}\n\nfunction mergeServerConversations(\n prev: AiChatSessionsState,\n conversations: AiServerConversation[],\n): AiChatSessionsState {\n const byConversationId = new Map(prev.sessions.map((session) => [session.conversationId, session]))\n const serverSessions = conversations.map((conversation) =>\n serverConversationToSession(conversation, byConversationId.get(conversation.conversationId)),\n )\n const serverIds = new Set(serverSessions.map((session) => session.conversationId))\n const localOnly = prev.sessions.filter((session) => !serverIds.has(session.conversationId))\n const sessions = [...serverSessions, ...localOnly]\n const activeByAgent = { ...prev.activeByAgent }\n const agentIds = new Set(sessions.map((session) => session.agentId))\n for (const agentId of agentIds) {\n const activeId = activeByAgent[agentId]\n const active = sessions.find((session) => session.id === activeId)\n if (active && active.status === 'open') continue\n const nextOpen = sessions\n .filter((session) => session.agentId === agentId && session.status === 'open')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0]\n if (nextOpen) activeByAgent[agentId] = nextOpen.id\n else delete activeByAgent[agentId]\n }\n return { sessions, activeByAgent }\n}\n\nexport function AiChatSessionsProvider({ children }: { children: React.ReactNode }) {\n const aiAvailable = useAiAssistantAvailable()\n // Hydrate synchronously via a lazy initializer. The previous \"empty\n // state + post-mount load effect\" pattern had a window where the\n // persistence effect ran with the empty closure value (because the\n // hydrate effect's queued setState had not committed yet) and clobbered\n // localStorage with `[]` before the loaded state's re-render wrote it\n // back. The lazy initializer puts the loaded state into the very first\n // render, so the persistence effect always sees the real data.\n // `readPersisted` already short-circuits to an empty object on the\n // server (no `window`), so SSR stays consistent with the bare-bones\n // shell \u2014 actual session-dependent UI only renders after a user\n // interaction opens a chat surface.\n //\n // The storage key is scoped to the current tenant/organization so a\n // scope switch in the topbar does not surface another tenant's\n // sessions. `getCurrentOrganizationScope()` reads module-level state\n // populated by the topbar before the provider mounts; a `null` scope\n // (pre-resolution) lands in a harmless `no-tenant:no-org` bucket that\n // the scope-change subscription below immediately corrects when the\n // real scope arrives.\n const [storageKey, setStorageKey] = React.useState<string>(() => {\n const scope = getCurrentOrganizationScope()\n return getScopedStorageKey(scope.tenantId, scope.organizationId)\n })\n const [state, setState] = React.useState<AiChatSessionsState>(() => readPersisted(storageKey))\n\n React.useEffect(() => {\n return subscribeOrganizationScopeChanged((detail) => {\n const nextKey = getScopedStorageKey(detail.tenantId, detail.organizationId)\n if (nextKey === storageKey) return\n // React 18 batches both updates within the synchronous handler so\n // there is no intermediate render with a mismatched key/state pair.\n setStorageKey(nextKey)\n setState(readPersisted(nextKey))\n })\n }, [storageKey])\n\n React.useEffect(() => {\n writePersisted(storageKey, state)\n }, [storageKey, state])\n\n // The provider wraps the whole backend shell, so it also mounts on\n // installations without the `ai_assistant` module and for users without\n // `ai_assistant.view`. Syncing there only produces a 404 / 403 and a warning\n // on every page load, so skip it \u2014 `aiAvailable` is in the dependency list\n // so the sync still runs once the backend chrome payload arrives.\n React.useEffect(() => {\n if (!aiAvailable) return\n let cancelled = false\n listAiServerConversations({ limit: 100 })\n .then((conversations) => {\n if (cancelled) return\n if (!conversations) {\n logger.warn('Could not load server conversations; keeping the locally persisted sessions')\n return\n }\n setState((prev) => mergeServerConversations(prev, conversations))\n })\n .catch((err) => {\n if (cancelled) return\n logger.error('Failed to load server conversations', { err })\n })\n return () => {\n cancelled = true\n }\n }, [aiAvailable, storageKey])\n\n const update = React.useCallback(\n (mutator: (prev: AiChatSessionsState) => AiChatSessionsState) => {\n setState((prev) => mutator(prev))\n },\n [],\n )\n\n const getOpenSessions = React.useCallback(\n (agentId: string) =>\n state.sessions\n .filter((s) => s.agentId === agentId && s.status === 'open')\n .sort((a, b) => a.createdAt - b.createdAt),\n [state.sessions],\n )\n\n const getClosedSessions = React.useCallback(\n (agentId: string, limit = HISTORY_LIMIT) =>\n state.sessions\n .filter((s) => s.agentId === agentId && s.status === 'closed')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)\n .slice(0, limit),\n [state.sessions],\n )\n\n const getActiveSession = React.useCallback(\n (agentId: string) => {\n const id = state.activeByAgent[agentId]\n if (!id) return null\n const session = state.sessions.find((s) => s.id === id)\n if (!session || session.status !== 'open') return null\n return session\n },\n [state.activeByAgent, state.sessions],\n )\n\n const createSession = React.useCallback((agentId: string): AiChatSession => {\n const session: AiChatSession = {\n id: makeId(),\n agentId,\n conversationId: makeId(),\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }\n void createAiServerConversation({\n agentId: session.agentId,\n conversationId: session.conversationId,\n })\n update((prev) => ({\n sessions: [...prev.sessions, session],\n activeByAgent: { ...prev.activeByAgent, [agentId]: session.id },\n }))\n return session\n }, [update])\n\n const closeSession = React.useCallback(\n (sessionId: string) => {\n const serverTarget = state.sessions.find((s) => s.id === sessionId)\n if (serverTarget) {\n void updateAiServerConversation(serverTarget.conversationId, { status: 'closed' })\n }\n update((prev) => {\n const target = prev.sessions.find((s) => s.id === sessionId)\n if (!target) return prev\n const sessions = prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, status: 'closed' as const, lastUsedAt: Date.now() } : s,\n )\n const activeByAgent = { ...prev.activeByAgent }\n if (activeByAgent[target.agentId] === sessionId) {\n // Pick the next open session for this agent (most-recent first).\n const fallback = sessions\n .filter((s) => s.agentId === target.agentId && s.status === 'open')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0]\n if (fallback) {\n activeByAgent[target.agentId] = fallback.id\n } else {\n delete activeByAgent[target.agentId]\n }\n }\n return { sessions, activeByAgent }\n })\n },\n [state.sessions, update],\n )\n\n const reopenSession = React.useCallback(\n (sessionId: string) => {\n const serverTarget = state.sessions.find((s) => s.id === sessionId)\n if (serverTarget) {\n void updateAiServerConversation(serverTarget.conversationId, { status: 'open' })\n }\n update((prev) => {\n const target = prev.sessions.find((s) => s.id === sessionId)\n if (!target) return prev\n const sessions = prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, status: 'open' as const, lastUsedAt: Date.now() } : s,\n )\n return {\n sessions,\n activeByAgent: { ...prev.activeByAgent, [target.agentId]: sessionId },\n }\n })\n },\n [state.sessions, update],\n )\n\n const setActiveSession = React.useCallback(\n (sessionId: string) => {\n update((prev) => {\n const target = prev.sessions.find((s) => s.id === sessionId)\n if (!target || target.status !== 'open') return prev\n if (prev.activeByAgent[target.agentId] === sessionId) return prev\n const sessions = prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, lastUsedAt: Date.now() } : s,\n )\n return {\n sessions,\n activeByAgent: { ...prev.activeByAgent, [target.agentId]: sessionId },\n }\n })\n },\n [update],\n )\n\n const renameSession = React.useCallback(\n (sessionId: string, name: string) => {\n const trimmed = name.trim()\n const serverTarget = state.sessions.find((s) => s.id === sessionId)\n if (serverTarget) {\n void updateAiServerConversation(serverTarget.conversationId, {\n title: trimmed.length > 0 ? trimmed : null,\n })\n }\n update((prev) => ({\n ...prev,\n sessions: prev.sessions.map((s) =>\n s.id === sessionId\n ? { ...s, name: trimmed.length > 0 ? trimmed : undefined }\n : s,\n ),\n }))\n },\n [state.sessions, update],\n )\n\n const touchSession = React.useCallback(\n (sessionId: string) => {\n update((prev) => ({\n ...prev,\n sessions: prev.sessions.map((s) =>\n s.id === sessionId ? { ...s, lastUsedAt: Date.now() } : s,\n ),\n }))\n },\n [update],\n )\n\n const ensureSession = React.useCallback(\n (agentId: string): AiChatSession => {\n // Everything happens inside a single functional setState so we always\n // see the latest pending state (not a stale closure). React Strict\n // Mode double-invokes both effects AND setState updaters in dev,\n // which previously caused the auto-bootstrap path to mint two\n // sessions on first chat-pane open. The `mintedSession` closure\n // cache makes the updater itself idempotent across double-invokes\n // (one fresh id per `ensureSession` call, regardless of how many\n // times React replays the updater for purity testing); the\n // functional `prev` lookup handles the case where two queued\n // ensureSession calls resolve back-to-back \u2014 the second one sees\n // the first's pending append and short-circuits.\n let resolved: AiChatSession | null = null\n let mintedSession: AiChatSession | null = null\n setState((prev) => {\n const activeId = prev.activeByAgent[agentId]\n if (activeId) {\n const active = prev.sessions.find((s) => s.id === activeId)\n if (active && active.status === 'open') {\n resolved = active\n return prev\n }\n }\n const anyOpen = prev.sessions\n .filter((s) => s.agentId === agentId && s.status === 'open')\n .sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0]\n if (anyOpen) {\n resolved = anyOpen\n return {\n ...prev,\n activeByAgent: { ...prev.activeByAgent, [agentId]: anyOpen.id },\n }\n }\n if (!mintedSession) {\n mintedSession = {\n id: makeId(),\n agentId,\n conversationId: makeId(),\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }\n void createAiServerConversation({\n agentId: mintedSession.agentId,\n conversationId: mintedSession.conversationId,\n })\n }\n resolved = mintedSession\n return {\n sessions: [...prev.sessions, mintedSession],\n activeByAgent: { ...prev.activeByAgent, [agentId]: mintedSession.id },\n }\n })\n // `setState` returns synchronously after invoking the updater (twice\n // in Strict Mode dev), so `resolved` is guaranteed to be set here.\n return resolved as unknown as AiChatSession\n },\n [],\n )\n\n const api = React.useMemo<AiChatSessionsApi>(\n () => ({\n state,\n getOpenSessions,\n getClosedSessions,\n getActiveSession,\n createSession,\n closeSession,\n reopenSession,\n setActiveSession,\n renameSession,\n touchSession,\n ensureSession,\n }),\n [\n state,\n getOpenSessions,\n getClosedSessions,\n getActiveSession,\n createSession,\n closeSession,\n reopenSession,\n setActiveSession,\n renameSession,\n touchSession,\n ensureSession,\n ],\n )\n\n return <AiChatSessionsContext.Provider value={api}>{children}</AiChatSessionsContext.Provider>\n}\n\nexport function useAiChatSessions(): AiChatSessionsApi {\n const ctx = React.useContext(AiChatSessionsContext)\n if (ctx) return ctx\n // Fallback no-op API \u2014 keeps consumers safe when the provider is absent\n // (legacy code paths, isolated unit tests). Every method is a no-op so a\n // chat surface without the provider behaves like a single anonymous\n // session (the behavior shipped before the multi-tab work).\n return {\n state: { sessions: [], activeByAgent: {} },\n getOpenSessions: () => [],\n getClosedSessions: () => [],\n getActiveSession: () => null,\n createSession: (agentId) => ({\n id: 'noop',\n agentId,\n conversationId: 'noop',\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }),\n closeSession: () => {},\n reopenSession: () => {},\n setActiveSession: () => {},\n renameSession: () => {},\n touchSession: () => {},\n ensureSession: (agentId) => ({\n id: 'noop',\n agentId,\n conversationId: 'noop',\n createdAt: Date.now(),\n lastUsedAt: Date.now(),\n status: 'open',\n }),\n }\n}\n\nexport function defaultSessionLabel(session: AiChatSession): string {\n if (session.name && session.name.trim().length > 0) return session.name.trim()\n const date = new Date(session.createdAt)\n if (Number.isNaN(date.getTime())) return 'Session'\n return date.toLocaleString(undefined, {\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/ai/index.js
CHANGED
|
@@ -17,6 +17,11 @@ import { ChatPaneTabs } from "./ChatPaneTabs.js";
|
|
|
17
17
|
import {
|
|
18
18
|
useAiChat
|
|
19
19
|
} from "./useAiChat.js";
|
|
20
|
+
import {
|
|
21
|
+
useAiAssistantAvailable,
|
|
22
|
+
AI_ASSISTANT_MODULE_ID,
|
|
23
|
+
AI_ASSISTANT_VIEW_FEATURE
|
|
24
|
+
} from "./useAiAssistantAvailable.js";
|
|
20
25
|
import {
|
|
21
26
|
useAiShortcuts
|
|
22
27
|
} from "./useAiShortcuts.js";
|
|
@@ -75,6 +80,8 @@ import {
|
|
|
75
80
|
} from "./LoopTracePanel.js";
|
|
76
81
|
export {
|
|
77
82
|
AI_ASSISTANT_LAUNCHER_OPEN_EVENT,
|
|
83
|
+
AI_ASSISTANT_MODULE_ID,
|
|
84
|
+
AI_ASSISTANT_VIEW_FEATURE,
|
|
78
85
|
AI_MUTATION_APPROVAL_CARDS,
|
|
79
86
|
ActivityCard,
|
|
80
87
|
AiAssistantLauncher,
|
|
@@ -119,6 +126,7 @@ export {
|
|
|
119
126
|
statusToTagVariant,
|
|
120
127
|
unregisterAiUiPart,
|
|
121
128
|
uploadAttachmentsForChat,
|
|
129
|
+
useAiAssistantAvailable,
|
|
122
130
|
useAiChat,
|
|
123
131
|
useAiChatSessions,
|
|
124
132
|
useAiChatUpload,
|
package/dist/ai/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ai/index.ts"],
|
|
4
|
-
"sourcesContent": ["export { AiChat, type AiChatProps, type AiChatDebugTool, type AiChatDebugPromptSection } from './AiChat'\nexport { AiIcon } from './AiIcon'\nexport {\n AiAssistantLauncher,\n AI_ASSISTANT_LAUNCHER_OPEN_EVENT,\n type AiAssistantLauncherAgent,\n type AiAssistantLauncherProps,\n} from './AiAssistantLauncher'\nexport {\n AiDockProvider,\n useAiDock,\n type AiDockedAssistant,\n} from './AiDock'\nexport {\n AiChatSessionsProvider,\n useAiChatSessions,\n defaultSessionLabel,\n type AiChatSession,\n} from './AiChatSessions'\nexport { ChatPaneTabs } from './ChatPaneTabs'\nexport {\n useAiChat,\n type AiChatMessage,\n type AiChatToolCallSnapshot,\n type AiChatMessageFile,\n type AiChatErrorEnvelope,\n type UseAiChatInput,\n type UseAiChatResult,\n} from './useAiChat'\nexport {\n useAiShortcuts,\n type UseAiShortcutsOptions,\n type UseAiShortcutsResult,\n} from './useAiShortcuts'\nexport {\n registerAiUiPart,\n resolveAiUiPart,\n unregisterAiUiPart,\n resetAiUiPartRegistryForTests,\n listAiUiParts,\n createAiUiPartRegistry,\n defaultAiUiPartRegistry,\n RESERVED_AI_UI_PART_IDS,\n isReservedAiUiPartId,\n type AiUiPartComponent,\n type AiUiPartComponentId,\n type AiUiPartProps,\n type AiUiPartRegistry,\n type AiUiPartRegistryEntry,\n type CreateAiUiPartRegistryOptions,\n type ReservedAiUiPartId,\n} from './ui-part-registry'\nexport { PendingPhase3Placeholder } from './ui-parts/pending-phase3-placeholder'\nexport {\n MutationPreviewCard,\n FieldDiffCard,\n ConfirmationCard,\n MutationResultCard,\n AI_MUTATION_APPROVAL_CARDS,\n useAiPendingActionPolling,\n confirmPendingAction,\n cancelPendingAction,\n type UseAiPendingActionPollingOptions,\n type UseAiPendingActionPollingResult,\n type PendingActionMutationOk,\n type PendingActionMutationError,\n type PendingActionMutationResult,\n type AiPendingActionCardAction,\n type AiPendingActionCardStatus,\n type AiPendingActionCardFieldDiff,\n type AiPendingActionCardRecordDiff,\n type AiPendingActionCardFailedRecord,\n type AiPendingActionCardExecutionResult,\n} from './parts'\nexport {\n uploadAttachmentsForChat,\n type UploadAttachmentsForChatOptions,\n type UploadAttachmentsForChatResult,\n type UploadedAttachment,\n type UploadFailure,\n type UploadFailureReason,\n} from './upload-adapter'\nexport {\n RecordCard,\n DealCard,\n PersonCard,\n CompanyCard,\n ProductCard,\n ActivityCard,\n RecordCardShell,\n KeyValueList,\n TagRow,\n statusToTagVariant,\n type RecordCardProps,\n type RecordCardShellProps,\n type KeyValueListItem,\n type DealCardProps,\n type PersonCardProps,\n type CompanyCardProps,\n type ProductCardProps,\n type ActivityCardProps,\n type RecordCardKind,\n type RecordCardPayload,\n type RecordCardBaseProps,\n type DealRecordPayload,\n type PersonRecordPayload,\n type CompanyRecordPayload,\n type ProductRecordPayload,\n type ActivityRecordPayload,\n registerRecordCardUiParts,\n RECORD_CARD_COMPONENT_IDS,\n type RecordCardComponentId,\n} from './records'\nexport {\n AiMessageContent,\n parseAiContentSegments,\n RECORD_CARD_FENCE_INFO_PREFIX,\n type AiMessageContentSegment,\n type AiMessageContentProps,\n} from './AiMessageContent'\nexport {\n useAiChatUpload,\n type UseAiChatUploadOptions,\n type UseAiChatUploadState,\n type AiChatUploadFileState,\n type AiChatUploadFileStatus,\n} from './useAiChatUpload'\nexport { LoopDisabledBanner } from './LoopDisabledBanner'\nexport { ConversationShareButton } from './ConversationShareButton'\nexport { ConversationShareDialog } from './ConversationShareDialog'\nexport {\n LoopTracePanel,\n type LoopTracePanelProps,\n type LoopTracePanelTrace,\n type LoopTracePanelStepRecord,\n} from './LoopTracePanel'\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,cAAqF;AAC9F,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,OAOK;AACP;AAAA,EACE;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAQK;AACP,SAAS,gCAAgC;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAYK;AACP;AAAA,EACE;AAAA,OAMK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAiBA;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,OAKK;AACP,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,+BAA+B;AACxC;AAAA,EACE;AAAA,OAIK;",
|
|
4
|
+
"sourcesContent": ["export { AiChat, type AiChatProps, type AiChatDebugTool, type AiChatDebugPromptSection } from './AiChat'\nexport { AiIcon } from './AiIcon'\nexport {\n AiAssistantLauncher,\n AI_ASSISTANT_LAUNCHER_OPEN_EVENT,\n type AiAssistantLauncherAgent,\n type AiAssistantLauncherProps,\n} from './AiAssistantLauncher'\nexport {\n AiDockProvider,\n useAiDock,\n type AiDockedAssistant,\n} from './AiDock'\nexport {\n AiChatSessionsProvider,\n useAiChatSessions,\n defaultSessionLabel,\n type AiChatSession,\n} from './AiChatSessions'\nexport { ChatPaneTabs } from './ChatPaneTabs'\nexport {\n useAiChat,\n type AiChatMessage,\n type AiChatToolCallSnapshot,\n type AiChatMessageFile,\n type AiChatErrorEnvelope,\n type UseAiChatInput,\n type UseAiChatResult,\n} from './useAiChat'\nexport {\n useAiAssistantAvailable,\n AI_ASSISTANT_MODULE_ID,\n AI_ASSISTANT_VIEW_FEATURE,\n} from './useAiAssistantAvailable'\nexport {\n useAiShortcuts,\n type UseAiShortcutsOptions,\n type UseAiShortcutsResult,\n} from './useAiShortcuts'\nexport {\n registerAiUiPart,\n resolveAiUiPart,\n unregisterAiUiPart,\n resetAiUiPartRegistryForTests,\n listAiUiParts,\n createAiUiPartRegistry,\n defaultAiUiPartRegistry,\n RESERVED_AI_UI_PART_IDS,\n isReservedAiUiPartId,\n type AiUiPartComponent,\n type AiUiPartComponentId,\n type AiUiPartProps,\n type AiUiPartRegistry,\n type AiUiPartRegistryEntry,\n type CreateAiUiPartRegistryOptions,\n type ReservedAiUiPartId,\n} from './ui-part-registry'\nexport { PendingPhase3Placeholder } from './ui-parts/pending-phase3-placeholder'\nexport {\n MutationPreviewCard,\n FieldDiffCard,\n ConfirmationCard,\n MutationResultCard,\n AI_MUTATION_APPROVAL_CARDS,\n useAiPendingActionPolling,\n confirmPendingAction,\n cancelPendingAction,\n type UseAiPendingActionPollingOptions,\n type UseAiPendingActionPollingResult,\n type PendingActionMutationOk,\n type PendingActionMutationError,\n type PendingActionMutationResult,\n type AiPendingActionCardAction,\n type AiPendingActionCardStatus,\n type AiPendingActionCardFieldDiff,\n type AiPendingActionCardRecordDiff,\n type AiPendingActionCardFailedRecord,\n type AiPendingActionCardExecutionResult,\n} from './parts'\nexport {\n uploadAttachmentsForChat,\n type UploadAttachmentsForChatOptions,\n type UploadAttachmentsForChatResult,\n type UploadedAttachment,\n type UploadFailure,\n type UploadFailureReason,\n} from './upload-adapter'\nexport {\n RecordCard,\n DealCard,\n PersonCard,\n CompanyCard,\n ProductCard,\n ActivityCard,\n RecordCardShell,\n KeyValueList,\n TagRow,\n statusToTagVariant,\n type RecordCardProps,\n type RecordCardShellProps,\n type KeyValueListItem,\n type DealCardProps,\n type PersonCardProps,\n type CompanyCardProps,\n type ProductCardProps,\n type ActivityCardProps,\n type RecordCardKind,\n type RecordCardPayload,\n type RecordCardBaseProps,\n type DealRecordPayload,\n type PersonRecordPayload,\n type CompanyRecordPayload,\n type ProductRecordPayload,\n type ActivityRecordPayload,\n registerRecordCardUiParts,\n RECORD_CARD_COMPONENT_IDS,\n type RecordCardComponentId,\n} from './records'\nexport {\n AiMessageContent,\n parseAiContentSegments,\n RECORD_CARD_FENCE_INFO_PREFIX,\n type AiMessageContentSegment,\n type AiMessageContentProps,\n} from './AiMessageContent'\nexport {\n useAiChatUpload,\n type UseAiChatUploadOptions,\n type UseAiChatUploadState,\n type AiChatUploadFileState,\n type AiChatUploadFileStatus,\n} from './useAiChatUpload'\nexport { LoopDisabledBanner } from './LoopDisabledBanner'\nexport { ConversationShareButton } from './ConversationShareButton'\nexport { ConversationShareDialog } from './ConversationShareDialog'\nexport {\n LoopTracePanel,\n type LoopTracePanelProps,\n type LoopTracePanelTrace,\n type LoopTracePanelStepRecord,\n} from './LoopTracePanel'\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,cAAqF;AAC9F,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,OAOK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAQK;AACP,SAAS,gCAAgC;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAYK;AACP;AAAA,EACE;AAAA,OAMK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAiBA;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,OAKK;AACP,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,+BAA+B;AACxC;AAAA,EACE;AAAA,OAIK;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {
|
|
4
|
+
getEnabledModuleIds,
|
|
5
|
+
subscribeToInjectionRegistryChanges
|
|
6
|
+
} from "@open-mercato/shared/modules/widgets/injection-loader";
|
|
7
|
+
import { hasFeature } from "@open-mercato/shared/security/features";
|
|
8
|
+
import { useBackendChrome } from "../backend/BackendChromeProvider.js";
|
|
9
|
+
const AI_ASSISTANT_MODULE_ID = "ai_assistant";
|
|
10
|
+
const AI_ASSISTANT_VIEW_FEATURE = "ai_assistant.view";
|
|
11
|
+
function readEnabledModuleIds() {
|
|
12
|
+
return getEnabledModuleIds();
|
|
13
|
+
}
|
|
14
|
+
function readEnabledModuleIdsOnServer() {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function useAiAssistantAvailable() {
|
|
18
|
+
const { payload } = useBackendChrome();
|
|
19
|
+
const enabledModuleIds = React.useSyncExternalStore(
|
|
20
|
+
subscribeToInjectionRegistryChanges,
|
|
21
|
+
readEnabledModuleIds,
|
|
22
|
+
readEnabledModuleIdsOnServer
|
|
23
|
+
);
|
|
24
|
+
const moduleEnabled = enabledModuleIds === null || enabledModuleIds.has(AI_ASSISTANT_MODULE_ID);
|
|
25
|
+
return moduleEnabled && hasFeature(payload?.grantedFeatures, AI_ASSISTANT_VIEW_FEATURE);
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
AI_ASSISTANT_MODULE_ID,
|
|
29
|
+
AI_ASSISTANT_VIEW_FEATURE,
|
|
30
|
+
useAiAssistantAvailable
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=useAiAssistantAvailable.js.map
|