@open-mercato/ui 0.6.4-develop.3949.1.adc3d0b3b1 → 0.6.4-develop.3968.1.9d87f5fa16

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.
@@ -1,13 +1,21 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import {
5
+ getCurrentOrganizationScope,
6
+ subscribeOrganizationScopeChanged
7
+ } from "@open-mercato/shared/lib/frontend/organizationEvents";
4
8
  import {
5
9
  createAiServerConversation,
6
10
  listAiServerConversations,
7
11
  updateAiServerConversation
8
12
  } from "./conversation-store.js";
9
- const STORAGE_KEY = "om-ai-chat-sessions-v1";
13
+ const LEGACY_STORAGE_KEY = "om-ai-chat-sessions-v1";
14
+ const STORAGE_KEY_PREFIX = "om-ai-chat-sessions-v1";
10
15
  const HISTORY_LIMIT = 50;
16
+ function getScopedStorageKey(tenantId, organizationId) {
17
+ return `${STORAGE_KEY_PREFIX}:${tenantId ?? "no-tenant"}:${organizationId ?? "no-org"}`;
18
+ }
11
19
  const AiChatSessionsContext = React.createContext(null);
12
20
  function makeId() {
13
21
  const g = globalThis;
@@ -20,10 +28,10 @@ function makeId() {
20
28
  const rand = () => Math.random().toString(16).slice(2, 10);
21
29
  return `${Date.now().toString(16)}-${rand()}-${rand()}`;
22
30
  }
23
- function readPersisted() {
31
+ function readPersisted(storageKey) {
24
32
  if (typeof window === "undefined") return { sessions: [], activeByAgent: {} };
25
33
  try {
26
- const raw = window.localStorage.getItem(STORAGE_KEY);
34
+ const raw = window.localStorage.getItem(storageKey);
27
35
  if (!raw) return { sessions: [], activeByAgent: {} };
28
36
  const parsed = JSON.parse(raw);
29
37
  const sessions = Array.isArray(parsed?.sessions) ? parsed.sessions.filter((entry) => {
@@ -48,10 +56,10 @@ function readPersisted() {
48
56
  return { sessions: [], activeByAgent: {} };
49
57
  }
50
58
  }
51
- function writePersisted(state) {
59
+ function writePersisted(storageKey, state) {
52
60
  if (typeof window === "undefined") return;
53
61
  try {
54
- window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
62
+ window.localStorage.setItem(storageKey, JSON.stringify(state));
55
63
  } catch {
56
64
  }
57
65
  }
@@ -89,10 +97,22 @@ function mergeServerConversations(prev, conversations) {
89
97
  return { sessions, activeByAgent };
90
98
  }
91
99
  function AiChatSessionsProvider({ children }) {
92
- const [state, setState] = React.useState(() => readPersisted());
100
+ const [storageKey, setStorageKey] = React.useState(() => {
101
+ const scope = getCurrentOrganizationScope();
102
+ return getScopedStorageKey(scope.tenantId, scope.organizationId);
103
+ });
104
+ const [state, setState] = React.useState(() => readPersisted(storageKey));
105
+ React.useEffect(() => {
106
+ return subscribeOrganizationScopeChanged((detail) => {
107
+ const nextKey = getScopedStorageKey(detail.tenantId, detail.organizationId);
108
+ if (nextKey === storageKey) return;
109
+ setStorageKey(nextKey);
110
+ setState(readPersisted(nextKey));
111
+ });
112
+ }, [storageKey]);
93
113
  React.useEffect(() => {
94
- writePersisted(state);
95
- }, [state]);
114
+ writePersisted(storageKey, state);
115
+ }, [storageKey, state]);
96
116
  React.useEffect(() => {
97
117
  let cancelled = false;
98
118
  void listAiServerConversations({ limit: 100 }).then((conversations) => {
@@ -102,7 +122,7 @@ function AiChatSessionsProvider({ children }) {
102
122
  return () => {
103
123
  cancelled = true;
104
124
  };
105
- }, []);
125
+ }, [storageKey]);
106
126
  const update = React.useCallback(
107
127
  (mutator) => {
108
128
  setState((prev) => mutator(prev));
@@ -362,6 +382,7 @@ function defaultSessionLabel(session) {
362
382
  }
363
383
  export {
364
384
  AiChatSessionsProvider,
385
+ LEGACY_STORAGE_KEY,
365
386
  defaultSessionLabel,
366
387
  useAiChatSessions
367
388
  };
@@ -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 createAiServerConversation,\n listAiServerConversations,\n updateAiServerConversation,\n type AiServerConversation,\n} from './conversation-store'\n\nconst STORAGE_KEY = 'om-ai-chat-sessions-v1'\nconst HISTORY_LIMIT = 50\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\nfunction readPersisted(): AiChatSessionsState {\n if (typeof window === 'undefined') return { sessions: [], activeByAgent: {} }\n try {\n const raw = window.localStorage.getItem(STORAGE_KEY)\n if (!raw) return { sessions: [], activeByAgent: {} }\n const parsed = JSON.parse(raw) as Partial<AiChatSessionsState> | null\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 } catch {\n return { sessions: [], activeByAgent: {} }\n }\n}\n\nfunction writePersisted(state: AiChatSessionsState): void {\n if (typeof window === 'undefined') return\n try {\n window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state))\n } catch {\n /* quota / privacy mode \u2014 drop silently */\n }\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 const [state, setState] = React.useState<AiChatSessionsState>(() => readPersisted())\n\n React.useEffect(() => {\n writePersisted(state)\n }, [state])\n\n React.useEffect(() => {\n let cancelled = false\n void listAiServerConversations({ limit: 100 }).then((conversations) => {\n if (cancelled || !conversations) return\n setState((prev) => mergeServerConversations(prev, conversations))\n })\n return () => {\n cancelled = true\n }\n }, [])\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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",
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 {\n createAiServerConversation,\n listAiServerConversations,\n updateAiServerConversation,\n type AiServerConversation,\n} from './conversation-store'\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\nfunction readPersisted(storageKey: string): AiChatSessionsState {\n if (typeof window === 'undefined') return { sessions: [], activeByAgent: {} }\n try {\n const raw = window.localStorage.getItem(storageKey)\n if (!raw) return { sessions: [], activeByAgent: {} }\n const parsed = JSON.parse(raw) as Partial<AiChatSessionsState> | null\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 } catch {\n return { sessions: [], activeByAgent: {} }\n }\n}\n\nfunction writePersisted(storageKey: string, state: AiChatSessionsState): void {\n if (typeof window === 'undefined') return\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(state))\n } catch {\n /* quota / privacy mode \u2014 drop silently */\n }\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 void listAiServerConversations({ limit: 100 }).then((conversations) => {\n if (cancelled || !conversations) return\n setState((prev) => mergeServerConversations(prev, conversations))\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": "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",
6
6
  "names": []
7
7
  }
package/dist/ai/AiDock.js CHANGED
@@ -322,6 +322,10 @@ function DockedChatBody({ assistant }) {
322
322
  React.useEffect(() => {
323
323
  if (!session) sessions.ensureSession(assistant.agent);
324
324
  }, [assistant.agent, session, sessions]);
325
+ const activeSessionId = session?.id ?? null;
326
+ const handleConversationNotFound = React.useCallback(() => {
327
+ if (activeSessionId) sessions.closeSession(activeSessionId);
328
+ }, [activeSessionId, sessions]);
325
329
  return /* @__PURE__ */ jsxs(Fragment, { children: [
326
330
  /* @__PURE__ */ jsx(ChatPaneTabs, { agentId: assistant.agent, className: "border-b" }),
327
331
  /* @__PURE__ */ jsx("div", { className: "min-h-0 min-w-0 flex-1 overflow-hidden", "data-ai-dock-chat-container": "", children: session ? /* @__PURE__ */ jsx(React.Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
@@ -337,7 +341,8 @@ function DockedChatBody({ assistant }) {
337
341
  contextItems: assistant.contextItems,
338
342
  welcomeTitle: assistant.welcomeTitle,
339
343
  welcomeDescription: assistant.welcomeDescription,
340
- headerActions: /* @__PURE__ */ jsx(ConversationShareButton, { conversationId: session.conversationId })
344
+ headerActions: /* @__PURE__ */ jsx(ConversationShareButton, { conversationId: session.conversationId }),
345
+ onConversationNotFound: handleConversationNotFound
341
346
  },
342
347
  session.id
343
348
  ) }) : null })
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ai/AiDock.tsx"],
4
- "sourcesContent": ["\"use client\"\n\n/**\n * Global AI Dock \u2014 a persistent right-side panel that hosts an `<AiChat>`\n * surface across page navigations.\n *\n * Modules invoke `useAiDock().dock({ agent, label, pageContext })` (typically\n * from the dialog header of an injection trigger) to move an active\n * assistant from a transient dialog into the dock. The dock survives router\n * navigation because the provider is mounted at the layout root (AppShell).\n *\n * The panel renders only when something is docked. The docked assistant, its\n * collapsed state, and its width are persisted in localStorage so refreshing\n * the page restores the same open dock.\n */\n\nimport * as React from 'react'\nimport { Maximize2, Minimize2, X } from 'lucide-react'\nimport type { AiChatContextItem, AiChatSuggestion } from './AiChat'\nimport { ChatPaneTabs } from './ChatPaneTabs'\nimport { useAiChatSessions } from './AiChatSessions'\nimport { ConversationShareButton } from './ConversationShareButton'\nimport { IconButton } from '../primitives/icon-button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\n// Lazy import keeps the heavy chat surface (AI SDK + streaming runtime) out\n// of the AppShell import graph. The dock provider only renders the chat when\n// something is actually docked, so tests that never dock skip the import.\nconst LazyAiChat = React.lazy(async () => {\n const mod = await import('./AiChat')\n return { default: mod.AiChat }\n})\n\nconst STORAGE_KEY = 'om-ai-dock-v1'\nconst MIN_WIDTH = 320\nconst MAX_WIDTH = 960\nconst DEFAULT_WIDTH = 420\n\nexport interface AiDockedAssistant {\n /** AI agent id (must be enabled for the current user). */\n agent: string\n /** Human-readable label shown in the dock header. */\n label: string\n /** Optional secondary description (e.g. module name). */\n description?: string\n /** Spec \u00A710.1 page-context payload sent with each turn. */\n pageContext?: Record<string, unknown>\n /** Composer placeholder copy. */\n placeholder?: string\n /** Welcome card title. */\n welcomeTitle?: string\n /** Welcome card description. */\n welcomeDescription?: string\n /** Optional starter suggestions. */\n suggestions?: AiChatSuggestion[]\n /** Optional pinned context chips (e.g. \"3 selected\"). */\n contextItems?: AiChatContextItem[]\n}\n\ninterface AiDockState {\n assistant: AiDockedAssistant | null\n width: number\n collapsed: boolean\n}\n\ninterface PersistedAiChatSuggestion {\n label: string\n prompt: string\n}\n\ninterface PersistedAiDockedAssistant {\n agent: string\n label: string\n description?: string\n pageContext?: Record<string, unknown>\n placeholder?: string\n welcomeTitle?: string\n welcomeDescription?: string\n suggestions?: PersistedAiChatSuggestion[]\n contextItems?: AiChatContextItem[]\n}\n\ninterface PersistedAiDockState {\n assistant?: PersistedAiDockedAssistant | null\n width?: number\n collapsed?: boolean\n}\n\ninterface AiDockApi {\n state: AiDockState\n /** Open / replace the docked assistant. */\n dock: (assistant: AiDockedAssistant) => void\n /** Close the dock. */\n undock: () => void\n /** True when the docked agent matches `agentId`. */\n isDocked: (agentId: string) => boolean\n}\n\nconst COLLAPSED_WIDTH = 48\n\nconst AiDockContext = React.createContext<AiDockApi | null>(null)\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction readOptionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nfunction readWidth(value: unknown): number {\n return Math.min(\n MAX_WIDTH,\n Math.max(MIN_WIDTH, typeof value === 'number' ? value : DEFAULT_WIDTH),\n )\n}\n\nfunction readPersistedSuggestions(value: unknown): PersistedAiChatSuggestion[] | undefined {\n if (!Array.isArray(value)) return undefined\n const suggestions = value\n .filter((entry): entry is Record<string, unknown> => isRecord(entry))\n .map((entry) => {\n const label = readOptionalString(entry.label)\n const prompt = readOptionalString(entry.prompt)\n if (!label || !prompt) return null\n return { label, prompt }\n })\n .filter((entry): entry is PersistedAiChatSuggestion => entry !== null)\n return suggestions.length > 0 ? suggestions : undefined\n}\n\nfunction readPersistedContextItems(value: unknown): AiChatContextItem[] | undefined {\n if (!Array.isArray(value)) return undefined\n const contextItems = value\n .filter((entry): entry is Record<string, unknown> => isRecord(entry))\n .map((entry) => {\n const label = readOptionalString(entry.label)\n if (!label) return null\n const detail = readOptionalString(entry.detail)\n return detail ? { label, detail } : { label }\n })\n .filter((entry): entry is AiChatContextItem => entry !== null)\n return contextItems.length > 0 ? contextItems : undefined\n}\n\nfunction readPersistedAssistant(value: unknown): AiDockedAssistant | null {\n if (!isRecord(value)) return null\n const agent = readOptionalString(value.agent)\n const label = readOptionalString(value.label)\n if (!agent || !label) return null\n\n return {\n agent,\n label,\n description: readOptionalString(value.description),\n pageContext: isRecord(value.pageContext) ? value.pageContext : undefined,\n placeholder: readOptionalString(value.placeholder),\n welcomeTitle: readOptionalString(value.welcomeTitle),\n welcomeDescription: readOptionalString(value.welcomeDescription),\n suggestions: readPersistedSuggestions(value.suggestions),\n contextItems: readPersistedContextItems(value.contextItems),\n }\n}\n\nfunction serializeAssistant(assistant: AiDockedAssistant | null): PersistedAiDockedAssistant | null {\n if (!assistant) return null\n return {\n agent: assistant.agent,\n label: assistant.label,\n description: assistant.description,\n pageContext: assistant.pageContext,\n placeholder: assistant.placeholder,\n welcomeTitle: assistant.welcomeTitle,\n welcomeDescription: assistant.welcomeDescription,\n suggestions: assistant.suggestions?.map((suggestion) => ({\n label: suggestion.label,\n prompt: suggestion.prompt,\n })),\n contextItems: assistant.contextItems?.map((item) => ({\n label: item.label,\n detail: item.detail,\n })),\n }\n}\n\nfunction readPersisted(): AiDockState {\n if (typeof window === 'undefined') return { assistant: null, width: DEFAULT_WIDTH, collapsed: false }\n try {\n const raw = window.localStorage.getItem(STORAGE_KEY)\n if (!raw) return { assistant: null, width: DEFAULT_WIDTH, collapsed: false }\n const parsed = JSON.parse(raw) as PersistedAiDockState | null\n return {\n assistant: readPersistedAssistant(parsed?.assistant),\n width: readWidth(parsed?.width),\n collapsed: parsed?.collapsed === true,\n }\n } catch {\n return { assistant: null, width: DEFAULT_WIDTH, collapsed: false }\n }\n}\n\nfunction writePersisted(state: AiDockState) {\n if (typeof window === 'undefined') return\n try {\n window.localStorage.setItem(\n STORAGE_KEY,\n JSON.stringify({\n assistant: serializeAssistant(state.assistant),\n width: state.width,\n collapsed: state.collapsed,\n } satisfies PersistedAiDockState),\n )\n } catch {\n /* ignore */\n }\n}\n\nexport function AiDockProvider({ children }: { children: React.ReactNode }) {\n const [state, setState] = React.useState<AiDockState>(() => ({\n assistant: null,\n width: DEFAULT_WIDTH,\n collapsed: false,\n }))\n const [hydrated, setHydrated] = React.useState(false)\n\n React.useEffect(() => {\n const persisted = readPersisted()\n setState(persisted)\n setHydrated(true)\n }, [])\n\n React.useEffect(() => {\n if (!hydrated) return\n writePersisted(state)\n }, [hydrated, state])\n\n const dock = React.useCallback((assistant: AiDockedAssistant) => {\n // Always reset `collapsed` when (re)docking \u2014 the operator just clicked\n // \"dock to side\" and expects the panel to be visible at full width.\n setState((prev) => ({ ...prev, assistant, collapsed: false }))\n }, [])\n\n const undock = React.useCallback(() => {\n // Hard reset: drop the assistant AND the collapsed flag so the next\n // dock call starts from a clean slate. The wrapper observes `assistant`\n // turning null and clears its layout shift in the same render tick.\n setState((prev) => ({ ...prev, assistant: null, collapsed: false }))\n }, [])\n\n const isDocked = React.useCallback(\n (agentId: string) => state.assistant?.agent === agentId,\n [state.assistant?.agent],\n )\n\n const setWidth = React.useCallback((width: number) => {\n setState((prev) => ({ ...prev, width }))\n }, [])\n\n const setCollapsed = React.useCallback((collapsed: boolean) => {\n setState((prev) => ({ ...prev, collapsed }))\n }, [])\n\n const api = React.useMemo<AiDockApi>(\n () => ({ state, dock, undock, isDocked }),\n [state, dock, undock, isDocked],\n )\n\n // The right-side padding the page must reserve for the dock panel. Stays\n // null while nothing is docked so the wrapper renders no className/style\n // and the underlying layout (DataTable, sidebar grid) reclaims its full\n // width on undock.\n const reservedWidth = state.assistant\n ? state.collapsed\n ? COLLAPSED_WIDTH\n : state.width\n : null\n\n return (\n <AiDockContext.Provider value={api}>\n <div\n // The dock is desktop-only (`lg+`); reserve right-side padding only\n // at that breakpoint so mobile layout stays full-width.\n className={reservedWidth != null ? 'lg:pr-[var(--om-ai-dock-width)]' : undefined}\n style={\n reservedWidth != null\n ? ({ ['--om-ai-dock-width' as string]: `${reservedWidth}px` } as React.CSSProperties)\n : undefined\n }\n >\n {children}\n </div>\n {state.assistant ? (\n <AiDockPanel\n assistant={state.assistant}\n width={state.width}\n collapsed={state.collapsed}\n onCollapsedChange={setCollapsed}\n onWidthChange={setWidth}\n onClose={undock}\n />\n ) : null}\n </AiDockContext.Provider>\n )\n}\n\nexport function useAiDock(): AiDockApi {\n const ctx = React.useContext(AiDockContext)\n if (ctx) return ctx\n // Fallback no-op API \u2014 keeps consumers safe when the provider is absent\n // (e.g. unit tests rendering a widget in isolation).\n return {\n state: { assistant: null, width: DEFAULT_WIDTH, collapsed: false },\n dock: () => {},\n undock: () => {},\n isDocked: () => false,\n }\n}\n\ninterface AiDockPanelProps {\n assistant: AiDockedAssistant\n width: number\n collapsed: boolean\n onCollapsedChange: (collapsed: boolean) => void\n onWidthChange: (width: number) => void\n onClose: () => void\n}\n\nfunction AiDockPanel({\n assistant,\n width,\n collapsed,\n onCollapsedChange,\n onWidthChange,\n onClose,\n}: AiDockPanelProps) {\n const t = useT()\n const dragStateRef = React.useRef<{ startX: number; startWidth: number } | null>(null)\n\n const handlePointerDown = React.useCallback(\n (event: React.PointerEvent<HTMLDivElement>) => {\n event.preventDefault()\n const target = event.currentTarget\n target.setPointerCapture(event.pointerId)\n dragStateRef.current = { startX: event.clientX, startWidth: width }\n },\n [width],\n )\n\n const handlePointerMove = React.useCallback(\n (event: React.PointerEvent<HTMLDivElement>) => {\n if (!dragStateRef.current) return\n const delta = dragStateRef.current.startX - event.clientX\n const next = Math.min(\n MAX_WIDTH,\n Math.max(MIN_WIDTH, dragStateRef.current.startWidth + delta),\n )\n onWidthChange(next)\n },\n [onWidthChange],\n )\n\n const handlePointerUp = React.useCallback(\n (event: React.PointerEvent<HTMLDivElement>) => {\n const target = event.currentTarget\n try {\n target.releasePointerCapture(event.pointerId)\n } catch {\n /* ignore */\n }\n dragStateRef.current = null\n },\n [],\n )\n\n return (\n <aside\n data-ai-dock-panel=\"\"\n data-ai-dock-agent={assistant.agent}\n className={cn(\n // Dock is desktop-only \u2014 on small screens the AiChat dialog is the\n // primary surface (full-screen sheet) and a fixed side panel would\n // crowd the viewport.\n 'hidden lg:flex',\n 'fixed top-0 right-0 z-overlay h-svh min-w-0 flex-col overflow-hidden border-l bg-background shadow-lg',\n collapsed ? 'w-12' : '',\n )}\n style={collapsed ? undefined : { width }}\n aria-label={assistant.label}\n >\n {!collapsed ? (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n tabIndex={-1}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n onPointerCancel={handlePointerUp}\n className=\"absolute left-0 top-0 h-full w-1 cursor-col-resize bg-transparent hover:bg-primary/20\"\n data-ai-dock-resize-handle=\"\"\n />\n ) : null}\n <header className=\"flex items-center gap-2 px-3 py-2\">\n {collapsed ? (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ai_assistant.chat.dock.expand', 'Expand AI dock')}\n title={t('ai_assistant.chat.dock.expand', 'Expand AI dock')}\n onClick={() => onCollapsedChange(false)}\n >\n <Maximize2 className=\"size-4\" />\n </IconButton>\n ) : (\n <>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2 text-sm font-medium\" data-ai-dock-label=\"\">\n <span className=\"truncate\">{assistant.label}</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 </div>\n {assistant.description ? (\n <div className=\"truncate text-xs text-muted-foreground\">\n {assistant.description}\n </div>\n ) : null}\n </div>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ai_assistant.chat.dock.collapse', 'Collapse AI dock')}\n title={t('ai_assistant.chat.dock.collapse', 'Collapse AI dock')}\n onClick={() => onCollapsedChange(true)}\n >\n <Minimize2 className=\"size-4\" />\n </IconButton>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ai_assistant.chat.dock.close', 'Close AI dock')}\n title={t('ai_assistant.chat.dock.close', 'Close AI dock')}\n onClick={onClose}\n data-ai-dock-close=\"\"\n >\n <X className=\"size-4\" />\n </IconButton>\n </>\n )}\n </header>\n {!collapsed ? (\n <DockedChatBody assistant={assistant} />\n ) : null}\n </aside>\n )\n}\n\nfunction DockedChatBody({ assistant }: { assistant: AiDockedAssistant }) {\n const sessions = useAiChatSessions()\n const session = sessions.getActiveSession(assistant.agent)\n\n // Lazily ensure an open session exists. Running `ensureSession` inside an\n // effect (not inline during render) keeps the provider's setState calls\n // outside of the render phase. The first frame may render without a\n // session \u2014 that's fine, we render the tab strip alone until the next\n // tick when the new session is committed and `getActiveSession` returns\n // it.\n React.useEffect(() => {\n if (!session) sessions.ensureSession(assistant.agent)\n }, [assistant.agent, session, sessions])\n\n return (\n <>\n <ChatPaneTabs agentId={assistant.agent} className=\"border-b\" />\n <div className=\"min-h-0 min-w-0 flex-1 overflow-hidden\" data-ai-dock-chat-container=\"\">\n {session ? (\n <React.Suspense fallback={null}>\n <LazyAiChat\n // `key` forces a fresh `<AiChat>` mount whenever the active\n // session changes \u2014 without it the AI SDK's internal status\n // would carry across tabs and surface the previous tab's\n // streaming indicator on a brand-new conversation.\n key={session.id}\n agent={assistant.agent}\n conversationId={session.conversationId}\n pageContext={assistant.pageContext}\n className=\"h-full\"\n defaultCompactFooter\n placeholder={assistant.placeholder}\n suggestions={assistant.suggestions}\n contextItems={assistant.contextItems}\n welcomeTitle={assistant.welcomeTitle}\n welcomeDescription={assistant.welcomeDescription}\n headerActions={<ConversationShareButton conversationId={session.conversationId} />}\n />\n </React.Suspense>\n ) : null}\n </div>\n </>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\n/**\n * Global AI Dock \u2014 a persistent right-side panel that hosts an `<AiChat>`\n * surface across page navigations.\n *\n * Modules invoke `useAiDock().dock({ agent, label, pageContext })` (typically\n * from the dialog header of an injection trigger) to move an active\n * assistant from a transient dialog into the dock. The dock survives router\n * navigation because the provider is mounted at the layout root (AppShell).\n *\n * The panel renders only when something is docked. The docked assistant, its\n * collapsed state, and its width are persisted in localStorage so refreshing\n * the page restores the same open dock.\n */\n\nimport * as React from 'react'\nimport { Maximize2, Minimize2, X } from 'lucide-react'\nimport type { AiChatContextItem, AiChatSuggestion } from './AiChat'\nimport { ChatPaneTabs } from './ChatPaneTabs'\nimport { useAiChatSessions } from './AiChatSessions'\nimport { ConversationShareButton } from './ConversationShareButton'\nimport { IconButton } from '../primitives/icon-button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\n// Lazy import keeps the heavy chat surface (AI SDK + streaming runtime) out\n// of the AppShell import graph. The dock provider only renders the chat when\n// something is actually docked, so tests that never dock skip the import.\nconst LazyAiChat = React.lazy(async () => {\n const mod = await import('./AiChat')\n return { default: mod.AiChat }\n})\n\nconst STORAGE_KEY = 'om-ai-dock-v1'\nconst MIN_WIDTH = 320\nconst MAX_WIDTH = 960\nconst DEFAULT_WIDTH = 420\n\nexport interface AiDockedAssistant {\n /** AI agent id (must be enabled for the current user). */\n agent: string\n /** Human-readable label shown in the dock header. */\n label: string\n /** Optional secondary description (e.g. module name). */\n description?: string\n /** Spec \u00A710.1 page-context payload sent with each turn. */\n pageContext?: Record<string, unknown>\n /** Composer placeholder copy. */\n placeholder?: string\n /** Welcome card title. */\n welcomeTitle?: string\n /** Welcome card description. */\n welcomeDescription?: string\n /** Optional starter suggestions. */\n suggestions?: AiChatSuggestion[]\n /** Optional pinned context chips (e.g. \"3 selected\"). */\n contextItems?: AiChatContextItem[]\n}\n\ninterface AiDockState {\n assistant: AiDockedAssistant | null\n width: number\n collapsed: boolean\n}\n\ninterface PersistedAiChatSuggestion {\n label: string\n prompt: string\n}\n\ninterface PersistedAiDockedAssistant {\n agent: string\n label: string\n description?: string\n pageContext?: Record<string, unknown>\n placeholder?: string\n welcomeTitle?: string\n welcomeDescription?: string\n suggestions?: PersistedAiChatSuggestion[]\n contextItems?: AiChatContextItem[]\n}\n\ninterface PersistedAiDockState {\n assistant?: PersistedAiDockedAssistant | null\n width?: number\n collapsed?: boolean\n}\n\ninterface AiDockApi {\n state: AiDockState\n /** Open / replace the docked assistant. */\n dock: (assistant: AiDockedAssistant) => void\n /** Close the dock. */\n undock: () => void\n /** True when the docked agent matches `agentId`. */\n isDocked: (agentId: string) => boolean\n}\n\nconst COLLAPSED_WIDTH = 48\n\nconst AiDockContext = React.createContext<AiDockApi | null>(null)\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction readOptionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nfunction readWidth(value: unknown): number {\n return Math.min(\n MAX_WIDTH,\n Math.max(MIN_WIDTH, typeof value === 'number' ? value : DEFAULT_WIDTH),\n )\n}\n\nfunction readPersistedSuggestions(value: unknown): PersistedAiChatSuggestion[] | undefined {\n if (!Array.isArray(value)) return undefined\n const suggestions = value\n .filter((entry): entry is Record<string, unknown> => isRecord(entry))\n .map((entry) => {\n const label = readOptionalString(entry.label)\n const prompt = readOptionalString(entry.prompt)\n if (!label || !prompt) return null\n return { label, prompt }\n })\n .filter((entry): entry is PersistedAiChatSuggestion => entry !== null)\n return suggestions.length > 0 ? suggestions : undefined\n}\n\nfunction readPersistedContextItems(value: unknown): AiChatContextItem[] | undefined {\n if (!Array.isArray(value)) return undefined\n const contextItems = value\n .filter((entry): entry is Record<string, unknown> => isRecord(entry))\n .map((entry) => {\n const label = readOptionalString(entry.label)\n if (!label) return null\n const detail = readOptionalString(entry.detail)\n return detail ? { label, detail } : { label }\n })\n .filter((entry): entry is AiChatContextItem => entry !== null)\n return contextItems.length > 0 ? contextItems : undefined\n}\n\nfunction readPersistedAssistant(value: unknown): AiDockedAssistant | null {\n if (!isRecord(value)) return null\n const agent = readOptionalString(value.agent)\n const label = readOptionalString(value.label)\n if (!agent || !label) return null\n\n return {\n agent,\n label,\n description: readOptionalString(value.description),\n pageContext: isRecord(value.pageContext) ? value.pageContext : undefined,\n placeholder: readOptionalString(value.placeholder),\n welcomeTitle: readOptionalString(value.welcomeTitle),\n welcomeDescription: readOptionalString(value.welcomeDescription),\n suggestions: readPersistedSuggestions(value.suggestions),\n contextItems: readPersistedContextItems(value.contextItems),\n }\n}\n\nfunction serializeAssistant(assistant: AiDockedAssistant | null): PersistedAiDockedAssistant | null {\n if (!assistant) return null\n return {\n agent: assistant.agent,\n label: assistant.label,\n description: assistant.description,\n pageContext: assistant.pageContext,\n placeholder: assistant.placeholder,\n welcomeTitle: assistant.welcomeTitle,\n welcomeDescription: assistant.welcomeDescription,\n suggestions: assistant.suggestions?.map((suggestion) => ({\n label: suggestion.label,\n prompt: suggestion.prompt,\n })),\n contextItems: assistant.contextItems?.map((item) => ({\n label: item.label,\n detail: item.detail,\n })),\n }\n}\n\nfunction readPersisted(): AiDockState {\n if (typeof window === 'undefined') return { assistant: null, width: DEFAULT_WIDTH, collapsed: false }\n try {\n const raw = window.localStorage.getItem(STORAGE_KEY)\n if (!raw) return { assistant: null, width: DEFAULT_WIDTH, collapsed: false }\n const parsed = JSON.parse(raw) as PersistedAiDockState | null\n return {\n assistant: readPersistedAssistant(parsed?.assistant),\n width: readWidth(parsed?.width),\n collapsed: parsed?.collapsed === true,\n }\n } catch {\n return { assistant: null, width: DEFAULT_WIDTH, collapsed: false }\n }\n}\n\nfunction writePersisted(state: AiDockState) {\n if (typeof window === 'undefined') return\n try {\n window.localStorage.setItem(\n STORAGE_KEY,\n JSON.stringify({\n assistant: serializeAssistant(state.assistant),\n width: state.width,\n collapsed: state.collapsed,\n } satisfies PersistedAiDockState),\n )\n } catch {\n /* ignore */\n }\n}\n\nexport function AiDockProvider({ children }: { children: React.ReactNode }) {\n const [state, setState] = React.useState<AiDockState>(() => ({\n assistant: null,\n width: DEFAULT_WIDTH,\n collapsed: false,\n }))\n const [hydrated, setHydrated] = React.useState(false)\n\n React.useEffect(() => {\n const persisted = readPersisted()\n setState(persisted)\n setHydrated(true)\n }, [])\n\n React.useEffect(() => {\n if (!hydrated) return\n writePersisted(state)\n }, [hydrated, state])\n\n const dock = React.useCallback((assistant: AiDockedAssistant) => {\n // Always reset `collapsed` when (re)docking \u2014 the operator just clicked\n // \"dock to side\" and expects the panel to be visible at full width.\n setState((prev) => ({ ...prev, assistant, collapsed: false }))\n }, [])\n\n const undock = React.useCallback(() => {\n // Hard reset: drop the assistant AND the collapsed flag so the next\n // dock call starts from a clean slate. The wrapper observes `assistant`\n // turning null and clears its layout shift in the same render tick.\n setState((prev) => ({ ...prev, assistant: null, collapsed: false }))\n }, [])\n\n const isDocked = React.useCallback(\n (agentId: string) => state.assistant?.agent === agentId,\n [state.assistant?.agent],\n )\n\n const setWidth = React.useCallback((width: number) => {\n setState((prev) => ({ ...prev, width }))\n }, [])\n\n const setCollapsed = React.useCallback((collapsed: boolean) => {\n setState((prev) => ({ ...prev, collapsed }))\n }, [])\n\n const api = React.useMemo<AiDockApi>(\n () => ({ state, dock, undock, isDocked }),\n [state, dock, undock, isDocked],\n )\n\n // The right-side padding the page must reserve for the dock panel. Stays\n // null while nothing is docked so the wrapper renders no className/style\n // and the underlying layout (DataTable, sidebar grid) reclaims its full\n // width on undock.\n const reservedWidth = state.assistant\n ? state.collapsed\n ? COLLAPSED_WIDTH\n : state.width\n : null\n\n return (\n <AiDockContext.Provider value={api}>\n <div\n // The dock is desktop-only (`lg+`); reserve right-side padding only\n // at that breakpoint so mobile layout stays full-width.\n className={reservedWidth != null ? 'lg:pr-[var(--om-ai-dock-width)]' : undefined}\n style={\n reservedWidth != null\n ? ({ ['--om-ai-dock-width' as string]: `${reservedWidth}px` } as React.CSSProperties)\n : undefined\n }\n >\n {children}\n </div>\n {state.assistant ? (\n <AiDockPanel\n assistant={state.assistant}\n width={state.width}\n collapsed={state.collapsed}\n onCollapsedChange={setCollapsed}\n onWidthChange={setWidth}\n onClose={undock}\n />\n ) : null}\n </AiDockContext.Provider>\n )\n}\n\nexport function useAiDock(): AiDockApi {\n const ctx = React.useContext(AiDockContext)\n if (ctx) return ctx\n // Fallback no-op API \u2014 keeps consumers safe when the provider is absent\n // (e.g. unit tests rendering a widget in isolation).\n return {\n state: { assistant: null, width: DEFAULT_WIDTH, collapsed: false },\n dock: () => {},\n undock: () => {},\n isDocked: () => false,\n }\n}\n\ninterface AiDockPanelProps {\n assistant: AiDockedAssistant\n width: number\n collapsed: boolean\n onCollapsedChange: (collapsed: boolean) => void\n onWidthChange: (width: number) => void\n onClose: () => void\n}\n\nfunction AiDockPanel({\n assistant,\n width,\n collapsed,\n onCollapsedChange,\n onWidthChange,\n onClose,\n}: AiDockPanelProps) {\n const t = useT()\n const dragStateRef = React.useRef<{ startX: number; startWidth: number } | null>(null)\n\n const handlePointerDown = React.useCallback(\n (event: React.PointerEvent<HTMLDivElement>) => {\n event.preventDefault()\n const target = event.currentTarget\n target.setPointerCapture(event.pointerId)\n dragStateRef.current = { startX: event.clientX, startWidth: width }\n },\n [width],\n )\n\n const handlePointerMove = React.useCallback(\n (event: React.PointerEvent<HTMLDivElement>) => {\n if (!dragStateRef.current) return\n const delta = dragStateRef.current.startX - event.clientX\n const next = Math.min(\n MAX_WIDTH,\n Math.max(MIN_WIDTH, dragStateRef.current.startWidth + delta),\n )\n onWidthChange(next)\n },\n [onWidthChange],\n )\n\n const handlePointerUp = React.useCallback(\n (event: React.PointerEvent<HTMLDivElement>) => {\n const target = event.currentTarget\n try {\n target.releasePointerCapture(event.pointerId)\n } catch {\n /* ignore */\n }\n dragStateRef.current = null\n },\n [],\n )\n\n return (\n <aside\n data-ai-dock-panel=\"\"\n data-ai-dock-agent={assistant.agent}\n className={cn(\n // Dock is desktop-only \u2014 on small screens the AiChat dialog is the\n // primary surface (full-screen sheet) and a fixed side panel would\n // crowd the viewport.\n 'hidden lg:flex',\n 'fixed top-0 right-0 z-overlay h-svh min-w-0 flex-col overflow-hidden border-l bg-background shadow-lg',\n collapsed ? 'w-12' : '',\n )}\n style={collapsed ? undefined : { width }}\n aria-label={assistant.label}\n >\n {!collapsed ? (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n tabIndex={-1}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n onPointerCancel={handlePointerUp}\n className=\"absolute left-0 top-0 h-full w-1 cursor-col-resize bg-transparent hover:bg-primary/20\"\n data-ai-dock-resize-handle=\"\"\n />\n ) : null}\n <header className=\"flex items-center gap-2 px-3 py-2\">\n {collapsed ? (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ai_assistant.chat.dock.expand', 'Expand AI dock')}\n title={t('ai_assistant.chat.dock.expand', 'Expand AI dock')}\n onClick={() => onCollapsedChange(false)}\n >\n <Maximize2 className=\"size-4\" />\n </IconButton>\n ) : (\n <>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2 text-sm font-medium\" data-ai-dock-label=\"\">\n <span className=\"truncate\">{assistant.label}</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 </div>\n {assistant.description ? (\n <div className=\"truncate text-xs text-muted-foreground\">\n {assistant.description}\n </div>\n ) : null}\n </div>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ai_assistant.chat.dock.collapse', 'Collapse AI dock')}\n title={t('ai_assistant.chat.dock.collapse', 'Collapse AI dock')}\n onClick={() => onCollapsedChange(true)}\n >\n <Minimize2 className=\"size-4\" />\n </IconButton>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={t('ai_assistant.chat.dock.close', 'Close AI dock')}\n title={t('ai_assistant.chat.dock.close', 'Close AI dock')}\n onClick={onClose}\n data-ai-dock-close=\"\"\n >\n <X className=\"size-4\" />\n </IconButton>\n </>\n )}\n </header>\n {!collapsed ? (\n <DockedChatBody assistant={assistant} />\n ) : null}\n </aside>\n )\n}\n\nfunction DockedChatBody({ assistant }: { assistant: AiDockedAssistant }) {\n const sessions = useAiChatSessions()\n const session = sessions.getActiveSession(assistant.agent)\n\n // Lazily ensure an open session exists. Running `ensureSession` inside an\n // effect (not inline during render) keeps the provider's setState calls\n // outside of the render phase. The first frame may render without a\n // session \u2014 that's fine, we render the tab strip alone until the next\n // tick when the new session is committed and `getActiveSession` returns\n // it.\n React.useEffect(() => {\n if (!session) sessions.ensureSession(assistant.agent)\n }, [assistant.agent, session, sessions])\n\n const activeSessionId = session?.id ?? null\n const handleConversationNotFound = React.useCallback(() => {\n // The server returned 404 for this session's conversationId. The\n // conversation does not exist in the current tenant/org scope (most\n // commonly the user just switched scope and a stale id rode along in\n // localStorage). Close the tab so the dock surfaces the empty state\n // for the new scope rather than a perpetual blank chat.\n if (activeSessionId) sessions.closeSession(activeSessionId)\n }, [activeSessionId, sessions])\n\n return (\n <>\n <ChatPaneTabs agentId={assistant.agent} className=\"border-b\" />\n <div className=\"min-h-0 min-w-0 flex-1 overflow-hidden\" data-ai-dock-chat-container=\"\">\n {session ? (\n <React.Suspense fallback={null}>\n <LazyAiChat\n // `key` forces a fresh `<AiChat>` mount whenever the active\n // session changes \u2014 without it the AI SDK's internal status\n // would carry across tabs and surface the previous tab's\n // streaming indicator on a brand-new conversation.\n key={session.id}\n agent={assistant.agent}\n conversationId={session.conversationId}\n pageContext={assistant.pageContext}\n className=\"h-full\"\n defaultCompactFooter\n placeholder={assistant.placeholder}\n suggestions={assistant.suggestions}\n contextItems={assistant.contextItems}\n welcomeTitle={assistant.welcomeTitle}\n welcomeDescription={assistant.welcomeDescription}\n headerActions={<ConversationShareButton conversationId={session.conversationId} />}\n onConversationNotFound={handleConversationNotFound}\n />\n </React.Suspense>\n ) : null}\n </div>\n </>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -26,6 +26,7 @@ function ConversationShareDialog({ open, onOpenChange, conversationId }) {
26
26
  const t = useT();
27
27
  const currentUserId = useCurrentUserId();
28
28
  const [participants, setParticipants] = React.useState([]);
29
+ const [ownerUserId, setOwnerUserId] = React.useState(null);
29
30
  const [users, setUsers] = React.useState([]);
30
31
  const [canListUsers, setCanListUsers] = React.useState(true);
31
32
  const [loading, setLoading] = React.useState(false);
@@ -43,6 +44,7 @@ function ConversationShareDialog({ open, onOpenChange, conversationId }) {
43
44
  const res = await apiCall(baseUrl);
44
45
  if (!res.ok || !res.result) throw new Error("fetch failed");
45
46
  setParticipants(res.result.participants.filter((p) => p.role !== "owner"));
47
+ setOwnerUserId(res.result.ownerUserId ?? null);
46
48
  } catch {
47
49
  setError(t("common.error", "Something went wrong."));
48
50
  } finally {
@@ -78,8 +80,10 @@ function ConversationShareDialog({ open, onOpenChange, conversationId }) {
78
80
  [participants]
79
81
  );
80
82
  const availableUsers = React.useMemo(
81
- () => users.filter((u) => !participantIds.has(u.id) && u.id !== currentUserId),
82
- [users, participantIds, currentUserId]
83
+ () => users.filter(
84
+ (u) => !participantIds.has(u.id) && u.id !== currentUserId && u.id !== ownerUserId
85
+ ),
86
+ [users, participantIds, currentUserId, ownerUserId]
83
87
  );
84
88
  const activeUserId = canListUsers ? selectedUserId : textUserId.trim();
85
89
  const handleAdd = React.useCallback(async () => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ai/ConversationShareDialog.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { UserPlus, X, Loader2 } from 'lucide-react'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../primitives/dialog'\nimport { Button } from '../primitives/button'\nimport { Input } from '../primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../primitives/select'\nimport { apiCall } from '../backend/utils/apiCall'\nimport { useCurrentUserId } from '../backend/utils/useCurrentUserId'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ninterface Participant {\n userId: string\n role: string\n lastReadAt: string | null\n addedAt: string\n}\n\ninterface UserOption {\n id: string\n email: string\n name: string | null\n}\n\ninterface Props {\n open: boolean\n onOpenChange: (next: boolean) => void\n conversationId: string\n}\n\nexport function ConversationShareDialog({ open, onOpenChange, conversationId }: Props) {\n const t = useT()\n const currentUserId = useCurrentUserId()\n const [participants, setParticipants] = React.useState<Participant[]>([])\n const [users, setUsers] = React.useState<UserOption[]>([])\n const [canListUsers, setCanListUsers] = React.useState(true)\n const [loading, setLoading] = React.useState(false)\n const [loadingUsers, setLoadingUsers] = React.useState(false)\n const [selectedUserId, setSelectedUserId] = React.useState('')\n const [textUserId, setTextUserId] = React.useState('')\n const [adding, setAdding] = React.useState(false)\n const [removingId, setRemovingId] = React.useState<string | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n\n const baseUrl = `/api/ai_assistant/ai/conversations/${conversationId}/participants`\n\n const fetchParticipants = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const res = await apiCall<{ participants: Participant[] }>(baseUrl)\n if (!res.ok || !res.result) throw new Error('fetch failed')\n setParticipants(res.result.participants.filter((p) => p.role !== 'owner'))\n } catch {\n setError(t('common.error', 'Something went wrong.'))\n } finally {\n setLoading(false)\n }\n }, [conversationId, t])\n\n const fetchUsers = React.useCallback(async () => {\n setLoadingUsers(true)\n try {\n const res = await apiCall<{ items: UserOption[] }>('/api/auth/users?limit=200')\n if (res.ok && res.result) {\n setUsers(res.result.items)\n setCanListUsers(true)\n } else {\n setCanListUsers(false)\n }\n } catch {\n setCanListUsers(false)\n } finally {\n setLoadingUsers(false)\n }\n }, [])\n\n React.useEffect(() => {\n if (open) {\n setSelectedUserId('')\n setTextUserId('')\n setError(null)\n void Promise.all([fetchParticipants(), fetchUsers()])\n }\n }, [open, fetchParticipants, fetchUsers])\n\n const participantIds = React.useMemo(\n () => new Set(participants.map((p) => p.userId)),\n [participants],\n )\n\n const availableUsers = React.useMemo(\n () => users.filter((u) => !participantIds.has(u.id) && u.id !== currentUserId),\n [users, participantIds, currentUserId],\n )\n\n const activeUserId = canListUsers ? selectedUserId : textUserId.trim()\n\n const handleAdd = React.useCallback(async () => {\n if (!activeUserId) return\n setAdding(true)\n setError(null)\n try {\n const res = await apiCall<{ participant: Participant }>(baseUrl, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ userId: activeUserId, role: 'viewer' }),\n })\n if (!res.ok || !res.result) throw new Error('fetch failed')\n setParticipants((prev) => {\n const next = res.result!.participant\n const exists = prev.find((p) => p.userId === next.userId)\n if (exists) return prev.map((p) => (p.userId === next.userId ? next : p))\n return [...prev, next]\n })\n setSelectedUserId('')\n setTextUserId('')\n } catch {\n setError(t('common.error', 'Something went wrong.'))\n } finally {\n setAdding(false)\n }\n }, [activeUserId, baseUrl, t])\n\n React.useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault()\n if (activeUserId && !adding) void handleAdd()\n }\n }\n if (open) document.addEventListener('keydown', handler)\n return () => document.removeEventListener('keydown', handler)\n }, [open, activeUserId, adding, handleAdd])\n\n const handleRemove = async (userId: string) => {\n setRemovingId(userId)\n setError(null)\n try {\n await apiCall(`${baseUrl}/${userId}`, { method: 'DELETE', headers: { 'content-type': 'application/json' } })\n setParticipants((prev) => prev.filter((p) => p.userId !== userId))\n } catch {\n setError(t('common.error', 'Something went wrong.'))\n } finally {\n setRemovingId(null)\n }\n }\n\n const formatUserLabel = (u: UserOption) => (u.name ? `${u.name} \u2014 ${u.email}` : u.email)\n\n const getUserLabel = (userId: string) => {\n const u = users.find((u) => u.id === userId)\n return u ? formatUserLabel(u) : userId\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"sm:max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {t('ai_assistant.share.dialogTitle', 'Share Conversation')}\n </DialogTitle>\n <DialogDescription>\n {t(\n 'ai_assistant.share.dialogDescription',\n 'Share this conversation with other users. They will get read-only access.',\n )}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4\">\n <div className=\"flex gap-2\">\n {canListUsers === false ? (\n <Input\n placeholder={t('ai_assistant.share.participantPlaceholder', 'User ID...')}\n value={textUserId}\n onChange={(e) => setTextUserId(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') { e.preventDefault(); void handleAdd() }\n }}\n className=\"flex-1\"\n />\n ) : (\n <Select\n value={selectedUserId}\n onValueChange={setSelectedUserId}\n disabled={loadingUsers || availableUsers.length === 0}\n >\n <SelectTrigger className=\"flex-1\">\n <SelectValue\n placeholder={\n loadingUsers\n ? t('common.loading', 'Loading...')\n : availableUsers.length === 0\n ? t('ai_assistant.share.allUsersAdded', 'All users already added')\n : t('ai_assistant.share.selectUser', 'Select a user...')\n }\n />\n </SelectTrigger>\n <SelectContent>\n {availableUsers.map((u) => (\n <SelectItem key={u.id} value={u.id}>\n {formatUserLabel(u)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n <Button\n type=\"button\"\n onClick={() => void handleAdd()}\n disabled={!activeUserId || adding}\n size=\"default\"\n >\n {adding ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <UserPlus className=\"size-4\" />\n )}\n {t('ai_assistant.share.addParticipant', 'Add')}\n </Button>\n </div>\n\n {error ? (\n <p className=\"text-sm text-status-danger-text\">{error}</p>\n ) : null}\n\n <div className=\"space-y-2 min-h-[60px]\">\n {loading ? (\n <div className=\"flex items-center justify-center py-4\">\n <Loader2 className=\"size-5 animate-spin text-muted-foreground\" />\n </div>\n ) : participants.length === 0 ? (\n <p className=\"text-sm text-muted-foreground py-2\">\n {t(\n 'ai_assistant.share.noParticipants',\n 'No participants yet. Add someone to share this conversation.',\n )}\n </p>\n ) : (\n participants.map((p) => (\n <div\n key={p.userId}\n className=\"flex items-center justify-between rounded-lg border border-border px-3 py-2\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm text-foreground\">{getUserLabel(p.userId)}</p>\n <p className=\"text-xs text-muted-foreground capitalize\">{p.role}</p>\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => void handleRemove(p.userId)}\n disabled={removingId === p.userId}\n aria-label={t('ai_assistant.share.removeParticipant', 'Remove')}\n >\n {removingId === p.userId ? (\n <Loader2 className=\"size-3 animate-spin\" />\n ) : (\n <X className=\"size-4\" />\n )}\n </Button>\n </div>\n ))\n )}\n </div>\n </div>\n\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => onOpenChange(false)}>\n {t('common.close', 'Close')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\nexport default ConversationShareDialog\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { UserPlus, X, Loader2 } from 'lucide-react'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../primitives/dialog'\nimport { Button } from '../primitives/button'\nimport { Input } from '../primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../primitives/select'\nimport { apiCall } from '../backend/utils/apiCall'\nimport { useCurrentUserId } from '../backend/utils/useCurrentUserId'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ninterface Participant {\n userId: string\n role: string\n lastReadAt: string | null\n addedAt: string\n}\n\ninterface UserOption {\n id: string\n email: string\n name: string | null\n}\n\ninterface Props {\n open: boolean\n onOpenChange: (next: boolean) => void\n conversationId: string\n}\n\nexport function ConversationShareDialog({ open, onOpenChange, conversationId }: Props) {\n const t = useT()\n const currentUserId = useCurrentUserId()\n const [participants, setParticipants] = React.useState<Participant[]>([])\n const [ownerUserId, setOwnerUserId] = React.useState<string | null>(null)\n const [users, setUsers] = React.useState<UserOption[]>([])\n const [canListUsers, setCanListUsers] = React.useState(true)\n const [loading, setLoading] = React.useState(false)\n const [loadingUsers, setLoadingUsers] = React.useState(false)\n const [selectedUserId, setSelectedUserId] = React.useState('')\n const [textUserId, setTextUserId] = React.useState('')\n const [adding, setAdding] = React.useState(false)\n const [removingId, setRemovingId] = React.useState<string | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n\n const baseUrl = `/api/ai_assistant/ai/conversations/${conversationId}/participants`\n\n const fetchParticipants = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const res = await apiCall<{ participants: Participant[]; ownerUserId?: string | null }>(baseUrl)\n if (!res.ok || !res.result) throw new Error('fetch failed')\n setParticipants(res.result.participants.filter((p) => p.role !== 'owner'))\n setOwnerUserId(res.result.ownerUserId ?? null)\n } catch {\n setError(t('common.error', 'Something went wrong.'))\n } finally {\n setLoading(false)\n }\n }, [conversationId, t])\n\n const fetchUsers = React.useCallback(async () => {\n setLoadingUsers(true)\n try {\n const res = await apiCall<{ items: UserOption[] }>('/api/auth/users?limit=200')\n if (res.ok && res.result) {\n setUsers(res.result.items)\n setCanListUsers(true)\n } else {\n setCanListUsers(false)\n }\n } catch {\n setCanListUsers(false)\n } finally {\n setLoadingUsers(false)\n }\n }, [])\n\n React.useEffect(() => {\n if (open) {\n setSelectedUserId('')\n setTextUserId('')\n setError(null)\n void Promise.all([fetchParticipants(), fetchUsers()])\n }\n }, [open, fetchParticipants, fetchUsers])\n\n const participantIds = React.useMemo(\n () => new Set(participants.map((p) => p.userId)),\n [participants],\n )\n\n const availableUsers = React.useMemo(\n () =>\n users.filter(\n (u) => !participantIds.has(u.id) && u.id !== currentUserId && u.id !== ownerUserId,\n ),\n [users, participantIds, currentUserId, ownerUserId],\n )\n\n const activeUserId = canListUsers ? selectedUserId : textUserId.trim()\n\n const handleAdd = React.useCallback(async () => {\n if (!activeUserId) return\n setAdding(true)\n setError(null)\n try {\n const res = await apiCall<{ participant: Participant }>(baseUrl, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ userId: activeUserId, role: 'viewer' }),\n })\n if (!res.ok || !res.result) throw new Error('fetch failed')\n setParticipants((prev) => {\n const next = res.result!.participant\n const exists = prev.find((p) => p.userId === next.userId)\n if (exists) return prev.map((p) => (p.userId === next.userId ? next : p))\n return [...prev, next]\n })\n setSelectedUserId('')\n setTextUserId('')\n } catch {\n setError(t('common.error', 'Something went wrong.'))\n } finally {\n setAdding(false)\n }\n }, [activeUserId, baseUrl, t])\n\n React.useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault()\n if (activeUserId && !adding) void handleAdd()\n }\n }\n if (open) document.addEventListener('keydown', handler)\n return () => document.removeEventListener('keydown', handler)\n }, [open, activeUserId, adding, handleAdd])\n\n const handleRemove = async (userId: string) => {\n setRemovingId(userId)\n setError(null)\n try {\n await apiCall(`${baseUrl}/${userId}`, { method: 'DELETE', headers: { 'content-type': 'application/json' } })\n setParticipants((prev) => prev.filter((p) => p.userId !== userId))\n } catch {\n setError(t('common.error', 'Something went wrong.'))\n } finally {\n setRemovingId(null)\n }\n }\n\n const formatUserLabel = (u: UserOption) => (u.name ? `${u.name} \u2014 ${u.email}` : u.email)\n\n const getUserLabel = (userId: string) => {\n const u = users.find((u) => u.id === userId)\n return u ? formatUserLabel(u) : userId\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"sm:max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {t('ai_assistant.share.dialogTitle', 'Share Conversation')}\n </DialogTitle>\n <DialogDescription>\n {t(\n 'ai_assistant.share.dialogDescription',\n 'Share this conversation with other users. They will get read-only access.',\n )}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4\">\n <div className=\"flex gap-2\">\n {canListUsers === false ? (\n <Input\n placeholder={t('ai_assistant.share.participantPlaceholder', 'User ID...')}\n value={textUserId}\n onChange={(e) => setTextUserId(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') { e.preventDefault(); void handleAdd() }\n }}\n className=\"flex-1\"\n />\n ) : (\n <Select\n value={selectedUserId}\n onValueChange={setSelectedUserId}\n disabled={loadingUsers || availableUsers.length === 0}\n >\n <SelectTrigger className=\"flex-1\">\n <SelectValue\n placeholder={\n loadingUsers\n ? t('common.loading', 'Loading...')\n : availableUsers.length === 0\n ? t('ai_assistant.share.allUsersAdded', 'All users already added')\n : t('ai_assistant.share.selectUser', 'Select a user...')\n }\n />\n </SelectTrigger>\n <SelectContent>\n {availableUsers.map((u) => (\n <SelectItem key={u.id} value={u.id}>\n {formatUserLabel(u)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n <Button\n type=\"button\"\n onClick={() => void handleAdd()}\n disabled={!activeUserId || adding}\n size=\"default\"\n >\n {adding ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <UserPlus className=\"size-4\" />\n )}\n {t('ai_assistant.share.addParticipant', 'Add')}\n </Button>\n </div>\n\n {error ? (\n <p className=\"text-sm text-status-danger-text\">{error}</p>\n ) : null}\n\n <div className=\"space-y-2 min-h-[60px]\">\n {loading ? (\n <div className=\"flex items-center justify-center py-4\">\n <Loader2 className=\"size-5 animate-spin text-muted-foreground\" />\n </div>\n ) : participants.length === 0 ? (\n <p className=\"text-sm text-muted-foreground py-2\">\n {t(\n 'ai_assistant.share.noParticipants',\n 'No participants yet. Add someone to share this conversation.',\n )}\n </p>\n ) : (\n participants.map((p) => (\n <div\n key={p.userId}\n className=\"flex items-center justify-between rounded-lg border border-border px-3 py-2\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm text-foreground\">{getUserLabel(p.userId)}</p>\n <p className=\"text-xs text-muted-foreground capitalize\">{p.role}</p>\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => void handleRemove(p.userId)}\n disabled={removingId === p.userId}\n aria-label={t('ai_assistant.share.removeParticipant', 'Remove')}\n >\n {removingId === p.userId ? (\n <Loader2 className=\"size-3 animate-spin\" />\n ) : (\n <X className=\"size-4\" />\n )}\n </Button>\n </div>\n ))\n )}\n </div>\n </div>\n\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => onOpenChange(false)}>\n {t('common.close', 'Close')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\nexport default ConversationShareDialog\n"],
5
+ "mappings": "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",
6
6
  "names": ["u"]
7
7
  }
@@ -74,7 +74,8 @@ async function loadAiServerTranscript(conversationId, options = {}) {
74
74
  const result = await requestJson(
75
75
  `${CONVERSATIONS_ENDPOINT}/${encodeURIComponent(conversationId)}?${params.toString()}`
76
76
  );
77
- return result.ok ? result.data : null;
77
+ if (result.ok) return { ok: true, data: result.data };
78
+ return { ok: false, notFound: result.status === 404 };
78
79
  }
79
80
  async function importAiLocalConversation(input) {
80
81
  const result = await requestJson(`${CONVERSATIONS_ENDPOINT}/import`, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/ai/conversation-store.ts"],
4
- "sourcesContent": ["\"use client\"\n\nimport type { AiChatMessage, AiChatMessageUiPart } from './useAiChat'\n\nconst CONVERSATIONS_ENDPOINT = '/api/ai_assistant/ai/conversations'\n\nexport interface AiServerConversation {\n conversationId: string\n agentId: string\n title: string | null\n status: 'open' | 'closed'\n visibility: 'private' | 'shared' | 'organization'\n pageContext: Record<string, unknown> | null\n createdAt: string\n updatedAt: string\n lastMessageAt: string | null\n importedFromLocalAt: string | null\n /** `true` when the authenticated caller created the conversation; `false` for shared participants; `null` when unknown. */\n isOwner: boolean | null\n}\n\nexport interface AiServerMessage {\n id: string\n clientMessageId: string | null\n role: 'user' | 'assistant' | 'system'\n content: string\n uiParts: unknown[]\n attachmentIds: string[]\n files: Array<Record<string, unknown>>\n model: string | null\n metadata: Record<string, unknown> | null\n createdAt: string\n senderUserId: string | null\n}\n\nexport interface AiServerConversationListResponse {\n items: AiServerConversation[]\n nextCursor: string | null\n}\n\nexport interface AiServerTranscriptResponse {\n conversation: AiServerConversation\n messages: AiServerMessage[]\n nextCursor: string | null\n}\n\nexport interface AiServerImportResponse {\n conversation: AiServerConversation\n importedMessageCount: number\n skippedMessageCount: number\n}\n\nfunction readString(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction buildAttachmentImagePreviewUrl(attachmentId: string): string {\n const params = new URLSearchParams({\n width: '320',\n height: '320',\n cropType: 'contain',\n })\n return `/api/attachments/image/${encodeURIComponent(attachmentId)}?${params.toString()}`\n}\n\nfunction getFetch(): typeof fetch | null {\n const fetchImpl = (globalThis as typeof globalThis & { fetch?: typeof fetch }).fetch\n return typeof fetchImpl === 'function' ? fetchImpl.bind(globalThis) : null\n}\n\nasync function requestJson<T>(\n input: string,\n init?: RequestInit,\n): Promise<{ ok: true; status: number; data: T } | { ok: false; status: number; data: null }> {\n const fetchImpl = getFetch()\n if (!fetchImpl) return { ok: false, status: 0, data: null }\n try {\n const response = await fetchImpl(input, {\n credentials: 'include',\n ...init,\n headers: {\n Accept: 'application/json',\n 'x-om-forbidden-redirect': '0',\n 'x-om-unauthorized-redirect': '0',\n ...(init?.body ? { 'Content-Type': 'application/json' } : {}),\n ...(init?.headers ?? {}),\n },\n })\n if (!response.ok) return { ok: false, status: response.status, data: null }\n return { ok: true, status: response.status, data: (await response.json()) as T }\n } catch {\n return { ok: false, status: 0, data: null }\n }\n}\n\nexport async function listAiServerConversations(options: {\n agentId?: string | null\n status?: 'open' | 'closed' | null\n limit?: number\n} = {}): Promise<AiServerConversation[] | null> {\n const params = new URLSearchParams()\n if (options.agentId) params.set('agent', options.agentId)\n if (options.status) params.set('status', options.status)\n params.set('limit', String(options.limit ?? 100))\n const result = await requestJson<AiServerConversationListResponse>(\n `${CONVERSATIONS_ENDPOINT}?${params.toString()}`,\n )\n return result.ok ? result.data.items : null\n}\n\nexport async function createAiServerConversation(input: {\n agentId: string\n conversationId: string\n title?: string | null\n pageContext?: Record<string, unknown> | null\n}): Promise<AiServerConversation | null> {\n const result = await requestJson<AiServerConversation>(CONVERSATIONS_ENDPOINT, {\n method: 'POST',\n body: JSON.stringify({\n agentId: input.agentId,\n conversationId: input.conversationId,\n title: input.title ?? undefined,\n pageContext: input.pageContext ?? undefined,\n }),\n })\n return result.ok ? result.data : null\n}\n\nexport async function updateAiServerConversation(\n conversationId: string,\n patch: {\n title?: string | null\n status?: 'open' | 'closed'\n pageContext?: Record<string, unknown> | null\n },\n): Promise<AiServerConversation | null> {\n const result = await requestJson<AiServerConversation>(\n `${CONVERSATIONS_ENDPOINT}/${encodeURIComponent(conversationId)}`,\n {\n method: 'PATCH',\n body: JSON.stringify(patch),\n },\n )\n return result.ok ? result.data : null\n}\n\nexport async function loadAiServerTranscript(\n conversationId: string,\n options: { limit?: number } = {},\n): Promise<AiServerTranscriptResponse | null> {\n const params = new URLSearchParams()\n params.set('limit', String(options.limit ?? 100))\n const result = await requestJson<AiServerTranscriptResponse>(\n `${CONVERSATIONS_ENDPOINT}/${encodeURIComponent(conversationId)}?${params.toString()}`,\n )\n return result.ok ? result.data : null\n}\n\nexport async function importAiLocalConversation(input: {\n agentId: string\n conversationId: string\n title?: string | null\n status?: 'open' | 'closed'\n pageContext?: Record<string, unknown> | null\n messages: AiChatMessage[]\n}): Promise<AiServerImportResponse | null> {\n const result = await requestJson<AiServerImportResponse>(`${CONVERSATIONS_ENDPOINT}/import`, {\n method: 'POST',\n body: JSON.stringify({\n conversation: {\n conversationId: input.conversationId,\n agentId: input.agentId,\n title: input.title ?? undefined,\n status: input.status ?? 'open',\n pageContext: input.pageContext ?? undefined,\n },\n messages: input.messages.slice(-100).map((message) => ({\n clientMessageId: message.id,\n role: message.role,\n content: message.content,\n uiParts: message.uiParts ?? [],\n files: (message.files ?? []).map((file) => ({\n id: file.id,\n name: file.name,\n mimeType: file.type,\n })),\n })),\n }),\n })\n return result.ok ? result.data : null\n}\n\nexport function serverMessageToChatMessage(message: AiServerMessage): AiChatMessage | null {\n if (message.role !== 'user' && message.role !== 'assistant') return null\n const uiParts: AiChatMessageUiPart[] = []\n message.uiParts.forEach((part, index) => {\n if (!part || typeof part !== 'object') return\n const value = part as Record<string, unknown>\n const componentId =\n typeof value.componentId === 'string'\n ? value.componentId\n : typeof value.type === 'string'\n ? value.type\n : null\n if (!componentId) return\n uiParts.push({\n componentId,\n payload: value.payload ?? value.props,\n pendingActionId:\n typeof value.pendingActionId === 'string' ? value.pendingActionId : undefined,\n key:\n typeof value.key === 'string'\n ? value.key\n : `${message.id}:${index}:${componentId}`,\n })\n })\n return {\n id: message.clientMessageId ?? message.id,\n role: message.role,\n content: message.content,\n files: message.files\n .map((file, index) => {\n const name = readString(file.name)\n if (!name) return null\n const type =\n typeof file.mimeType === 'string'\n ? file.mimeType\n : typeof file.type === 'string'\n ? file.type\n : 'application/octet-stream'\n const id = readString(file.id) ?? readString(message.attachmentIds[index])\n const rawPreviewUrl =\n readString(file.previewUrl) ??\n readString(file.thumbnailUrl) ??\n readString(file.url)\n const previewUrl = type.startsWith('image/')\n ? rawPreviewUrl ?? (id ? buildAttachmentImagePreviewUrl(id) : undefined)\n : undefined\n return {\n ...(id ? { id } : {}),\n name,\n type,\n ...(previewUrl ? { previewUrl } : {}),\n }\n })\n .filter((file): file is NonNullable<AiChatMessage['files']>[number] => file !== null),\n uiParts,\n senderUserId: message.senderUserId ?? null,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport type { AiChatMessage, AiChatMessageUiPart } from './useAiChat'\n\nconst CONVERSATIONS_ENDPOINT = '/api/ai_assistant/ai/conversations'\n\nexport interface AiServerConversation {\n conversationId: string\n agentId: string\n title: string | null\n status: 'open' | 'closed'\n visibility: 'private' | 'shared' | 'organization'\n pageContext: Record<string, unknown> | null\n createdAt: string\n updatedAt: string\n lastMessageAt: string | null\n importedFromLocalAt: string | null\n /** `true` when the authenticated caller created the conversation; `false` for shared participants; `null` when unknown. */\n isOwner: boolean | null\n}\n\nexport interface AiServerMessage {\n id: string\n clientMessageId: string | null\n role: 'user' | 'assistant' | 'system'\n content: string\n uiParts: unknown[]\n attachmentIds: string[]\n files: Array<Record<string, unknown>>\n model: string | null\n metadata: Record<string, unknown> | null\n createdAt: string\n senderUserId: string | null\n}\n\nexport interface AiServerConversationListResponse {\n items: AiServerConversation[]\n nextCursor: string | null\n}\n\nexport interface AiServerTranscriptResponse {\n conversation: AiServerConversation\n messages: AiServerMessage[]\n nextCursor: string | null\n}\n\nexport interface AiServerImportResponse {\n conversation: AiServerConversation\n importedMessageCount: number\n skippedMessageCount: number\n}\n\n/**\n * Discriminated result of {@link loadAiServerTranscript}. Callers must\n * distinguish 404 (the conversation does not exist for the current scope \u2014\n * e.g. it belonged to a previous tenant/org) from other failures (network\n * down, server error) because the recovery strategies are opposite:\n * - `notFound: true` \u2192 drop the local cache, optionally surface a\n * not-found callback. NEVER import local messages\n * onto the new scope.\n * - `notFound: false` \u2192 preserve local state and retry later.\n */\nexport type LoadTranscriptResult =\n | { ok: true; data: AiServerTranscriptResponse }\n | { ok: false; notFound: true }\n | { ok: false; notFound: false }\n\nfunction readString(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction buildAttachmentImagePreviewUrl(attachmentId: string): string {\n const params = new URLSearchParams({\n width: '320',\n height: '320',\n cropType: 'contain',\n })\n return `/api/attachments/image/${encodeURIComponent(attachmentId)}?${params.toString()}`\n}\n\nfunction getFetch(): typeof fetch | null {\n const fetchImpl = (globalThis as typeof globalThis & { fetch?: typeof fetch }).fetch\n return typeof fetchImpl === 'function' ? fetchImpl.bind(globalThis) : null\n}\n\nasync function requestJson<T>(\n input: string,\n init?: RequestInit,\n): Promise<{ ok: true; status: number; data: T } | { ok: false; status: number; data: null }> {\n const fetchImpl = getFetch()\n if (!fetchImpl) return { ok: false, status: 0, data: null }\n try {\n const response = await fetchImpl(input, {\n credentials: 'include',\n ...init,\n headers: {\n Accept: 'application/json',\n 'x-om-forbidden-redirect': '0',\n 'x-om-unauthorized-redirect': '0',\n ...(init?.body ? { 'Content-Type': 'application/json' } : {}),\n ...(init?.headers ?? {}),\n },\n })\n if (!response.ok) return { ok: false, status: response.status, data: null }\n return { ok: true, status: response.status, data: (await response.json()) as T }\n } catch {\n return { ok: false, status: 0, data: null }\n }\n}\n\nexport async function listAiServerConversations(options: {\n agentId?: string | null\n status?: 'open' | 'closed' | null\n limit?: number\n} = {}): Promise<AiServerConversation[] | null> {\n const params = new URLSearchParams()\n if (options.agentId) params.set('agent', options.agentId)\n if (options.status) params.set('status', options.status)\n params.set('limit', String(options.limit ?? 100))\n const result = await requestJson<AiServerConversationListResponse>(\n `${CONVERSATIONS_ENDPOINT}?${params.toString()}`,\n )\n return result.ok ? result.data.items : null\n}\n\nexport async function createAiServerConversation(input: {\n agentId: string\n conversationId: string\n title?: string | null\n pageContext?: Record<string, unknown> | null\n}): Promise<AiServerConversation | null> {\n const result = await requestJson<AiServerConversation>(CONVERSATIONS_ENDPOINT, {\n method: 'POST',\n body: JSON.stringify({\n agentId: input.agentId,\n conversationId: input.conversationId,\n title: input.title ?? undefined,\n pageContext: input.pageContext ?? undefined,\n }),\n })\n return result.ok ? result.data : null\n}\n\nexport async function updateAiServerConversation(\n conversationId: string,\n patch: {\n title?: string | null\n status?: 'open' | 'closed'\n pageContext?: Record<string, unknown> | null\n },\n): Promise<AiServerConversation | null> {\n const result = await requestJson<AiServerConversation>(\n `${CONVERSATIONS_ENDPOINT}/${encodeURIComponent(conversationId)}`,\n {\n method: 'PATCH',\n body: JSON.stringify(patch),\n },\n )\n return result.ok ? result.data : null\n}\n\nexport async function loadAiServerTranscript(\n conversationId: string,\n options: { limit?: number } = {},\n): Promise<LoadTranscriptResult> {\n const params = new URLSearchParams()\n params.set('limit', String(options.limit ?? 100))\n const result = await requestJson<AiServerTranscriptResponse>(\n `${CONVERSATIONS_ENDPOINT}/${encodeURIComponent(conversationId)}?${params.toString()}`,\n )\n if (result.ok) return { ok: true, data: result.data }\n return { ok: false, notFound: result.status === 404 }\n}\n\nexport async function importAiLocalConversation(input: {\n agentId: string\n conversationId: string\n title?: string | null\n status?: 'open' | 'closed'\n pageContext?: Record<string, unknown> | null\n messages: AiChatMessage[]\n}): Promise<AiServerImportResponse | null> {\n const result = await requestJson<AiServerImportResponse>(`${CONVERSATIONS_ENDPOINT}/import`, {\n method: 'POST',\n body: JSON.stringify({\n conversation: {\n conversationId: input.conversationId,\n agentId: input.agentId,\n title: input.title ?? undefined,\n status: input.status ?? 'open',\n pageContext: input.pageContext ?? undefined,\n },\n messages: input.messages.slice(-100).map((message) => ({\n clientMessageId: message.id,\n role: message.role,\n content: message.content,\n uiParts: message.uiParts ?? [],\n files: (message.files ?? []).map((file) => ({\n id: file.id,\n name: file.name,\n mimeType: file.type,\n })),\n })),\n }),\n })\n return result.ok ? result.data : null\n}\n\nexport function serverMessageToChatMessage(message: AiServerMessage): AiChatMessage | null {\n if (message.role !== 'user' && message.role !== 'assistant') return null\n const uiParts: AiChatMessageUiPart[] = []\n message.uiParts.forEach((part, index) => {\n if (!part || typeof part !== 'object') return\n const value = part as Record<string, unknown>\n const componentId =\n typeof value.componentId === 'string'\n ? value.componentId\n : typeof value.type === 'string'\n ? value.type\n : null\n if (!componentId) return\n uiParts.push({\n componentId,\n payload: value.payload ?? value.props,\n pendingActionId:\n typeof value.pendingActionId === 'string' ? value.pendingActionId : undefined,\n key:\n typeof value.key === 'string'\n ? value.key\n : `${message.id}:${index}:${componentId}`,\n })\n })\n return {\n id: message.clientMessageId ?? message.id,\n role: message.role,\n content: message.content,\n files: message.files\n .map((file, index) => {\n const name = readString(file.name)\n if (!name) return null\n const type =\n typeof file.mimeType === 'string'\n ? file.mimeType\n : typeof file.type === 'string'\n ? file.type\n : 'application/octet-stream'\n const id = readString(file.id) ?? readString(message.attachmentIds[index])\n const rawPreviewUrl =\n readString(file.previewUrl) ??\n readString(file.thumbnailUrl) ??\n readString(file.url)\n const previewUrl = type.startsWith('image/')\n ? rawPreviewUrl ?? (id ? buildAttachmentImagePreviewUrl(id) : undefined)\n : undefined\n return {\n ...(id ? { id } : {}),\n name,\n type,\n ...(previewUrl ? { previewUrl } : {}),\n }\n })\n .filter((file): file is NonNullable<AiChatMessage['files']>[number] => file !== null),\n uiParts,\n senderUserId: message.senderUserId ?? null,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }