@open-mercato/ui 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +2 -1
  3. package/__integration__/TC-AI-UI-003-aichat-registry.spec.tsx +204 -0
  4. package/dist/ai/AiAssistantLauncher.js +596 -0
  5. package/dist/ai/AiAssistantLauncher.js.map +7 -0
  6. package/dist/ai/AiChat.js +1092 -0
  7. package/dist/ai/AiChat.js.map +7 -0
  8. package/dist/ai/AiChatSessions.js +297 -0
  9. package/dist/ai/AiChatSessions.js.map +7 -0
  10. package/dist/ai/AiDock.js +347 -0
  11. package/dist/ai/AiDock.js.map +7 -0
  12. package/dist/ai/AiMessageContent.js +369 -0
  13. package/dist/ai/AiMessageContent.js.map +7 -0
  14. package/dist/ai/ChatPaneTabs.js +251 -0
  15. package/dist/ai/ChatPaneTabs.js.map +7 -0
  16. package/dist/ai/index.js +115 -0
  17. package/dist/ai/index.js.map +7 -0
  18. package/dist/ai/parts/ConfirmationCard.js +211 -0
  19. package/dist/ai/parts/ConfirmationCard.js.map +7 -0
  20. package/dist/ai/parts/FieldDiffCard.js +119 -0
  21. package/dist/ai/parts/FieldDiffCard.js.map +7 -0
  22. package/dist/ai/parts/MutationPreviewCard.js +224 -0
  23. package/dist/ai/parts/MutationPreviewCard.js.map +7 -0
  24. package/dist/ai/parts/MutationResultCard.js +240 -0
  25. package/dist/ai/parts/MutationResultCard.js.map +7 -0
  26. package/dist/ai/parts/approval-cards-map.js +15 -0
  27. package/dist/ai/parts/approval-cards-map.js.map +7 -0
  28. package/dist/ai/parts/index.js +24 -0
  29. package/dist/ai/parts/index.js.map +7 -0
  30. package/dist/ai/parts/pending-action-api.js +60 -0
  31. package/dist/ai/parts/pending-action-api.js.map +7 -0
  32. package/dist/ai/parts/types.js +1 -0
  33. package/dist/ai/parts/types.js.map +7 -0
  34. package/dist/ai/parts/useAiPendingActionPolling.js +126 -0
  35. package/dist/ai/parts/useAiPendingActionPolling.js.map +7 -0
  36. package/dist/ai/records/ActivityCard.js +83 -0
  37. package/dist/ai/records/ActivityCard.js.map +7 -0
  38. package/dist/ai/records/CompanyCard.js +81 -0
  39. package/dist/ai/records/CompanyCard.js.map +7 -0
  40. package/dist/ai/records/DealCard.js +76 -0
  41. package/dist/ai/records/DealCard.js.map +7 -0
  42. package/dist/ai/records/PersonCard.js +68 -0
  43. package/dist/ai/records/PersonCard.js.map +7 -0
  44. package/dist/ai/records/ProductCard.js +68 -0
  45. package/dist/ai/records/ProductCard.js.map +7 -0
  46. package/dist/ai/records/RecordCard.js +29 -0
  47. package/dist/ai/records/RecordCard.js.map +7 -0
  48. package/dist/ai/records/RecordCardShell.js +103 -0
  49. package/dist/ai/records/RecordCardShell.js.map +7 -0
  50. package/dist/ai/records/index.js +31 -0
  51. package/dist/ai/records/index.js.map +7 -0
  52. package/dist/ai/records/registry.js +51 -0
  53. package/dist/ai/records/registry.js.map +7 -0
  54. package/dist/ai/records/types.js +1 -0
  55. package/dist/ai/records/types.js.map +7 -0
  56. package/dist/ai/ui-part-registry.js +112 -0
  57. package/dist/ai/ui-part-registry.js.map +7 -0
  58. package/dist/ai/ui-part-slots.js +14 -0
  59. package/dist/ai/ui-part-slots.js.map +7 -0
  60. package/dist/ai/ui-parts/pending-phase3-placeholder.js +35 -0
  61. package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +7 -0
  62. package/dist/ai/upload-adapter.js +256 -0
  63. package/dist/ai/upload-adapter.js.map +7 -0
  64. package/dist/ai/useAiChat.js +549 -0
  65. package/dist/ai/useAiChat.js.map +7 -0
  66. package/dist/ai/useAiChatUpload.js +127 -0
  67. package/dist/ai/useAiChatUpload.js.map +7 -0
  68. package/dist/ai/useAiShortcuts.js +43 -0
  69. package/dist/ai/useAiShortcuts.js.map +7 -0
  70. package/dist/backend/AppShell.js +8 -4
  71. package/dist/backend/AppShell.js.map +2 -2
  72. package/dist/backend/BackendChromeProvider.js +2 -0
  73. package/dist/backend/BackendChromeProvider.js.map +2 -2
  74. package/dist/backend/DataTable.js +19 -2
  75. package/dist/backend/DataTable.js.map +2 -2
  76. package/dist/backend/FilterBar.js +19 -15
  77. package/dist/backend/FilterBar.js.map +2 -2
  78. package/dist/backend/dashboard/DashboardScreen.js +31 -3
  79. package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
  80. package/dist/backend/injection/spotIds.js +6 -0
  81. package/dist/backend/injection/spotIds.js.map +2 -2
  82. package/dist/backend/notifications/useNotificationEffect.js +38 -2
  83. package/dist/backend/notifications/useNotificationEffect.js.map +2 -2
  84. package/dist/index.js +1 -0
  85. package/dist/index.js.map +2 -2
  86. package/jest.config.cjs +7 -1
  87. package/jest.markdown-mock.tsx +7 -0
  88. package/package.json +10 -4
  89. package/src/ai/AiAssistantLauncher.tsx +805 -0
  90. package/src/ai/AiChat.tsx +1483 -0
  91. package/src/ai/AiChatSessions.tsx +429 -0
  92. package/src/ai/AiDock.tsx +505 -0
  93. package/src/ai/AiMessageContent.tsx +515 -0
  94. package/src/ai/ChatPaneTabs.tsx +310 -0
  95. package/src/ai/__tests__/AiChat.conversation.test.tsx +160 -0
  96. package/src/ai/__tests__/AiChat.debug.test.tsx +152 -0
  97. package/src/ai/__tests__/AiChat.registry.test.tsx +213 -0
  98. package/src/ai/__tests__/AiChat.test.tsx +257 -0
  99. package/src/ai/__tests__/AiDock.test.tsx +124 -0
  100. package/src/ai/__tests__/AiMessageContent.test.ts +111 -0
  101. package/src/ai/__tests__/ui-part-registry.test.ts +199 -0
  102. package/src/ai/__tests__/ui-part-slots.test.ts +43 -0
  103. package/src/ai/__tests__/upload-adapter.test.ts +213 -0
  104. package/src/ai/__tests__/useAiChatUpload.test.tsx +163 -0
  105. package/src/ai/__tests__/useAiShortcuts.test.tsx +100 -0
  106. package/src/ai/index.ts +125 -0
  107. package/src/ai/parts/ConfirmationCard.tsx +310 -0
  108. package/src/ai/parts/FieldDiffCard.tsx +173 -0
  109. package/src/ai/parts/MutationPreviewCard.tsx +302 -0
  110. package/src/ai/parts/MutationResultCard.tsx +360 -0
  111. package/src/ai/parts/__tests__/ConfirmationCard.test.tsx +169 -0
  112. package/src/ai/parts/__tests__/FieldDiffCard.test.tsx +74 -0
  113. package/src/ai/parts/__tests__/MutationPreviewCard.test.tsx +177 -0
  114. package/src/ai/parts/__tests__/MutationResultCard.test.tsx +127 -0
  115. package/src/ai/parts/__tests__/useAiPendingActionPolling.test.tsx +151 -0
  116. package/src/ai/parts/approval-cards-map.ts +24 -0
  117. package/src/ai/parts/index.ts +27 -0
  118. package/src/ai/parts/pending-action-api.ts +123 -0
  119. package/src/ai/parts/types.ts +84 -0
  120. package/src/ai/parts/useAiPendingActionPolling.ts +210 -0
  121. package/src/ai/records/ActivityCard.tsx +102 -0
  122. package/src/ai/records/CompanyCard.tsx +89 -0
  123. package/src/ai/records/DealCard.tsx +85 -0
  124. package/src/ai/records/PersonCard.tsx +77 -0
  125. package/src/ai/records/ProductCard.tsx +83 -0
  126. package/src/ai/records/RecordCard.tsx +37 -0
  127. package/src/ai/records/RecordCardShell.tsx +169 -0
  128. package/src/ai/records/index.ts +30 -0
  129. package/src/ai/records/registry.tsx +80 -0
  130. package/src/ai/records/types.ts +90 -0
  131. package/src/ai/ui-part-registry.ts +233 -0
  132. package/src/ai/ui-part-slots.ts +32 -0
  133. package/src/ai/ui-parts/pending-phase3-placeholder.tsx +50 -0
  134. package/src/ai/upload-adapter.ts +421 -0
  135. package/src/ai/useAiChat.ts +865 -0
  136. package/src/ai/useAiChatUpload.ts +180 -0
  137. package/src/ai/useAiShortcuts.ts +79 -0
  138. package/src/backend/AppShell.tsx +12 -5
  139. package/src/backend/BackendChromeProvider.tsx +2 -0
  140. package/src/backend/DataTable.tsx +20 -1
  141. package/src/backend/FilterBar.tsx +26 -13
  142. package/src/backend/__tests__/BackendChromeProvider.test.tsx +45 -0
  143. package/src/backend/dashboard/DashboardScreen.tsx +38 -3
  144. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +24 -1
  145. package/src/backend/injection/spotIds.ts +6 -0
  146. package/src/backend/notifications/__tests__/useNotificationEffect.test.tsx +77 -0
  147. package/src/backend/notifications/useNotificationEffect.ts +47 -2
  148. package/src/index.ts +1 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ai/useAiChat.ts"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { createAiAgentTransport } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/agent-transport'\nimport { apiFetch } from '../backend/utils/api'\n\n/**\n * Chat message shape used by {@link AiChat}. Kept intentionally minimal so the\n * component stays independent of the AI SDK's evolving `UIMessage` type. The\n * dispatcher route (`POST /api/ai_assistant/ai/chat`) accepts exactly this\n * shape for `messages`.\n */\nexport interface AiChatMessageFile {\n name: string\n type: string\n previewUrl?: string\n}\n\nexport interface AiChatToolCallSnapshot {\n id: string\n toolName: string\n state: 'pending' | 'complete' | 'error'\n input?: unknown\n output?: unknown\n errorMessage?: string\n}\n\nexport interface AiChatMessageUiPart {\n componentId: string\n payload?: unknown\n pendingActionId?: string\n /** Stable id used as React key when rendering. */\n key: string\n}\n\nexport interface AiChatMessage {\n id: string\n role: 'user' | 'assistant'\n content: string\n files?: AiChatMessageFile[]\n reasoning?: string\n reasoningStreaming?: boolean\n toolCalls?: AiChatToolCallSnapshot[]\n /**\n * UI parts emitted by the agent during this message's lifecycle. Today\n * the only producer is `prepareMutation` (mutation approval flow):\n * the dispatcher's mutation tool returns an `awaiting-confirmation`\n * envelope, useAiChat parses it and attaches a `mutation-preview-card`\n * part here so AiChat can render the approval card inline. Phase 3\n * WS-C wiring \u2014 without this, the `MutationPreviewCard` registered in\n * the UI-part registry never surfaces.\n */\n uiParts?: AiChatMessageUiPart[]\n}\n\nexport interface UseAiChatInput {\n agent: string\n apiPath?: string\n pageContext?: Record<string, unknown>\n attachmentIds?: string[]\n debug?: boolean\n initialMessages?: Array<Pick<AiChatMessage, 'role' | 'content'>>\n onError?: (err: { code?: string; message: string }) => void\n /**\n * Optional stable conversation id. When provided, the same id is forwarded\n * to the dispatcher on every turn so `prepareMutation`'s idempotency hash\n * (Step 5.6) stays stable across mutation preview / confirm / retry cycles.\n * When omitted, the hook mints a fresh random id once on mount and reuses\n * it for the lifetime of the component \u2014 callers can still override via\n * props at any time to reset the conversation.\n */\n conversationId?: string\n}\n\nexport interface AiChatErrorEnvelope {\n code?: string\n message: string\n}\n\nexport interface UseAiChatResult {\n messages: AiChatMessage[]\n status: 'idle' | 'submitting' | 'streaming'\n error: AiChatErrorEnvelope | null\n lastRequestDebug: { url: string; body: unknown } | null\n lastResponseDebug: { status: number; text: string } | null\n /**\n * The conversation id currently in use for this chat instance. Equal to\n * the caller-provided `conversationId` input when one is supplied;\n * otherwise the random id minted on mount. Stable across re-renders for a\n * given mount (Phase 3 WS-D contract with `prepareMutation`).\n */\n conversationId: string\n sendMessage: (input: string, files?: AiChatMessageFile[]) => Promise<void>\n cancel: () => void\n reset: () => void\n}\n\nfunction makeMessageId(): string {\n const random = Math.random().toString(36).slice(2, 10)\n const time = Date.now().toString(36)\n return `msg_${time}_${random}`\n}\n\nfunction makeConversationId(): string {\n // Use crypto.randomUUID() when the browser exposes it (all evergreen\n // runtimes do), otherwise fall back to a low-entropy token that is still\n // unique enough for the idempotency-hash use case.\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 to the random fallback\n }\n }\n const rand = () => Math.random().toString(16).slice(2, 10)\n return `conv_${Date.now().toString(16)}_${rand()}${rand()}`\n}\n\nconst SESSION_STORAGE_PREFIX = 'om-ai-chat:'\nconst SESSION_STORAGE_VERSION = 1\n\ninterface PersistedAiChatSession {\n v: number\n conversationId: string\n messages: AiChatMessage[]\n}\n\nfunction getSessionStorageKey(agent: string, conversationId?: string | null): string {\n // When the caller pins a `conversationId` (e.g. via the AiChatSessions\n // provider's tabs), namespace the persisted slot per session so multiple\n // open conversations for the same agent don't overwrite each other. The\n // legacy single-session-per-agent layout (no externally-supplied id) is\n // kept for backward compatibility with code that still relies on it.\n if (typeof conversationId === 'string' && conversationId.length > 0) {\n return `${SESSION_STORAGE_PREFIX}${agent}:${conversationId}`\n }\n return `${SESSION_STORAGE_PREFIX}${agent}`\n}\n\nfunction readPersistedSession(\n agent: string,\n conversationId?: string | null,\n): PersistedAiChatSession | null {\n if (typeof window === 'undefined') return null\n try {\n const raw = window.localStorage.getItem(getSessionStorageKey(agent, conversationId))\n if (!raw) return null\n const parsed = JSON.parse(raw) as PersistedAiChatSession | null\n if (!parsed || parsed.v !== SESSION_STORAGE_VERSION) return null\n if (typeof parsed.conversationId !== 'string') return null\n if (!Array.isArray(parsed.messages)) return null\n const messages = parsed.messages.filter((entry): entry is AiChatMessage => {\n return (\n !!entry &&\n typeof entry === 'object' &&\n typeof (entry as AiChatMessage).id === 'string' &&\n typeof (entry as AiChatMessage).content === 'string' &&\n ((entry as AiChatMessage).role === 'user' || (entry as AiChatMessage).role === 'assistant')\n )\n })\n return { v: SESSION_STORAGE_VERSION, conversationId: parsed.conversationId, messages }\n } catch {\n return null\n }\n}\n\nfunction writePersistedSession(\n agent: string,\n session: PersistedAiChatSession,\n conversationId?: string | null,\n): void {\n if (typeof window === 'undefined') return\n try {\n // Strip transient blob/object preview URLs before persisting (they would\n // not survive a reload). Self-contained `data:` URLs are kept so image\n // previews come back unchanged after the chat is reopened \u2014 public\n // attachment URLs are intentionally not used because the LLM provider\n // cannot reach a localhost origin and we want a single durable shape\n // that works for both transport and reload.\n const messages = session.messages.map((message) => {\n if (!message.files || message.files.length === 0) return message\n const safeFiles = message.files.map(({ name, type, previewUrl }) => {\n const durable =\n typeof previewUrl === 'string' && previewUrl.startsWith('data:')\n ? previewUrl\n : undefined\n return durable ? { name, type, previewUrl: durable } : { name, type }\n })\n return { ...message, files: safeFiles }\n })\n window.localStorage.setItem(\n getSessionStorageKey(agent, conversationId),\n JSON.stringify({ ...session, messages }),\n )\n } catch {\n // Quota exceeded / privacy mode \u2014 silently drop persistence.\n }\n}\n\nfunction clearPersistedSession(agent: string, conversationId?: string | null): void {\n if (typeof window === 'undefined') return\n try {\n window.localStorage.removeItem(getSessionStorageKey(agent, conversationId))\n } catch {\n // ignore\n }\n}\n\nfunction getTransportEndpoint(agent: string, apiPath?: string): string {\n // Reuse the transport factory so UI consumers share the dispatcher URL\n // convention with server-side callers (e.g. runAiAgentText / Playwright\n // fixtures). The factory returns a ChatTransport<UI_MESSAGE> whose internal\n // endpoint we do not directly read \u2014 instead we reconstruct the same URL\n // shape here so downstream error handling stays deterministic.\n //\n // When the AI SDK exposes a public endpoint getter (or the stream format\n // switches from plain text to UIMessageChunk) we can call\n // transport.sendMessages(...) directly.\n const transport = createAiAgentTransport({ agentId: agent, endpoint: apiPath })\n void transport\n const base = apiPath && apiPath.length > 0 ? apiPath : '/api/ai_assistant/ai/chat'\n const separator = base.includes('?') ? '&' : '?'\n return `${base}${separator}agent=${encodeURIComponent(agent)}`\n}\n\ninterface AssistantBuilderState {\n text: string\n reasoning: string\n reasoningStreaming: boolean\n toolCalls: AiChatToolCallSnapshot[]\n uiParts: AiChatMessageUiPart[]\n}\n\nfunction createBuilder(): AssistantBuilderState {\n return { text: '', reasoning: '', reasoningStreaming: false, toolCalls: [], uiParts: [] }\n}\n\n/**\n * Generic extractor for UI parts emitted by tool outputs. A tool can\n * surface inline UI to the chat by returning JSON in any of these\n * shapes \u2014 each tool call produces zero or more UI parts:\n *\n * 1. The dispatcher's mutation envelope:\n * `{ status: 'awaiting-confirmation', pendingActionId, expiresAt,\n * agent, toolName, message }`\n * \u2192 synthesizes a `mutation-preview-card` part (the registered\n * card fetches the live diff via `useAiPendingActionPolling`).\n *\n * 2. A single explicit UI part:\n * `{ uiPart: { componentId, payload?, pendingActionId? } }`\n *\n * 3. Multiple explicit UI parts:\n * `{ uiParts: [{ componentId, payload? }, ...] }`\n *\n * Tool authors only need to JSON-encode an object whose `uiPart` /\n * `uiParts` reference component ids that the host has registered on\n * `defaultAiUiPartRegistry` (or a scoped registry passed through\n * `<AiChat registry={...}/>`). Unknown component ids fall back to the\n * `UnknownUiPartPlaceholder` so an unregistered id never blows up the\n * transcript.\n */\nfunction extractUiPartsFromOutput(\n output: unknown,\n toolCallId: string,\n): AiChatMessageUiPart[] {\n let parsed: unknown = output\n if (typeof output === 'string') {\n const trimmed = output.trim()\n if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return []\n try {\n parsed = JSON.parse(trimmed)\n } catch {\n return []\n }\n }\n if (!parsed || typeof parsed !== 'object') return []\n const value = parsed as Record<string, unknown>\n const parts: AiChatMessageUiPart[] = []\n\n // (1) Mutation approval envelope. The dispatcher's `prepareMutation`\n // interceptor in `agent-tools.ts` formats the result via\n // `formatPendingActionToolResult` as\n // { status: 'pending-confirmation', agentId, toolName, pendingActionId,\n // expiresAt, message }\n // (NOTE: status is `pending-confirmation` and the field is `agentId`,\n // not `agent`). We also accept `awaiting-confirmation` / `agent` for\n // forward compat with older / alternative dispatchers.\n if (value.status === 'pending-confirmation' || value.status === 'awaiting-confirmation') {\n const pendingActionId =\n typeof value.pendingActionId === 'string' && value.pendingActionId.length > 0\n ? value.pendingActionId\n : null\n if (pendingActionId) {\n const agentId =\n typeof value.agentId === 'string'\n ? value.agentId\n : typeof value.agent === 'string'\n ? value.agent\n : undefined\n parts.push({\n componentId: 'mutation-preview-card',\n pendingActionId,\n payload: {\n pendingActionId,\n expiresAt: typeof value.expiresAt === 'string' ? value.expiresAt : undefined,\n agentId,\n toolName: typeof value.toolName === 'string' ? value.toolName : undefined,\n },\n key: `${toolCallId}:mutation-preview-card`,\n })\n }\n }\n\n // (2) Explicit single UI part.\n if (value.uiPart && typeof value.uiPart === 'object') {\n const part = value.uiPart as Record<string, unknown>\n if (typeof part.componentId === 'string' && part.componentId.length > 0) {\n parts.push({\n componentId: part.componentId,\n payload: part.payload,\n pendingActionId:\n typeof part.pendingActionId === 'string' ? part.pendingActionId : undefined,\n key: `${toolCallId}:${part.componentId}`,\n })\n }\n }\n\n // (3) Explicit list of UI parts.\n if (Array.isArray(value.uiParts)) {\n value.uiParts.forEach((entry, index) => {\n if (!entry || typeof entry !== 'object') return\n const part = entry as Record<string, unknown>\n if (typeof part.componentId !== 'string' || part.componentId.length === 0) return\n parts.push({\n componentId: part.componentId,\n payload: part.payload,\n pendingActionId:\n typeof part.pendingActionId === 'string' ? part.pendingActionId : undefined,\n key: `${toolCallId}:${index}:${part.componentId}`,\n })\n })\n }\n\n return parts\n}\n\nfunction updateToolCall(\n state: AssistantBuilderState,\n id: string,\n patch: Partial<AiChatToolCallSnapshot> & { toolName?: string },\n): AssistantBuilderState {\n if (!id) return state\n const idx = state.toolCalls.findIndex((entry) => entry.id === id)\n if (idx === -1) {\n const next: AiChatToolCallSnapshot = {\n id,\n toolName: patch.toolName ?? 'tool',\n state: patch.state ?? 'pending',\n input: patch.input,\n output: patch.output,\n errorMessage: patch.errorMessage,\n }\n return { ...state, toolCalls: [...state.toolCalls, next] }\n }\n const current = state.toolCalls[idx]\n const merged: AiChatToolCallSnapshot = {\n ...current,\n toolName: patch.toolName ?? current.toolName,\n state: patch.state ?? current.state,\n input: patch.input !== undefined ? patch.input : current.input,\n output: patch.output !== undefined ? patch.output : current.output,\n errorMessage: patch.errorMessage ?? current.errorMessage,\n }\n const nextCalls = state.toolCalls.slice()\n nextCalls[idx] = merged\n return { ...state, toolCalls: nextCalls }\n}\n\nfunction applyChunk(\n state: AssistantBuilderState,\n chunk: { type: string; [key: string]: unknown },\n): AssistantBuilderState {\n switch (chunk.type) {\n case 'text-delta':\n return {\n ...state,\n text: state.text + (typeof chunk.delta === 'string' ? chunk.delta : ''),\n }\n case 'reasoning-start':\n return { ...state, reasoningStreaming: true }\n case 'reasoning-delta':\n return {\n ...state,\n reasoning:\n state.reasoning + (typeof chunk.delta === 'string' ? chunk.delta : ''),\n reasoningStreaming: true,\n }\n case 'reasoning-end':\n return { ...state, reasoningStreaming: false }\n case 'tool-input-start':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n toolName: typeof chunk.toolName === 'string' ? chunk.toolName : undefined,\n state: 'pending',\n })\n case 'tool-input-available':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n toolName: typeof chunk.toolName === 'string' ? chunk.toolName : undefined,\n input: chunk.input,\n state: 'pending',\n })\n case 'tool-output-available': {\n const toolCallId = String(chunk.toolCallId ?? '')\n const next = updateToolCall(state, toolCallId, {\n output: chunk.output,\n state: 'complete',\n })\n // Phase 3 WS-C \u2014 surface ANY UI parts the tool output advertises:\n // the legacy `awaiting-confirmation` mutation envelope plus the\n // generic `{ uiPart }` / `{ uiParts: [...] }` shapes. This lets\n // module authors define their own dynamic cards (stats panels,\n // record summaries, charts\u2026) without touching the dispatcher or\n // the chat client.\n const newParts = extractUiPartsFromOutput(chunk.output, toolCallId)\n if (newParts.length === 0) return next\n const seen = new Set(next.uiParts.map((entry) => entry.key))\n const merged = [...next.uiParts]\n for (const part of newParts) {\n if (seen.has(part.key)) continue\n seen.add(part.key)\n merged.push(part)\n }\n if (merged.length === next.uiParts.length) return next\n return { ...next, uiParts: merged }\n }\n case 'tool-output-error':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n state: 'error',\n errorMessage:\n typeof chunk.errorText === 'string' ? chunk.errorText : 'Tool error',\n })\n case 'tool-input-error':\n return updateToolCall(state, String(chunk.toolCallId ?? ''), {\n toolName: typeof chunk.toolName === 'string' ? chunk.toolName : undefined,\n input: chunk.input,\n state: 'error',\n errorMessage:\n typeof chunk.errorText === 'string' ? chunk.errorText : 'Tool error',\n })\n default:\n return state\n }\n}\n\nfunction mergeAssistantMessage(\n current: AiChatMessage,\n state: AssistantBuilderState,\n): AiChatMessage {\n return {\n ...current,\n content: state.text,\n reasoning: state.reasoning ? state.reasoning : undefined,\n reasoningStreaming: state.reasoning ? state.reasoningStreaming : undefined,\n toolCalls: state.toolCalls.length > 0 ? state.toolCalls : undefined,\n uiParts: state.uiParts.length > 0 ? state.uiParts : undefined,\n }\n}\n\nfunction parseSseLines(buffer: string): { events: string[]; rest: string } {\n const events: string[] = []\n let rest = buffer\n for (;;) {\n const idx = rest.indexOf('\\n\\n')\n if (idx === -1) break\n events.push(rest.slice(0, idx))\n rest = rest.slice(idx + 2)\n }\n return { events, rest }\n}\n\nfunction extractDataPayload(eventBlock: string): string | null {\n const lines = eventBlock.split('\\n')\n const dataLines: string[] = []\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n dataLines.push(line.slice(6))\n } else if (line.startsWith('data:')) {\n dataLines.push(line.slice(5))\n }\n }\n if (dataLines.length === 0) return null\n return dataLines.join('\\n')\n}\n\nasync function readErrorEnvelope(response: Response): Promise<AiChatErrorEnvelope> {\n try {\n const data = (await response.clone().json()) as\n | { error?: unknown; code?: unknown; message?: unknown }\n | null\n if (data && typeof data === 'object') {\n const rawMessage =\n (typeof data.error === 'string' && data.error) ||\n (typeof data.message === 'string' && data.message) ||\n ''\n const rawCode = typeof data.code === 'string' ? data.code : undefined\n if (rawMessage || rawCode) {\n return {\n code: rawCode,\n message: rawMessage || 'Agent dispatch failed.',\n }\n }\n }\n } catch {\n // Fall through to text fallback\n }\n const text = await response.text().catch(() => '')\n return { message: text || `Agent dispatch failed (${response.status}).` }\n}\n\nexport function useAiChat(input: UseAiChatInput): UseAiChatResult {\n const { agent, apiPath, pageContext, attachmentIds, debug, initialMessages, onError, conversationId: conversationIdInput } = input\n\n // Minted once on mount when the caller does not supply a conversationId.\n // The ref keeps the id stable across re-renders and is reused for every\n // turn so the Phase 3 WS-C `prepareMutation` idempotency hash stays\n // stable within the same chat. When the agent has a persisted session in\n // localStorage we re-hydrate the conversationId from it so re-opening the\n // chat continues the previous turn instead of starting fresh.\n const persistedRef = React.useRef<PersistedAiChatSession | null | 'unread'>('unread')\n if (persistedRef.current === 'unread') {\n // When the caller pins a `conversationId` (multi-tab session mode) we\n // read ONLY from that per-conversation slot. Falling back to the\n // legacy agent-only slot here would make every brand-new tab inherit\n // the previous tab's messages \u2014 the \"+ shows the same chat\" bug \u2014 so\n // unknown conversationIds always start clean. Without a pinned id we\n // keep the legacy single-session-per-agent layout for backward\n // compatibility.\n persistedRef.current =\n typeof conversationIdInput === 'string' && conversationIdInput.length > 0\n ? readPersistedSession(agent, conversationIdInput)\n : readPersistedSession(agent)\n }\n const persisted = persistedRef.current\n\n const mintedConversationIdRef = React.useRef<string | null>(null)\n if (mintedConversationIdRef.current === null) {\n mintedConversationIdRef.current = persisted?.conversationId ?? makeConversationId()\n }\n const effectiveConversationId =\n typeof conversationIdInput === 'string' && conversationIdInput.length > 0\n ? conversationIdInput\n : mintedConversationIdRef.current\n\n const [messages, setMessages] = React.useState<AiChatMessage[]>(() => {\n if (persisted && persisted.messages.length > 0) {\n return persisted.messages\n }\n return (initialMessages ?? []).map((entry) => ({\n id: makeMessageId(),\n role: entry.role,\n content: entry.content,\n }))\n })\n\n // Persist messages + conversationId on every change. Skip during in-flight\n // streaming so we do not write the same growing string on every chunk \u2014\n // the next idle tick captures the final assistant content.\n const [status, setStatusInternal] = React.useState<'idle' | 'submitting' | 'streaming'>('idle')\n React.useEffect(() => {\n if (status !== 'idle') return\n const persistKey =\n typeof conversationIdInput === 'string' && conversationIdInput.length > 0\n ? conversationIdInput\n : null\n if (messages.length === 0) {\n clearPersistedSession(agent, persistKey)\n return\n }\n writePersistedSession(\n agent,\n {\n v: SESSION_STORAGE_VERSION,\n conversationId: effectiveConversationId,\n messages,\n },\n persistKey,\n )\n }, [agent, conversationIdInput, effectiveConversationId, messages, status])\n const setStatus = setStatusInternal\n const [error, setError] = React.useState<AiChatErrorEnvelope | null>(null)\n const [lastRequestDebug, setLastRequestDebug] = React.useState<\n { url: string; body: unknown } | null\n >(null)\n const [lastResponseDebug, setLastResponseDebug] = React.useState<\n { status: number; text: string } | null\n >(null)\n\n const abortRef = React.useRef<AbortController | null>(null)\n const onErrorRef = React.useRef(onError)\n React.useEffect(() => {\n onErrorRef.current = onError\n }, [onError])\n\n const emitError = React.useCallback((envelope: AiChatErrorEnvelope) => {\n setError(envelope)\n try {\n onErrorRef.current?.(envelope)\n } catch {\n // UI layer must never throw because a caller-supplied error handler\n // misbehaved.\n }\n }, [])\n\n const cancel = React.useCallback(() => {\n if (abortRef.current) {\n abortRef.current.abort()\n abortRef.current = null\n }\n setStatus('idle')\n }, [])\n\n const reset = React.useCallback(() => {\n cancel()\n setMessages([])\n setError(null)\n setLastRequestDebug(null)\n setLastResponseDebug(null)\n clearPersistedSession(agent)\n mintedConversationIdRef.current = makeConversationId()\n }, [agent, cancel])\n\n const sendMessage = React.useCallback(\n async (textInput: string, files?: AiChatMessageFile[]) => {\n const trimmed = textInput.trim()\n if (!trimmed) return\n if (abortRef.current) {\n abortRef.current.abort()\n }\n\n setError(null)\n const userMessage: AiChatMessage = {\n id: makeMessageId(),\n role: 'user',\n content: trimmed,\n files: files && files.length > 0 ? files : undefined,\n }\n const assistantMessage: AiChatMessage = {\n id: makeMessageId(),\n role: 'assistant',\n content: '',\n }\n const assistantId = assistantMessage.id\n // Snapshot prior messages for request payload so the dispatcher sees the\n // full turn history including the just-added user message.\n const outgoingHistory = [...messages, userMessage]\n setMessages([...outgoingHistory, assistantMessage])\n setStatus('submitting')\n\n const controller = new AbortController()\n abortRef.current = controller\n\n const url = getTransportEndpoint(agent, apiPath)\n const body = {\n messages: outgoingHistory.map((message) => ({\n role: message.role,\n content: message.content,\n })),\n pageContext,\n attachmentIds,\n debug,\n conversationId: effectiveConversationId,\n }\n setLastRequestDebug({ url, body })\n\n let response: Response\n try {\n response = await apiFetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'text/event-stream, text/plain, application/json',\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n })\n } catch (requestError) {\n if ((requestError as { name?: string })?.name === 'AbortError') {\n setStatus('idle')\n abortRef.current = null\n return\n }\n const message =\n requestError instanceof Error\n ? requestError.message\n : 'Network request failed.'\n emitError({ message })\n setStatus('idle')\n abortRef.current = null\n return\n }\n\n if (!response.ok) {\n const envelope = await readErrorEnvelope(response)\n setLastResponseDebug({ status: response.status, text: envelope.message })\n emitError(envelope)\n setStatus('idle')\n setMessages((current) => current.filter((entry) => entry.id !== assistantId))\n abortRef.current = null\n return\n }\n\n const bodyStream = response.body\n if (!bodyStream) {\n setLastResponseDebug({ status: response.status, text: '' })\n setStatus('idle')\n abortRef.current = null\n return\n }\n\n const headerGet = (name: string): string | null => {\n const headers = (response as { headers?: { get?: (k: string) => string | null } })\n .headers\n if (!headers || typeof headers.get !== 'function') return null\n try {\n return headers.get(name)\n } catch {\n return null\n }\n }\n const isUiMessageStream =\n headerGet('x-vercel-ai-ui-message-stream') !== null ||\n (headerGet('content-type') ?? '').includes('event-stream')\n\n setStatus('streaming')\n const reader = bodyStream.getReader()\n const decoder = new TextDecoder()\n let streamedRaw = ''\n let builder = createBuilder()\n let sseBuffer = ''\n const flushUiMessageBuffer = (extra?: string) => {\n if (extra) sseBuffer += extra\n const { events, rest } = parseSseLines(sseBuffer)\n sseBuffer = rest\n for (const block of events) {\n const data = extractDataPayload(block)\n if (!data) continue\n if (data === '[DONE]') continue\n try {\n const parsed = JSON.parse(data) as { type?: string }\n if (parsed && typeof parsed.type === 'string') {\n builder = applyChunk(builder, parsed as { type: string })\n }\n } catch {\n // Tolerate malformed events / SSE comments.\n }\n }\n }\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n if (!value) continue\n const piece = decoder.decode(value, { stream: true })\n if (!piece) continue\n streamedRaw += piece\n\n if (isUiMessageStream) {\n flushUiMessageBuffer(piece)\n } else {\n // Plain text fallback (legacy `toTextStreamResponse`).\n builder = { ...builder, text: streamedRaw }\n }\n const snapshotBuilder = builder\n setMessages((current) =>\n current.map((entry) =>\n entry.id === assistantId\n ? mergeAssistantMessage(entry, snapshotBuilder)\n : entry,\n ),\n )\n }\n const tail = decoder.decode()\n if (tail) {\n streamedRaw += tail\n if (isUiMessageStream) {\n flushUiMessageBuffer(tail)\n } else {\n builder = { ...builder, text: streamedRaw }\n }\n }\n if (isUiMessageStream && sseBuffer.length > 0) {\n flushUiMessageBuffer('\\n\\n')\n }\n builder = { ...builder, reasoningStreaming: false }\n const finalSnapshot = builder\n setMessages((current) =>\n current.map((entry) =>\n entry.id === assistantId\n ? mergeAssistantMessage(entry, finalSnapshot)\n : entry,\n ),\n )\n setLastResponseDebug({ status: response.status, text: streamedRaw })\n const isEmpty =\n !builder.text.trim() && builder.toolCalls.length === 0 && !builder.reasoning\n if (isEmpty) {\n emitError({\n code: 'empty_response',\n message:\n 'The AI agent returned an empty response. This usually means the LLM provider rejected the request (invalid API key, rate limit, or model error). Check your server logs for details.',\n })\n setMessages((current) => current.filter((entry) => entry.id !== assistantId))\n }\n } catch (streamError) {\n if ((streamError as { name?: string })?.name === 'AbortError') {\n // Cancelled by the user \u2014 keep whatever we have so far and exit\n // quietly.\n } else {\n const rawMessage =\n streamError instanceof Error\n ? streamError.message\n : 'Stream interrupted.'\n // LLM provider errors (auth failures, rate limits, invalid tool\n // schemas) surface as stream read errors. Include a hint so the\n // operator can check server logs for the full stack trace.\n const message = rawMessage.includes('API')\n ? rawMessage\n : `${rawMessage} \u2014 check server logs for LLM provider details.`\n emitError({ code: 'stream_error', message })\n // Remove the empty assistant placeholder so the error alert is\n // the only visible feedback.\n setMessages((current) => current.filter((entry) => entry.id !== assistantId))\n }\n } finally {\n reader.releaseLock()\n if (abortRef.current === controller) {\n abortRef.current = null\n }\n setStatus('idle')\n }\n },\n [agent, apiPath, attachmentIds, debug, effectiveConversationId, emitError, messages, pageContext],\n )\n\n React.useEffect(() => {\n return () => {\n if (abortRef.current) {\n abortRef.current.abort()\n abortRef.current = null\n }\n }\n }, [])\n\n return {\n messages,\n status,\n error,\n lastRequestDebug,\n lastResponseDebug,\n conversationId: effectiveConversationId,\n sendMessage,\n cancel,\n reset,\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,127 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import {
