@open-mercato/ui 0.6.3-develop.3894.1.352abf4240 → 0.6.3
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 +2 -2
- package/dist/ai/AiAssistantLauncher.js +61 -8
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChat.js +29 -6
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiChatSessions.js +31 -9
- package/dist/ai/AiChatSessions.js.map +2 -2
- package/dist/ai/AiDock.js +8 -1
- package/dist/ai/AiDock.js.map +2 -2
- package/dist/ai/ConversationShareButton.js +40 -0
- package/dist/ai/ConversationShareButton.js.map +7 -0
- package/dist/ai/ConversationShareDialog.js +234 -0
- package/dist/ai/ConversationShareDialog.js.map +7 -0
- package/dist/ai/conversation-store.js +4 -2
- package/dist/ai/conversation-store.js.map +2 -2
- package/dist/ai/index.js +4 -0
- package/dist/ai/index.js.map +2 -2
- package/dist/ai/useAiChat.js +20 -4
- package/dist/ai/useAiChat.js.map +2 -2
- package/dist/backend/charts/BarChart.js +19 -76
- package/dist/backend/charts/BarChart.js.map +2 -2
- package/dist/backend/charts/BarChartImpl.js +91 -0
- package/dist/backend/charts/BarChartImpl.js.map +7 -0
- package/dist/backend/charts/LineChart.js +18 -91
- package/dist/backend/charts/LineChart.js.map +2 -2
- package/dist/backend/charts/LineChartImpl.js +112 -0
- package/dist/backend/charts/LineChartImpl.js.map +7 -0
- package/dist/backend/charts/PieChart.js +17 -62
- package/dist/backend/charts/PieChart.js.map +2 -2
- package/dist/backend/charts/PieChartImpl.js +76 -0
- package/dist/backend/charts/PieChartImpl.js.map +7 -0
- package/dist/backend/dashboard/DashboardScreen.js +9 -1
- package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
- package/dist/backend/dashboard/widgetRegistry.js +18 -2
- package/dist/backend/dashboard/widgetRegistry.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +2 -0
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/injection/useInjectionDataWidgets.js +12 -2
- package/dist/backend/injection/useInjectionDataWidgets.js.map +2 -2
- package/package.json +4 -5
- package/src/ai/AiAssistantLauncher.tsx +81 -5
- package/src/ai/AiChat.tsx +42 -2
- package/src/ai/AiChatSessions.tsx +55 -9
- package/src/ai/AiDock.tsx +13 -0
- package/src/ai/ConversationShareButton.tsx +39 -0
- package/src/ai/ConversationShareDialog.tsx +299 -0
- package/src/ai/__tests__/AiAssistantLauncher.test.tsx +6 -0
- package/src/ai/__tests__/AiChat.conversation.test.tsx +97 -0
- package/src/ai/__tests__/AiChatSessions.test.tsx +228 -0
- package/src/ai/__tests__/AiDock.test.tsx +31 -1
- package/src/ai/__tests__/conversation-store.test.ts +74 -1
- package/src/ai/conversation-store.ts +22 -2
- package/src/ai/index.ts +2 -0
- package/src/ai/useAiChat.ts +47 -4
- package/src/backend/__tests__/lazy-heavy-libraries.test.ts +53 -0
- package/src/backend/charts/BarChart.tsx +21 -72
- package/src/backend/charts/BarChartImpl.tsx +99 -0
- package/src/backend/charts/LineChart.tsx +23 -85
- package/src/backend/charts/LineChartImpl.tsx +120 -0
- package/src/backend/charts/PieChart.tsx +19 -60
- package/src/backend/charts/PieChartImpl.tsx +82 -0
- package/src/backend/dashboard/DashboardScreen.tsx +9 -1
- package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +45 -0
- package/src/backend/dashboard/__tests__/widgetRegistry.test.ts +60 -0
- package/src/backend/dashboard/widgetRegistry.ts +23 -5
- package/src/backend/icons/lucideRegistry.generated.tsx +2 -0
- package/src/backend/injection/useInjectionDataWidgets.ts +18 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Generated lucide registry with
|
|
2
|
-
[build:ui] found
|
|
1
|
+
Generated lucide registry with 136 icons -> /home/runner/work/open-mercato/open-mercato/packages/ui/src/backend/icons/lucideRegistry.generated.tsx
|
|
2
|
+
[build:ui] found 359 entry points
|
|
3
3
|
[build:ui] built successfully
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
4
5
|
import {
|
|
5
6
|
AlertTriangle,
|
|
6
7
|
BadgeQuestionMark,
|
|
@@ -28,6 +29,7 @@ import { Kbd, KbdShortcut } from "../primitives/kbd.js";
|
|
|
28
29
|
import { useAiDock } from "./AiDock.js";
|
|
29
30
|
import { useAiChatSessions } from "./AiChatSessions.js";
|
|
30
31
|
import { ChatPaneTabs } from "./ChatPaneTabs.js";
|
|
32
|
+
import { ConversationShareButton } from "./ConversationShareButton.js";
|
|
31
33
|
import { AiIcon } from "./AiIcon.js";
|
|
32
34
|
const LazyAiChat = React.lazy(async () => {
|
|
33
35
|
const mod = await import("./AiChat.js");
|
|
@@ -94,6 +96,10 @@ function AiAssistantLauncher({
|
|
|
94
96
|
}) {
|
|
95
97
|
const t = useT();
|
|
96
98
|
const dock = useAiDock();
|
|
99
|
+
const router = useRouter();
|
|
100
|
+
const pathname = usePathname();
|
|
101
|
+
const searchParams = useSearchParams();
|
|
102
|
+
const deepLinkParam = searchParams.get("openAiConversation");
|
|
97
103
|
const [healthy, setHealthy] = React.useState(skipHealthCheck ? true : null);
|
|
98
104
|
const [agents, setAgents] = React.useState([]);
|
|
99
105
|
const [agentsLoaded, setAgentsLoaded] = React.useState(false);
|
|
@@ -104,6 +110,32 @@ function AiAssistantLauncher({
|
|
|
104
110
|
const [chatOpen, setChatOpen] = React.useState(false);
|
|
105
111
|
const [query, setQuery] = React.useState("");
|
|
106
112
|
const [highlight, setHighlight] = React.useState(0);
|
|
113
|
+
const [deepLinkConversationId, setDeepLinkConversationId] = React.useState(null);
|
|
114
|
+
const deepLinkHandledRef = React.useRef(null);
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
if (deepLinkParam) setDeepLinkConversationId(deepLinkParam);
|
|
117
|
+
}, [deepLinkParam]);
|
|
118
|
+
React.useEffect(() => {
|
|
119
|
+
if (!deepLinkConversationId || !agentsLoaded || agents.length === 0) return;
|
|
120
|
+
if (deepLinkHandledRef.current === deepLinkConversationId) return;
|
|
121
|
+
deepLinkHandledRef.current = deepLinkConversationId;
|
|
122
|
+
let cancelled = false;
|
|
123
|
+
apiCall(
|
|
124
|
+
`/api/ai_assistant/ai/conversations/${deepLinkConversationId}`,
|
|
125
|
+
{ headers: { "x-om-forbidden-redirect": "0", "x-om-unauthorized-redirect": "0" } }
|
|
126
|
+
).then((res) => {
|
|
127
|
+
if (cancelled || !res.ok || !res.result) return;
|
|
128
|
+
const { agentId } = res.result.conversation;
|
|
129
|
+
const matched = agents.find((a) => a.id === agentId) ?? agents[0];
|
|
130
|
+
if (!matched) return;
|
|
131
|
+
setActiveAgent(matched);
|
|
132
|
+
setChatOpen(true);
|
|
133
|
+
router.replace(pathname, { scroll: false });
|
|
134
|
+
}).catch(() => void 0);
|
|
135
|
+
return () => {
|
|
136
|
+
cancelled = true;
|
|
137
|
+
};
|
|
138
|
+
}, [deepLinkConversationId, agentsLoaded, agents, router, pathname]);
|
|
107
139
|
React.useEffect(() => {
|
|
108
140
|
if (skipHealthCheck) return;
|
|
109
141
|
let cancelled = false;
|
|
@@ -188,6 +220,7 @@ function AiAssistantLauncher({
|
|
|
188
220
|
return () => window.removeEventListener(AI_ASSISTANT_LAUNCHER_OPEN_EVENT, listener);
|
|
189
221
|
}, [openPicker]);
|
|
190
222
|
const handleSelectAgent = React.useCallback((agent) => {
|
|
223
|
+
setDeepLinkConversationId(null);
|
|
191
224
|
if (dock.state.assistant?.agent === agent.id) {
|
|
192
225
|
dock.dock(dock.state.assistant);
|
|
193
226
|
setPickerOpen(false);
|
|
@@ -440,7 +473,13 @@ function AiAssistantLauncher({
|
|
|
440
473
|
]
|
|
441
474
|
}
|
|
442
475
|
) }),
|
|
443
|
-
/* @__PURE__ */ jsx(Dialog, { open: chatOpen, onOpenChange:
|
|
476
|
+
/* @__PURE__ */ jsx(Dialog, { open: chatOpen, onOpenChange: (open) => {
|
|
477
|
+
setChatOpen(open);
|
|
478
|
+
if (!open) {
|
|
479
|
+
setDeepLinkConversationId(null);
|
|
480
|
+
deepLinkHandledRef.current = null;
|
|
481
|
+
}
|
|
482
|
+
}, children: /* @__PURE__ */ jsxs(
|
|
444
483
|
DialogContent,
|
|
445
484
|
{
|
|
446
485
|
className: cn(
|
|
@@ -523,7 +562,8 @@ function AiAssistantLauncher({
|
|
|
523
562
|
placeholder: t(
|
|
524
563
|
"ai_assistant.launcher.composerPlaceholder",
|
|
525
564
|
"Ask anything\u2026"
|
|
526
|
-
)
|
|
565
|
+
),
|
|
566
|
+
sharedConversationId: deepLinkConversationId ?? void 0
|
|
527
567
|
}
|
|
528
568
|
) : null
|
|
529
569
|
]
|
|
@@ -536,29 +576,42 @@ function LauncherChatBody({
|
|
|
536
576
|
suggestions,
|
|
537
577
|
contextItems,
|
|
538
578
|
welcomeFallback,
|
|
539
|
-
placeholder
|
|
579
|
+
placeholder,
|
|
580
|
+
sharedConversationId
|
|
540
581
|
}) {
|
|
541
582
|
const sessions = useAiChatSessions();
|
|
583
|
+
const sharedSession = React.useMemo(
|
|
584
|
+
() => sharedConversationId ? sessions.state.sessions.find((s) => s.conversationId === sharedConversationId) ?? null : null,
|
|
585
|
+
[sharedConversationId, sessions.state.sessions]
|
|
586
|
+
);
|
|
542
587
|
const session = sessions.getActiveSession(activeAgent.id);
|
|
543
588
|
React.useEffect(() => {
|
|
589
|
+
if (sharedSession) {
|
|
590
|
+
sessions.setActiveSession(sharedSession.id);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (sharedConversationId) return;
|
|
544
594
|
if (!session) sessions.ensureSession(activeAgent.id);
|
|
545
|
-
}, [activeAgent.id, session, sessions]);
|
|
595
|
+
}, [activeAgent.id, session, sessions, sharedConversationId, sharedSession]);
|
|
596
|
+
const conversationId = session?.conversationId ?? sharedConversationId;
|
|
597
|
+
const chatKey = session?.id ?? sharedConversationId;
|
|
546
598
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
547
599
|
/* @__PURE__ */ jsx(ChatPaneTabs, { agentId: activeAgent.id, className: "border-b" }),
|
|
548
|
-
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", "data-ai-launcher-chat-container": "", children:
|
|
600
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", "data-ai-launcher-chat-container": "", children: conversationId ? /* @__PURE__ */ jsx(React.Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
|
|
549
601
|
LazyAiChat,
|
|
550
602
|
{
|
|
551
603
|
agent: activeAgent.id,
|
|
552
|
-
conversationId
|
|
604
|
+
conversationId,
|
|
553
605
|
pageContext: {},
|
|
554
606
|
className: "h-full",
|
|
555
607
|
placeholder,
|
|
556
608
|
suggestions,
|
|
557
609
|
contextItems,
|
|
558
610
|
welcomeTitle: activeAgent.label,
|
|
559
|
-
welcomeDescription: activeAgent.description ?? welcomeFallback
|
|
611
|
+
welcomeDescription: activeAgent.description ?? welcomeFallback,
|
|
612
|
+
headerActions: /* @__PURE__ */ jsx(ConversationShareButton, { conversationId })
|
|
560
613
|
},
|
|
561
|
-
|
|
614
|
+
chatKey
|
|
562
615
|
) }) : null })
|
|
563
616
|
] });
|
|
564
617
|
}
|
|
@@ -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 {\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 { 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 [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\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 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={setChatOpen}>\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 />\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}\n\nfunction LauncherChatBody({\n activeAgent,\n suggestions,\n contextItems,\n welcomeFallback,\n placeholder,\n}: LauncherChatBodyProps) {\n const sessions = useAiChatSessions()\n const session = sessions.getActiveSession(activeAgent.id)\n\n React.useEffect(() => {\n if (!session) sessions.ensureSession(activeAgent.id)\n }, [activeAgent.id, session, sessions])\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 {session ? (\n <React.Suspense fallback={null}>\n <LazyAiChat\n key={session.id}\n agent={activeAgent.id}\n conversationId={session.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 />\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 * - 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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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/ai/AiChat.js
CHANGED
|
@@ -521,11 +521,13 @@ function MessageFileAttachment({ file }) {
|
|
|
521
521
|
function MessageRow({
|
|
522
522
|
message,
|
|
523
523
|
registry,
|
|
524
|
-
onMutationRequested
|
|
524
|
+
onMutationRequested,
|
|
525
|
+
isOwner
|
|
525
526
|
}) {
|
|
526
527
|
const t = useT();
|
|
527
528
|
const isAssistant = message.role === "assistant";
|
|
528
|
-
const
|
|
529
|
+
const isOtherUsersMessage = !isAssistant && isOwner === false;
|
|
530
|
+
const label = isAssistant ? t("ai_assistant.chat.assistantRoleLabel", "Assistant") : isOtherUsersMessage ? t("ai_assistant.chat.ownerRoleLabel", "Owner") : t("ai_assistant.chat.userRoleLabel", "You");
|
|
529
531
|
const Icon = isAssistant ? Bot : User;
|
|
530
532
|
const [copied, setCopied] = React.useState(false);
|
|
531
533
|
const copyTimerRef = React.useRef(null);
|
|
@@ -813,9 +815,16 @@ function AiChat({
|
|
|
813
815
|
contextItems,
|
|
814
816
|
welcomeTitle,
|
|
815
817
|
welcomeDescription,
|
|
816
|
-
defaultCompactFooter = false
|
|
818
|
+
defaultCompactFooter = false,
|
|
819
|
+
headerActions,
|
|
820
|
+
onConversationIdChange,
|
|
821
|
+
onConversationNotFound
|
|
817
822
|
}) {
|
|
818
823
|
const t = useT();
|
|
824
|
+
const onConversationIdChangeRef = React.useRef(onConversationIdChange);
|
|
825
|
+
React.useLayoutEffect(() => {
|
|
826
|
+
onConversationIdChangeRef.current = onConversationIdChange;
|
|
827
|
+
});
|
|
819
828
|
const textareaRef = React.useRef(null);
|
|
820
829
|
const transcriptRef = React.useRef(null);
|
|
821
830
|
const fileInputRef = React.useRef(null);
|
|
@@ -912,11 +921,15 @@ function AiChat({
|
|
|
912
921
|
onError,
|
|
913
922
|
conversationId,
|
|
914
923
|
providerOverride: effectiveModelPickerValue?.providerId ?? null,
|
|
915
|
-
modelOverride: effectiveModelPickerValue?.modelId ?? null
|
|
924
|
+
modelOverride: effectiveModelPickerValue?.modelId ?? null,
|
|
925
|
+
onConversationNotFound
|
|
916
926
|
});
|
|
917
927
|
const isStreaming = chat.status === "streaming";
|
|
918
928
|
const isSubmitting = chat.status === "submitting";
|
|
919
929
|
const isBusy = isStreaming || isSubmitting;
|
|
930
|
+
React.useEffect(() => {
|
|
931
|
+
onConversationIdChangeRef.current?.(chat.conversationId);
|
|
932
|
+
}, [chat.conversationId]);
|
|
920
933
|
const lastAssistant = React.useMemo(() => {
|
|
921
934
|
for (let index = chat.messages.length - 1; index >= 0; index -= 1) {
|
|
922
935
|
const candidate = chat.messages[index];
|
|
@@ -1120,6 +1133,7 @@ function AiChat({
|
|
|
1120
1133
|
children: [
|
|
1121
1134
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center justify-between gap-2 border-b border-border pb-2", children: [
|
|
1122
1135
|
/* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2 text-xs text-muted-foreground", children: contextItems && contextItems.length > 0 ? /* @__PURE__ */ jsx(ContextItemsPill, { items: contextItems }) : /* @__PURE__ */ jsx("span", { className: "font-mono opacity-70", "aria-hidden": true, children: chat.conversationId.slice(0, 8) }) }),
|
|
1136
|
+
headerActions ?? null,
|
|
1123
1137
|
/* @__PURE__ */ jsx(
|
|
1124
1138
|
IconButton,
|
|
1125
1139
|
{
|
|
@@ -1157,7 +1171,8 @@ function AiChat({
|
|
|
1157
1171
|
{
|
|
1158
1172
|
message,
|
|
1159
1173
|
registry: activeRegistry,
|
|
1160
|
-
onMutationRequested
|
|
1174
|
+
onMutationRequested,
|
|
1175
|
+
isOwner: chat.isOwner
|
|
1161
1176
|
},
|
|
1162
1177
|
message.id
|
|
1163
1178
|
)),
|
|
@@ -1191,10 +1206,18 @@ function AiChat({
|
|
|
1191
1206
|
chat.error.message
|
|
1192
1207
|
] })
|
|
1193
1208
|
] }) : null,
|
|
1209
|
+
chat.isOwner === false ? /* @__PURE__ */ jsx(
|
|
1210
|
+
"div",
|
|
1211
|
+
{
|
|
1212
|
+
className: "flex items-center justify-center rounded-md border border-border bg-muted/30 px-3 py-3 text-xs text-muted-foreground",
|
|
1213
|
+
"data-ai-chat-read-only-notice": "",
|
|
1214
|
+
children: t("ai_assistant.chat.readOnlyNotice", "This is a shared conversation. You can read but not reply.")
|
|
1215
|
+
}
|
|
1216
|
+
) : null,
|
|
1194
1217
|
/* @__PURE__ */ jsxs(
|
|
1195
1218
|
"form",
|
|
1196
1219
|
{
|
|
1197
|
-
className: "flex min-w-0 flex-col gap-2",
|
|
1220
|
+
className: cn("flex min-w-0 flex-col gap-2", chat.isOwner === false && "hidden"),
|
|
1198
1221
|
onSubmit: (event) => {
|
|
1199
1222
|
event.preventDefault();
|
|
1200
1223
|
handleSubmit();
|