@conduction/nextcloud-vue 2.145.0 → 2.147.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue2.js.map +1 -1
- package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js.map +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +12 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +42 -30
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +22 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +112 -15
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +40 -16
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js +1 -0
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js +21 -2
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
- package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +2 -2
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +634 -301
- package/dist/esm/utils/detailObjectContext.js +7 -2
- package/dist/esm/utils/detailObjectContext.js.map +1 -1
- package/dist/esm/utils/objectParties.js +30 -1
- package/dist/esm/utils/objectParties.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/visibleWhen.js +6 -0
- package/dist/esm/utils/visibleWhen.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +362 -138
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +634 -301
- package/package.json +1 -1
- package/src/components/CnAiCompanion/CnAiChatPanel.vue +3 -0
- package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +4 -2
- package/src/components/CnDataTable/CnDataTable.vue +19 -2
- package/src/components/CnDetailPage/CnDetailPage.md +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +23 -0
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +1 -0
- package/src/components/CnFactsWidget/CnFactsWidget.vue +168 -16
- package/src/components/CnNotesCard/CnNotesCard.vue +7 -2
- package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +21 -2
- package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +2 -1
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +1 -0
- package/src/components/CnRetentionWidget/CnRetentionWidget.vue +2 -0
- package/src/components/CnTabsWidget/CnTabsWidget.vue +6 -1
- package/src/css/dialog.css +7 -0
- package/src/css/look-board-detail.css +152 -8
- package/src/css/look-board-index.css +78 -0
- package/src/css/look-board.css +33 -3
- package/src/css/table.css +9 -0
- package/src/schemas/app-manifest-v2.schema.json +2 -2
- package/src/utils/detailObjectContext.js +7 -2
- package/src/utils/objectParties.js +29 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
- package/src/utils/visibleWhen.js +6 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnAiChatPanel.vue3.js","sources":["../../../../src/components/CnAiCompanion/CnAiChatPanel.vue?vue&type=template&id=7359d924&lang.js"],"sourcesContent":["<!--\n CnAiChatPanel — the AI Chat Companion's own floating window.\n\n ⚠️ DELIBERATELY NOT AN NcAppSidebar. It used to be one, and the reasons it\n stopped are worth keeping, because \"use the native component\" is otherwise the\n right instinct:\n\n - A sidebar belongs to the app that owns the page. This companion is injected\n on EVERY page, including third-party office editors (onlyoffice, eurooffice,\n richdocuments) that own their own chrome. Observed on the Euro-Office editor:\n NcAppSidebar docked itself to the LEFT edge, over the editor's own rail, and\n rendered without a usable close control — because its host page provides\n neither the layout container nor the app-content siblings NcAppSidebar\n positions against.\n - A sidebar reflows the host application. A window overlays it. Only the second\n is honest about being a guest.\n\n So this is a self-contained fixed-position window anchored to the launcher hex.\n It carries its own close control and never assumes anything about the page it\n is dropped onto.\n\n Titlebar: the AGENT's icon and name left-aligned (a long name gets the whole\n remaining row), then the controls pushed to the RIGHT, icon-only — labels ride\n on `title` + `aria-label`, so they reach hover and screen readers without\n spending width the agent's name needs:\n - Settings — NcActions menu. The AGENT LIVES IN HERE: choosing one is a\n setting of the session, not a peer of \"close\".\n - Sessions — NcActions menu: start a new chat, pick a recent conversation,\n or open the full searchable history.\n - Close — plain button, at the outside edge where it is hardest to hit by\n accident.\n\n Both selectors are MENUS rather than inline controls on purpose: the window is\n ~380px wide, and an inline agent dropdown plus a recent-sessions list consumed\n most of the first screen before a single message was visible.\n\n The window is anchored ABOVE the launcher with a white pointer aimed at it, and\n animates from that point, so it reads as speech coming out of the hex rather\n than as an unrelated panel that appeared nearby.\n\n The title is the AGENT, not the product. \"Hermiq\" was the same string on every\n page for every agent; the one thing a user needs to see before typing is which\n agent is about to answer — the same reason the agent list is one click away\n rather than buried in settings.\n\n Two views, switched by `activeView`: 'chat' (message list + input) and\n 'history' (the full searchable CnAiHistoryList). No tab bar — the sessions menu\n is how you reach history, which keeps the header to one row.\n-->\n<template>\n\t<!--\n\t The window grows out of the hex and shrinks back into it.\n\n\t `transform-origin` is set per corner to the pointer's tip — the point the\n\t hex sits under — so the scale animation converges on the launcher rather\n\t than on the window's own centre. That single property is what makes it read\n\t as \"this came from there\" instead of \"a box faded in\".\n\t-->\n\t<transition :name=\"`cn-ai-chat-window-pop-${position}`\">\n\t\t<div\n\t\t\tv-if=\"visible\"\n\t\t\tclass=\"cn-ai-chat-window\"\n\t\t\t:class=\"[embedded ? 'cn-ai-chat-window--embedded' : `cn-ai-chat-window--${position}`, { 'cn-ai-chat-window--has-mark': assistantMark, 'cn-ai-chat-window--titled': titled }]\"\n\t\t\t:role=\"embedded ? 'region' : 'dialog'\"\n\t\t\t:aria-label=\"agentLabel\"\n\t\t\tdata-testid=\"cn-ai-panel\"\n\t\t\t@keydown.esc=\"onEscape\">\n\t\t\t<!-- In a titled card the card names the panel; its controls go to the card's menu (`menu` event). -->\n\t\t\t<header v-if=\"!titled\" class=\"cn-ai-chat-window__titlebar\">\n\t\t\t\t<!--\n\t\t\t Identity FIRST in source order and left-aligned: a long agent name\n\t\t\t gets the whole remaining row, and the controls are pushed right by\n\t\t\t the identity's `flex: 1`. Reading order matches visual order, so\n\t\t\t screen readers announce whose window this is before its controls.\n\t\t\t-->\n\t\t\t\t<div class=\"cn-ai-chat-window__identity\" :title=\"agentLabel\">\n\t\t\t\t\t<!--\n\t\t\t\t The AGENT's icon, not a generic sparkle. `Agent.icon` is an MDI\n\t\t\t\t registry name picked with CnIconPicker, so the same icon shown in\n\t\t\t\t the agent list and on its detail page is shown here — the window\n\t\t\t\t is that agent's window, and a shared mark is what says so.\n\t\t\t\t Falls back to the list's default agent icon when unset.\n\t\t\t\t-->\n\t\t\t\t\t<CnDashboardIcon\n\t\t\t\t\t\t:name=\"agentIconName\"\n\t\t\t\t\t\t:size=\"16\"\n\t\t\t\t\t\tclass=\"cn-ai-chat-window__identity-icon\" />\n\t\t\t\t\t<span class=\"cn-ai-chat-window__identity-name\">{{ agentLabel }}</span>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"cn-ai-chat-window__controls\">\n\t\t\t\t\t<!--\n\t\t\t\t Settings first, close last — destructive-adjacent controls sit\n\t\t\t\t at the outside edge where they are hardest to hit by accident,\n\t\t\t\t and the menus a user opens repeatedly sit inboard.\n\n\t\t\t\t The AGENT LIVES IN HERE rather than in its own button: picking\n\t\t\t\t an agent is a setting of the session, not a peer of \"close\".\n\t\t\t\t-->\n\t\t\t\t\t<NcActions\n\t\t\t\t\t\t:aria-label=\"cnTranslate('Session settings')\"\n\t\t\t\t\t\t:title=\"cnTranslate('Session settings')\"\n\t\t\t\t\t\t:forceMenu=\"true\"\n\t\t\t\t\t\tdata-testid=\"cn-ai-panel-settings\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Cog :size=\"18\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t Read-aloud lives in SESSION settings, not per message: it is\n\t\t\t\t\t a preference about how you want to receive answers, not an\n\t\t\t\t\t action on one of them.\n\n\t\t\t\t\t Unlike dictation this works on an insecure origin —\n\t\t\t\t\t speechSynthesis has no secure-context requirement, so it is\n\t\t\t\t\t offered here even where the microphone cannot be.\n\t\t\t\t\t-->\n\t\t\t\t\t\t<NcActionCheckbox\n\t\t\t\t\t\t\t:modelValue=\"speakReplies\"\n\t\t\t\t\t\t\t:disabled=\"speechSynthesisSupported === false\"\n\t\t\t\t\t\t\tdata-testid=\"cn-ai-panel-speak-toggle\"\n\t\t\t\t\t\t\t@update:modelValue=\"onSpeakRepliesToggled\">\n\t\t\t\t\t\t\t{{ speechSynthesisSupported\n\t\t\t\t\t\t\t\t? cnTranslate('Read replies aloud')\n\t\t\t\t\t\t\t\t: cnTranslate('Read replies aloud (not supported here)') }}\n\t\t\t\t\t\t</NcActionCheckbox>\n\t\t\t\t\t\t<NcActionSeparator />\n\t\t\t\t\t\t<NcActionCaption :name=\"agentCaption\" />\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-for=\"agent in visibleAgentOptions\"\n\t\t\t\t\t\t\t:key=\"agent.id\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"onAgentSelected(agent.id)\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<Check v-if=\"agent.id === selectedAgentUuid\" :size=\"20\" />\n\t\t\t\t\t\t\t\t<CnDashboardIcon v-else :name=\"agent.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ agent.label }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t The escape hatch is NOT optional. The relevance rule below is a\n\t\t\t\t\t heuristic over tool ids, so it will be wrong sometimes; an agent\n\t\t\t\t\t the user knows exists and cannot reach reads as data loss. This\n\t\t\t\t\t stays visible whenever anything is being hidden.\n\t\t\t\t\t-->\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-if=\"hiddenAgentCount > 0\"\n\t\t\t\t\t\t\t:closeAfterClick=\"false\"\n\t\t\t\t\t\t\tdata-testid=\"cn-ai-panel-all-agents\"\n\t\t\t\t\t\t\t@click=\"showAllAgents = true\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<DotsHorizontal :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ cnTranslate('All agents') }} ({{ agentOptions.length }})\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<NcActionCaption\n\t\t\t\t\t\t\tv-if=\"agentOptions.length === 0\"\n\t\t\t\t\t\t\t:name=\"agentsFetchError ? cnTranslate('Could not load agents') : cnTranslate('No agents available')\" />\n\t\t\t\t\t</NcActions>\n\n\t\t\t\t\t<NcActions\n\t\t\t\t\t\t:aria-label=\"cnTranslate('Sessions')\"\n\t\t\t\t\t\t:title=\"cnTranslate('Sessions')\"\n\t\t\t\t\t\t:forceMenu=\"true\"\n\t\t\t\t\t\tdata-testid=\"cn-ai-panel-sessions\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<History :size=\"18\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\t:disabled=\"isOnNewChatScreen\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"onNewChat\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<Plus :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ cnTranslate('Start new chat') }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<NcActionCaption :name=\"cnTranslate('Recent conversations')\" />\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-for=\"conversation in recentConversations\"\n\t\t\t\t\t\t\t:key=\"conversation.uuid\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"onConversationSelect(conversation.uuid)\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<MessageTextOutline :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ conversationLabel(conversation) }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"activeView = 'history'\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<History :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ cnTranslate('View all conversations') }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t</NcActions>\n\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-if=\"!embedded\"\n\t\t\t\t\t\t:aria-label=\"cnTranslate('Close')\"\n\t\t\t\t\t\t:title=\"cnTranslate('Close')\"\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\tdata-testid=\"cn-ai-panel-close\"\n\t\t\t\t\t\t@click=\"onClose\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Close :size=\"18\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</header>\n\n\t\t\t<div v-if=\"activeView === 'chat'\" class=\"cn-ai-chat-window__body\">\n\t\t\t\t<div class=\"cn-ai-chat-window__messages\">\n\t\t\t\t\t<CnAiMessageList\n\t\t\t\t\t\t:messages=\"streamState.messages\"\n\t\t\t\t\t\t:currentText=\"streamState.currentText\"\n\t\t\t\t\t\t:isStreaming=\"streamState.isStreaming\">\n\t\t\t\t\t\t<template #empty>\n\t\t\t\t\t\t\t<div class=\"cn-ai-chat-window__start\" data-testid=\"cn-ai-chat-tab-start\">\n\t\t\t\t\t\t\t\t<NcEmptyContent :name=\"agentLabel\">\n\t\t\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t\t\t<Creation :size=\"40\" />\n\t\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t\t<template #description>\n\t\t\t\t\t\t\t\t\t\t{{ cnTranslate('Ask me anything about what you are viewing.') }}\n\t\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t</NcEmptyContent>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnAiMessageList>\n\t\t\t\t</div>\n\n\t\t\t\t<!--\n\t\t\t\t Decide it HERE. An approval the agent is blocked on is\n\t\t\t\t actionable where the user already is, rather than on an\n\t\t\t\t oversight screen they have to know about and navigate to.\n\n\t\t\t\t Rendered from a GENERIC shape: each entry brings its own `kind`\n\t\t\t\t and its own `resolveUrl`, so a future approval (fetching a URL,\n\t\t\t\t editing a file that is not open) renders and resolves through\n\t\t\t\t this same block unchanged.\n\t\t\t\t-->\n\t\t\t\t<ul\n\t\t\t\t\tv-if=\"pendingApprovals.length\"\n\t\t\t\t\tclass=\"cn-ai-chat-window__approvals\"\n\t\t\t\t\tdata-testid=\"cn-ai-panel-approvals\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"approval in pendingApprovals\"\n\t\t\t\t\t\t:key=\"approval.id\"\n\t\t\t\t\t\tclass=\"cn-ai-approval\">\n\t\t\t\t\t\t<div class=\"cn-ai-approval__head\">\n\t\t\t\t\t\t\t<span class=\"cn-ai-approval__title\">{{ approval.title }}</span>\n\t\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t Reach is a FACT about the tool. The reason below is\n\t\t\t\t\t\t\t agent-authored text aimed at the person holding the\n\t\t\t\t\t\t\t permission, so the two are kept visually distinct.\n\t\t\t\t\t\t\t-->\n\t\t\t\t\t\t\t<span class=\"cn-ai-approval__reach\">{{ approval.reach }}</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<p v-if=\"approval.reason\" class=\"cn-ai-approval__reason\">\n\t\t\t\t\t\t\t{{ approval.reason }}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<div class=\"cn-ai-approval__actions\">\n\t\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\t\t\t:disabled=\"resolvingApprovalId === approval.id\"\n\t\t\t\t\t\t\t\t:data-testid=\"'cn-ai-approval-allow-' + approval.id\"\n\t\t\t\t\t\t\t\t@click=\"onApprovalDecision(approval, 'granted')\">\n\t\t\t\t\t\t\t\t{{ cnTranslate('Allow') }}\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t\t:disabled=\"resolvingApprovalId === approval.id\"\n\t\t\t\t\t\t\t\t:data-testid=\"'cn-ai-approval-deny-' + approval.id\"\n\t\t\t\t\t\t\t\t@click=\"onApprovalDecision(approval, 'refused')\">\n\t\t\t\t\t\t\t\t{{ cnTranslate('Decline') }}\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\n\t\t\t\t<!--\n\t\t\t\t Say why a turn failed, where the answer would have been.\n\n\t\t\t\t The stream already carries `{code, message}` for a refused or\n\t\t\t\t broken turn, and nothing rendered it: measured 2026-09-18 on a\n\t\t\t\t dev instance, an agent whose tool grants resolved to no tools\n\t\t\t\t answered with an SSE `error` event, and the window showed\n\t\t\t\t \"Thinking\", then nothing at all. Silence reads as a hung app.\n\t\t\t\t-->\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"streamState.error\"\n\t\t\t\t\tclass=\"cn-ai-chat-window__error\"\n\t\t\t\t\trole=\"alert\"\n\t\t\t\t\tdata-testid=\"cn-ai-panel-error\">\n\t\t\t\t\t{{ streamState.error.message || cnTranslate('The assistant could not answer.') }}\n\t\t\t\t</p>\n\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"note\"\n\t\t\t\t\tclass=\"cn-ai-chat-window__note\"\n\t\t\t\t\tdata-testid=\"cn-ai-panel-note\">\n\t\t\t\t\t<InformationOutline :size=\"18\" class=\"cn-ai-chat-window__note-icon\" />\n\t\t\t\t\t<span>{{ note }}</span>\n\t\t\t\t</p>\n\n\t\t\t\t<div class=\"cn-ai-chat-window__input\">\n\t\t\t\t\t<CnAiInput\n\t\t\t\t\t\tref=\"input\"\n\t\t\t\t\t\t:disabled=\"streamState.isStreaming\"\n\t\t\t\t\t\t:placeholder=\"inputPlaceholder\"\n\t\t\t\t\t\t:sendLabel=\"sendLabel\"\n\t\t\t\t\t\t:stacked=\"titled\"\n\t\t\t\t\t\t:chatAppId=\"chatAppId\"\n\t\t\t\t\t\t:speechInputEngine=\"speechPolicy.inputEngine\"\n\t\t\t\t\t\t:dictationSilenceTimeout=\"speechPolicy.silenceTimeout\"\n\t\t\t\t\t\t:localSpeechAvailable=\"localSpeechAvailable\"\n\t\t\t\t\t\t:conversationEnabled=\"speechPolicy.conversationEnabled\"\n\t\t\t\t\t\t@send=\"onSend\"\n\t\t\t\t\t\t@conversationState=\"onConversationTurn\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div v-else class=\"cn-ai-chat-window__body cn-ai-chat-window__body--history\">\n\t\t\t\t<CnAiHistoryList\n\t\t\t\t\t:conversations=\"conversations\"\n\t\t\t\t\t:loading=\"conversationsLoading\"\n\t\t\t\t\t:fetchError=\"conversationsFetchError\"\n\t\t\t\t\t:activeConversationUuid=\"activeConversationUuid\"\n\t\t\t\t\t:chatAppId=\"chatAppId\"\n\t\t\t\t\t:searchable=\"true\"\n\t\t\t\t\t@select=\"onConversationSelect\"\n\t\t\t\t\t@renamed=\"onConversationRenamed\" />\n\t\t\t</div>\n\n\t\t\t<!--\n\t\t\t The organisation's approved-assistant mark (thematiq). Rendered\n\t\t\t only when thematiq reports it on; the label arrives translated\n\t\t\t and is shown as received. Present in the chat and history views.\n\t\t\t-->\n\t\t\t<div\n\t\t\t\tv-if=\"assistantMark\"\n\t\t\t\tclass=\"cn-ai-chat-window__mark\"\n\t\t\t\trole=\"note\"\n\t\t\t\tdata-testid=\"cn-ai-panel-mark\">\n\t\t\t\t<img\n\t\t\t\t\tv-if=\"assistantMark.logo\"\n\t\t\t\t\tclass=\"cn-ai-chat-window__mark-logo\"\n\t\t\t\t\t:src=\"assistantMark.logo.url\"\n\t\t\t\t\t:alt=\"assistantMark.logo.alt\">\n\t\t\t\t<span class=\"cn-ai-chat-window__mark-label\">{{ assistantMark.label }}</span>\n\t\t\t</div>\n\t\t</div>\n\t</transition>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { NcActionButton, NcActionCaption, NcActionCheckbox, NcActions, NcActionSeparator, NcButton, NcEmptyContent } from '@nextcloud/vue'\nimport Check from 'vue-material-design-icons/Check.vue'\nimport Close from 'vue-material-design-icons/Close.vue'\nimport Cog from 'vue-material-design-icons/Cog.vue'\nimport Creation from 'vue-material-design-icons/Creation.vue'\nimport DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'\nimport History from 'vue-material-design-icons/History.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport MessageTextOutline from 'vue-material-design-icons/MessageTextOutline.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport CnDashboardIcon from '../CnIconPicker/CnDashboardIcon.vue'\nimport CnAiHistoryList from './CnAiHistoryList.vue'\nimport CnAiInput from './CnAiInput.vue'\nimport CnAiMessageList from './CnAiMessageList.vue'\nimport {\n\tagentsUrl,\n\tconversationsUrl,\n\tDEFAULT_CHAT_APP_ID,\n\tnormalizeConversation,\n\tspeechCapabilitiesUrl,\n\tspeechSynthesisUrl,\n} from '../../composables/aiChatConfig.js'\nimport {\n\tbrowserSynthesisUsable,\n\tnormalizeAgentSpeechPolicy,\n\tresolveSpeakingEngine,\n\tSPEECH_LOCAL,\n\tSPEECH_OFF,\n} from '../../composables/aiSpeechPolicy.js'\nimport { getAssistantMark } from '../../utils/assistantMark.js'\nimport { libraryTranslate } from '../../utils/libraryTranslate.js'\n\n/**\n * Shown when an agent has no `icon` set, which is most of them. Matches the\n * icon the agent list has always used, so an agent without an icon does not\n * change appearance between the list and the window title.\n *\n * @type {string}\n */\nconst DEFAULT_AGENT_ICON = 'RobotOutline'\n\n/** localStorage key remembering whether replies are read aloud. */\nconst SPEAK_PREFERENCE_KEY = 'cn-ai-speak-replies'\n\n/** Recent conversations offered directly in the sessions menu — top N of the fetched list. */\nconst RECENT_CONVERSATIONS_LIMIT = 5\n\n/** Longest conversation title shown in the sessions menu before ellipsis. */\nconst MENU_TITLE_MAX = 40\n\nexport default {\n\tname: 'CnAiChatPanel',\n\n\tcomponents: {\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcActionCaption,\n\t\tNcActionCheckbox,\n\t\tNcActionSeparator,\n\t\tNcButton,\n\t\tNcEmptyContent,\n\t\tPlus,\n\t\tHistory,\n\t\tInformationOutline,\n\t\tCreation,\n\t\tClose,\n\t\tCheck,\n\t\tCog,\n\t\tMessageTextOutline,\n\t\tDotsHorizontal,\n\t\tCnAiMessageList,\n\t\tCnAiInput,\n\t\tCnAiHistoryList,\n\t\tCnDashboardIcon,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: () => libraryTranslate },\n\t},\n\n\tprops: {\n\t\t/** Controls panel visibility */\n\t\tvisible: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Draw the window in the page's flow, as a panel of the page (a tab\n\t\t * of a detail page), instead of floating above it from the launcher:\n\t\t * no pointer, no close button, and Escape does not close it.\n\t\t */\n\t\tembedded: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * The panel stands in a card that draws its title: no titlebar of its\n\t\t * own, and the session controls (new chat, conversations, read aloud,\n\t\t * agent) are reported through the `menu` event for the card's menu.\n\t\t */\n\t\ttitled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** A line above the input, already translated (what the assistant can see). */\n\t\tnote: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The input's placeholder, already translated; '' keeps the default. */\n\t\tinputPlaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** A label on the send button, already translated; '' keeps the icon. */\n\t\tsendLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Reactive state from useAiChatStream: `{ messages, currentText, isStreaming, conversationUuid, error }`.\n\t\t */\n\t\tstreamState: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * The launcher button, kept for focus return on close.\n\t\t */\n\t\tfabRef: {\n\t\t\ttype: [Object, null],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Corner the window is anchored to — mirrors the launcher hex's own\n\t\t * `position`, so the window opens from the button rather than across\n\t\t * the page from it.\n\t\t *\n\t\t * @type {'bottom-right'|'bottom-left'|'top-right'|'top-left'}\n\t\t */\n\t\tposition: {\n\t\t\ttype: String,\n\t\t\tdefault: 'bottom-right',\n\t\t},\n\n\t\t/**\n\t\t * Backend app id that answers the chat/agents/conversations HTTP calls.\n\t\t */\n\t\tchatAppId: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_CHAT_APP_ID,\n\t\t},\n\n\t\t/**\n\t\t * The page the companion is sitting on: `{ appId, pageKind, fileId, route }`.\n\t\t *\n\t\t * Used ONLY to decide which agents are worth offering first. An empty\n\t\t * object means \"no page context\", and every agent is listed — the\n\t\t * behaviour before filtering existed.\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\t/**\n\t * Declared so the linter can check them and a consumer can read the\n\t * component's outward surface without grepping for `$emit`. These four were\n\t * always emitted; only the declaration is new.\n\t */\n\temits: ['close', 'send', 'new-thread', 'load-conversation', 'menu'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** The approved-assistant mark from thematiq, or null when there is none. */\n\t\t\tassistantMark: null,\n\t\t\tactiveView: 'chat',\n\t\t\tactiveConversationUuid: null,\n\t\t\t/**\n\t\t\t * Whether the user has asked past the page-relevance filter. Sticky\n\t\t\t * for the life of the panel: having asked for everything once, being\n\t\t\t * silently re-filtered on the next open is the annoying half of this\n\t\t\t * feature.\n\t\t\t */\n\t\t\tshowAllAgents: false,\n\t\t\t/**\n\t\t\t * Whether assistant replies are read aloud. Remembered across\n\t\t\t * sessions — a preference a user turns on once, not per chat.\n\t\t\t */\n\t\t\tspeakReplies: false,\n\t\t\t/** Text already spoken, so a re-render never repeats an answer. */\n\t\t\tspokenText: '',\n\t\t\t/** Approvals decided locally, hidden until the server confirms. */\n\t\t\tresolvedApprovalIds: [],\n\t\t\t/** The approval currently being posted, so it cannot double-fire. */\n\t\t\tresolvingApprovalId: '',\n\t\t\tagents: [],\n\t\t\tagentsLoading: false,\n\t\t\tagentsFetchError: false,\n\t\t\tselectedAgentUuid: null,\n\t\t\t/**\n\t\t\t * Whether the backend's own speech service answered its probe.\n\t\t\t *\n\t\t\t * Probed ONCE per panel and passed down, rather than asked by each\n\t\t\t * composer. Starts false so nothing offers a private engine before\n\t\t\t * the answer arrives — the wrong direction to be wrong in is the one\n\t\t\t * where a \"private\" microphone turns out to be a cloud one.\n\t\t\t */\n\t\t\tlocalSpeechAvailable: false,\n\t\t\t/** The `<audio>` element playing a locally synthesised reply, if any. */\n\t\t\tlocalSpeechAudio: null,\n\t\t\t/** Whether the composer is in a hands-free conversation. */\n\t\t\tconversationActive: false,\n\t\t\tconversations: [],\n\t\t\tconversationsLoading: false,\n\t\t\tconversationsFetchError: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Top N conversations offered directly in the sessions menu. */\n\t\trecentConversations() {\n\t\t\treturn this.conversations.slice(0, RECENT_CONVERSATIONS_LIMIT)\n\t\t},\n\n\t\t/**\n\t\t * Agents as `{ id, label }`, matching the shape the old inline picker\n\t\t * used — the agents API returns `uuid` on some deployments and `id` on\n\t\t * others, and both have been seen in the wild.\n\t\t *\n\t\t * @return {Array<{id: string, label: string}>} Selectable agents.\n\t\t */\n\t\t/**\n\t\t * The raw agent object currently selected, or null.\n\t\t *\n\t\t * ⚠️ The RAW object, not the `agentOptions` entry. Those carry only an\n\t\t * id, a label and an icon — everything the speech policy needs is\n\t\t * dropped by that mapping, so reading the policy from an option would\n\t\t * silently produce defaults for every agent.\n\t\t *\n\t\t * @return {object|null} The selected agent.\n\t\t */\n\t\tselectedAgent() {\n\t\t\tif (!this.selectedAgentUuid) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\treturn this.agents.find((agent) => (agent.uuid || agent.id) === this.selectedAgentUuid) || null\n\t\t},\n\n\t\t/**\n\t\t * The selected agent's speech policy, defaulted.\n\t\t *\n\t\t * @return {{inputEngine: string, outputEngine: string, silenceTimeout: number, conversationEnabled: boolean}} The policy.\n\t\t */\n\t\tspeechPolicy() {\n\t\t\treturn normalizeAgentSpeechPolicy(this.selectedAgent)\n\t\t},\n\n\t\t/**\n\t\t * Which engine may speak this agent's replies.\n\t\t *\n\t\t * @return {{engine: string, reason: string}} The decision.\n\t\t */\n\t\tspeakingDecision() {\n\t\t\treturn resolveSpeakingEngine(this.speechPolicy, {\n\t\t\t\tbrowserUsable: browserSynthesisUsable(),\n\t\t\t\tlocalUsable: this.localSpeechAvailable === true,\n\t\t\t})\n\t\t},\n\n\t\tagentOptions() {\n\t\t\treturn this.agents.map((agent) => ({\n\t\t\t\tid: agent.uuid || agent.id,\n\t\t\t\tlabel: agent.name || agent.title || this.cnTranslate('Untitled agent'),\n\t\t\t\t// `Agent.icon` is an MDI registry name (CnIconPicker); empty means\n\t\t\t\t// \"use the default agent icon\", which is what the list showed for\n\t\t\t\t// every agent before any of them had one set.\n\t\t\t\ticon: agent.icon || DEFAULT_AGENT_ICON,\n\t\t\t\ttools: Array.isArray(agent.tools) ? agent.tools : [],\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The agents worth offering on THIS page, most specific first.\n\t\t *\n\t\t * ⚠️ This is a HEURISTIC over tool ids, and it is written to fail open.\n\t\t * An agent is relevant when it holds a tool whose owning app is the app\n\t\t * of the current page, OR — on a file/document page — a tool that is\n\t\t * about documents at all.\n\t\t *\n\t\t * The second clause is not decoration. The demo's document agent holds\n\t\t * `docudesk.*` tools while the page it is used on is `eurooffice`, so a\n\t\t * plain \"tool app === page app\" rule HIDES exactly the agent the user\n\t\t * wants. Matching page KIND as well as page APP is what makes the rule\n\t\t * describe capability rather than packaging.\n\t\t *\n\t\t * Falls back to every agent when the filter would empty the list — an\n\t\t * empty agent menu is indistinguishable from a broken one.\n\t\t *\n\t\t * @return {Array<object>} Agents judged relevant to the current page.\n\t\t */\n\t\trelevantAgentOptions() {\n\t\t\tconst appId = (this.context && this.context.appId) || ''\n\t\t\tconst pageKind = (this.context && this.context.pageKind) || ''\n\t\t\tif (appId === '' && pageKind === '') {\n\t\t\t\treturn this.agentOptions\n\t\t\t}\n\n\t\t\tconst isDocumentPage = pageKind === 'file' || pageKind === 'document'\n\t\t\tconst matches = this.agentOptions.filter((agent) => agent.tools.some((tool) => {\n\t\t\t\tconst id = String(tool).toLowerCase()\n\t\t\t\t// Tool ids are `app.verb` or `app_verb`; the app is the first segment.\n\t\t\t\tconst app = id.split(/[._]/)[0]\n\t\t\t\tif (appId !== '' && app === appId.toLowerCase()) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\treturn isDocumentPage && /document|file/.test(id)\n\t\t\t}))\n\n\t\t\treturn matches.length > 0 ? matches : this.agentOptions\n\t\t},\n\n\t\t/**\n\t\t * What the menu actually lists: the relevant subset, or everything once\n\t\t * the user has asked for everything.\n\t\t *\n\t\t * @return {Array<object>} Agents to render.\n\t\t */\n\t\tvisibleAgentOptions() {\n\t\t\treturn this.showAllAgents ? this.agentOptions : this.relevantAgentOptions\n\t\t},\n\n\t\t/**\n\t\t * How many agents the filter is currently hiding.\n\t\t *\n\t\t * @return {number} Hidden count, 0 when nothing is hidden.\n\t\t */\n\t\thiddenAgentCount() {\n\t\t\treturn this.agentOptions.length - this.visibleAgentOptions.length\n\t\t},\n\n\t\t/**\n\t\t * The menu heading, which must SAY when the list is filtered — a silently\n\t\t * shortened list is the same failure as a silently truncated one.\n\t\t *\n\t\t * @return {string} Caption text.\n\t\t */\n\t\tagentCaption() {\n\t\t\treturn this.hiddenAgentCount > 0\n\t\t\t\t? this.cnTranslate('Agents for this page')\n\t\t\t\t: this.cnTranslate('Agent')\n\t\t},\n\n\t\t/**\n\t\t * The selected agent's icon name, for the titlebar.\n\t\t *\n\t\t * @return {string} An MDI registry name.\n\t\t */\n\t\tagentIconName() {\n\t\t\tconst selected = this.agentOptions.find((agent) => agent.id === this.selectedAgentUuid)\n\t\t\treturn (selected && selected.icon) || DEFAULT_AGENT_ICON\n\t\t},\n\n\t\t/**\n\t\t * Whether this browser can speak at all.\n\t\t *\n\t\t * Checked as a CAPABILITY, not a constructor: the sibling dictation\n\t\t * button shipped broken because `webkitSpeechRecognition` exists on an\n\t\t * insecure origin while refusing to run. `speechSynthesis` differs —\n\t\t * it has no secure-context requirement — but it is still checked rather\n\t\t * than assumed, and the toggle says so when absent instead of silently\n\t\t * doing nothing.\n\t\t *\n\t\t * @return {boolean} true when speech synthesis is usable.\n\t\t */\n\t\t/**\n\t\t * Approvals still awaiting a decision, minus any just decided here.\n\t\t *\n\t\t * The local filter matters: the server list is refreshed only on the\n\t\t * next turn, so without it a card the user just answered would sit there\n\t\t * looking unanswered until they sent another message.\n\t\t *\n\t\t * @return {Array<object>} Approvals to render.\n\t\t */\n\t\tpendingApprovals() {\n\t\t\tconst list = (this.streamState && this.streamState.pendingApprovals) || []\n\t\t\treturn list.filter((a) => this.resolvedApprovalIds.includes(a.id) === false)\n\t\t},\n\n\t\tspeechSynthesisSupported() {\n\t\t\treturn typeof window !== 'undefined'\n\t\t\t\t&& typeof window.speechSynthesis !== 'undefined'\n\t\t\t\t&& typeof window.SpeechSynthesisUtterance === 'function'\n\t\t},\n\n\t\t/**\n\t\t * The window's title: the selected agent's name.\n\t\t *\n\t\t * Falls back while the agent list is still loading, and again if it\n\t\t * failed — a titlebar with an empty title reads as a broken window, so\n\t\t * every branch names something.\n\t\t *\n\t\t * @return {string} Title text.\n\t\t */\n\t\tagentLabel() {\n\t\t\tconst selected = this.agentOptions.find((agent) => agent.id === this.selectedAgentUuid)\n\t\t\tif (selected) {\n\t\t\t\treturn selected.label\n\t\t\t}\n\n\t\t\treturn this.agentsLoading\n\t\t\t\t? this.cnTranslate('Loading…')\n\t\t\t\t: this.cnTranslate('AI assistant')\n\t\t},\n\n\t\t/**\n\t\t * Whether the panel is already showing a fresh, empty new-chat screen —\n\t\t * used to disable \"Start new chat\" (disabled, not hidden, per design).\n\t\t */\n\t\tisOnNewChatScreen() {\n\t\t\treturn !this.streamState.isStreaming && this.streamState.messages.length === 0\n\t\t},\n\n\t\t/**\n\t\t * The titlebar's controls as menu items, for a titled panel's card.\n\t\t *\n\t\t * @return {Array<{label: string, icon: string, run: () => void}>} The items.\n\t\t */\n\t\tmenuActions() {\n\t\t\tconst actions = []\n\t\t\tif (!this.isOnNewChatScreen) {\n\t\t\t\tactions.push({ label: this.cnTranslate('Start new chat'), icon: 'Plus', run: () => this.onNewChat() })\n\t\t\t}\n\t\t\tactions.push(this.activeView === 'history'\n\t\t\t\t? { label: this.cnTranslate('Back to the chat'), icon: 'MessageTextOutline', run: () => { this.activeView = 'chat' } }\n\t\t\t\t: { label: this.cnTranslate('View all conversations'), icon: 'History', run: () => { this.activeView = 'history' } })\n\t\t\tif (this.speechSynthesisSupported) {\n\t\t\t\tactions.push(this.speakReplies\n\t\t\t\t\t? { label: this.cnTranslate('Stop reading replies aloud'), icon: 'VolumeOff', run: () => this.onSpeakRepliesToggled(false) }\n\t\t\t\t\t: { label: this.cnTranslate('Read replies aloud'), icon: 'VolumeHigh', run: () => this.onSpeakRepliesToggled(true) })\n\t\t\t}\n\t\t\tfor (const agent of this.agentOptions) {\n\t\t\t\tif (agent.id !== this.selectedAgentUuid) {\n\t\t\t\t\tactions.push({ label: this.cnTranslate('Talk to {agent}', { agent: agent.label }), icon: agent.icon || DEFAULT_AGENT_ICON, run: () => this.onAgentSelected(agent.id) })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn actions\n\t\t},\n\t},\n\n\twatch: {\n\t\tmenuActions: {\n\t\t\timmediate: true,\n\t\t\thandler(actions) {\n\t\t\t\tif (this.titled) {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event menu The session controls of a titled panel, for its card's menu.\n\t\t\t\t\t * @type {Array<{label: string, icon: string, run: () => void}>}\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('menu', actions)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\tvisible(newVal) {\n\t\t\tif (newVal) {\n\t\t\t\tthis.$nextTick(() => {\n\t\t\t\t\tif (this.$refs.input) {\n\t\t\t\t\t\tthis.$refs.input.focus()\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The transport composable learns the server-assigned conversation uuid\n\t\t * from the first turn's response (see useAiChatStream.js's finalise()).\n\t\t * Mirror it into the panel's own tracked uuid so a brand-new conversation\n\t\t * shows as \"active\" immediately — the same tracking that drives\n\t\t * isOnNewChatScreen and the History/recent-sessions active-row indicator.\n\t\t *\n\t\t * @param {string|null} newVal The stream's current conversation uuid.\n\t\t */\n\t\t'streamState.conversationUuid': function(newVal) {\n\t\t\tif (newVal) {\n\t\t\t\tthis.activeConversationUuid = newVal\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Speak a finished assistant reply when read-aloud is on.\n\t\t *\n\t\t * Waits for `isStreaming` to go false rather than speaking the growing\n\t\t * text: speaking a partial answer and then speaking it again complete is\n\t\t * worse than a short wait.\n\t\t *\n\t\t * @param {boolean} streaming Whether a turn is still in flight.\n\t\t */\n\t\t'streamState.isStreaming': function(streaming) {\n\t\t\tif (streaming === true) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A spoken conversation speaks regardless of the toggle: the user is\n\t\t\t// talking to the agent with the screen out of reach, and a silent\n\t\t\t// reply in that mode is no reply at all.\n\t\t\tif (this.speakReplies !== true && this.conversationActive !== true) {\n\t\t\t\t// Nothing to speak, so the user's next turn can start now.\n\t\t\t\tthis.resumeConversationIfActive()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.speakLatestReply()\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.fetchAgents()\n\t\tthis.fetchConversations()\n\t\tthis.restoreSpeakPreference()\n\t\tthis.probeLocalSpeech()\n\t\tthis.loadAssistantMark()\n\t},\n\n\tbeforeUnmount() {\n\t\t// Never leave the browser talking to a panel that has closed.\n\t\tthis.stopSpeaking()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Load the approved-assistant mark; stays null when there is none.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadAssistantMark() {\n\t\t\tthis.assistantMark = await getAssistantMark()\n\t\t},\n\n\t\t/**\n\t\t * Read the remembered read-aloud preference.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\trestoreSpeakPreference() {\n\t\t\ttry {\n\t\t\t\tthis.speakReplies = (window.localStorage.getItem(SPEAK_PREFERENCE_KEY) === '1')\n\t\t\t} catch {\n\t\t\t\t// Storage can be unavailable (private mode, blocked cookies).\n\t\t\t\t// The toggle then simply does not persist.\n\t\t\t\tthis.speakReplies = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Turn read-aloud on or off and remember it.\n\t\t *\n\t\t * @param {boolean} value The new state.\n\t\t * @return {void}\n\t\t */\n\t\tonSpeakRepliesToggled(value) {\n\t\t\tthis.speakReplies = (value === true)\n\t\t\tif (this.speakReplies === false) {\n\t\t\t\tthis.stopSpeaking()\n\t\t\t}\n\t\t\ttry {\n\t\t\t\twindow.localStorage.setItem(SPEAK_PREFERENCE_KEY, this.speakReplies ? '1' : '0')\n\t\t\t} catch {\n\t\t\t\t// Preference is still honoured for this session.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Speak the most recent assistant message, once.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\t/**\n\t\t * Track the composer's conversation state.\n\t\t *\n\t\t * @param {boolean} active Whether a spoken conversation is running.\n\t\t * @return {void}\n\t\t */\n\t\tonConversationTurn(active) {\n\t\t\tthis.conversationActive = (active === true)\n\t\t\tif (this.conversationActive === false) {\n\t\t\t\tthis.stopSpeaking()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Hand the microphone back to the user, if a conversation is running.\n\t\t *\n\t\t * 🔴 CALLED WHEN THE SPEAKING STOPS, NOT WHEN THE STREAM DOES. Reopening\n\t\t * the microphone while the reply is still being spoken records the agent\n\t\t * through the speakers and sends it back as the user's next turn — the\n\t\t * agent then answers itself, on a loop, hands-free.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tresumeConversationIfActive() {\n\t\t\tif (this.conversationActive !== true) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst input = this.$refs.input\n\n\t\t\tif (input && typeof input.resumeConversation === 'function') {\n\t\t\t\tinput.resumeConversation()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Ask the backend whether its own speech service can actually perform.\n\t\t *\n\t\t * 🔴 ASKED, NOT ASSUMED, and asked ONCE for the whole panel. A backend\n\t\t * that has the endpoint but no reachable speech service answers\n\t\t * `available: false`, and a backend that has no such endpoint at all\n\t\t * (any app other than the agent engine) 404s — both mean the same thing\n\t\t * here: do not offer the private engine. Failing to the safe answer\n\t\t * matters more than usual, because the alternative engine is a cloud\n\t\t * one.\n\t\t *\n\t\t * @return {Promise<void>} Resolves once the answer is stored.\n\t\t */\n\t\tasync probeLocalSpeech() {\n\t\t\ttry {\n\t\t\t\tconst response = await axios.get(speechCapabilitiesUrl(this.chatAppId))\n\t\t\t\tthis.localSpeechAvailable = !!(response && response.data && response.data.available)\n\t\t\t} catch {\n\t\t\t\tthis.localSpeechAvailable = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Speak a reply through the instance's own speech service.\n\t\t *\n\t\t * @param {string} text The text to speak.\n\t\t * @return {Promise<void>} Resolves when playback has started.\n\t\t */\n\t\tasync speakLocally(text) {\n\t\t\ttry {\n\t\t\t\tconst response = await axios.post(\n\t\t\t\t\tspeechSynthesisUrl(this.chatAppId),\n\t\t\t\t\t{ text },\n\t\t\t\t\t{ responseType: 'blob' },\n\t\t\t\t)\n\t\t\t\tconst url = URL.createObjectURL(response.data)\n\t\t\t\tthis.localSpeechAudio = new Audio(url)\n\t\t\t\t// Revoked on end, not left to the page's lifetime: a spoken\n\t\t\t\t// conversation would otherwise accumulate one object URL per\n\t\t\t\t// turn for as long as the panel stays open.\n\t\t\t\tthis.localSpeechAudio.onended = () => {\n\t\t\t\t\tURL.revokeObjectURL(url)\n\t\t\t\t\tthis.localSpeechAudio = null\n\t\t\t\t\tthis.resumeConversationIfActive()\n\t\t\t\t}\n\t\t\t\tawait this.localSpeechAudio.play()\n\t\t\t} catch {\n\t\t\t\t// Speaking is a convenience; a failure must not disturb the chat\n\t\t\t\t// — but a conversation waiting on it must not stall either.\n\t\t\t\tthis.resumeConversationIfActive()\n\t\t\t}\n\t\t},\n\n\t\tspeakLatestReply() {\n\t\t\t// ⚠️ EVERY EARLY RETURN HERE HANDS THE TURN BACK. In a conversation\n\t\t\t// the microphone reopens when the speaking ENDS, so a path that\n\t\t\t// returns without speaking and without saying so leaves the user\n\t\t\t// holding a dead microphone, waiting for an agent that already\n\t\t\t// answered.\n\t\t\tif (this.speakingDecision.engine === SPEECH_OFF) {\n\t\t\t\tthis.resumeConversationIfActive()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst messages = (this.streamState && this.streamState.messages) || []\n\t\t\tlet latest = ''\n\t\t\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\t\t\tif (messages[i] && messages[i].role === 'assistant') {\n\t\t\t\t\tlatest = String(messages[i].content || '')\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (latest === '' || latest === this.spokenText) {\n\t\t\t\tthis.resumeConversationIfActive()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.stopSpeaking()\n\t\t\tthis.spokenText = latest\n\n\t\t\t// Strip the markdown the model writes — speaking \"asterisk asterisk\n\t\t\t// Klant asterisk asterisk\" is worse than not speaking at all.\n\t\t\tconst spoken = latest\n\t\t\t\t.replace(/`{1,3}[^`]*`{1,3}/g, ' ')\n\t\t\t\t.replace(/[*_#>|]/g, ' ')\n\t\t\t\t.replace(/\\[(.*?)\\]\\(.*?\\)/g, '$1')\n\t\t\t\t.replace(/\\s+/g, ' ')\n\t\t\t\t.trim()\n\n\t\t\t// 🔴 The engine comes from the DECISION, not from what the browser\n\t\t\t// happens to support. An agent whose replies may quote a case file\n\t\t\t// is configured `local`, and speaking that reply through the\n\t\t\t// browser's synthesis would hand the quote to the same vendor the\n\t\t\t// dictation setting was written to avoid.\n\t\t\tif (this.speakingDecision.engine === SPEECH_LOCAL) {\n\t\t\t\tthis.speakLocally(spoken)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst utterance = new window.SpeechSynthesisUtterance(spoken)\n\t\t\t\tutterance.lang = (typeof document !== 'undefined' && document.documentElement.lang)\n\t\t\t\t\t? document.documentElement.lang\n\t\t\t\t\t: 'en-US'\n\t\t\t\tutterance.onend = () => this.resumeConversationIfActive()\n\t\t\t\twindow.speechSynthesis.speak(utterance)\n\t\t\t} catch {\n\t\t\t\t// Speaking is a convenience; a failure must not disturb the chat\n\t\t\t\t// — but a conversation waiting on it must not stall either.\n\t\t\t\tthis.resumeConversationIfActive()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Stop any speech in progress.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstopSpeaking() {\n\t\t\t// Both engines, unconditionally. Stopping only the one currently in\n\t\t\t// use leaves the other playing when the agent — and with it the\n\t\t\t// engine — changes mid-sentence.\n\t\t\tif (this.localSpeechAudio !== null) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.localSpeechAudio.pause()\n\t\t\t\t} catch {\n\t\t\t\t\t// Already stopped.\n\t\t\t\t}\n\t\t\t\tthis.localSpeechAudio = null\n\t\t\t}\n\t\t\tif (this.speechSynthesisSupported === false) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\twindow.speechSynthesis.cancel()\n\t\t\t} catch {\n\t\t\t\t// Nothing to cancel.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Record the owner's decision on an approval, then let the agent carry on.\n\t\t *\n\t\t * Posts to the approval's OWN `resolveUrl` rather than to a URL built\n\t\t * here, which is what keeps this generic — a new approval kind supplies\n\t\t * its endpoint and this code is unchanged. The endpoint is the existing\n\t\t * owner-only one, so \"only the owner decides\" is enforced in one place\n\t\t * and is not re-implemented for the chat.\n\t\t *\n\t\t * On a grant the agent is nudged to continue: it stopped because it could\n\t\t * not proceed, and making the user retype their request would be the\n\t\t * whole point of deciding in the chat, lost.\n\t\t *\n\t\t * @param {object} approval The approval being decided.\n\t\t * @param {string} decision 'granted' or 'refused'.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onApprovalDecision(approval, decision) {\n\t\t\tif (this.resolvingApprovalId !== '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.resolvingApprovalId = approval.id\n\n\t\t\ttry {\n\t\t\t\tawait axios.post(approval.resolveUrl, { decision })\n\t\t\t\tthis.resolvedApprovalIds = this.resolvedApprovalIds.concat([approval.id])\n\n\t\t\t\tif (decision === 'granted') {\n\t\t\t\t\t// A grant only takes effect on the NEXT turn — the agent's tool\n\t\t\t\t\t// set is resolved at turn assembly — so continuing is what makes\n\t\t\t\t\t// the new capability usable without the user repeating themselves.\n\t\t\t\t\tthis.$emit(\n\t\t\t\t\t\t'send',\n\t\t\t\t\t\tthis.cnTranslate('I approved that access. Please continue.'),\n\t\t\t\t\t\tthis.selectedAgentUuid,\n\t\t\t\t\t\t[],\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Leave the card in place: a decision that did not record must not\n\t\t\t\t// look as though it did.\n\t\t\t\tthis.resolvingApprovalId = ''\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.resolvingApprovalId = ''\n\t\t},\n\n\t\t/**\n\t\t * Escape closes the floating window; an embedded panel is part of the\n\t\t * page and stays.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonEscape() {\n\t\t\tif (!this.embedded) {\n\t\t\t\tthis.onClose()\n\t\t\t}\n\t\t},\n\n\t\tonClose() {\n\t\t\tthis.stopSpeaking()\n\t\t\tthis.$emit('close')\n\t\t\tthis.returnFocusToLauncher()\n\t\t},\n\n\t\t/**\n\t\t * Put focus back on the launcher the panel was opened from (WCAG 2.2\n\t\t * SC 2.4.3 Focus Order). The panel is hidden rather than unmounted, so\n\t\t * without this the focused control disappears and focus falls to the\n\t\t * document body, stranding a keyboard user at the top of the page.\n\t\t *\n\t\t * `fabRef` may be a component instance or a plain element, so both\n\t\t * shapes are accepted.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\treturnFocusToLauncher() {\n\t\t\tconst el = this.fabRef?.$el ?? this.fabRef\n\t\t\tif (el && typeof el.focus === 'function') {\n\t\t\t\tel.focus()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A conversation's menu label, trimmed to keep the menu one line per row.\n\t\t *\n\t\t * @param {{title: string, uuid: string}} conversation The conversation.\n\t\t * @return {string} Display label.\n\t\t */\n\t\tconversationLabel(conversation) {\n\t\t\tconst title = (conversation.title || '').trim() || this.cnTranslate('Untitled conversation')\n\n\t\t\treturn title.length > MENU_TITLE_MAX\n\t\t\t\t? `${title.slice(0, MENU_TITLE_MAX - 1)}…`\n\t\t\t\t: title\n\t\t},\n\n\t\t/**\n\t\t * Re-emit CnAiInput's `{ text, attachments }` payload up to\n\t\t * CnAiCompanion, which owns the stream composable, adding the\n\t\t * currently-selected agent uuid (this panel's own picker state).\n\t\t *\n\t\t * @param {{text: string, attachments: Array<{path: string, name: string}>}} payload CnAiInput's send payload.\n\t\t */\n\t\tonSend({ text, attachments }) {\n\t\t\tthis.$emit('send', text, this.selectedAgentUuid, attachments)\n\t\t},\n\n\t\tonNewChat() {\n\t\t\tthis.activeView = 'chat'\n\t\t\tif (this.isOnNewChatScreen) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.activeConversationUuid = null\n\t\t\tthis.$emit('new-thread')\n\t\t},\n\n\t\tonAgentSelected(uuid) {\n\t\t\tthis.selectedAgentUuid = uuid\n\t\t},\n\n\t\tonConversationSelect(uuid) {\n\t\t\tthis.activeConversationUuid = uuid\n\t\t\tthis.activeView = 'chat'\n\t\t\tthis.$emit('load-conversation', uuid)\n\t\t},\n\n\t\t/**\n\t\t * Keep the panel's own conversation list (feeding both the sessions menu\n\t\t * and the history view) in sync after an inline rename/describe save,\n\t\t * without a full refetch.\n\t\t *\n\t\t * @param {{uuid: string, title: string, description: string}} payload Renamed fields.\n\t\t */\n\t\tonConversationRenamed({ uuid, title, description }) {\n\t\t\tconst index = this.conversations.findIndex((conv) => conv.uuid === uuid)\n\t\t\tif (index !== -1) {\n\t\t\t\tthis.conversations.splice(index, 1, { ...this.conversations[index], title, description })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch the agent list for the agent menu. Defaults the selection to the\n\t\t * first agent this page has a use for, which is the same subset the menu\n\t\t * shows: `relevantAgentOptions`. The agents API has no \"default agent\"\n\t\t * indicator to prefer instead (verified against hermiq's\n\t\t * AgentsController::index()/serializeAgent()), so page relevance is the\n\t\t * best signal available.\n\t\t *\n\t\t * Defaulting to the raw first agent instead, as this did, is not a\n\t\t * cosmetic difference. On a Buildiq page it handed the user an agent\n\t\t * holding no Buildiq tools, so the first thing the assistant did was\n\t\t * fail to do the one thing the page is for, and the fix was a settings\n\t\t * menu the user had no reason to open. `relevantAgentOptions` already\n\t\t * falls back to every agent when nothing matches, so on a page with no\n\t\t * relevant agent this still selects exactly what it selected before.\n\t\t *\n\t\t * Degrades gracefully on failure: the menu shows an inline notice but\n\t\t * the rest of the window (history, message input) stays usable.\n\t\t *\n\t\t * @returns {Promise<void>}\n\t\t */\n\t\tasync fetchAgents() {\n\t\t\tthis.agentsLoading = true\n\t\t\tthis.agentsFetchError = false\n\t\t\ttry {\n\t\t\t\tconst response = await axios.get(agentsUrl(this.chatAppId))\n\t\t\t\tconst data = response.data\n\t\t\t\tconst list = Array.isArray(data) ? data : (data.results || [])\n\t\t\t\tthis.agents = list\n\t\t\t\tif (!this.selectedAgentUuid && list.length > 0) {\n\t\t\t\t\tconst preferred = this.relevantAgentOptions[0]\n\t\t\t\t\tthis.selectedAgentUuid = (preferred && preferred.id) || list[0].uuid || list[0].id || null\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.agentsFetchError = true\n\t\t\t\tthis.agents = []\n\t\t\t} finally {\n\t\t\t\tthis.agentsLoading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch the caller's conversations once, feeding both the sessions menu's\n\t\t * recent entries (top 5) and the history view's full searchable list — a\n\t\t * single fetch keeps both surfaces consistent.\n\t\t *\n\t\t * @returns {Promise<void>}\n\t\t */\n\t\tasync fetchConversations() {\n\t\t\tthis.conversationsLoading = true\n\t\t\tthis.conversationsFetchError = false\n\t\t\ttry {\n\t\t\t\tconst response = await axios.get(conversationsUrl(this.chatAppId), { params: { limit: 50 } })\n\t\t\t\tconst data = response.data\n\t\t\t\tconst list = Array.isArray(data) ? data : (data.results || data.conversations || [])\n\t\t\t\tthis.conversations = list.map(normalizeConversation)\n\t\t\t} catch {\n\t\t\t\tthis.conversationsFetchError = true\n\t\t\t\tthis.conversations = []\n\t\t\t} finally {\n\t\t\t\tthis.conversationsLoading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style>\n/* The window itself. Fixed, anchored to the same corner as the launcher hex,\n and sized to sit comfortably on a laptop viewport without covering the\n host application it is a guest on.\n\n `!important` is deliberate and matches CnAiFloatingButton's reasoning: this\n markup lands inside third-party apps whose stylesheets we do not control. */\n.cn-ai-chat-window {\n\tposition: fixed !important;\n\tz-index: 9001 !important; /* above the launcher hex (9000) */\n\tdisplay: flex !important;\n\tflex-direction: column !important;\n\twidth: 380px;\n\tmax-width: calc(100vw - 32px);\n\theight: 600px;\n\tmax-height: calc(100vh - 160px);\n\t/* NOT `overflow: hidden` — the pointer below is drawn outside this box and\n\t would be clipped away. The rounded corners are held by the titlebar and\n\t input rows clipping their own ends instead. */\n\tborder: 1px solid var(--color-border, #d0d0d0);\n\tborder-radius: 12px;\n\tbackground: var(--color-background-hover, #f5f5f5);\n\tbox-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);\n\n\t/* ⚠️ THE POINTER IS AIMED AT THE HEX, and this number is the only thing\n\t keeping it there. The hex is 36px wide at a 56px inset, so its centre is\n\t 56 + 18 = 74px from the viewport edge. The window sits at a 44px inset, so\n\t the pointer's centre must be 74 - 44 = 30px in from the window's edge; the\n\t triangle is 22px wide, so its leading edge is 30 - 11 = 19px.\n\n\t Change the hex size, the hex inset, or the window inset and this is wrong\n\t — the arrow points at empty space beside the button, which reads as a\n\t rendering bug rather than a measurement one. */\n\t--cn-ai-pointer-offset: 19px;\n}\n\n/* Anchored ABOVE the hex and inboard of it.\n *\n * The hex sits at a 56px inset (see CnAiFloatingButton) to clear the host\n * application's own bottom-right chrome — an office editor puts a zoom control\n * and a status bar exactly there. The window clears the hex itself: 56px inset\n * + 30px tall + 14px for the pointer + breathing room = 108px.\n *\n * The pointer offset these lean on is declared with the rest of the window's\n * own properties above, not repeated here — a second `.cn-ai-chat-window` block\n * is a duplicate selector, and the two halves drift the moment someone edits the\n * one they happen to find first. */\n.cn-ai-chat-window--bottom-right {\n\tright: 44px;\n\tbottom: 108px;\n}\n\n.cn-ai-chat-window--bottom-left {\n\tleft: 44px;\n\tbottom: 108px;\n}\n\n.cn-ai-chat-window--top-right {\n\ttop: 108px;\n\tright: 44px;\n}\n\n.cn-ai-chat-window--top-left {\n\ttop: 108px;\n\tleft: 44px;\n}\n\n/* Embedded: a panel in the page's flow (a detail page tab), not a window over\n it. It fills its container's width and takes no pointer, since no position\n class is set. The doubled class outranks the window's `!important` rules. */\n.cn-ai-chat-window.cn-ai-chat-window--embedded {\n\tposition: relative !important;\n\tz-index: auto !important;\n\twidth: auto;\n\tmax-width: none;\n\theight: 520px;\n\tmax-height: none;\n\tbox-shadow: none;\n}\n\n/* The pointer: a white triangle under the window, aimed at the hex, so the\n window reads as speech FROM the launcher rather than as an unrelated panel\n that happened to appear. Built from two stacked triangles — the lower one is\n the border colour and sits 1px further down, which is what gives the visible\n 1px edge along the two diagonals. `overflow: hidden` on the window would clip\n these, which is why the window does not use it. */\n.cn-ai-chat-window--bottom-right::after,\n.cn-ai-chat-window--bottom-left::after,\n.cn-ai-chat-window--bottom-right::before,\n.cn-ai-chat-window--bottom-left::before {\n\tposition: absolute;\n\ttop: 100%;\n\twidth: 0;\n\theight: 0;\n\tcontent: '';\n\tborder-right: 11px solid transparent;\n\tborder-left: 11px solid transparent;\n}\n\n/* Border layer, 1px lower and 1px wider than the fill. */\n.cn-ai-chat-window--bottom-right::before,\n.cn-ai-chat-window--bottom-left::before {\n\tborder-top: 12px solid var(--color-border, #d0d0d0);\n}\n\n/* Fill layer, in the titlebar's white so the pointer belongs to the chrome. */\n.cn-ai-chat-window--bottom-right::after,\n.cn-ai-chat-window--bottom-left::after {\n\tmargin-top: -1px;\n\tborder-top: 12px solid var(--color-main-background, #ffffff);\n}\n\n.cn-ai-chat-window--bottom-right::before,\n.cn-ai-chat-window--bottom-right::after {\n\tright: var(--cn-ai-pointer-offset);\n}\n\n.cn-ai-chat-window--bottom-left::before,\n.cn-ai-chat-window--bottom-left::after {\n\tleft: var(--cn-ai-pointer-offset);\n}\n\n/* White titlebar over the light-grey body — the one contrast that tells a\n guest window's chrome apart from its content. */\n.cn-ai-chat-window__titlebar {\n\tdisplay: flex;\n\tflex: 0 0 auto;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 4px 8px;\n\tborder-bottom: 1px solid var(--color-border, #d0d0d0);\n\t/* Clips its own top corners, since the window can no longer use\n\t `overflow: hidden` (it would eat the pointer). */\n\tborder-radius: 11px 11px 0 0;\n\tbackground: var(--color-main-background, #ffffff);\n}\n\n.cn-ai-chat-window__controls {\n\tdisplay: flex;\n\tflex: 0 0 auto;\n\talign-items: center;\n\tgap: 2px;\n}\n\n/* The agent's identity, after the controls. `min-width: 0` + ellipsis so a\n long agent name shortens instead of pushing the controls off the edge. */\n.cn-ai-chat-window__identity {\n\tdisplay: flex;\n\tflex: 1 1 auto;\n\tgap: 6px;\n\talign-items: center;\n\tmin-width: 0;\n}\n\n.cn-ai-chat-window__identity-icon {\n\tflex: 0 0 auto;\n\tcolor: var(--color-primary-element, #21468b);\n}\n\n.cn-ai-chat-window__identity-name {\n\toverflow: hidden;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n}\n\n.cn-ai-chat-window__body {\n\tdisplay: flex;\n\tflex: 1 1 auto;\n\tflex-direction: column;\n\tmin-height: 0; /* allow flex children to shrink past their content size */\n}\n\n.cn-ai-chat-window__body--history {\n\toverflow-y: auto;\n\tpadding: 8px;\n}\n\n.cn-ai-chat-window__messages {\n\tflex: 1 1 auto;\n\tmin-height: 0;\n\toverflow-y: auto;\n\tpadding: 8px;\n}\n\n/* Approvals sit between the transcript and the composer: the last thing read\n before the box you would otherwise type into, which is where a blocked\n agent's request belongs. */\n.cn-ai-chat-window__approvals {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 8px 12px 0;\n\tlist-style: none;\n}\n\n.cn-ai-approval {\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbackground: var(--color-main-background);\n}\n\n.cn-ai-approval__head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-ai-approval__title {\n\tflex: 1;\n\tfont-weight: 600;\n\toverflow-wrap: anywhere;\n}\n\n/* The reach is the fact the decision rests on, so it reads as a label rather\n than as part of the agent's prose. */\n.cn-ai-approval__reach {\n\tflex-shrink: 0;\n\tpadding: 1px 8px;\n\tborder-radius: 12px;\n\tbackground: var(--color-background-dark);\n\tfont-size: 11px;\n\ttext-transform: uppercase;\n\tletter-spacing: .04em;\n}\n\n.cn-ai-approval__reason {\n\tmargin: 6px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-ai-approval__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-top: 10px;\n}\n\n.cn-ai-chat-window__error {\n\tflex: 0 0 auto;\n\tmargin: 0;\n\tpadding: 8px 12px;\n\tcolor: var(--color-error-text, #a10000);\n\tbackground: var(--color-error-hover, #f7e5e5);\n\tfont-size: 0.9em;\n}\n\n.cn-ai-chat-window__note {\n\tdisplay: flex;\n\tgap: 10px;\n\talign-items: flex-start;\n\tmargin: 0 8px 8px;\n\tpadding: 10px 14px;\n\tborder-radius: 10px;\n\tbackground: var(--color-primary-element-light, #eaf0f7);\n\tcolor: var(--color-primary-element-light-text, #234a78);\n\tline-height: 1.5;\n}\n\n.cn-ai-chat-window__note-icon {\n\tflex: none;\n\tmargin-top: 1px;\n}\n\n/* A titled panel is the body of its card: the card draws border and corners. */\n.cn-ai-chat-window.cn-ai-chat-window--titled {\n\tborder: 0;\n\tborder-radius: 0;\n\tbackground: transparent;\n}\n\n.cn-ai-chat-window--titled .cn-ai-chat-window__input {\n\tpadding: 0;\n\tborder-top: 0;\n\tborder-radius: 0;\n\tbackground: transparent;\n}\n\n/* In a card the panel's body takes the card's 16px 18px inset. */\n.cn-ai-chat-window--titled .cn-ai-chat-window__messages {\n\tpadding: 16px 18px;\n}\n\n.cn-ai-chat-window--titled .cn-ai-chat-window__note {\n\tmargin: 0 18px 16px;\n}\n\n.cn-ai-chat-window__input {\n\tflex: 0 0 auto;\n\tpadding: 8px;\n\tborder-top: 1px solid var(--color-border, #d0d0d0);\n\t/* Mirrors the titlebar: clips its own bottom corners. */\n\tborder-radius: 0 0 11px 11px;\n\tbackground: var(--color-main-background, #ffffff);\n}\n\n/* The approved-assistant mark: same colour pair as the panel body. */\n.cn-ai-chat-window--has-mark .cn-ai-chat-window__input {\n\tborder-radius: 0;\n}\n\n.cn-ai-chat-window__mark {\n\tflex: 0 0 auto;\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 6px 12px;\n\tborder-top: 1px solid var(--color-border, #d0d0d0);\n\tborder-radius: 0 0 11px 11px;\n\tbackground: var(--color-main-background, #ffffff);\n\tcolor: var(--color-main-text, #222222);\n\tfont-size: 0.85em;\n}\n\n.cn-ai-chat-window__mark-logo {\n\tmax-height: 24px;\n\twidth: auto;\n}\n\n/* ── The pop animation ──────────────────────────────────────────────────────\n *\n * Scale + fade, with `transform-origin` at the corner the hex sits under, so the\n * window converges on the launcher. 0.14 rather than 0 as the start scale: a\n * true zero briefly renders a 0x0 box that some engines treat as invisible for\n * hit-testing, and the tiny non-zero keeps the transition interruptible if the\n * user toggles the hex twice quickly.\n *\n * Leave is faster than enter (140ms vs 180ms) — dismissal should feel immediate,\n * while appearance can afford to be seen. */\n.cn-ai-chat-window-pop-bottom-right-enter-active,\n.cn-ai-chat-window-pop-bottom-left-enter-active,\n.cn-ai-chat-window-pop-top-right-enter-active,\n.cn-ai-chat-window-pop-top-left-enter-active {\n\ttransition: transform 0.18s cubic-bezier(0.2, 0.9, 0.3, 1.2), opacity 0.14s ease-out;\n}\n\n.cn-ai-chat-window-pop-bottom-right-leave-active,\n.cn-ai-chat-window-pop-bottom-left-leave-active,\n.cn-ai-chat-window-pop-top-right-leave-active,\n.cn-ai-chat-window-pop-top-left-leave-active {\n\ttransition: transform 0.14s ease-in, opacity 0.12s ease-in;\n}\n\n.cn-ai-chat-window-pop-bottom-right-enter,\n.cn-ai-chat-window-pop-bottom-right-enter-from,\n.cn-ai-chat-window-pop-bottom-right-leave-to {\n\ttransform: scale(0.14);\n\ttransform-origin: calc(100% - var(--cn-ai-pointer-offset) - 11px) 100%;\n\topacity: 0;\n}\n\n.cn-ai-chat-window-pop-bottom-left-enter,\n.cn-ai-chat-window-pop-bottom-left-enter-from,\n.cn-ai-chat-window-pop-bottom-left-leave-to {\n\ttransform: scale(0.14);\n\ttransform-origin: calc(var(--cn-ai-pointer-offset) + 11px) 100%;\n\topacity: 0;\n}\n\n.cn-ai-chat-window-pop-top-right-enter,\n.cn-ai-chat-window-pop-top-right-enter-from,\n.cn-ai-chat-window-pop-top-right-leave-to {\n\ttransform: scale(0.14);\n\ttransform-origin: calc(100% - var(--cn-ai-pointer-offset) - 11px) 0%;\n\topacity: 0;\n}\n\n.cn-ai-chat-window-pop-top-left-enter,\n.cn-ai-chat-window-pop-top-left-enter-from,\n.cn-ai-chat-window-pop-top-left-leave-to {\n\ttransform: scale(0.14);\n\ttransform-origin: calc(var(--cn-ai-pointer-offset) + 11px) 0%;\n\topacity: 0;\n}\n\n/* Someone who asked not to be moved gets the window without the flight. */\n@media (prefers-reduced-motion: reduce) {\n\t.cn-ai-chat-window-pop-bottom-right-enter-active,\n\t.cn-ai-chat-window-pop-bottom-left-enter-active,\n\t.cn-ai-chat-window-pop-top-right-enter-active,\n\t.cn-ai-chat-window-pop-top-left-enter-active,\n\t.cn-ai-chat-window-pop-bottom-right-leave-active,\n\t.cn-ai-chat-window-pop-bottom-left-leave-active,\n\t.cn-ai-chat-window-pop-top-right-leave-active,\n\t.cn-ai-chat-window-pop-top-left-leave-active {\n\t\ttransition: opacity 0.1s linear;\n\t}\n\n\t.cn-ai-chat-window-pop-bottom-right-enter,\n\t.cn-ai-chat-window-pop-bottom-left-enter,\n\t.cn-ai-chat-window-pop-top-right-enter,\n\t.cn-ai-chat-window-pop-top-left-enter,\n\t.cn-ai-chat-window-pop-bottom-right-leave-to,\n\t.cn-ai-chat-window-pop-bottom-left-leave-to,\n\t.cn-ai-chat-window-pop-top-right-leave-to,\n\t.cn-ai-chat-window-pop-top-left-leave-to {\n\t\ttransform: none;\n\t}\n}\n\n/* Empty state: just the agent's name and the prompt. The agent picker and the\n recent-sessions list used to live here and now live in the titlebar menus —\n on a 380px window they filled the first screen before any message did. */\n.cn-ai-chat-window__start {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-self: stretch;\n\tgap: 16px;\n\twidth: 100%;\n\tmargin: 0 auto;\n\tpadding: 0 12px;\n}\n</style>\n"],"names":["_createBlock","_normalizeClass","_createElementVNode","_createVNode","_toDisplayString","_createElementBlock","_Fragment","_renderList","_createTextVNode","_openBlock"],"mappings":";;;;;AAqH0B,EAAA,KAAK,EAAC,6BAAA;;;AAmBrB,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,kCAAM,GAAA;AAGd,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,6BAA4B,GAAA;;;AAyHhB,EAAA,KAAA,EAAA,yBAAA;;AAClB,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,6BAAA,GAAA;;AAMG,EAAA,KAAK,EAAC;AAA2B,EAAA,aAAW,EAAA,sBAAA;;;;AA0BnD,EAAA,KAAK,EAAC,8BAAK;AACX,EAAA,aAAW,EAAC,uBAAmB;;AAKzB,MAAA,WAAA,GAAA,EAAA,KAAI,EAAA,sBAAA,GAAA;AACF,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,uBAAgB,GAAA;AAMtB,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,uBAAsB,GAAA;;;AAEzB,EAAA,KAAA,EAAA,wBAAA;;AAGL,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,yBAAU,GAAA;;;AA8BtB,EAAA,KAAK,EAAC,0BAAY;AAClB,EAAA,IAAI,EAAC,OAAO;AACZ,EAAA,aAAS,EAAA,mBAAA;;;;AAMT,EAAA,KAAK,EAAC,yBAAuB;AAC7B,EAAA,aAAW,EAAC,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAnP7BA;AAAG,IAAA,IAAA,2BAAA,MAAA,CAAA,QAAA,CAAA,CAAA;;;OAEA,MAAO,CAAA,OAAA;;;YACT,KAAC,EAAAC,cAAA,CAAA,CAAC,mBAAmB;AAErB,YAAA,IAAC,EAAE,MAAQ,CAAA,QAAA,GAAA,QAAA,GAAA,QAAA;wBACJ,EAAE,QAAS,CAAA,UAAA;AAClB,YAAA,eAAQ,aAAa;AACvB,YAAA,SAAE,mDAAA,QAAA,CAAA,QAAA,IAAA,QAAA,CAAA,QAAA,CAAA,GAAA,IAAA,CAAA,CAAA,EAAA,CAAA,KAAA,CAAA,CAAA,CAAA;;cAEW,MAAM,CAAA,MAAA;iDA6Ib,oBAAA,EAAA;kBAtILC,kBAaI,CAAA,KAAA,EAAA;AAbD,oBAAA,KAAK,EAAC,6BAAS;AAAA,oBAAA,KAAA,EAAA,QAAA,CAAA,UAAA;;oBAQjBC,WAGU,CAAA,0BAAA,EAAA;0BAFN,EAAE,QAAa,CAAA,aAAA;0BACf,EAAE,EAAE;2BACJ,EAAC,kCAAK;;oBACVD,kBAAgB,CAAA,MAAA,EAAhB,UAAgB,EAAAE,eAAA,CAAA,QAAA,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;;qCAyHb,iBAAA,EAAA;+BAnDM,CAAA,oBAAA,EAAA;AAzDT,sBAAA,YAAU,EAAE,QAAG,CAAA,WAAA,CAAA,kBAAA,CAAA;2BACV,EAAE,QAAW,CAAA,WAAA,CAAA,kBAAA,CAAA;AAClB,sBAAA,SAAS,EAAC,IAAA;mCACA,EAAC,sBAAsB;;AACvB,sBAAA,IAAI,UACd,MAAc;wBAAdD,WAAc,CAAA,cAAA,EAAA,EAAR,IAAI,EAAE,EAAE,EAAA,CAAA;;uCAWd,MAQU;wBARVA,WAQU,CAAA,2BAAA,EAAA;AAPT,0BAAA,UAAQ,EAAA,KAAA,CAAA,YAAA;AACR,0BAAA,QAAQ,EAAE,QAAE,CAAA,wBAAA,KAAA,KAAA;AACb,0BAAA,aAAW,EAAC,0BAAgB;AAC3B,0BAAA,qBAAiB,EAAE,QAAE,CAAA,qBAAA;;2CACtB,MAEmB;4DAFhB,QAAe,CAAA,wBAAA;UAAA,QAAA,CAAA,WAAA,CAAA,oBAAA,CAAA;UAAA,QAAA,CAAA,WAAA,CAAA,yCAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;wBAIlBA,WAAoB,CAAA,4BAAA,CAAA;wBACrBA,WAAgB,CAAA,0BAAA,EAAA,EAAA,IAAA,EAAA,QAAA,CAAA,YAAA,EAAA,EAAA,IAAA,EAAA,CAAA,cAAA,CAAA,MAAA,CAAA,CAAA;0CAChBE,kBAUgB,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CATF,QAAA,CAAA,mBAAA,EAAA,CAAN,KAAK,KAAA;+CADbP,WAUgB,CAAA,yBAAA,EAAA;4BARd,GAAG,EAAE,KAAK,CAAC,EAAE;AACb,4BAAA,eAAe,EAAE,IAAI;AACrB,4BAAA,OAAK,EAAE,MAAA,KAAA,QAAA,CAAA,eAAc,CAAA,KAAA,CAAA,EAAA,CAAA,CAAA;;AACX,4BAAA,IAAI,UACd,MAAc;+BAAD,KAAC,CAAA,EAAA,KAAA,KAAA,CAAA,iBAAA;gDAAdA,WAAc,CAAA,gBAAA,EAAA;;AAAA,oCAAA,IAAA,EAAA,EAAA;;gDACdA,WAAgC,CAAA,0BAAA,EAAA;;oCAAP,IAAI,EAAE,KAAC,CAAA,IAAA;AAAA,oCAAA,IAAA,EAAA,EAAA;;;6CACvB,MACV;8CADU,GACV,GAAAI,eAAA,CAAG,KAAK,CAAC,KAAK,CAAA,EAAA,CAAA,YAAA;;;;;yBASR,QAAS,CAAA,gBAAA,GAAA,CAAA;0CADfJ,WASgB,CAAA,yBAAA,EAAA;;AAPf,8BAAA,eAAQ,EAAA,KAAA;;AAER,8BAAA,OAAK,uCAAA,KAAA,CAAA,aAAA,GAAA,IAAA,CAAA,CAAA;;AACK,8BAAA,IAAG,UACb,MAA6B;gCAA7BG,WAA6B,CAAA,yBAAA,EAAA,EAAZ,IAAI,EAAE,EAAE,EAAA,CAAA;;+CACf,MACX;gDADW,GACX,GAAAC,eAAA,CAAG,oBAAW,CAAE,YAAA,CAAA,CAAA,GAAA,IAAA,mBAAA,QAAA,CAAA,YAAA,CAAA,MAAA,CAAA,GAAA,IACjB,EAAA,CAAA,YAAA;;;;;AAEO,wBAAA,CAAA,QAAA,CAAA,YAAQ,CAAA,MAAA,KAAA,CAAA;0CADfJ,WAEoB,CAAA,0BAAA,EAAA;;AAAlB,8BAAA,IAAI,EAAE,KAAA,CAAA,gBAAY,GAAA,QAAA,CAAA,WAAA,4BAAA,QAAA,CAAA,WAAA,CAAA,qBAAA,CAAA;;;;;;oBAGrBG,WAoCY,CAAA,oBAAA,EAAA;AAnCV,sBAAA,YAAI,EAAA,QAAA,CAAA,WAAA,CAAA,UAAA,CAAA;;AAEL,sBAAA,SAAU,EAAE,IAAI;AAChB,sBAAA,aAAW,EAAC,sBAAsB;;AACvB,sBAAA,IAAG,UACb,MAAsB;wBAAtBA,WAAsB,CAAA,kBAAA,EAAA,EAAZ,IAAI,EAAE,EAAE,EAAA,CAAA;;uCAEnB,MAQiB;wBARjBA,WAQiB,CAAA,yBAAA,EAAA;AAPf,0BAAA,QAAQ,EAAE,QAAI,CAAA,iBAAA;AACd,0BAAA,eAAe,EAAE,IAAI;AACrB,0BAAA,OAAK,EAAE,QAAS,CAAA,SAAA;;AACP,0BAAA,IAAK,UACZ,MAAiB;4BAAjBA,WAAiB,CAAA,eAAA,EAAA,EAAZ,IAAK,EAAC,EAAE,EAAA,CAAA;;2CACL,MACT;AADS,4BAAAK,eAAA,CAAA,GACT,mBAAC,QAAW,CAAA,WAAA,CAAA,gBAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;mCAEF,CAAA,0BAAA,EAAA;AAAA,0BAAA,IAAA,EAAA,QAAA,CAAA,WAAA,CAAA,sBAAA,CAAA;;0CACXH,kBASe,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CARX,QAAA,CAAA,mBAAA,EAAA,CAAA,YAAA,KAAA;+CADJP,WASe,CAAA,yBAAA,EAAA;4BAPf,GAAG,EAAE,YAAG,CAAA,IAAA;AACR,4BAAA,eAAc,EAAA,IAAA;AACd,4BAAA,OAAC,EAAA,MAAA,KAAA,QAAA,CAAA,oBAAA,CAAA,YAAA,CAAA,IAAA,CAAA,CAAA;;0CAEF,MAAA;8BAAAG,WAAA,CAAA,6BAAA,EAAA,EAAA,IAAA,EAAA,EAAA,EAAA,CAAA;;6CACW,MACX;8CADW,GACX,GAAAC,eAAA,CAAG,QAAiB,CAAA,iBAAA,CAAC,YAAY,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;mCASjB,CAAA,yBAAA,EAAA;AANf,0BAAA,eAAe,EAAE;AACjB,0BAAA,8CAAO,KAAU,CAAA,UAAA,GAAA,SAAA,CAAA,CAAA;;AACP,0BAAA,IAAI,UACd,MAAsB;4BAAtBD,WAAsB,CAAA,kBAAA,EAAA,EAAZ,IAAI,EAAE,EAAE,EAAA,CAAA;;2CACpB,MACA;AADA,4BAAAK,eAAA,CAAA,GACA,mBAAA,QAAA,CAAA,WAAA,CAAA,wBAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;sBAKO,MAAS,CAAA,QAAA;sCADjBR,WAUI,CAAA,mBAAA,EAAA;;AARF,0BAAA,YAAU,EAAE,QAAI,CAAA,WAAA,CAAA,OAAA,CAAA;AAChB,0BAAA,KAAK,EAAE,QAAW,CAAA,WAAA,CAAA,OAAA,CAAA;AACnB,0BAAA,OAAO,EAAC,UAAU;uCACP,EAAC,mBAAmB;AAC9B,0BAAA,OAAG,EAAA,QAAA,CAAA,OAAA;;8BACW,UACZ,MAAkB;4BAAlBG,WAAkB,CAAA,gBAAA,EAAA,EAAZ,IAAI,EAAE,EAAG,EAAA,CAAA;;;;;;;;aAMV,KAAU,CAAA,UAAA,KAAA,MAAA;AAAjB,iBAAAM,SAAA,EAAA,EAAAJ,kBAAA,QAAA;AACD,kBAAAH,kBAAA,CAkBE,OAlBF,UAkBE,EAAA;oBAjBFC,WAgBG,CAAA,0BAAA,EAAA;sBAfF,QAAQ,EAAA,MAAA,CAAA,WAAA,CAAA,QAAA;sBACP,WAAU,EAAE,MAAI,CAAA,WAAA,CAAA,WAAA;sBAChB,WAAU,EAAE,MAAW,CAAA,WAAA,CAAC,WAAW;;AAC1B,sBAAA,KAAK,gBAUT;2CAAA,iBAAA,EAAA;0BARLA,WAOiB,CAAA,yBAAA,EAAA,EAPA,IAAI,EAAC,QAAG,CAAA,UAAA,EAAA,EAAA;AAChB,4BAAA,IAAA,UACP,MAAK;8BAALA,WAAK,CAAA,mBAAA,EAAA,EAAA,IAAA,EAAA,EAAA,EAAA,CAAA;;AAEK,4BAAA,WAAW,UACrB,MAAgD;8DAA7C,QAAW,CAAA,WAAA,CAAA,6CAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;AAmBb,kBAAA,CAAA,QAAA,CAAA,gBAAgB,CAAC,MAAM;uDAoCzB,gBAAA,EAAA;0CAjCJE,kBAgCK,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CA/BH,QAAA,CAAA,gBAAA,EAAA,CAAA,QAAA,KAAA;+CADFF,kBAgCK,CAAA,IAAA,EAAA;;AA7BL,4BAAA,KAAA,EAAA,gBAAA;;AACC,4BAAAH,kBAAA,CAQM,OARN,WAQM,EAAA;AAPL,8BAAAA,kBAAA,CAA4B,MAA5B,EAAA,WAA4B,EAAAE,eAAA,CAAA,QAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;AAM5B,8BAAAF,kBAAA,CAAkC,MAAlC,EAAA,WAAkC,EAAAE,eAAA,CAAA,QAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;AAE1B,4BAAA,CAAA,QAAC,CAAA,MAAA;AAAV,iCAAAK,SAAA,EAAA,EAAAJ,kBAAA,CAEI,GAFJ,EAAA,WAEI,EADAD,eAAA,CAAA,QAAQ,CAAC,MAAM,CAAA,EAAA,CAAA,YAAA;;AAEnB,4BAAAF,kBAAA,CAeM,OAfN,WAeM,EAAA;8BAdLC,WAMW,CAAA,mBAAA,EAAA;AALV,gCAAA,OAAO,EAAC,SAAS;AAChB,gCAAA,QAAQ,EAAE,KAAA,CAAA,mBAAmB,KAAK,QAAO,CAAA,EAAA;gCACzC,aAAW,EAAA,uBAAA,GAA4B,QAAQ,CAAC,EAAE;gCAClD,OAAK,EAAA,MAAA,KAAE,QAAkB,CAAA,kBAAA,CAAC,QAAQ,EAAA,SAAA,CAAA,CAAA;;iDACnC,MAA0B;kEAAvB,QAAW,CAAA,WAAA,CAAA,OAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;8BAEfA,WAMW,CAAA,mBAAA,EAAA;AALV,gCAAA,OAAC,EAAA,UAAA;AACD,gCAAA,QAAA,EAAA,KAAA,CAAA,mBAAA,KAAA,QAAA,CAAA,EAAA;;gCAEC,OAAI,EAAA,MAAA,KAAG,QAAkB,CAAA,kBAAA,CAAC,QAAQ,EAAA,SAAA,CAAA,CAAA;;iDACnC,MAAW;kEAAR,QAAQ,CAAA,WAAA,CAAA,SAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;AAgBb,kBAAA,CAAA,MAAA,CAAA,WAAA,CAAA,KAAA;oCADFE,kBAMG,CAAA,GAAA,EANH,WAMG,EADCD,eAAA,CAAA,MAAA,CAAA,WAAW,CAAC,KAAK,CAAC,OAAO,IAAC,QAAA,CAAA,WAAA,CAAA,iCAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;mBAIvB,MAAI,CAAA,IAAA;AADV,uBAAAK,SAAA,EAAA,EAAAJ,kBAAA,CAMG,KANH,WAMG,EAAA;wBAFHF,WAAG,CAAA,6BAAA,EAAA;AAAA,0BAAA,IAAA,EAAA,EAAA;AAAA,0BAAA,KAAA,EAAA,8BAAA;;AACH,wBAAAD,kBAAA,CAAE,8BAAA,MAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnAiChatPanel.vue3.js","sources":["../../../../src/components/CnAiCompanion/CnAiChatPanel.vue?vue&type=template&id=558b3615&lang.js"],"sourcesContent":["<!--\n CnAiChatPanel — the AI Chat Companion's own floating window.\n\n ⚠️ DELIBERATELY NOT AN NcAppSidebar. It used to be one, and the reasons it\n stopped are worth keeping, because \"use the native component\" is otherwise the\n right instinct:\n\n - A sidebar belongs to the app that owns the page. This companion is injected\n on EVERY page, including third-party office editors (onlyoffice, eurooffice,\n richdocuments) that own their own chrome. Observed on the Euro-Office editor:\n NcAppSidebar docked itself to the LEFT edge, over the editor's own rail, and\n rendered without a usable close control — because its host page provides\n neither the layout container nor the app-content siblings NcAppSidebar\n positions against.\n - A sidebar reflows the host application. A window overlays it. Only the second\n is honest about being a guest.\n\n So this is a self-contained fixed-position window anchored to the launcher hex.\n It carries its own close control and never assumes anything about the page it\n is dropped onto.\n\n Titlebar: the AGENT's icon and name left-aligned (a long name gets the whole\n remaining row), then the controls pushed to the RIGHT, icon-only — labels ride\n on `title` + `aria-label`, so they reach hover and screen readers without\n spending width the agent's name needs:\n - Settings — NcActions menu. The AGENT LIVES IN HERE: choosing one is a\n setting of the session, not a peer of \"close\".\n - Sessions — NcActions menu: start a new chat, pick a recent conversation,\n or open the full searchable history.\n - Close — plain button, at the outside edge where it is hardest to hit by\n accident.\n\n Both selectors are MENUS rather than inline controls on purpose: the window is\n ~380px wide, and an inline agent dropdown plus a recent-sessions list consumed\n most of the first screen before a single message was visible.\n\n The window is anchored ABOVE the launcher with a white pointer aimed at it, and\n animates from that point, so it reads as speech coming out of the hex rather\n than as an unrelated panel that appeared nearby.\n\n The title is the AGENT, not the product. \"Hermiq\" was the same string on every\n page for every agent; the one thing a user needs to see before typing is which\n agent is about to answer — the same reason the agent list is one click away\n rather than buried in settings.\n\n Two views, switched by `activeView`: 'chat' (message list + input) and\n 'history' (the full searchable CnAiHistoryList). No tab bar — the sessions menu\n is how you reach history, which keeps the header to one row.\n-->\n<template>\n\t<!--\n\t The window grows out of the hex and shrinks back into it.\n\n\t `transform-origin` is set per corner to the pointer's tip — the point the\n\t hex sits under — so the scale animation converges on the launcher rather\n\t than on the window's own centre. That single property is what makes it read\n\t as \"this came from there\" instead of \"a box faded in\".\n\t-->\n\t<transition :name=\"`cn-ai-chat-window-pop-${position}`\">\n\t\t<div\n\t\t\tv-if=\"visible\"\n\t\t\tclass=\"cn-ai-chat-window\"\n\t\t\t:class=\"[embedded ? 'cn-ai-chat-window--embedded' : `cn-ai-chat-window--${position}`, { 'cn-ai-chat-window--has-mark': assistantMark, 'cn-ai-chat-window--titled': titled }]\"\n\t\t\t:role=\"embedded ? 'region' : 'dialog'\"\n\t\t\t:aria-label=\"agentLabel\"\n\t\t\tdata-testid=\"cn-ai-panel\"\n\t\t\t@keydown.esc=\"onEscape\">\n\t\t\t<!-- In a titled card the card names the panel; its controls go to the card's menu (`menu` event). -->\n\t\t\t<header v-if=\"!titled\" class=\"cn-ai-chat-window__titlebar\">\n\t\t\t\t<!--\n\t\t\t Identity FIRST in source order and left-aligned: a long agent name\n\t\t\t gets the whole remaining row, and the controls are pushed right by\n\t\t\t the identity's `flex: 1`. Reading order matches visual order, so\n\t\t\t screen readers announce whose window this is before its controls.\n\t\t\t-->\n\t\t\t\t<div class=\"cn-ai-chat-window__identity\" :title=\"agentLabel\">\n\t\t\t\t\t<!--\n\t\t\t\t The AGENT's icon, not a generic sparkle. `Agent.icon` is an MDI\n\t\t\t\t registry name picked with CnIconPicker, so the same icon shown in\n\t\t\t\t the agent list and on its detail page is shown here — the window\n\t\t\t\t is that agent's window, and a shared mark is what says so.\n\t\t\t\t Falls back to the list's default agent icon when unset.\n\t\t\t\t-->\n\t\t\t\t\t<CnDashboardIcon\n\t\t\t\t\t\t:name=\"agentIconName\"\n\t\t\t\t\t\t:size=\"16\"\n\t\t\t\t\t\tclass=\"cn-ai-chat-window__identity-icon\" />\n\t\t\t\t\t<span class=\"cn-ai-chat-window__identity-name\">{{ agentLabel }}</span>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"cn-ai-chat-window__controls\">\n\t\t\t\t\t<!--\n\t\t\t\t Settings first, close last — destructive-adjacent controls sit\n\t\t\t\t at the outside edge where they are hardest to hit by accident,\n\t\t\t\t and the menus a user opens repeatedly sit inboard.\n\n\t\t\t\t The AGENT LIVES IN HERE rather than in its own button: picking\n\t\t\t\t an agent is a setting of the session, not a peer of \"close\".\n\t\t\t\t-->\n\t\t\t\t\t<NcActions\n\t\t\t\t\t\t:aria-label=\"cnTranslate('Session settings')\"\n\t\t\t\t\t\t:title=\"cnTranslate('Session settings')\"\n\t\t\t\t\t\t:forceMenu=\"true\"\n\t\t\t\t\t\tdata-testid=\"cn-ai-panel-settings\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Cog :size=\"18\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t Read-aloud lives in SESSION settings, not per message: it is\n\t\t\t\t\t a preference about how you want to receive answers, not an\n\t\t\t\t\t action on one of them.\n\n\t\t\t\t\t Unlike dictation this works on an insecure origin —\n\t\t\t\t\t speechSynthesis has no secure-context requirement, so it is\n\t\t\t\t\t offered here even where the microphone cannot be.\n\t\t\t\t\t-->\n\t\t\t\t\t\t<NcActionCheckbox\n\t\t\t\t\t\t\t:modelValue=\"speakReplies\"\n\t\t\t\t\t\t\t:disabled=\"speechSynthesisSupported === false\"\n\t\t\t\t\t\t\tdata-testid=\"cn-ai-panel-speak-toggle\"\n\t\t\t\t\t\t\t@update:modelValue=\"onSpeakRepliesToggled\">\n\t\t\t\t\t\t\t{{ speechSynthesisSupported\n\t\t\t\t\t\t\t\t? cnTranslate('Read replies aloud')\n\t\t\t\t\t\t\t\t: cnTranslate('Read replies aloud (not supported here)') }}\n\t\t\t\t\t\t</NcActionCheckbox>\n\t\t\t\t\t\t<NcActionSeparator />\n\t\t\t\t\t\t<NcActionCaption :name=\"agentCaption\" />\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-for=\"agent in visibleAgentOptions\"\n\t\t\t\t\t\t\t:key=\"agent.id\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"onAgentSelected(agent.id)\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<Check v-if=\"agent.id === selectedAgentUuid\" :size=\"20\" />\n\t\t\t\t\t\t\t\t<CnDashboardIcon v-else :name=\"agent.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ agent.label }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t The escape hatch is NOT optional. The relevance rule below is a\n\t\t\t\t\t heuristic over tool ids, so it will be wrong sometimes; an agent\n\t\t\t\t\t the user knows exists and cannot reach reads as data loss. This\n\t\t\t\t\t stays visible whenever anything is being hidden.\n\t\t\t\t\t-->\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-if=\"hiddenAgentCount > 0\"\n\t\t\t\t\t\t\t:closeAfterClick=\"false\"\n\t\t\t\t\t\t\tdata-testid=\"cn-ai-panel-all-agents\"\n\t\t\t\t\t\t\t@click=\"showAllAgents = true\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<DotsHorizontal :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ cnTranslate('All agents') }} ({{ agentOptions.length }})\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<NcActionCaption\n\t\t\t\t\t\t\tv-if=\"agentOptions.length === 0\"\n\t\t\t\t\t\t\t:name=\"agentsFetchError ? cnTranslate('Could not load agents') : cnTranslate('No agents available')\" />\n\t\t\t\t\t</NcActions>\n\n\t\t\t\t\t<NcActions\n\t\t\t\t\t\t:aria-label=\"cnTranslate('Sessions')\"\n\t\t\t\t\t\t:title=\"cnTranslate('Sessions')\"\n\t\t\t\t\t\t:forceMenu=\"true\"\n\t\t\t\t\t\tdata-testid=\"cn-ai-panel-sessions\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<History :size=\"18\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\t:disabled=\"isOnNewChatScreen\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"onNewChat\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<Plus :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ cnTranslate('Start new chat') }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<NcActionCaption :name=\"cnTranslate('Recent conversations')\" />\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-for=\"conversation in recentConversations\"\n\t\t\t\t\t\t\t:key=\"conversation.uuid\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"onConversationSelect(conversation.uuid)\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<MessageTextOutline :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ conversationLabel(conversation) }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"activeView = 'history'\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<History :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ cnTranslate('View all conversations') }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t</NcActions>\n\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-if=\"!embedded\"\n\t\t\t\t\t\t:aria-label=\"cnTranslate('Close')\"\n\t\t\t\t\t\t:title=\"cnTranslate('Close')\"\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\tdata-testid=\"cn-ai-panel-close\"\n\t\t\t\t\t\t@click=\"onClose\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Close :size=\"18\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</header>\n\n\t\t\t<div v-if=\"activeView === 'chat'\" class=\"cn-ai-chat-window__body\">\n\t\t\t\t<div class=\"cn-ai-chat-window__messages\">\n\t\t\t\t\t<CnAiMessageList\n\t\t\t\t\t\t:messages=\"streamState.messages\"\n\t\t\t\t\t\t:currentText=\"streamState.currentText\"\n\t\t\t\t\t\t:isStreaming=\"streamState.isStreaming\">\n\t\t\t\t\t\t<template #empty>\n\t\t\t\t\t\t\t<div class=\"cn-ai-chat-window__start\" data-testid=\"cn-ai-chat-tab-start\">\n\t\t\t\t\t\t\t\t<NcEmptyContent :name=\"agentLabel\">\n\t\t\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t\t\t<Creation :size=\"40\" />\n\t\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t\t<template #description>\n\t\t\t\t\t\t\t\t\t\t{{ cnTranslate('Ask me anything about what you are viewing.') }}\n\t\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t</NcEmptyContent>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnAiMessageList>\n\t\t\t\t</div>\n\n\t\t\t\t<!--\n\t\t\t\t Decide it HERE. An approval the agent is blocked on is\n\t\t\t\t actionable where the user already is, rather than on an\n\t\t\t\t oversight screen they have to know about and navigate to.\n\n\t\t\t\t Rendered from a GENERIC shape: each entry brings its own `kind`\n\t\t\t\t and its own `resolveUrl`, so a future approval (fetching a URL,\n\t\t\t\t editing a file that is not open) renders and resolves through\n\t\t\t\t this same block unchanged.\n\t\t\t\t-->\n\t\t\t\t<ul\n\t\t\t\t\tv-if=\"pendingApprovals.length\"\n\t\t\t\t\tclass=\"cn-ai-chat-window__approvals\"\n\t\t\t\t\tdata-testid=\"cn-ai-panel-approvals\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"approval in pendingApprovals\"\n\t\t\t\t\t\t:key=\"approval.id\"\n\t\t\t\t\t\tclass=\"cn-ai-approval\">\n\t\t\t\t\t\t<div class=\"cn-ai-approval__head\">\n\t\t\t\t\t\t\t<span class=\"cn-ai-approval__title\">{{ approval.title }}</span>\n\t\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t Reach is a FACT about the tool. The reason below is\n\t\t\t\t\t\t\t agent-authored text aimed at the person holding the\n\t\t\t\t\t\t\t permission, so the two are kept visually distinct.\n\t\t\t\t\t\t\t-->\n\t\t\t\t\t\t\t<span class=\"cn-ai-approval__reach\">{{ approval.reach }}</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<p v-if=\"approval.reason\" class=\"cn-ai-approval__reason\">\n\t\t\t\t\t\t\t{{ approval.reason }}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<div class=\"cn-ai-approval__actions\">\n\t\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\t\t\t:disabled=\"resolvingApprovalId === approval.id\"\n\t\t\t\t\t\t\t\t:data-testid=\"'cn-ai-approval-allow-' + approval.id\"\n\t\t\t\t\t\t\t\t@click=\"onApprovalDecision(approval, 'granted')\">\n\t\t\t\t\t\t\t\t{{ cnTranslate('Allow') }}\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t\t:disabled=\"resolvingApprovalId === approval.id\"\n\t\t\t\t\t\t\t\t:data-testid=\"'cn-ai-approval-deny-' + approval.id\"\n\t\t\t\t\t\t\t\t@click=\"onApprovalDecision(approval, 'refused')\">\n\t\t\t\t\t\t\t\t{{ cnTranslate('Decline') }}\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\n\t\t\t\t<!--\n\t\t\t\t Say why a turn failed, where the answer would have been.\n\n\t\t\t\t The stream already carries `{code, message}` for a refused or\n\t\t\t\t broken turn, and nothing rendered it: measured 2026-09-18 on a\n\t\t\t\t dev instance, an agent whose tool grants resolved to no tools\n\t\t\t\t answered with an SSE `error` event, and the window showed\n\t\t\t\t \"Thinking\", then nothing at all. Silence reads as a hung app.\n\t\t\t\t-->\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"streamState.error\"\n\t\t\t\t\tclass=\"cn-ai-chat-window__error\"\n\t\t\t\t\trole=\"alert\"\n\t\t\t\t\tdata-testid=\"cn-ai-panel-error\">\n\t\t\t\t\t{{ streamState.error.message || cnTranslate('The assistant could not answer.') }}\n\t\t\t\t</p>\n\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"note\"\n\t\t\t\t\tclass=\"cn-ai-chat-window__note\"\n\t\t\t\t\tdata-testid=\"cn-ai-panel-note\">\n\t\t\t\t\t<InformationOutline :size=\"18\" class=\"cn-ai-chat-window__note-icon\" />\n\t\t\t\t\t<span>{{ note }}</span>\n\t\t\t\t</p>\n\n\t\t\t\t<div class=\"cn-ai-chat-window__input\">\n\t\t\t\t\t<CnAiInput\n\t\t\t\t\t\tref=\"input\"\n\t\t\t\t\t\t:disabled=\"streamState.isStreaming\"\n\t\t\t\t\t\t:placeholder=\"inputPlaceholder\"\n\t\t\t\t\t\t:sendLabel=\"sendLabel\"\n\t\t\t\t\t\t:stacked=\"titled\"\n\t\t\t\t\t\t:chatAppId=\"chatAppId\"\n\t\t\t\t\t\t:speechInputEngine=\"speechPolicy.inputEngine\"\n\t\t\t\t\t\t:dictationSilenceTimeout=\"speechPolicy.silenceTimeout\"\n\t\t\t\t\t\t:localSpeechAvailable=\"localSpeechAvailable\"\n\t\t\t\t\t\t:conversationEnabled=\"speechPolicy.conversationEnabled\"\n\t\t\t\t\t\t@send=\"onSend\"\n\t\t\t\t\t\t@conversationState=\"onConversationTurn\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div v-else class=\"cn-ai-chat-window__body cn-ai-chat-window__body--history\">\n\t\t\t\t<CnAiHistoryList\n\t\t\t\t\t:conversations=\"conversations\"\n\t\t\t\t\t:loading=\"conversationsLoading\"\n\t\t\t\t\t:fetchError=\"conversationsFetchError\"\n\t\t\t\t\t:activeConversationUuid=\"activeConversationUuid\"\n\t\t\t\t\t:chatAppId=\"chatAppId\"\n\t\t\t\t\t:searchable=\"true\"\n\t\t\t\t\t@select=\"onConversationSelect\"\n\t\t\t\t\t@renamed=\"onConversationRenamed\" />\n\t\t\t</div>\n\n\t\t\t<!--\n\t\t\t The organisation's approved-assistant mark (thematiq). Rendered\n\t\t\t only when thematiq reports it on; the label arrives translated\n\t\t\t and is shown as received. Present in the chat and history views.\n\t\t\t-->\n\t\t\t<div\n\t\t\t\tv-if=\"assistantMark\"\n\t\t\t\tclass=\"cn-ai-chat-window__mark\"\n\t\t\t\trole=\"note\"\n\t\t\t\tdata-testid=\"cn-ai-panel-mark\">\n\t\t\t\t<img\n\t\t\t\t\tv-if=\"assistantMark.logo\"\n\t\t\t\t\tclass=\"cn-ai-chat-window__mark-logo\"\n\t\t\t\t\t:src=\"assistantMark.logo.url\"\n\t\t\t\t\t:alt=\"assistantMark.logo.alt\">\n\t\t\t\t<span class=\"cn-ai-chat-window__mark-label\">{{ assistantMark.label }}</span>\n\t\t\t</div>\n\t\t</div>\n\t</transition>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { NcActionButton, NcActionCaption, NcActionCheckbox, NcActions, NcActionSeparator, NcButton, NcEmptyContent } from '@nextcloud/vue'\nimport Check from 'vue-material-design-icons/Check.vue'\nimport Close from 'vue-material-design-icons/Close.vue'\nimport Cog from 'vue-material-design-icons/Cog.vue'\nimport Creation from 'vue-material-design-icons/Creation.vue'\nimport DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'\nimport History from 'vue-material-design-icons/History.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport MessageTextOutline from 'vue-material-design-icons/MessageTextOutline.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport CnDashboardIcon from '../CnIconPicker/CnDashboardIcon.vue'\nimport CnAiHistoryList from './CnAiHistoryList.vue'\nimport CnAiInput from './CnAiInput.vue'\nimport CnAiMessageList from './CnAiMessageList.vue'\nimport {\n\tagentsUrl,\n\tconversationsUrl,\n\tDEFAULT_CHAT_APP_ID,\n\tnormalizeConversation,\n\tspeechCapabilitiesUrl,\n\tspeechSynthesisUrl,\n} from '../../composables/aiChatConfig.js'\nimport {\n\tbrowserSynthesisUsable,\n\tnormalizeAgentSpeechPolicy,\n\tresolveSpeakingEngine,\n\tSPEECH_LOCAL,\n\tSPEECH_OFF,\n} from '../../composables/aiSpeechPolicy.js'\nimport { getAssistantMark } from '../../utils/assistantMark.js'\nimport { libraryTranslate } from '../../utils/libraryTranslate.js'\n\n/**\n * Shown when an agent has no `icon` set, which is most of them. Matches the\n * icon the agent list has always used, so an agent without an icon does not\n * change appearance between the list and the window title.\n *\n * @type {string}\n */\nconst DEFAULT_AGENT_ICON = 'RobotOutline'\n\n/** localStorage key remembering whether replies are read aloud. */\nconst SPEAK_PREFERENCE_KEY = 'cn-ai-speak-replies'\n\n/** Recent conversations offered directly in the sessions menu — top N of the fetched list. */\nconst RECENT_CONVERSATIONS_LIMIT = 5\n\n/** Longest conversation title shown in the sessions menu before ellipsis. */\nconst MENU_TITLE_MAX = 40\n\nexport default {\n\tname: 'CnAiChatPanel',\n\n\tcomponents: {\n\t\tNcActions,\n\t\tNcActionButton,\n\t\tNcActionCaption,\n\t\tNcActionCheckbox,\n\t\tNcActionSeparator,\n\t\tNcButton,\n\t\tNcEmptyContent,\n\t\tPlus,\n\t\tHistory,\n\t\tInformationOutline,\n\t\tCreation,\n\t\tClose,\n\t\tCheck,\n\t\tCog,\n\t\tMessageTextOutline,\n\t\tDotsHorizontal,\n\t\tCnAiMessageList,\n\t\tCnAiInput,\n\t\tCnAiHistoryList,\n\t\tCnDashboardIcon,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: () => libraryTranslate },\n\t},\n\n\tprops: {\n\t\t/** Controls panel visibility */\n\t\tvisible: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Draw the window in the page's flow, as a panel of the page (a tab\n\t\t * of a detail page), instead of floating above it from the launcher:\n\t\t * no pointer, no close button, and Escape does not close it.\n\t\t */\n\t\tembedded: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * The panel stands in a card that draws its title: no titlebar of its\n\t\t * own, and the session controls (new chat, conversations, read aloud,\n\t\t * agent) are reported through the `menu` event for the card's menu.\n\t\t */\n\t\ttitled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** A line above the input, already translated (what the assistant can see). */\n\t\tnote: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The input's placeholder, already translated; '' keeps the default. */\n\t\tinputPlaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** A label on the send button, already translated; '' keeps the icon. */\n\t\tsendLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Reactive state from useAiChatStream: `{ messages, currentText, isStreaming, conversationUuid, error }`.\n\t\t */\n\t\tstreamState: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * The launcher button, kept for focus return on close.\n\t\t */\n\t\tfabRef: {\n\t\t\ttype: [Object, null],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Corner the window is anchored to — mirrors the launcher hex's own\n\t\t * `position`, so the window opens from the button rather than across\n\t\t * the page from it.\n\t\t *\n\t\t * @type {'bottom-right'|'bottom-left'|'top-right'|'top-left'}\n\t\t */\n\t\tposition: {\n\t\t\ttype: String,\n\t\t\tdefault: 'bottom-right',\n\t\t},\n\n\t\t/**\n\t\t * Backend app id that answers the chat/agents/conversations HTTP calls.\n\t\t */\n\t\tchatAppId: {\n\t\t\ttype: String,\n\t\t\tdefault: DEFAULT_CHAT_APP_ID,\n\t\t},\n\n\t\t/**\n\t\t * The page the companion is sitting on: `{ appId, pageKind, fileId, route }`.\n\t\t *\n\t\t * Used ONLY to decide which agents are worth offering first. An empty\n\t\t * object means \"no page context\", and every agent is listed — the\n\t\t * behaviour before filtering existed.\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\t/**\n\t * Declared so the linter can check them and a consumer can read the\n\t * component's outward surface without grepping for `$emit`. These four were\n\t * always emitted; only the declaration is new.\n\t */\n\temits: ['close', 'send', 'new-thread', 'load-conversation', 'menu'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** The approved-assistant mark from thematiq, or null when there is none. */\n\t\t\tassistantMark: null,\n\t\t\tactiveView: 'chat',\n\t\t\tactiveConversationUuid: null,\n\t\t\t/**\n\t\t\t * Whether the user has asked past the page-relevance filter. Sticky\n\t\t\t * for the life of the panel: having asked for everything once, being\n\t\t\t * silently re-filtered on the next open is the annoying half of this\n\t\t\t * feature.\n\t\t\t */\n\t\t\tshowAllAgents: false,\n\t\t\t/**\n\t\t\t * Whether assistant replies are read aloud. Remembered across\n\t\t\t * sessions — a preference a user turns on once, not per chat.\n\t\t\t */\n\t\t\tspeakReplies: false,\n\t\t\t/** Text already spoken, so a re-render never repeats an answer. */\n\t\t\tspokenText: '',\n\t\t\t/** Approvals decided locally, hidden until the server confirms. */\n\t\t\tresolvedApprovalIds: [],\n\t\t\t/** The approval currently being posted, so it cannot double-fire. */\n\t\t\tresolvingApprovalId: '',\n\t\t\tagents: [],\n\t\t\tagentsLoading: false,\n\t\t\tagentsFetchError: false,\n\t\t\tselectedAgentUuid: null,\n\t\t\t/**\n\t\t\t * Whether the backend's own speech service answered its probe.\n\t\t\t *\n\t\t\t * Probed ONCE per panel and passed down, rather than asked by each\n\t\t\t * composer. Starts false so nothing offers a private engine before\n\t\t\t * the answer arrives — the wrong direction to be wrong in is the one\n\t\t\t * where a \"private\" microphone turns out to be a cloud one.\n\t\t\t */\n\t\t\tlocalSpeechAvailable: false,\n\t\t\t/** The `<audio>` element playing a locally synthesised reply, if any. */\n\t\t\tlocalSpeechAudio: null,\n\t\t\t/** Whether the composer is in a hands-free conversation. */\n\t\t\tconversationActive: false,\n\t\t\tconversations: [],\n\t\t\tconversationsLoading: false,\n\t\t\tconversationsFetchError: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Top N conversations offered directly in the sessions menu. */\n\t\trecentConversations() {\n\t\t\treturn this.conversations.slice(0, RECENT_CONVERSATIONS_LIMIT)\n\t\t},\n\n\t\t/**\n\t\t * Agents as `{ id, label }`, matching the shape the old inline picker\n\t\t * used — the agents API returns `uuid` on some deployments and `id` on\n\t\t * others, and both have been seen in the wild.\n\t\t *\n\t\t * @return {Array<{id: string, label: string}>} Selectable agents.\n\t\t */\n\t\t/**\n\t\t * The raw agent object currently selected, or null.\n\t\t *\n\t\t * ⚠️ The RAW object, not the `agentOptions` entry. Those carry only an\n\t\t * id, a label and an icon — everything the speech policy needs is\n\t\t * dropped by that mapping, so reading the policy from an option would\n\t\t * silently produce defaults for every agent.\n\t\t *\n\t\t * @return {object|null} The selected agent.\n\t\t */\n\t\tselectedAgent() {\n\t\t\tif (!this.selectedAgentUuid) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\treturn this.agents.find((agent) => (agent.uuid || agent.id) === this.selectedAgentUuid) || null\n\t\t},\n\n\t\t/**\n\t\t * The selected agent's speech policy, defaulted.\n\t\t *\n\t\t * @return {{inputEngine: string, outputEngine: string, silenceTimeout: number, conversationEnabled: boolean}} The policy.\n\t\t */\n\t\tspeechPolicy() {\n\t\t\treturn normalizeAgentSpeechPolicy(this.selectedAgent)\n\t\t},\n\n\t\t/**\n\t\t * Which engine may speak this agent's replies.\n\t\t *\n\t\t * @return {{engine: string, reason: string}} The decision.\n\t\t */\n\t\tspeakingDecision() {\n\t\t\treturn resolveSpeakingEngine(this.speechPolicy, {\n\t\t\t\tbrowserUsable: browserSynthesisUsable(),\n\t\t\t\tlocalUsable: this.localSpeechAvailable === true,\n\t\t\t})\n\t\t},\n\n\t\tagentOptions() {\n\t\t\treturn this.agents.map((agent) => ({\n\t\t\t\tid: agent.uuid || agent.id,\n\t\t\t\tlabel: agent.name || agent.title || this.cnTranslate('Untitled agent'),\n\t\t\t\t// `Agent.icon` is an MDI registry name (CnIconPicker); empty means\n\t\t\t\t// \"use the default agent icon\", which is what the list showed for\n\t\t\t\t// every agent before any of them had one set.\n\t\t\t\ticon: agent.icon || DEFAULT_AGENT_ICON,\n\t\t\t\ttools: Array.isArray(agent.tools) ? agent.tools : [],\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The agents worth offering on THIS page, most specific first.\n\t\t *\n\t\t * ⚠️ This is a HEURISTIC over tool ids, and it is written to fail open.\n\t\t * An agent is relevant when it holds a tool whose owning app is the app\n\t\t * of the current page, OR — on a file/document page — a tool that is\n\t\t * about documents at all.\n\t\t *\n\t\t * The second clause is not decoration. The demo's document agent holds\n\t\t * `docudesk.*` tools while the page it is used on is `eurooffice`, so a\n\t\t * plain \"tool app === page app\" rule HIDES exactly the agent the user\n\t\t * wants. Matching page KIND as well as page APP is what makes the rule\n\t\t * describe capability rather than packaging.\n\t\t *\n\t\t * Falls back to every agent when the filter would empty the list — an\n\t\t * empty agent menu is indistinguishable from a broken one.\n\t\t *\n\t\t * @return {Array<object>} Agents judged relevant to the current page.\n\t\t */\n\t\trelevantAgentOptions() {\n\t\t\tconst appId = (this.context && this.context.appId) || ''\n\t\t\tconst pageKind = (this.context && this.context.pageKind) || ''\n\t\t\tif (appId === '' && pageKind === '') {\n\t\t\t\treturn this.agentOptions\n\t\t\t}\n\n\t\t\tconst isDocumentPage = pageKind === 'file' || pageKind === 'document'\n\t\t\tconst matches = this.agentOptions.filter((agent) => agent.tools.some((tool) => {\n\t\t\t\tconst id = String(tool).toLowerCase()\n\t\t\t\t// Tool ids are `app.verb` or `app_verb`; the app is the first segment.\n\t\t\t\tconst app = id.split(/[._]/)[0]\n\t\t\t\tif (appId !== '' && app === appId.toLowerCase()) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\treturn isDocumentPage && /document|file/.test(id)\n\t\t\t}))\n\n\t\t\treturn matches.length > 0 ? matches : this.agentOptions\n\t\t},\n\n\t\t/**\n\t\t * What the menu actually lists: the relevant subset, or everything once\n\t\t * the user has asked for everything.\n\t\t *\n\t\t * @return {Array<object>} Agents to render.\n\t\t */\n\t\tvisibleAgentOptions() {\n\t\t\treturn this.showAllAgents ? this.agentOptions : this.relevantAgentOptions\n\t\t},\n\n\t\t/**\n\t\t * How many agents the filter is currently hiding.\n\t\t *\n\t\t * @return {number} Hidden count, 0 when nothing is hidden.\n\t\t */\n\t\thiddenAgentCount() {\n\t\t\treturn this.agentOptions.length - this.visibleAgentOptions.length\n\t\t},\n\n\t\t/**\n\t\t * The menu heading, which must SAY when the list is filtered — a silently\n\t\t * shortened list is the same failure as a silently truncated one.\n\t\t *\n\t\t * @return {string} Caption text.\n\t\t */\n\t\tagentCaption() {\n\t\t\treturn this.hiddenAgentCount > 0\n\t\t\t\t? this.cnTranslate('Agents for this page')\n\t\t\t\t: this.cnTranslate('Agent')\n\t\t},\n\n\t\t/**\n\t\t * The selected agent's icon name, for the titlebar.\n\t\t *\n\t\t * @return {string} An MDI registry name.\n\t\t */\n\t\tagentIconName() {\n\t\t\tconst selected = this.agentOptions.find((agent) => agent.id === this.selectedAgentUuid)\n\t\t\treturn (selected && selected.icon) || DEFAULT_AGENT_ICON\n\t\t},\n\n\t\t/**\n\t\t * Whether this browser can speak at all.\n\t\t *\n\t\t * Checked as a CAPABILITY, not a constructor: the sibling dictation\n\t\t * button shipped broken because `webkitSpeechRecognition` exists on an\n\t\t * insecure origin while refusing to run. `speechSynthesis` differs —\n\t\t * it has no secure-context requirement — but it is still checked rather\n\t\t * than assumed, and the toggle says so when absent instead of silently\n\t\t * doing nothing.\n\t\t *\n\t\t * @return {boolean} true when speech synthesis is usable.\n\t\t */\n\t\t/**\n\t\t * Approvals still awaiting a decision, minus any just decided here.\n\t\t *\n\t\t * The local filter matters: the server list is refreshed only on the\n\t\t * next turn, so without it a card the user just answered would sit there\n\t\t * looking unanswered until they sent another message.\n\t\t *\n\t\t * @return {Array<object>} Approvals to render.\n\t\t */\n\t\tpendingApprovals() {\n\t\t\tconst list = (this.streamState && this.streamState.pendingApprovals) || []\n\t\t\treturn list.filter((a) => this.resolvedApprovalIds.includes(a.id) === false)\n\t\t},\n\n\t\tspeechSynthesisSupported() {\n\t\t\treturn typeof window !== 'undefined'\n\t\t\t\t&& typeof window.speechSynthesis !== 'undefined'\n\t\t\t\t&& typeof window.SpeechSynthesisUtterance === 'function'\n\t\t},\n\n\t\t/**\n\t\t * The window's title: the selected agent's name.\n\t\t *\n\t\t * Falls back while the agent list is still loading, and again if it\n\t\t * failed — a titlebar with an empty title reads as a broken window, so\n\t\t * every branch names something.\n\t\t *\n\t\t * @return {string} Title text.\n\t\t */\n\t\tagentLabel() {\n\t\t\tconst selected = this.agentOptions.find((agent) => agent.id === this.selectedAgentUuid)\n\t\t\tif (selected) {\n\t\t\t\treturn selected.label\n\t\t\t}\n\n\t\t\treturn this.agentsLoading\n\t\t\t\t? this.cnTranslate('Loading…')\n\t\t\t\t: this.cnTranslate('AI assistant')\n\t\t},\n\n\t\t/**\n\t\t * Whether the panel is already showing a fresh, empty new-chat screen —\n\t\t * used to disable \"Start new chat\" (disabled, not hidden, per design).\n\t\t */\n\t\tisOnNewChatScreen() {\n\t\t\treturn !this.streamState.isStreaming && this.streamState.messages.length === 0\n\t\t},\n\n\t\t/**\n\t\t * The titlebar's controls as menu items, for a titled panel's card.\n\t\t *\n\t\t * @return {Array<{label: string, icon: string, run: () => void}>} The items.\n\t\t */\n\t\tmenuActions() {\n\t\t\tconst actions = []\n\t\t\tif (!this.isOnNewChatScreen) {\n\t\t\t\tactions.push({ label: this.cnTranslate('Start new chat'), icon: 'Plus', run: () => this.onNewChat() })\n\t\t\t}\n\t\t\tactions.push(this.activeView === 'history'\n\t\t\t\t? { label: this.cnTranslate('Back to the chat'), icon: 'MessageTextOutline', run: () => { this.activeView = 'chat' } }\n\t\t\t\t: { label: this.cnTranslate('View all conversations'), icon: 'History', run: () => { this.activeView = 'history' } })\n\t\t\tif (this.speechSynthesisSupported) {\n\t\t\t\tactions.push(this.speakReplies\n\t\t\t\t\t? { label: this.cnTranslate('Stop reading replies aloud'), icon: 'VolumeOff', run: () => this.onSpeakRepliesToggled(false) }\n\t\t\t\t\t: { label: this.cnTranslate('Read replies aloud'), icon: 'VolumeHigh', run: () => this.onSpeakRepliesToggled(true) })\n\t\t\t}\n\t\t\tfor (const agent of this.agentOptions) {\n\t\t\t\tif (agent.id !== this.selectedAgentUuid) {\n\t\t\t\t\tactions.push({ label: this.cnTranslate('Talk to {agent}', { agent: agent.label }), icon: agent.icon || DEFAULT_AGENT_ICON, run: () => this.onAgentSelected(agent.id) })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn actions\n\t\t},\n\t},\n\n\twatch: {\n\t\tmenuActions: {\n\t\t\timmediate: true,\n\t\t\thandler(actions) {\n\t\t\t\tif (this.titled) {\n\t\t\t\t\t/**\n\t\t\t\t\t * @event menu The session controls of a titled panel, for its card's menu.\n\t\t\t\t\t * @type {Array<{label: string, icon: string, run: () => void}>}\n\t\t\t\t\t */\n\t\t\t\t\tthis.$emit('menu', actions)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\tvisible(newVal) {\n\t\t\tif (newVal) {\n\t\t\t\tthis.$nextTick(() => {\n\t\t\t\t\tif (this.$refs.input) {\n\t\t\t\t\t\tthis.$refs.input.focus()\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The transport composable learns the server-assigned conversation uuid\n\t\t * from the first turn's response (see useAiChatStream.js's finalise()).\n\t\t * Mirror it into the panel's own tracked uuid so a brand-new conversation\n\t\t * shows as \"active\" immediately — the same tracking that drives\n\t\t * isOnNewChatScreen and the History/recent-sessions active-row indicator.\n\t\t *\n\t\t * @param {string|null} newVal The stream's current conversation uuid.\n\t\t */\n\t\t'streamState.conversationUuid': function(newVal) {\n\t\t\tif (newVal) {\n\t\t\t\tthis.activeConversationUuid = newVal\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Speak a finished assistant reply when read-aloud is on.\n\t\t *\n\t\t * Waits for `isStreaming` to go false rather than speaking the growing\n\t\t * text: speaking a partial answer and then speaking it again complete is\n\t\t * worse than a short wait.\n\t\t *\n\t\t * @param {boolean} streaming Whether a turn is still in flight.\n\t\t */\n\t\t'streamState.isStreaming': function(streaming) {\n\t\t\tif (streaming === true) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A spoken conversation speaks regardless of the toggle: the user is\n\t\t\t// talking to the agent with the screen out of reach, and a silent\n\t\t\t// reply in that mode is no reply at all.\n\t\t\tif (this.speakReplies !== true && this.conversationActive !== true) {\n\t\t\t\t// Nothing to speak, so the user's next turn can start now.\n\t\t\t\tthis.resumeConversationIfActive()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.speakLatestReply()\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.fetchAgents()\n\t\tthis.fetchConversations()\n\t\tthis.restoreSpeakPreference()\n\t\tthis.probeLocalSpeech()\n\t\tthis.loadAssistantMark()\n\t},\n\n\tbeforeUnmount() {\n\t\t// Never leave the browser talking to a panel that has closed.\n\t\tthis.stopSpeaking()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Load the approved-assistant mark; stays null when there is none.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadAssistantMark() {\n\t\t\tthis.assistantMark = await getAssistantMark()\n\t\t},\n\n\t\t/**\n\t\t * Read the remembered read-aloud preference.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\trestoreSpeakPreference() {\n\t\t\ttry {\n\t\t\t\tthis.speakReplies = (window.localStorage.getItem(SPEAK_PREFERENCE_KEY) === '1')\n\t\t\t} catch {\n\t\t\t\t// Storage can be unavailable (private mode, blocked cookies).\n\t\t\t\t// The toggle then simply does not persist.\n\t\t\t\tthis.speakReplies = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Turn read-aloud on or off and remember it.\n\t\t *\n\t\t * @param {boolean} value The new state.\n\t\t * @return {void}\n\t\t */\n\t\tonSpeakRepliesToggled(value) {\n\t\t\tthis.speakReplies = (value === true)\n\t\t\tif (this.speakReplies === false) {\n\t\t\t\tthis.stopSpeaking()\n\t\t\t}\n\t\t\ttry {\n\t\t\t\twindow.localStorage.setItem(SPEAK_PREFERENCE_KEY, this.speakReplies ? '1' : '0')\n\t\t\t} catch {\n\t\t\t\t// Preference is still honoured for this session.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Speak the most recent assistant message, once.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\t/**\n\t\t * Track the composer's conversation state.\n\t\t *\n\t\t * @param {boolean} active Whether a spoken conversation is running.\n\t\t * @return {void}\n\t\t */\n\t\tonConversationTurn(active) {\n\t\t\tthis.conversationActive = (active === true)\n\t\t\tif (this.conversationActive === false) {\n\t\t\t\tthis.stopSpeaking()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Hand the microphone back to the user, if a conversation is running.\n\t\t *\n\t\t * 🔴 CALLED WHEN THE SPEAKING STOPS, NOT WHEN THE STREAM DOES. Reopening\n\t\t * the microphone while the reply is still being spoken records the agent\n\t\t * through the speakers and sends it back as the user's next turn — the\n\t\t * agent then answers itself, on a loop, hands-free.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tresumeConversationIfActive() {\n\t\t\tif (this.conversationActive !== true) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst input = this.$refs.input\n\n\t\t\tif (input && typeof input.resumeConversation === 'function') {\n\t\t\t\tinput.resumeConversation()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Ask the backend whether its own speech service can actually perform.\n\t\t *\n\t\t * 🔴 ASKED, NOT ASSUMED, and asked ONCE for the whole panel. A backend\n\t\t * that has the endpoint but no reachable speech service answers\n\t\t * `available: false`, and a backend that has no such endpoint at all\n\t\t * (any app other than the agent engine) 404s — both mean the same thing\n\t\t * here: do not offer the private engine. Failing to the safe answer\n\t\t * matters more than usual, because the alternative engine is a cloud\n\t\t * one.\n\t\t *\n\t\t * @return {Promise<void>} Resolves once the answer is stored.\n\t\t */\n\t\tasync probeLocalSpeech() {\n\t\t\ttry {\n\t\t\t\tconst response = await axios.get(speechCapabilitiesUrl(this.chatAppId))\n\t\t\t\tthis.localSpeechAvailable = !!(response && response.data && response.data.available)\n\t\t\t} catch {\n\t\t\t\tthis.localSpeechAvailable = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Speak a reply through the instance's own speech service.\n\t\t *\n\t\t * @param {string} text The text to speak.\n\t\t * @return {Promise<void>} Resolves when playback has started.\n\t\t */\n\t\tasync speakLocally(text) {\n\t\t\ttry {\n\t\t\t\tconst response = await axios.post(\n\t\t\t\t\tspeechSynthesisUrl(this.chatAppId),\n\t\t\t\t\t{ text },\n\t\t\t\t\t{ responseType: 'blob' },\n\t\t\t\t)\n\t\t\t\tconst url = URL.createObjectURL(response.data)\n\t\t\t\tthis.localSpeechAudio = new Audio(url)\n\t\t\t\t// Revoked on end, not left to the page's lifetime: a spoken\n\t\t\t\t// conversation would otherwise accumulate one object URL per\n\t\t\t\t// turn for as long as the panel stays open.\n\t\t\t\tthis.localSpeechAudio.onended = () => {\n\t\t\t\t\tURL.revokeObjectURL(url)\n\t\t\t\t\tthis.localSpeechAudio = null\n\t\t\t\t\tthis.resumeConversationIfActive()\n\t\t\t\t}\n\t\t\t\tawait this.localSpeechAudio.play()\n\t\t\t} catch {\n\t\t\t\t// Speaking is a convenience; a failure must not disturb the chat\n\t\t\t\t// — but a conversation waiting on it must not stall either.\n\t\t\t\tthis.resumeConversationIfActive()\n\t\t\t}\n\t\t},\n\n\t\tspeakLatestReply() {\n\t\t\t// ⚠️ EVERY EARLY RETURN HERE HANDS THE TURN BACK. In a conversation\n\t\t\t// the microphone reopens when the speaking ENDS, so a path that\n\t\t\t// returns without speaking and without saying so leaves the user\n\t\t\t// holding a dead microphone, waiting for an agent that already\n\t\t\t// answered.\n\t\t\tif (this.speakingDecision.engine === SPEECH_OFF) {\n\t\t\t\tthis.resumeConversationIfActive()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst messages = (this.streamState && this.streamState.messages) || []\n\t\t\tlet latest = ''\n\t\t\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\t\t\tif (messages[i] && messages[i].role === 'assistant') {\n\t\t\t\t\tlatest = String(messages[i].content || '')\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (latest === '' || latest === this.spokenText) {\n\t\t\t\tthis.resumeConversationIfActive()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.stopSpeaking()\n\t\t\tthis.spokenText = latest\n\n\t\t\t// Strip the markdown the model writes — speaking \"asterisk asterisk\n\t\t\t// Klant asterisk asterisk\" is worse than not speaking at all.\n\t\t\tconst spoken = latest\n\t\t\t\t.replace(/`{1,3}[^`]*`{1,3}/g, ' ')\n\t\t\t\t.replace(/[*_#>|]/g, ' ')\n\t\t\t\t.replace(/\\[(.*?)\\]\\(.*?\\)/g, '$1')\n\t\t\t\t.replace(/\\s+/g, ' ')\n\t\t\t\t.trim()\n\n\t\t\t// 🔴 The engine comes from the DECISION, not from what the browser\n\t\t\t// happens to support. An agent whose replies may quote a case file\n\t\t\t// is configured `local`, and speaking that reply through the\n\t\t\t// browser's synthesis would hand the quote to the same vendor the\n\t\t\t// dictation setting was written to avoid.\n\t\t\tif (this.speakingDecision.engine === SPEECH_LOCAL) {\n\t\t\t\tthis.speakLocally(spoken)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst utterance = new window.SpeechSynthesisUtterance(spoken)\n\t\t\t\tutterance.lang = (typeof document !== 'undefined' && document.documentElement.lang)\n\t\t\t\t\t? document.documentElement.lang\n\t\t\t\t\t: 'en-US'\n\t\t\t\tutterance.onend = () => this.resumeConversationIfActive()\n\t\t\t\twindow.speechSynthesis.speak(utterance)\n\t\t\t} catch {\n\t\t\t\t// Speaking is a convenience; a failure must not disturb the chat\n\t\t\t\t// — but a conversation waiting on it must not stall either.\n\t\t\t\tthis.resumeConversationIfActive()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Stop any speech in progress.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstopSpeaking() {\n\t\t\t// Both engines, unconditionally. Stopping only the one currently in\n\t\t\t// use leaves the other playing when the agent — and with it the\n\t\t\t// engine — changes mid-sentence.\n\t\t\tif (this.localSpeechAudio !== null) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.localSpeechAudio.pause()\n\t\t\t\t} catch {\n\t\t\t\t\t// Already stopped.\n\t\t\t\t}\n\t\t\t\tthis.localSpeechAudio = null\n\t\t\t}\n\t\t\tif (this.speechSynthesisSupported === false) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\twindow.speechSynthesis.cancel()\n\t\t\t} catch {\n\t\t\t\t// Nothing to cancel.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Record the owner's decision on an approval, then let the agent carry on.\n\t\t *\n\t\t * Posts to the approval's OWN `resolveUrl` rather than to a URL built\n\t\t * here, which is what keeps this generic — a new approval kind supplies\n\t\t * its endpoint and this code is unchanged. The endpoint is the existing\n\t\t * owner-only one, so \"only the owner decides\" is enforced in one place\n\t\t * and is not re-implemented for the chat.\n\t\t *\n\t\t * On a grant the agent is nudged to continue: it stopped because it could\n\t\t * not proceed, and making the user retype their request would be the\n\t\t * whole point of deciding in the chat, lost.\n\t\t *\n\t\t * @param {object} approval The approval being decided.\n\t\t * @param {string} decision 'granted' or 'refused'.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onApprovalDecision(approval, decision) {\n\t\t\tif (this.resolvingApprovalId !== '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.resolvingApprovalId = approval.id\n\n\t\t\ttry {\n\t\t\t\tawait axios.post(approval.resolveUrl, { decision })\n\t\t\t\tthis.resolvedApprovalIds = this.resolvedApprovalIds.concat([approval.id])\n\n\t\t\t\tif (decision === 'granted') {\n\t\t\t\t\t// A grant only takes effect on the NEXT turn — the agent's tool\n\t\t\t\t\t// set is resolved at turn assembly — so continuing is what makes\n\t\t\t\t\t// the new capability usable without the user repeating themselves.\n\t\t\t\t\tthis.$emit(\n\t\t\t\t\t\t'send',\n\t\t\t\t\t\tthis.cnTranslate('I approved that access. Please continue.'),\n\t\t\t\t\t\tthis.selectedAgentUuid,\n\t\t\t\t\t\t[],\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Leave the card in place: a decision that did not record must not\n\t\t\t\t// look as though it did.\n\t\t\t\tthis.resolvingApprovalId = ''\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.resolvingApprovalId = ''\n\t\t},\n\n\t\t/**\n\t\t * Escape closes the floating window; an embedded panel is part of the\n\t\t * page and stays.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonEscape() {\n\t\t\tif (!this.embedded) {\n\t\t\t\tthis.onClose()\n\t\t\t}\n\t\t},\n\n\t\tonClose() {\n\t\t\tthis.stopSpeaking()\n\t\t\tthis.$emit('close')\n\t\t\tthis.returnFocusToLauncher()\n\t\t},\n\n\t\t/**\n\t\t * Put focus back on the launcher the panel was opened from (WCAG 2.2\n\t\t * SC 2.4.3 Focus Order). The panel is hidden rather than unmounted, so\n\t\t * without this the focused control disappears and focus falls to the\n\t\t * document body, stranding a keyboard user at the top of the page.\n\t\t *\n\t\t * `fabRef` may be a component instance or a plain element, so both\n\t\t * shapes are accepted.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\treturnFocusToLauncher() {\n\t\t\tconst el = this.fabRef?.$el ?? this.fabRef\n\t\t\tif (el && typeof el.focus === 'function') {\n\t\t\t\tel.focus()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A conversation's menu label, trimmed to keep the menu one line per row.\n\t\t *\n\t\t * @param {{title: string, uuid: string}} conversation The conversation.\n\t\t * @return {string} Display label.\n\t\t */\n\t\tconversationLabel(conversation) {\n\t\t\tconst title = (conversation.title || '').trim() || this.cnTranslate('Untitled conversation')\n\n\t\t\treturn title.length > MENU_TITLE_MAX\n\t\t\t\t? `${title.slice(0, MENU_TITLE_MAX - 1)}…`\n\t\t\t\t: title\n\t\t},\n\n\t\t/**\n\t\t * Re-emit CnAiInput's `{ text, attachments }` payload up to\n\t\t * CnAiCompanion, which owns the stream composable, adding the\n\t\t * currently-selected agent uuid (this panel's own picker state).\n\t\t *\n\t\t * @param {{text: string, attachments: Array<{path: string, name: string}>}} payload CnAiInput's send payload.\n\t\t */\n\t\tonSend({ text, attachments }) {\n\t\t\tthis.$emit('send', text, this.selectedAgentUuid, attachments)\n\t\t},\n\n\t\tonNewChat() {\n\t\t\tthis.activeView = 'chat'\n\t\t\tif (this.isOnNewChatScreen) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.activeConversationUuid = null\n\t\t\tthis.$emit('new-thread')\n\t\t},\n\n\t\tonAgentSelected(uuid) {\n\t\t\tthis.selectedAgentUuid = uuid\n\t\t},\n\n\t\tonConversationSelect(uuid) {\n\t\t\tthis.activeConversationUuid = uuid\n\t\t\tthis.activeView = 'chat'\n\t\t\tthis.$emit('load-conversation', uuid)\n\t\t},\n\n\t\t/**\n\t\t * Keep the panel's own conversation list (feeding both the sessions menu\n\t\t * and the history view) in sync after an inline rename/describe save,\n\t\t * without a full refetch.\n\t\t *\n\t\t * @param {{uuid: string, title: string, description: string}} payload Renamed fields.\n\t\t */\n\t\tonConversationRenamed({ uuid, title, description }) {\n\t\t\tconst index = this.conversations.findIndex((conv) => conv.uuid === uuid)\n\t\t\tif (index !== -1) {\n\t\t\t\tthis.conversations.splice(index, 1, { ...this.conversations[index], title, description })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch the agent list for the agent menu. Defaults the selection to the\n\t\t * first agent this page has a use for, which is the same subset the menu\n\t\t * shows: `relevantAgentOptions`. The agents API has no \"default agent\"\n\t\t * indicator to prefer instead (verified against hermiq's\n\t\t * AgentsController::index()/serializeAgent()), so page relevance is the\n\t\t * best signal available.\n\t\t *\n\t\t * Defaulting to the raw first agent instead, as this did, is not a\n\t\t * cosmetic difference. On a Buildiq page it handed the user an agent\n\t\t * holding no Buildiq tools, so the first thing the assistant did was\n\t\t * fail to do the one thing the page is for, and the fix was a settings\n\t\t * menu the user had no reason to open. `relevantAgentOptions` already\n\t\t * falls back to every agent when nothing matches, so on a page with no\n\t\t * relevant agent this still selects exactly what it selected before.\n\t\t *\n\t\t * Degrades gracefully on failure: the menu shows an inline notice but\n\t\t * the rest of the window (history, message input) stays usable.\n\t\t *\n\t\t * @returns {Promise<void>}\n\t\t */\n\t\tasync fetchAgents() {\n\t\t\tthis.agentsLoading = true\n\t\t\tthis.agentsFetchError = false\n\t\t\ttry {\n\t\t\t\tconst response = await axios.get(agentsUrl(this.chatAppId))\n\t\t\t\tconst data = response.data\n\t\t\t\tconst list = Array.isArray(data) ? data : (data.results || [])\n\t\t\t\tthis.agents = list\n\t\t\t\tif (!this.selectedAgentUuid && list.length > 0) {\n\t\t\t\t\tconst preferred = this.relevantAgentOptions[0]\n\t\t\t\t\tthis.selectedAgentUuid = (preferred && preferred.id) || list[0].uuid || list[0].id || null\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.agentsFetchError = true\n\t\t\t\tthis.agents = []\n\t\t\t} finally {\n\t\t\t\tthis.agentsLoading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch the caller's conversations once, feeding both the sessions menu's\n\t\t * recent entries (top 5) and the history view's full searchable list — a\n\t\t * single fetch keeps both surfaces consistent.\n\t\t *\n\t\t * @returns {Promise<void>}\n\t\t */\n\t\tasync fetchConversations() {\n\t\t\tthis.conversationsLoading = true\n\t\t\tthis.conversationsFetchError = false\n\t\t\ttry {\n\t\t\t\tconst response = await axios.get(conversationsUrl(this.chatAppId), { params: { limit: 50 } })\n\t\t\t\tconst data = response.data\n\t\t\t\tconst list = Array.isArray(data) ? data : (data.results || data.conversations || [])\n\t\t\t\tthis.conversations = list.map(normalizeConversation)\n\t\t\t} catch {\n\t\t\t\tthis.conversationsFetchError = true\n\t\t\t\tthis.conversations = []\n\t\t\t} finally {\n\t\t\t\tthis.conversationsLoading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style>\n/* The window itself. Fixed, anchored to the same corner as the launcher hex,\n and sized to sit comfortably on a laptop viewport without covering the\n host application it is a guest on.\n\n `!important` is deliberate and matches CnAiFloatingButton's reasoning: this\n markup lands inside third-party apps whose stylesheets we do not control. */\n.cn-ai-chat-window {\n\tposition: fixed !important;\n\tz-index: 9001 !important; /* above the launcher hex (9000) */\n\tdisplay: flex !important;\n\tflex-direction: column !important;\n\twidth: 380px;\n\tmax-width: calc(100vw - 32px);\n\theight: 600px;\n\tmax-height: calc(100vh - 160px);\n\t/* NOT `overflow: hidden` — the pointer below is drawn outside this box and\n\t would be clipped away. The rounded corners are held by the titlebar and\n\t input rows clipping their own ends instead. */\n\tborder: 1px solid var(--color-border, #d0d0d0);\n\tborder-radius: 12px;\n\tbackground: var(--color-background-hover, #f5f5f5);\n\tbox-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);\n\n\t/* ⚠️ THE POINTER IS AIMED AT THE HEX, and this number is the only thing\n\t keeping it there. The hex is 36px wide at a 56px inset, so its centre is\n\t 56 + 18 = 74px from the viewport edge. The window sits at a 44px inset, so\n\t the pointer's centre must be 74 - 44 = 30px in from the window's edge; the\n\t triangle is 22px wide, so its leading edge is 30 - 11 = 19px.\n\n\t Change the hex size, the hex inset, or the window inset and this is wrong\n\t — the arrow points at empty space beside the button, which reads as a\n\t rendering bug rather than a measurement one. */\n\t--cn-ai-pointer-offset: 19px;\n}\n\n/* Anchored ABOVE the hex and inboard of it.\n *\n * The hex sits at a 56px inset (see CnAiFloatingButton) to clear the host\n * application's own bottom-right chrome — an office editor puts a zoom control\n * and a status bar exactly there. The window clears the hex itself: 56px inset\n * + 30px tall + 14px for the pointer + breathing room = 108px.\n *\n * The pointer offset these lean on is declared with the rest of the window's\n * own properties above, not repeated here — a second `.cn-ai-chat-window` block\n * is a duplicate selector, and the two halves drift the moment someone edits the\n * one they happen to find first. */\n.cn-ai-chat-window--bottom-right {\n\tright: 44px;\n\tbottom: 108px;\n}\n\n.cn-ai-chat-window--bottom-left {\n\tleft: 44px;\n\tbottom: 108px;\n}\n\n.cn-ai-chat-window--top-right {\n\ttop: 108px;\n\tright: 44px;\n}\n\n.cn-ai-chat-window--top-left {\n\ttop: 108px;\n\tleft: 44px;\n}\n\n/* Embedded: a panel in the page's flow (a detail page tab), not a window over\n it. It fills its container's width and takes no pointer, since no position\n class is set. The doubled class outranks the window's `!important` rules. */\n.cn-ai-chat-window.cn-ai-chat-window--embedded {\n\tposition: relative !important;\n\tz-index: auto !important;\n\twidth: auto;\n\tmax-width: none;\n\theight: 520px;\n\tmax-height: none;\n\tbox-shadow: none;\n}\n\n/* The pointer: a white triangle under the window, aimed at the hex, so the\n window reads as speech FROM the launcher rather than as an unrelated panel\n that happened to appear. Built from two stacked triangles — the lower one is\n the border colour and sits 1px further down, which is what gives the visible\n 1px edge along the two diagonals. `overflow: hidden` on the window would clip\n these, which is why the window does not use it. */\n.cn-ai-chat-window--bottom-right::after,\n.cn-ai-chat-window--bottom-left::after,\n.cn-ai-chat-window--bottom-right::before,\n.cn-ai-chat-window--bottom-left::before {\n\tposition: absolute;\n\ttop: 100%;\n\twidth: 0;\n\theight: 0;\n\tcontent: '';\n\tborder-right: 11px solid transparent;\n\tborder-left: 11px solid transparent;\n}\n\n/* Border layer, 1px lower and 1px wider than the fill. */\n.cn-ai-chat-window--bottom-right::before,\n.cn-ai-chat-window--bottom-left::before {\n\tborder-top: 12px solid var(--color-border, #d0d0d0);\n}\n\n/* Fill layer, in the titlebar's white so the pointer belongs to the chrome. */\n.cn-ai-chat-window--bottom-right::after,\n.cn-ai-chat-window--bottom-left::after {\n\tmargin-top: -1px;\n\tborder-top: 12px solid var(--color-main-background, #ffffff);\n}\n\n.cn-ai-chat-window--bottom-right::before,\n.cn-ai-chat-window--bottom-right::after {\n\tright: var(--cn-ai-pointer-offset);\n}\n\n.cn-ai-chat-window--bottom-left::before,\n.cn-ai-chat-window--bottom-left::after {\n\tleft: var(--cn-ai-pointer-offset);\n}\n\n/* White titlebar over the light-grey body — the one contrast that tells a\n guest window's chrome apart from its content. */\n.cn-ai-chat-window__titlebar {\n\tdisplay: flex;\n\tflex: 0 0 auto;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 4px 8px;\n\tborder-bottom: 1px solid var(--color-border, #d0d0d0);\n\t/* Clips its own top corners, since the window can no longer use\n\t `overflow: hidden` (it would eat the pointer). */\n\tborder-radius: 11px 11px 0 0;\n\tbackground: var(--color-main-background, #ffffff);\n}\n\n.cn-ai-chat-window__controls {\n\tdisplay: flex;\n\tflex: 0 0 auto;\n\talign-items: center;\n\tgap: 2px;\n}\n\n/* The agent's identity, after the controls. `min-width: 0` + ellipsis so a\n long agent name shortens instead of pushing the controls off the edge. */\n.cn-ai-chat-window__identity {\n\tdisplay: flex;\n\tflex: 1 1 auto;\n\tgap: 6px;\n\talign-items: center;\n\tmin-width: 0;\n}\n\n.cn-ai-chat-window__identity-icon {\n\tflex: 0 0 auto;\n\tcolor: var(--color-primary-element, #21468b);\n}\n\n.cn-ai-chat-window__identity-name {\n\toverflow: hidden;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n}\n\n.cn-ai-chat-window__body {\n\tdisplay: flex;\n\tflex: 1 1 auto;\n\tflex-direction: column;\n\tmin-height: 0; /* allow flex children to shrink past their content size */\n}\n\n.cn-ai-chat-window__body--history {\n\toverflow-y: auto;\n\tpadding: 8px;\n}\n\n.cn-ai-chat-window__messages {\n\tflex: 1 1 auto;\n\tmin-height: 0;\n\toverflow-y: auto;\n\tpadding: 8px;\n}\n\n/* Approvals sit between the transcript and the composer: the last thing read\n before the box you would otherwise type into, which is where a blocked\n agent's request belongs. */\n.cn-ai-chat-window__approvals {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 8px 12px 0;\n\tlist-style: none;\n}\n\n.cn-ai-approval {\n\tpadding: 10px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbackground: var(--color-main-background);\n}\n\n.cn-ai-approval__head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-ai-approval__title {\n\tflex: 1;\n\tfont-weight: 600;\n\toverflow-wrap: anywhere;\n}\n\n/* The reach is the fact the decision rests on, so it reads as a label rather\n than as part of the agent's prose. */\n.cn-ai-approval__reach {\n\tflex-shrink: 0;\n\tpadding: 1px 8px;\n\tborder-radius: 12px;\n\tbackground: var(--color-background-dark);\n\tfont-size: 11px;\n\ttext-transform: uppercase;\n\tletter-spacing: .04em;\n}\n\n.cn-ai-approval__reason {\n\tmargin: 6px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-ai-approval__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-top: 10px;\n}\n\n.cn-ai-chat-window__error {\n\tflex: 0 0 auto;\n\tmargin: 0;\n\tpadding: 8px 12px;\n\tcolor: var(--color-error-text, #a10000);\n\tbackground: var(--color-error-hover, #f7e5e5);\n\tfont-size: 0.9em;\n}\n\n.cn-ai-chat-window__note {\n\tdisplay: flex;\n\tgap: 10px;\n\talign-items: flex-start;\n\tmargin: 0 8px 8px;\n\tpadding: 10px 14px;\n\tborder-radius: 10px;\n\tbackground: var(--color-primary-element-light, #eaf0f7);\n\tcolor: var(--color-primary-element-light-text, #234a78);\n\tfont-size: 14px;\n\tline-height: 1.5;\n}\n\n/* The icon font's own align-self: center would centre it on a long note. */\n.cn-ai-chat-window__note-icon {\n\tflex: none;\n\talign-self: flex-start;\n\tmargin-top: 1px;\n}\n\n/* A titled panel is the body of its card: the card draws border and corners. */\n.cn-ai-chat-window.cn-ai-chat-window--titled {\n\tborder: 0;\n\tborder-radius: 0;\n\tbackground: transparent;\n}\n\n.cn-ai-chat-window--titled .cn-ai-chat-window__input {\n\tpadding: 0;\n\tborder-top: 0;\n\tborder-radius: 0;\n\tbackground: transparent;\n}\n\n/* In a card the panel's body takes the card's 16px 18px inset. */\n.cn-ai-chat-window--titled .cn-ai-chat-window__messages {\n\tpadding: 16px 18px;\n}\n\n.cn-ai-chat-window--titled .cn-ai-chat-window__note {\n\tmargin: 0 18px 16px;\n}\n\n.cn-ai-chat-window__input {\n\tflex: 0 0 auto;\n\tpadding: 8px;\n\tborder-top: 1px solid var(--color-border, #d0d0d0);\n\t/* Mirrors the titlebar: clips its own bottom corners. */\n\tborder-radius: 0 0 11px 11px;\n\tbackground: var(--color-main-background, #ffffff);\n}\n\n/* The approved-assistant mark: same colour pair as the panel body. */\n.cn-ai-chat-window--has-mark .cn-ai-chat-window__input {\n\tborder-radius: 0;\n}\n\n.cn-ai-chat-window__mark {\n\tflex: 0 0 auto;\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tpadding: 6px 12px;\n\tborder-top: 1px solid var(--color-border, #d0d0d0);\n\tborder-radius: 0 0 11px 11px;\n\tbackground: var(--color-main-background, #ffffff);\n\tcolor: var(--color-main-text, #222222);\n\tfont-size: 0.85em;\n}\n\n.cn-ai-chat-window__mark-logo {\n\tmax-height: 24px;\n\twidth: auto;\n}\n\n/* ── The pop animation ──────────────────────────────────────────────────────\n *\n * Scale + fade, with `transform-origin` at the corner the hex sits under, so the\n * window converges on the launcher. 0.14 rather than 0 as the start scale: a\n * true zero briefly renders a 0x0 box that some engines treat as invisible for\n * hit-testing, and the tiny non-zero keeps the transition interruptible if the\n * user toggles the hex twice quickly.\n *\n * Leave is faster than enter (140ms vs 180ms) — dismissal should feel immediate,\n * while appearance can afford to be seen. */\n.cn-ai-chat-window-pop-bottom-right-enter-active,\n.cn-ai-chat-window-pop-bottom-left-enter-active,\n.cn-ai-chat-window-pop-top-right-enter-active,\n.cn-ai-chat-window-pop-top-left-enter-active {\n\ttransition: transform 0.18s cubic-bezier(0.2, 0.9, 0.3, 1.2), opacity 0.14s ease-out;\n}\n\n.cn-ai-chat-window-pop-bottom-right-leave-active,\n.cn-ai-chat-window-pop-bottom-left-leave-active,\n.cn-ai-chat-window-pop-top-right-leave-active,\n.cn-ai-chat-window-pop-top-left-leave-active {\n\ttransition: transform 0.14s ease-in, opacity 0.12s ease-in;\n}\n\n.cn-ai-chat-window-pop-bottom-right-enter,\n.cn-ai-chat-window-pop-bottom-right-enter-from,\n.cn-ai-chat-window-pop-bottom-right-leave-to {\n\ttransform: scale(0.14);\n\ttransform-origin: calc(100% - var(--cn-ai-pointer-offset) - 11px) 100%;\n\topacity: 0;\n}\n\n.cn-ai-chat-window-pop-bottom-left-enter,\n.cn-ai-chat-window-pop-bottom-left-enter-from,\n.cn-ai-chat-window-pop-bottom-left-leave-to {\n\ttransform: scale(0.14);\n\ttransform-origin: calc(var(--cn-ai-pointer-offset) + 11px) 100%;\n\topacity: 0;\n}\n\n.cn-ai-chat-window-pop-top-right-enter,\n.cn-ai-chat-window-pop-top-right-enter-from,\n.cn-ai-chat-window-pop-top-right-leave-to {\n\ttransform: scale(0.14);\n\ttransform-origin: calc(100% - var(--cn-ai-pointer-offset) - 11px) 0%;\n\topacity: 0;\n}\n\n.cn-ai-chat-window-pop-top-left-enter,\n.cn-ai-chat-window-pop-top-left-enter-from,\n.cn-ai-chat-window-pop-top-left-leave-to {\n\ttransform: scale(0.14);\n\ttransform-origin: calc(var(--cn-ai-pointer-offset) + 11px) 0%;\n\topacity: 0;\n}\n\n/* Someone who asked not to be moved gets the window without the flight. */\n@media (prefers-reduced-motion: reduce) {\n\t.cn-ai-chat-window-pop-bottom-right-enter-active,\n\t.cn-ai-chat-window-pop-bottom-left-enter-active,\n\t.cn-ai-chat-window-pop-top-right-enter-active,\n\t.cn-ai-chat-window-pop-top-left-enter-active,\n\t.cn-ai-chat-window-pop-bottom-right-leave-active,\n\t.cn-ai-chat-window-pop-bottom-left-leave-active,\n\t.cn-ai-chat-window-pop-top-right-leave-active,\n\t.cn-ai-chat-window-pop-top-left-leave-active {\n\t\ttransition: opacity 0.1s linear;\n\t}\n\n\t.cn-ai-chat-window-pop-bottom-right-enter,\n\t.cn-ai-chat-window-pop-bottom-left-enter,\n\t.cn-ai-chat-window-pop-top-right-enter,\n\t.cn-ai-chat-window-pop-top-left-enter,\n\t.cn-ai-chat-window-pop-bottom-right-leave-to,\n\t.cn-ai-chat-window-pop-bottom-left-leave-to,\n\t.cn-ai-chat-window-pop-top-right-leave-to,\n\t.cn-ai-chat-window-pop-top-left-leave-to {\n\t\ttransform: none;\n\t}\n}\n\n/* Empty state: just the agent's name and the prompt. The agent picker and the\n recent-sessions list used to live here and now live in the titlebar menus —\n on a 380px window they filled the first screen before any message did. */\n.cn-ai-chat-window__start {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-self: stretch;\n\tgap: 16px;\n\twidth: 100%;\n\tmargin: 0 auto;\n\tpadding: 0 12px;\n}\n</style>\n"],"names":["_createBlock","_normalizeClass","_createElementVNode","_createVNode","_toDisplayString","_createElementBlock","_Fragment","_renderList","_createTextVNode","_openBlock"],"mappings":";;;;;AAqH0B,EAAA,KAAK,EAAC,6BAAA;;;AAmBrB,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,kCAAM,GAAA;AAGd,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,6BAA4B,GAAA;;;AAyHhB,EAAA,KAAA,EAAA,yBAAA;;AAClB,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,6BAAA,GAAA;;AAMG,EAAA,KAAK,EAAC;AAA2B,EAAA,aAAW,EAAA,sBAAA;;;;AA0BnD,EAAA,KAAK,EAAC,8BAAK;AACX,EAAA,aAAW,EAAC,uBAAmB;;AAKzB,MAAA,WAAA,GAAA,EAAA,KAAI,EAAA,sBAAA,GAAA;AACF,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,uBAAgB,GAAA;AAMtB,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,uBAAsB,GAAA;;;AAEzB,EAAA,KAAA,EAAA,wBAAA;;AAGL,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,yBAAU,GAAA;;;AA8BtB,EAAA,KAAK,EAAC,0BAAY;AAClB,EAAA,IAAI,EAAC,OAAO;AACZ,EAAA,aAAS,EAAA,mBAAA;;;;AAMT,EAAA,KAAK,EAAC,yBAAuB;AAC7B,EAAA,aAAW,EAAC,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAnP7BA;AAAG,IAAA,IAAA,2BAAA,MAAA,CAAA,QAAA,CAAA,CAAA;;;OAEA,MAAO,CAAA,OAAA;;;YACT,KAAC,EAAAC,cAAA,CAAA,CAAC,mBAAmB;AAErB,YAAA,IAAC,EAAE,MAAQ,CAAA,QAAA,GAAA,QAAA,GAAA,QAAA;wBACJ,EAAE,QAAS,CAAA,UAAA;AAClB,YAAA,eAAQ,aAAa;AACvB,YAAA,SAAE,mDAAA,QAAA,CAAA,QAAA,IAAA,QAAA,CAAA,QAAA,CAAA,GAAA,IAAA,CAAA,CAAA,EAAA,CAAA,KAAA,CAAA,CAAA,CAAA;;cAEW,MAAM,CAAA,MAAA;iDA6Ib,oBAAA,EAAA;kBAtILC,kBAaI,CAAA,KAAA,EAAA;AAbD,oBAAA,KAAK,EAAC,6BAAS;AAAA,oBAAA,KAAA,EAAA,QAAA,CAAA,UAAA;;oBAQjBC,WAGU,CAAA,0BAAA,EAAA;0BAFN,EAAE,QAAa,CAAA,aAAA;0BACf,EAAE,EAAE;2BACJ,EAAC,kCAAK;;oBACVD,kBAAgB,CAAA,MAAA,EAAhB,UAAgB,EAAAE,eAAA,CAAA,QAAA,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;;qCAyHb,iBAAA,EAAA;+BAnDM,CAAA,oBAAA,EAAA;AAzDT,sBAAA,YAAU,EAAE,QAAG,CAAA,WAAA,CAAA,kBAAA,CAAA;2BACV,EAAE,QAAW,CAAA,WAAA,CAAA,kBAAA,CAAA;AAClB,sBAAA,SAAS,EAAC,IAAA;mCACA,EAAC,sBAAsB;;AACvB,sBAAA,IAAI,UACd,MAAc;wBAAdD,WAAc,CAAA,cAAA,EAAA,EAAR,IAAI,EAAE,EAAE,EAAA,CAAA;;uCAWd,MAQU;wBARVA,WAQU,CAAA,2BAAA,EAAA;AAPT,0BAAA,UAAQ,EAAA,KAAA,CAAA,YAAA;AACR,0BAAA,QAAQ,EAAE,QAAE,CAAA,wBAAA,KAAA,KAAA;AACb,0BAAA,aAAW,EAAC,0BAAgB;AAC3B,0BAAA,qBAAiB,EAAE,QAAE,CAAA,qBAAA;;2CACtB,MAEmB;4DAFhB,QAAe,CAAA,wBAAA;UAAA,QAAA,CAAA,WAAA,CAAA,oBAAA,CAAA;UAAA,QAAA,CAAA,WAAA,CAAA,yCAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;wBAIlBA,WAAoB,CAAA,4BAAA,CAAA;wBACrBA,WAAgB,CAAA,0BAAA,EAAA,EAAA,IAAA,EAAA,QAAA,CAAA,YAAA,EAAA,EAAA,IAAA,EAAA,CAAA,cAAA,CAAA,MAAA,CAAA,CAAA;0CAChBE,kBAUgB,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CATF,QAAA,CAAA,mBAAA,EAAA,CAAN,KAAK,KAAA;+CADbP,WAUgB,CAAA,yBAAA,EAAA;4BARd,GAAG,EAAE,KAAK,CAAC,EAAE;AACb,4BAAA,eAAe,EAAE,IAAI;AACrB,4BAAA,OAAK,EAAE,MAAA,KAAA,QAAA,CAAA,eAAc,CAAA,KAAA,CAAA,EAAA,CAAA,CAAA;;AACX,4BAAA,IAAI,UACd,MAAc;+BAAD,KAAC,CAAA,EAAA,KAAA,KAAA,CAAA,iBAAA;gDAAdA,WAAc,CAAA,gBAAA,EAAA;;AAAA,oCAAA,IAAA,EAAA,EAAA;;gDACdA,WAAgC,CAAA,0BAAA,EAAA;;oCAAP,IAAI,EAAE,KAAC,CAAA,IAAA;AAAA,oCAAA,IAAA,EAAA,EAAA;;;6CACvB,MACV;8CADU,GACV,GAAAI,eAAA,CAAG,KAAK,CAAC,KAAK,CAAA,EAAA,CAAA,YAAA;;;;;yBASR,QAAS,CAAA,gBAAA,GAAA,CAAA;0CADfJ,WASgB,CAAA,yBAAA,EAAA;;AAPf,8BAAA,eAAQ,EAAA,KAAA;;AAER,8BAAA,OAAK,uCAAA,KAAA,CAAA,aAAA,GAAA,IAAA,CAAA,CAAA;;AACK,8BAAA,IAAG,UACb,MAA6B;gCAA7BG,WAA6B,CAAA,yBAAA,EAAA,EAAZ,IAAI,EAAE,EAAE,EAAA,CAAA;;+CACf,MACX;gDADW,GACX,GAAAC,eAAA,CAAG,oBAAW,CAAE,YAAA,CAAA,CAAA,GAAA,IAAA,mBAAA,QAAA,CAAA,YAAA,CAAA,MAAA,CAAA,GAAA,IACjB,EAAA,CAAA,YAAA;;;;;AAEO,wBAAA,CAAA,QAAA,CAAA,YAAQ,CAAA,MAAA,KAAA,CAAA;0CADfJ,WAEoB,CAAA,0BAAA,EAAA;;AAAlB,8BAAA,IAAI,EAAE,KAAA,CAAA,gBAAY,GAAA,QAAA,CAAA,WAAA,4BAAA,QAAA,CAAA,WAAA,CAAA,qBAAA,CAAA;;;;;;oBAGrBG,WAoCY,CAAA,oBAAA,EAAA;AAnCV,sBAAA,YAAI,EAAA,QAAA,CAAA,WAAA,CAAA,UAAA,CAAA;;AAEL,sBAAA,SAAU,EAAE,IAAI;AAChB,sBAAA,aAAW,EAAC,sBAAsB;;AACvB,sBAAA,IAAG,UACb,MAAsB;wBAAtBA,WAAsB,CAAA,kBAAA,EAAA,EAAZ,IAAI,EAAE,EAAE,EAAA,CAAA;;uCAEnB,MAQiB;wBARjBA,WAQiB,CAAA,yBAAA,EAAA;AAPf,0BAAA,QAAQ,EAAE,QAAI,CAAA,iBAAA;AACd,0BAAA,eAAe,EAAE,IAAI;AACrB,0BAAA,OAAK,EAAE,QAAS,CAAA,SAAA;;AACP,0BAAA,IAAK,UACZ,MAAiB;4BAAjBA,WAAiB,CAAA,eAAA,EAAA,EAAZ,IAAK,EAAC,EAAE,EAAA,CAAA;;2CACL,MACT;AADS,4BAAAK,eAAA,CAAA,GACT,mBAAC,QAAW,CAAA,WAAA,CAAA,gBAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;mCAEF,CAAA,0BAAA,EAAA;AAAA,0BAAA,IAAA,EAAA,QAAA,CAAA,WAAA,CAAA,sBAAA,CAAA;;0CACXH,kBASe,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CARX,QAAA,CAAA,mBAAA,EAAA,CAAA,YAAA,KAAA;+CADJP,WASe,CAAA,yBAAA,EAAA;4BAPf,GAAG,EAAE,YAAG,CAAA,IAAA;AACR,4BAAA,eAAc,EAAA,IAAA;AACd,4BAAA,OAAC,EAAA,MAAA,KAAA,QAAA,CAAA,oBAAA,CAAA,YAAA,CAAA,IAAA,CAAA,CAAA;;0CAEF,MAAA;8BAAAG,WAAA,CAAA,6BAAA,EAAA,EAAA,IAAA,EAAA,EAAA,EAAA,CAAA;;6CACW,MACX;8CADW,GACX,GAAAC,eAAA,CAAG,QAAiB,CAAA,iBAAA,CAAC,YAAY,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;mCASjB,CAAA,yBAAA,EAAA;AANf,0BAAA,eAAe,EAAE;AACjB,0BAAA,8CAAO,KAAU,CAAA,UAAA,GAAA,SAAA,CAAA,CAAA;;AACP,0BAAA,IAAI,UACd,MAAsB;4BAAtBD,WAAsB,CAAA,kBAAA,EAAA,EAAZ,IAAI,EAAE,EAAE,EAAA,CAAA;;2CACpB,MACA;AADA,4BAAAK,eAAA,CAAA,GACA,mBAAA,QAAA,CAAA,WAAA,CAAA,wBAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;sBAKO,MAAS,CAAA,QAAA;sCADjBR,WAUI,CAAA,mBAAA,EAAA;;AARF,0BAAA,YAAU,EAAE,QAAI,CAAA,WAAA,CAAA,OAAA,CAAA;AAChB,0BAAA,KAAK,EAAE,QAAW,CAAA,WAAA,CAAA,OAAA,CAAA;AACnB,0BAAA,OAAO,EAAC,UAAU;uCACP,EAAC,mBAAmB;AAC9B,0BAAA,OAAG,EAAA,QAAA,CAAA,OAAA;;8BACW,UACZ,MAAkB;4BAAlBG,WAAkB,CAAA,gBAAA,EAAA,EAAZ,IAAI,EAAE,EAAG,EAAA,CAAA;;;;;;;;aAMV,KAAU,CAAA,UAAA,KAAA,MAAA;AAAjB,iBAAAM,SAAA,EAAA,EAAAJ,kBAAA,QAAA;AACD,kBAAAH,kBAAA,CAkBE,OAlBF,UAkBE,EAAA;oBAjBFC,WAgBG,CAAA,0BAAA,EAAA;sBAfF,QAAQ,EAAA,MAAA,CAAA,WAAA,CAAA,QAAA;sBACP,WAAU,EAAE,MAAI,CAAA,WAAA,CAAA,WAAA;sBAChB,WAAU,EAAE,MAAW,CAAA,WAAA,CAAC,WAAW;;AAC1B,sBAAA,KAAK,gBAUT;2CAAA,iBAAA,EAAA;0BARLA,WAOiB,CAAA,yBAAA,EAAA,EAPA,IAAI,EAAC,QAAG,CAAA,UAAA,EAAA,EAAA;AAChB,4BAAA,IAAA,UACP,MAAK;8BAALA,WAAK,CAAA,mBAAA,EAAA,EAAA,IAAA,EAAA,EAAA,EAAA,CAAA;;AAEK,4BAAA,WAAW,UACrB,MAAgD;8DAA7C,QAAW,CAAA,WAAA,CAAA,6CAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;AAmBb,kBAAA,CAAA,QAAA,CAAA,gBAAgB,CAAC,MAAM;uDAoCzB,gBAAA,EAAA;0CAjCJE,kBAgCK,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CA/BH,QAAA,CAAA,gBAAA,EAAA,CAAA,QAAA,KAAA;+CADFF,kBAgCK,CAAA,IAAA,EAAA;;AA7BL,4BAAA,KAAA,EAAA,gBAAA;;AACC,4BAAAH,kBAAA,CAQM,OARN,WAQM,EAAA;AAPL,8BAAAA,kBAAA,CAA4B,MAA5B,EAAA,WAA4B,EAAAE,eAAA,CAAA,QAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;AAM5B,8BAAAF,kBAAA,CAAkC,MAAlC,EAAA,WAAkC,EAAAE,eAAA,CAAA,QAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;AAE1B,4BAAA,CAAA,QAAC,CAAA,MAAA;AAAV,iCAAAK,SAAA,EAAA,EAAAJ,kBAAA,CAEI,GAFJ,EAAA,WAEI,EADAD,eAAA,CAAA,QAAQ,CAAC,MAAM,CAAA,EAAA,CAAA,YAAA;;AAEnB,4BAAAF,kBAAA,CAeM,OAfN,WAeM,EAAA;8BAdLC,WAMW,CAAA,mBAAA,EAAA;AALV,gCAAA,OAAO,EAAC,SAAS;AAChB,gCAAA,QAAQ,EAAE,KAAA,CAAA,mBAAmB,KAAK,QAAO,CAAA,EAAA;gCACzC,aAAW,EAAA,uBAAA,GAA4B,QAAQ,CAAC,EAAE;gCAClD,OAAK,EAAA,MAAA,KAAE,QAAkB,CAAA,kBAAA,CAAC,QAAQ,EAAA,SAAA,CAAA,CAAA;;iDACnC,MAA0B;kEAAvB,QAAW,CAAA,WAAA,CAAA,OAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;8BAEfA,WAMW,CAAA,mBAAA,EAAA;AALV,gCAAA,OAAC,EAAA,UAAA;AACD,gCAAA,QAAA,EAAA,KAAA,CAAA,mBAAA,KAAA,QAAA,CAAA,EAAA;;gCAEC,OAAI,EAAA,MAAA,KAAG,QAAkB,CAAA,kBAAA,CAAC,QAAQ,EAAA,SAAA,CAAA,CAAA;;iDACnC,MAAW;kEAAR,QAAQ,CAAA,WAAA,CAAA,SAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;AAgBb,kBAAA,CAAA,MAAA,CAAA,WAAA,CAAA,KAAA;oCADFE,kBAMG,CAAA,GAAA,EANH,WAMG,EADCD,eAAA,CAAA,MAAA,CAAA,WAAW,CAAC,KAAK,CAAC,OAAO,IAAC,QAAA,CAAA,WAAA,CAAA,iCAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;mBAIvB,MAAI,CAAA,IAAA;AADV,uBAAAK,SAAA,EAAA,EAAAJ,kBAAA,CAMG,KANH,WAMG,EAAA;wBAFHF,WAAG,CAAA,6BAAA,EAAA;AAAA,0BAAA,IAAA,EAAA,EAAA;AAAA,0BAAA,KAAA,EAAA,8BAAA;;AACH,wBAAAD,kBAAA,CAAE,8BAAA,MAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -2,7 +2,7 @@ import script from './CnChecklistRunWidget.vue2.js';
|
|
|
2
2
|
import { render } from './CnChecklistRunWidget.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-f3678d20";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|