4
+ uploadAttachmentsForChat
5
+ } from "./upload-adapter.js";
6
+ const EMPTY_STATE = [];
7
+ function computeOverallProgress(entries) {
8
+ if (entries.length === 0) return 0;
9
+ const total = entries.reduce((sum, entry) => sum + entry.progress, 0);
10
+ const average = total / entries.length;
11
+ if (!Number.isFinite(average)) return 0;
12
+ if (average < 0) return 0;
13
+ if (average > 1) return 1;
14
+ return average;
15
+ }
16
+ function useAiChatUpload(options = {}) {
17
+ const [files, setFiles] = React.useState(EMPTY_STATE);
18
+ const [busy, setBusy] = React.useState(false);
19
+ const optionsRef = React.useRef(options);
20
+ React.useEffect(() => {
21
+ optionsRef.current = options;
22
+ }, [options]);
23
+ const overallProgress = React.useMemo(() => computeOverallProgress(files), [files]);
24
+ const reset = React.useCallback(() => {
25
+ setFiles(EMPTY_STATE);
26
+ setBusy(false);
27
+ }, []);
28
+ const upload = React.useCallback(
29
+ async (incoming) => {
30
+ if (!incoming || incoming.length === 0) {
31
+ return { items: [], failed: [] };
32
+ }
33
+ const initialEntries = incoming.map((file) => ({
34
+ fileName: file.name,
35
+ size: file.size,
36
+ progress: 0,
37
+ status: "uploading"
38
+ }));
39
+ setFiles(initialEntries);
40
+ setBusy(true);
41
+ const callerOptions = optionsRef.current;
42
+ const callerProgress = callerOptions.onProgress;
43
+ const result = await uploadAttachmentsForChat(incoming, {
44
+ ...callerOptions,
45
+ onProgress: (fileIndex, progress) => {
46
+ const ratio = progress.total > 0 ? Math.max(0, Math.min(1, progress.loaded / progress.total)) : 0;
47
+ setFiles((current) => {
48
+ if (fileIndex < 0 || fileIndex >= current.length) return current;
49
+ const next = current.slice();
50
+ const entry = next[fileIndex];
51
+ if (!entry) return current;
52
+ next[fileIndex] = { ...entry, progress: ratio };
53
+ return next;
54
+ });
55
+ if (callerProgress) {
56
+ try {
57
+ callerProgress(fileIndex, progress);
58
+ } catch {
59
+ }
60
+ }
61
+ }
62
+ }).catch((err) => {
63
+ const message = err instanceof Error ? err.message : "Upload batch failed.";
64
+ return {
65
+ items: [],
66
+ failed: incoming.map((file, inputIndex) => ({
67
+ fileName: file.name,
68
+ originalFileName: file.name,
69
+ inputIndex,
70
+ reason: "network",
71
+ message
72
+ }))
73
+ };
74
+ });
75
+ setFiles((current) => {
76
+ const failedByName = /* @__PURE__ */ new Map();
77
+ for (const failure of result.failed) {
78
+ if (!failedByName.has(failure.fileName)) {
79
+ failedByName.set(failure.fileName, failure);
80
+ }
81
+ }
82
+ return current.map((entry, index) => {
83
+ const success = result.items.find(
84
+ (item, itemIndex) => itemIndex === index && item.fileName === entry.fileName
85
+ );
86
+ if (success) {
87
+ return {
88
+ ...entry,
89
+ progress: 1,
90
+ status: "done",
91
+ attachmentId: success.attachmentId
92
+ };
93
+ }
94
+ const failure = failedByName.get(entry.fileName);
95
+ if (failure) {
96
+ failedByName.delete(entry.fileName);
97
+ return {
98
+ ...entry,
99
+ status: "error",
100
+ reason: failure.reason,
101
+ error: failure.message
102
+ };
103
+ }
104
+ return {
105
+ ...entry,
106
+ status: "error",
107
+ reason: "network"
108
+ };
109
+ });
110
+ });
111
+ setBusy(false);
112
+ return result;
113
+ },
114
+ []
115
+ );
116
+ return {
117
+ files,
118
+ overallProgress,
119
+ busy,
120
+ upload,
121
+ reset
122
+ };
123
+ }
124
+ export {
125
+ useAiChatUpload
126
+ };
127
+ //# sourceMappingURL=useAiChatUpload.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ai/useAiChatUpload.ts"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport {\n uploadAttachmentsForChat,\n type UploadAttachmentsForChatOptions,\n type UploadAttachmentsForChatResult,\n type UploadFailureReason,\n} from './upload-adapter'\n\n/**\n * React hook wrapping {@link uploadAttachmentsForChat} with per-file state so\n * the {@link AiChat} composer can render progress chips, error badges, and a\n * Clear action without each consumer re-implementing the machinery.\n *\n * The hook is DS-neutral: it exposes only state and {@link UploadFailureReason}\n * codes. Consumers translate user-facing strings through `useT()` at render\n * time \u2014 no hard-coded copy in the hook.\n */\n\nexport interface UseAiChatUploadOptions extends UploadAttachmentsForChatOptions {\n /** Identical to the adapter options; forwarded verbatim. */\n}\n\nexport type AiChatUploadFileStatus = 'queued' | 'uploading' | 'done' | 'error'\n\nexport interface AiChatUploadFileState {\n fileName: string\n size: number\n progress: number\n status: AiChatUploadFileStatus\n attachmentId?: string\n reason?: UploadFailureReason\n error?: string\n}\n\nexport interface UseAiChatUploadState {\n files: AiChatUploadFileState[]\n overallProgress: number\n busy: boolean\n upload: (files: File[]) => Promise<UploadAttachmentsForChatResult>\n reset: () => void\n}\n\nconst EMPTY_STATE: AiChatUploadFileState[] = []\n\nfunction computeOverallProgress(entries: AiChatUploadFileState[]): number {\n if (entries.length === 0) return 0\n const total = entries.reduce((sum, entry) => sum + entry.progress, 0)\n const average = total / entries.length\n if (!Number.isFinite(average)) return 0\n if (average < 0) return 0\n if (average > 1) return 1\n return average\n}\n\nexport function useAiChatUpload(\n options: UseAiChatUploadOptions = {},\n): UseAiChatUploadState {\n const [files, setFiles] = React.useState<AiChatUploadFileState[]>(EMPTY_STATE)\n const [busy, setBusy] = React.useState(false)\n const optionsRef = React.useRef(options)\n React.useEffect(() => {\n optionsRef.current = options\n }, [options])\n\n const overallProgress = React.useMemo(() => computeOverallProgress(files), [files])\n\n const reset = React.useCallback(() => {\n setFiles(EMPTY_STATE)\n setBusy(false)\n }, [])\n\n const upload = React.useCallback(\n async (incoming: File[]): Promise<UploadAttachmentsForChatResult> => {\n if (!incoming || incoming.length === 0) {\n return { items: [], failed: [] }\n }\n const initialEntries: AiChatUploadFileState[] = incoming.map((file) => ({\n fileName: file.name,\n size: file.size,\n progress: 0,\n status: 'uploading',\n }))\n setFiles(initialEntries)\n setBusy(true)\n\n const callerOptions = optionsRef.current\n const callerProgress = callerOptions.onProgress\n const result = await uploadAttachmentsForChat(incoming, {\n ...callerOptions,\n onProgress: (fileIndex, progress) => {\n const ratio =\n progress.total > 0\n ? Math.max(0, Math.min(1, progress.loaded / progress.total))\n : 0\n setFiles((current) => {\n if (fileIndex < 0 || fileIndex >= current.length) return current\n const next = current.slice()\n const entry = next[fileIndex]\n if (!entry) return current\n next[fileIndex] = { ...entry, progress: ratio }\n return next\n })\n if (callerProgress) {\n try {\n callerProgress(fileIndex, progress)\n } catch {\n // Consumer-supplied callbacks must never abort state updates.\n }\n }\n },\n }).catch((err) => {\n // uploadAttachmentsForChat only rejects on programming errors; coerce\n // to a failure envelope so the hook state never throws at consumers.\n const message = err instanceof Error ? err.message : 'Upload batch failed.'\n return {\n items: [],\n failed: incoming.map((file, inputIndex) => ({\n fileName: file.name,\n originalFileName: file.name,\n inputIndex,\n reason: 'network' as UploadFailureReason,\n message,\n })),\n } satisfies UploadAttachmentsForChatResult\n })\n\n setFiles((current) => {\n const failedByName = new Map<string, typeof result.failed[number]>()\n for (const failure of result.failed) {\n if (!failedByName.has(failure.fileName)) {\n failedByName.set(failure.fileName, failure)\n }\n }\n return current.map((entry, index) => {\n const success = result.items.find(\n (item, itemIndex) => itemIndex === index && item.fileName === entry.fileName,\n )\n if (success) {\n return {\n ...entry,\n progress: 1,\n status: 'done' as AiChatUploadFileStatus,\n attachmentId: success.attachmentId,\n }\n }\n const failure = failedByName.get(entry.fileName)\n if (failure) {\n failedByName.delete(entry.fileName)\n return {\n ...entry,\n status: 'error' as AiChatUploadFileStatus,\n reason: failure.reason,\n error: failure.message,\n }\n }\n // Defensive: a worker exited without producing either outcome.\n return {\n ...entry,\n status: 'error' as AiChatUploadFileStatus,\n reason: 'network' as UploadFailureReason,\n }\n })\n })\n\n setBusy(false)\n return result\n },\n [],\n )\n\n return {\n files,\n overallProgress,\n busy,\n upload,\n reset,\n }\n}\n"],
5
+ "mappings": ";AAEA,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,OAIK;AAoCP,MAAM,cAAuC,CAAC;AAE9C,SAAS,uBAAuB,SAA0C;AACxE,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAM,QAAQ,QAAQ,OAAO,CAAC,KAAK,UAAU,MAAM,MAAM,UAAU,CAAC;AACpE,QAAM,UAAU,QAAQ,QAAQ;AAChC,MAAI,CAAC,OAAO,SAAS,OAAO,EAAG,QAAO;AACtC,MAAI,UAAU,EAAG,QAAO;AACxB,MAAI,UAAU,EAAG,QAAO;AACxB,SAAO;AACT;AAEO,SAAS,gBACd,UAAkC,CAAC,GACb;AACtB,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAkC,WAAW;AAC7E,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,KAAK;AAC5C,QAAM,aAAa,MAAM,OAAO,OAAO;AACvC,QAAM,UAAU,MAAM;AACpB,eAAW,UAAU;AAAA,EACvB,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,kBAAkB,MAAM,QAAQ,MAAM,uBAAuB,KAAK,GAAG,CAAC,KAAK,CAAC;AAElF,QAAM,QAAQ,MAAM,YAAY,MAAM;AACpC,aAAS,WAAW;AACpB,YAAQ,KAAK;AAAA,EACf,GAAG,CAAC,CAAC;AAEL,QAAM,SAAS,MAAM;AAAA,IACnB,OAAO,aAA8D;AACnE,UAAI,CAAC,YAAY,SAAS,WAAW,GAAG;AACtC,eAAO,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,EAAE;AAAA,MACjC;AACA,YAAM,iBAA0C,SAAS,IAAI,CAAC,UAAU;AAAA,QACtE,UAAU,KAAK;AAAA,QACf,MAAM,KAAK;AAAA,QACX,UAAU;AAAA,QACV,QAAQ;AAAA,MACV,EAAE;AACF,eAAS,cAAc;AACvB,cAAQ,IAAI;AAEZ,YAAM,gBAAgB,WAAW;AACjC,YAAM,iBAAiB,cAAc;AACrC,YAAM,SAAS,MAAM,yBAAyB,UAAU;AAAA,QACtD,GAAG;AAAA,QACH,YAAY,CAAC,WAAW,aAAa;AACnC,gBAAM,QACJ,SAAS,QAAQ,IACb,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,SAAS,SAAS,KAAK,CAAC,IACzD;AACN,mBAAS,CAAC,YAAY;AACpB,gBAAI,YAAY,KAAK,aAAa,QAAQ,OAAQ,QAAO;AACzD,kBAAM,OAAO,QAAQ,MAAM;AAC3B,kBAAM,QAAQ,KAAK,SAAS;AAC5B,gBAAI,CAAC,MAAO,QAAO;AACnB,iBAAK,SAAS,IAAI,EAAE,GAAG,OAAO,UAAU,MAAM;AAC9C,mBAAO;AAAA,UACT,CAAC;AACD,cAAI,gBAAgB;AAClB,gBAAI;AACF,6BAAe,WAAW,QAAQ;AAAA,YACpC,QAAQ;AAAA,YAER;AAAA,UACF;AAAA,QACF;AAAA,MACF,CAAC,EAAE,MAAM,CAAC,QAAQ;AAGhB,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,eAAO;AAAA,UACL,OAAO,CAAC;AAAA,UACR,QAAQ,SAAS,IAAI,CAAC,MAAM,gBAAgB;AAAA,YAC1C,UAAU,KAAK;AAAA,YACf,kBAAkB,KAAK;AAAA,YACvB;AAAA,YACA,QAAQ;AAAA,YACR;AAAA,UACF,EAAE;AAAA,QACJ;AAAA,MACF,CAAC;AAED,eAAS,CAAC,YAAY;AACpB,cAAM,eAAe,oBAAI,IAA0C;AACnE,mBAAW,WAAW,OAAO,QAAQ;AACnC,cAAI,CAAC,aAAa,IAAI,QAAQ,QAAQ,GAAG;AACvC,yBAAa,IAAI,QAAQ,UAAU,OAAO;AAAA,UAC5C;AAAA,QACF;AACA,eAAO,QAAQ,IAAI,CAAC,OAAO,UAAU;AACnC,gBAAM,UAAU,OAAO,MAAM;AAAA,YAC3B,CAAC,MAAM,cAAc,cAAc,SAAS,KAAK,aAAa,MAAM;AAAA,UACtE;AACA,cAAI,SAAS;AACX,mBAAO;AAAA,cACL,GAAG;AAAA,cACH,UAAU;AAAA,cACV,QAAQ;AAAA,cACR,cAAc,QAAQ;AAAA,YACxB;AAAA,UACF;AACA,gBAAM,UAAU,aAAa,IAAI,MAAM,QAAQ;AAC/C,cAAI,SAAS;AACX,yBAAa,OAAO,MAAM,QAAQ;AAClC,mBAAO;AAAA,cACL,GAAG;AAAA,cACH,QAAQ;AAAA,cACR,QAAQ,QAAQ;AAAA,cAChB,OAAO,QAAQ;AAAA,YACjB;AAAA,UACF;AAEA,iBAAO;AAAA,YACL,GAAG;AAAA,YACH,QAAQ;AAAA,YACR,QAAQ;AAAA,UACV;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AAED,cAAQ,KAAK;AACb,aAAO;AAAA,IACT;AAAA,IACA,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ function useAiShortcuts(options) {
4
+ const { onSubmit, onCancel, enabled = true } = options;
5
+ const onSubmitRef = React.useRef(onSubmit);
6
+ const onCancelRef = React.useRef(onCancel);
7
+ React.useEffect(() => {
8
+ onSubmitRef.current = onSubmit;
9
+ }, [onSubmit]);
10
+ React.useEffect(() => {
11
+ onCancelRef.current = onCancel;
12
+ }, [onCancel]);
13
+ const handleKeyDown = React.useCallback(
14
+ (event) => {
15
+ if (!enabled) return false;
16
+ if (event.key === "Enter" && !event.shiftKey) {
17
+ if (onSubmitRef.current) {
18
+ event.preventDefault();
19
+ onSubmitRef.current();
20
+ return true;
21
+ }
22
+ return false;
23
+ }
24
+ if (event.key === "Escape") {
25
+ if (onCancelRef.current) {
26
+ event.preventDefault();
27
+ onCancelRef.current();
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ return false;
33
+ },
34
+ [enabled]
35
+ );
36
+ return { handleKeyDown };
37
+ }
38
+ var useAiShortcuts_default = useAiShortcuts;
39
+ export {
40
+ useAiShortcuts_default as default,
41
+ useAiShortcuts
42
+ };
43
+ //# sourceMappingURL=useAiShortcuts.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ai/useAiShortcuts.ts"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\n\n/**\n * Shared keyboard-shortcut hook for the AI surfaces shipped in Phase 2\n * (Step 4.6 / Phase 2 WS-B polish). Centralises the `Cmd/Ctrl+Enter` and\n * `Escape` handling used by `<AiChat>`, the AI playground, and the agent\n * settings page so every surface honours the same shortcuts without each\n * page rolling its own listener.\n *\n * - `onSubmit` fires on `Enter` (without `Shift`) when the shortcut is\n * triggered while focus is inside the bound element. `Shift+Enter` is\n * left to the browser for native newline insertion.\n * - `onCancel` fires on `Escape`. Callers decide what cancel means (abort an\n * in-flight stream, blur the composer, close a drawer, reset a draft).\n * - `enabled` gates the hook for conditional bindings without unmounting.\n *\n * The hook is deliberately minimal. It never stops propagation; callers that\n * embed modal dialogs keep their own Escape handling because React events\n * bubble predictably.\n */\nexport interface UseAiShortcutsOptions {\n onSubmit?: () => void\n onCancel?: () => void\n enabled?: boolean\n}\n\nexport interface UseAiShortcutsResult {\n /**\n * Keyboard handler ready to be attached via `onKeyDown`. Returns `true`\n * when the event matched a shortcut so callers can branch on the result.\n */\n handleKeyDown: (event: React.KeyboardEvent) => boolean\n}\n\nexport function useAiShortcuts(options: UseAiShortcutsOptions): UseAiShortcutsResult {\n const { onSubmit, onCancel, enabled = true } = options\n\n const onSubmitRef = React.useRef(onSubmit)\n const onCancelRef = React.useRef(onCancel)\n React.useEffect(() => {\n onSubmitRef.current = onSubmit\n }, [onSubmit])\n React.useEffect(() => {\n onCancelRef.current = onCancel\n }, [onCancel])\n\n const handleKeyDown = React.useCallback<UseAiShortcutsResult['handleKeyDown']>(\n (event) => {\n if (!enabled) return false\n // Enter \u2014 primary submit. Shift+Enter inserts a newline instead.\n if (event.key === 'Enter' && !event.shiftKey) {\n if (onSubmitRef.current) {\n event.preventDefault()\n onSubmitRef.current()\n return true\n }\n return false\n }\n // Escape \u2014 secondary cancel. Never swallow unless a handler is bound so\n // parent dialogs can still handle Escape the native way.\n if (event.key === 'Escape') {\n if (onCancelRef.current) {\n event.preventDefault()\n onCancelRef.current()\n return true\n }\n return false\n }\n return false\n },\n [enabled],\n )\n\n return { handleKeyDown }\n}\n\nexport default useAiShortcuts\n"],
5
+ "mappings": ";AAEA,YAAY,WAAW;AAkChB,SAAS,eAAe,SAAsD;AACnF,QAAM,EAAE,UAAU,UAAU,UAAU,KAAK,IAAI;AAE/C,QAAM,cAAc,MAAM,OAAO,QAAQ;AACzC,QAAM,cAAc,MAAM,OAAO,QAAQ;AACzC,QAAM,UAAU,MAAM;AACpB,gBAAY,UAAU;AAAA,EACxB,GAAG,CAAC,QAAQ,CAAC;AACb,QAAM,UAAU,MAAM;AACpB,gBAAY,UAAU;AAAA,EACxB,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,gBAAgB,MAAM;AAAA,IAC1B,CAAC,UAAU;AACT,UAAI,CAAC,QAAS,QAAO;AAErB,UAAI,MAAM,QAAQ,WAAW,CAAC,MAAM,UAAU;AAC5C,YAAI,YAAY,SAAS;AACvB,gBAAM,eAAe;AACrB,sBAAY,QAAQ;AACpB,iBAAO;AAAA,QACT;AACA,eAAO;AAAA,MACT;AAGA,UAAI,MAAM,QAAQ,UAAU;AAC1B,YAAI,YAAY,SAAS;AACvB,gBAAM,eAAe;AACrB,sBAAY,QAAQ;AACpB,iBAAO;AAAA,QACT;AACA,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAAA,IACA,CAAC,OAAO;AAAA,EACV;AAEA,SAAO,EAAE,cAAc;AACzB;AAEA,IAAO,yBAAQ;",
6
+ "names": []
7
+ }
@@ -26,6 +26,9 @@ import { resolveInjectedIcon } from "./injection/resolveInjectedIcon.js";
26
26
  import { useEventBridge } from "./injection/eventBridge.js";
27
27
  import { StatusBadgeInjectionSpot } from "./injection/StatusBadgeInjectionSpot.js";
28
28
  import { UmesDevToolsPanel } from "./devtools/index.js";
29
+ import { AiDockProvider } from "../ai/AiDock.js";
30
+ import { AiChatSessionsProvider } from "../ai/AiChatSessions.js";
31
+ import { AiAssistantLauncher } from "../ai/AiAssistantLauncher.js";
29
32
  import { BackendChromeProvider, useBackendChrome } from "./BackendChromeProvider.js";
30
33
  import {
31
34
  BACKEND_LAYOUT_FOOTER_INJECTION_SPOT_ID,
@@ -257,7 +260,7 @@ function Chevron({ open }) {
257
260
  return /* @__PURE__ */ jsx("svg", { className: `transition-transform ${open ? "rotate-180" : ""}`, width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("path", { d: "M6 9l6 6 6-6" }) });
258
261
  }
259
262
  function AppShell(props) {
260
- return /* @__PURE__ */ jsx(QueryProvider, { children: /* @__PURE__ */ jsx(BackendChromeProvider, { adminNavApi: props.adminNavApi, children: /* @__PURE__ */ jsx(AppShellBody, { ...props }) }) });
263
+ return /* @__PURE__ */ jsx(QueryProvider, { children: /* @__PURE__ */ jsx(BackendChromeProvider, { adminNavApi: props.adminNavApi, children: /* @__PURE__ */ jsx(AiChatSessionsProvider, { children: /* @__PURE__ */ jsx(AiDockProvider, { children: /* @__PURE__ */ jsx(AppShellBody, { ...props }) }) }) }) });
261
264
  }
262
265
  function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, children, sidebarCollapsedDefault = false, currentTitle, breadcrumb, version, settingsSectionTitle, settingsPathPrefixes = [], settingsSections, profileSections, profileSectionTitle, profilePathPrefixes = [], mobileSidebarSlot }) {
263
266
  const pathname = usePathname();
@@ -316,11 +319,11 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
316
319
  };
317
320
  update();
318
321
  target.addEventListener("scroll", update, { passive: true });
319
- const ro = new ResizeObserver(update);
320
- ro.observe(target);
322
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(update) : null;
323
+ ro?.observe(target);
321
324
  return () => {
322
325
  target.removeEventListener("scroll", update);
323
- ro.disconnect();
326
+ ro?.disconnect();
324
327
  };
325
328
  }, [pathname, effectiveCollapsed]);
326
329
  const injectionContext = React.useMemo(
@@ -964,6 +967,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
964
967
  }
965
968
  ),
966
969
  renderedTopbarInjectedActions,
970
+ /* @__PURE__ */ jsx(AiAssistantLauncher, { variant: "topbar" }),
967
971
  rightHeaderSlot ? rightHeaderSlot : /* @__PURE__ */ jsx("span", { className: "opacity-80", children: email || t("appShell.userFallback") })
968
972
  ] })
969
973
  ] }),