@conduction/nextcloud-vue 2.76.0 → 2.78.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/CnAiCompanion.vue2.js +23 -0
- package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js.map +1 -1
- package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue3.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +18 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +49 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +2 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +79 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +229 -203
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +37 -0
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +2 -2
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -4
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +48 -6
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue.js +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js +45 -2
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js.map +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +16 -28
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +4 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +104 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +13 -9
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +6 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +36 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +35 -23
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js +6 -2
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js.map +1 -1
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/composables/useFormDraft.js +11 -1
- package/dist/esm/composables/useFormDraft.js.map +1 -1
- package/dist/esm/composables/useListView.js +4 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/composables/useObjectPresence.js +15 -4
- package/dist/esm/composables/useObjectPresence.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +32 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +32 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +500 -75
- package/dist/esm/store/plugins/interactions.js +20 -10
- package/dist/esm/store/plugins/interactions.js.map +1 -1
- package/dist/esm/store/useObjectStore.js +17 -0
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/badgeVariants.js +32 -0
- package/dist/esm/utils/badgeVariants.js.map +1 -0
- package/dist/esm/utils/builtInFormatters.js +15 -7
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/groupAutocomplete.js +63 -1
- package/dist/esm/utils/groupAutocomplete.js.map +1 -1
- package/dist/esm/utils/lensAvailability.js +90 -0
- package/dist/esm/utils/lensAvailability.js.map +1 -0
- package/dist/esm/utils/personalLenses.js +13 -6
- package/dist/esm/utils/personalLenses.js.map +1 -1
- package/dist/esm/utils/recordInteractions.js +31 -13
- package/dist/esm/utils/recordInteractions.js.map +1 -1
- package/dist/esm/utils/resolveFilterTokens.js +46 -3
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/schema.js +1 -1
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +4 -4
- package/dist/esm/utils/sentinelTokens.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/widgetVisibility.js +46 -1
- package/dist/esm/utils/widgetVisibility.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +4389 -2890
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +500 -75
- package/l10n/en.json +32 -1
- package/l10n/nl.json +32 -1
- package/package.json +1 -1
- package/src/components/CnAiCompanion/CnAiCompanion.vue +23 -0
- package/src/components/CnAppNav/CnAppNav.vue +44 -5
- package/src/components/CnBoardView/CnBoardView.vue +2 -1
- package/src/components/CnCellRenderer/CnCellRenderer.md +1 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +106 -9
- package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
- package/src/components/CnChartWidget/CnChartWidget.vue +39 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -4
- package/src/components/CnDataTable/CnDataTable.md +9 -0
- package/src/components/CnDataTable/CnDataTable.vue +87 -22
- package/src/components/CnDateRangePicker/CnDateRangePicker.vue +44 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +10 -27
- package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
- package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
- package/src/components/CnFormDialog/CnFormDialog.vue +7 -1
- package/src/components/CnFormPage/CnFormPage.vue +4 -1
- package/src/components/CnIndexPage/CnIndexPage.md +36 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +112 -19
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
- package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +52 -2
- package/src/components/CnStatusBadge/CnStatusBadge.vue +6 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/composables/__tests__/useObjectPresence.spec.js +24 -0
- package/src/composables/useFormDraft.js +11 -1
- package/src/composables/useListView.js +4 -0
- package/src/composables/useObjectPresence.js +15 -4
- package/src/css/badge.css +31 -0
- package/src/css/kpi-card.css +51 -0
- package/src/css/look-board-detail.css +40 -2
- package/src/css/look-board-index.css +36 -0
- package/src/css/look-board.css +8 -0
- package/src/css/table.css +82 -17
- package/src/index.js +2 -1
- package/src/schemas/app-manifest-v2.schema.json +15 -11
- package/src/store/plugins/interactions.js +20 -10
- package/src/store/useObjectStore.js +17 -0
- package/src/utils/badgeVariants.js +29 -0
- package/src/utils/builtInFormatters.js +15 -7
- package/src/utils/groupAutocomplete.js +75 -0
- package/src/utils/index.js +1 -1
- package/src/utils/lensAvailability.js +86 -0
- package/src/utils/personalLenses.js +13 -6
- package/src/utils/recordInteractions.js +31 -13
- package/src/utils/resolveFilterTokens.js +45 -2
- package/src/utils/schema.js +1 -1
- package/src/utils/sentinelTokens.js +4 -4
- package/src/utils/validateManifestV2.compiled.js +2 -2
- package/src/utils/widgetVisibility.js +45 -0
|
@@ -3,6 +3,7 @@ import __sfc_main$1 from './CnAiChatPanel.vue.js';
|
|
|
3
3
|
import __sfc_main from './CnAiFloatingButton.vue.js';
|
|
4
4
|
import { DEFAULT_CHAT_APP_ID, chatHealthUrl } from '../../composables/aiChatConfig.js';
|
|
5
5
|
import { useAiChatStream } from '../../composables/useAiChatStream.js';
|
|
6
|
+
import { isAppInstalled } from '../../utils/appInstalled.js';
|
|
6
7
|
|
|
7
8
|
const HEALTH_TIMEOUT = 5000;
|
|
8
9
|
|
|
@@ -277,6 +278,15 @@ var script = {
|
|
|
277
278
|
}
|
|
278
279
|
},
|
|
279
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Ask the chat backend whether it can answer, and render only if so.
|
|
283
|
+
*
|
|
284
|
+
* Skipped without a request when the backend app is not installed for
|
|
285
|
+
* this user.
|
|
286
|
+
*
|
|
287
|
+
* @return {Promise<void>}
|
|
288
|
+
* @spec openspec/changes/no-calls-to-missing-apps-and-presence-beacon/specs/ai-companion/spec.md#requirement-the-companion-does-not-probe-a-backend-that-is-not-installed
|
|
289
|
+
*/
|
|
280
290
|
async runHealthProbe() {
|
|
281
291
|
// RETRY before hiding. A single probe makes the whole companion
|
|
282
292
|
// disappear on one slow response, and "slow" is normal: measured on a
|
|
@@ -288,6 +298,19 @@ var script = {
|
|
|
288
298
|
//
|
|
289
299
|
// A backend that is genuinely down fails all attempts and is still
|
|
290
300
|
// reported; this only stops one unlucky request from deciding.
|
|
301
|
+
//
|
|
302
|
+
// 🔴 NO PROBE AT AN APP THAT IS NOT THERE. Nextcloud already tells
|
|
303
|
+
// the page which apps the user has (`OC.appswebroots`), so asking a
|
|
304
|
+
// missing backend for its health is a guaranteed 404, three times
|
|
305
|
+
// per page, in every console. Measured on cloud.conduction.nl
|
|
306
|
+
// (round-5 check): hermiq not installed, three
|
|
307
|
+
// `/apps/hermiq/api/chat/health` 404s on every pipelinq and dossiq
|
|
308
|
+
// page. Hidden either way; now hidden without the requests.
|
|
309
|
+
if (!isAppInstalled(this.chatAppId || DEFAULT_CHAT_APP_ID)) {
|
|
310
|
+
this.probeSucceeded = false;
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
291
314
|
for (let attempt = 1; attempt <= HEALTH_PROBE_ATTEMPTS; attempt++) {
|
|
292
315
|
try {
|
|
293
316
|
const response = await axios__default.get(chatHealthUrl(this.chatAppId), {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnAiCompanion.vue2.js","sources":["../../../../src/components/CnAiCompanion/CnAiCompanion.vue"],"sourcesContent":["<!--\n CnAiCompanion — top-level mount for the AI Chat Companion widget.\n\n On created():\n - Issues GET /index.php/apps/{chatAppId}/api/chat/health via axios.\n - Renders nothing on non-2xx, network error, or timeout (5s).\n - Probe result cached for component lifetime.\n - Only console.info() on failure (never warn/error).\n\n When probe succeeds:\n - Renders CnAiFloatingButton ALWAYS — it toggles the window rather than only\n opening it, and stays visible while the window is open.\n - Renders CnAiChatPanel (when isPanelOpen) as a floating window anchored to the\n same corner as the hex, offset clear of it so both stay clickable.\n\n FAB and panel both hidden when cnAiContext.pageKind === 'chat'.\n-->\n<template>\n\t<div\n\t\tv-if=\"probeSucceeded && !isChatPage && isPrimaryCompanion\"\n\t\tclass=\"cn-ai-companion\"\n\t\tdata-testid=\"cn-ai-companion\">\n\t\t<!--\n\t\t The hex is rendered UNCONDITIONALLY and toggles the window.\n\n\t\t It used to be `:visible=\"!isPanelOpen\"` — the launcher vanished while\n\t\t the thing it launched was open. Three reasons it now stays, in order of\n\t\t weight:\n\n\t\t 1. It is the one control we can guarantee. The window's own close\n\t\t button lives in markup a host page's stylesheet could hide; the hex\n\t\t is fixed-position with explicit `!important` properties and has\n\t\t survived every host it has been dropped into, including third-party\n\t\t office editors.\n\t\t 2. A launcher that disappears is a dead end — the affordance you used\n\t\t to summon something should dismiss it.\n\t\t 3. It removes a state transition: no show/hide to coordinate with the\n\t\t window's own.\n\n\t\t ⚠️ This is why the window anchors 70px from the edge: the hex is 26x30\n\t\t at a 24px inset, and the two must not overlap or the guarantee above is\n\t\t worth nothing.\n\t\t-->\n\t\t<CnAiFloatingButton\n\t\t\t:visible=\"true\"\n\t\t\t:position=\"position\"\n\t\t\t@click=\"togglePanel\" />\n\t\t<CnAiChatPanel\n\t\t\tref=\"panel\"\n\t\t\t:visible=\"isPanelOpen\"\n\t\t\t:streamState=\"stream.state\"\n\t\t\t:chatAppId=\"chatAppId\"\n\t\t\t:context=\"effectiveContext\"\n\t\t\t:position=\"position\"\n\t\t\t:fabRef=\"$refs.fabButton\"\n\t\t\t@close=\"closePanel\"\n\t\t\t@send=\"onSend\"\n\t\t\t@newThread=\"onNewThread\"\n\t\t\t@loadConversation=\"onLoadConversation\" />\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport CnAiChatPanel from './CnAiChatPanel.vue'\nimport CnAiFloatingButton from './CnAiFloatingButton.vue'\nimport { chatHealthUrl, DEFAULT_CHAT_APP_ID } from '../../composables/aiChatConfig.js'\nimport { useAiChatStream } from '../../composables/useAiChatStream.js'\n\nconst HEALTH_TIMEOUT = 5000\n\n/** How many times the health probe may fail before the companion hides. */\nconst HEALTH_PROBE_ATTEMPTS = 3\n\n/** Base backoff between probe attempts, multiplied by the attempt number. */\nconst HEALTH_RETRY_DELAY = 750\n\n/**\n * The window key one companion claims so a second one stands down.\n *\n * On `window` rather than in module scope, because the two companions on a page\n * do not share a module instance: one comes from the host app's bundle and one\n * from a standalone bundle attached to every page. Two copies of this file are\n * loaded, so a module-level flag would be two flags and would guard nothing.\n */\nconst COMPANION_SINGLETON_KEY = '__cnAiCompanionPrimary'\n\n/**\n * How long to wait before looking for a companion that cannot stand down.\n *\n * Long enough for a host app's Vue tree to boot after a standalone mount at\n * DOMContentLoaded, short enough that a user never sees two hexes settle.\n */\nconst LEGACY_SIBLING_DELAY = 1200\n\nexport default {\n\tname: 'CnAiCompanion',\n\n\tcomponents: {\n\t\tCnAiFloatingButton,\n\t\tCnAiChatPanel,\n\t},\n\n\tinject: {\n\t\tcnAiContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Default position for the FAB.\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/health/conversation HTTP calls\n\t\t * (`/index.php/apps/{chatAppId}/api/...`). Single configuration point for\n\t\t * the AI Chat Companion's backend — see composables/aiChatConfig.js.\n\t\t * Defaults to `hermiq` (the agent engine's home per hydra ADR-034\n\t\t * \"Amendment 2026-07-05\"); `CnAppRoot` forwards its own `chatAppId`\n\t\t * prop here so a consuming app can point the widget at another backend\n\t\t * (e.g. `openregister` during its compat window) in one place.\n\t\t *\n\t\t * @type {string}\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 * What the user is looking at, stated by whoever mounted the companion.\n\t\t *\n\t\t * Inside a Conduction app, CnAppRoot provides this and the prop is\n\t\t * unnecessary. Mounted standalone on a page belonging to another app —\n\t\t * an office editor, the Files list — there is no provider, and the\n\t\t * injected fallback reports `appId: 'unknown'`. The agent then has no\n\t\t * idea what \"this document\" refers to and says so.\n\t\t *\n\t\t * Shape mirrors the injected context: `{ appId, pageKind, fileId,\n\t\t * objectUuid, registerSlug, schemaSlug, route }`. All optional; whatever\n\t\t * the host knows is better than 'unknown'.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tprobeSucceeded: false,\n\t\t\tisPanelOpen: false,\n\t\t\tisPrimaryCompanion: false,\n\t\t\t// Cleared on unmount so a torn-down companion cannot stand itself\n\t\t\t// down after the fact.\n\t\t\tlegacySiblingTimer: null,\n\t\t\tstream: useAiChatStream(this, { chatAppId: this.chatAppId, context: this.context }),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tisChatPage() {\n\t\t\tconst ctx = this.cnAiContext\n\t\t\treturn ctx && ctx.pageKind === 'chat'\n\t\t},\n\n\t\t/**\n\t\t * The page context the panel reasons with: the prop when a standalone\n\t\t * mount passed one, otherwise the context CnAppRoot provides.\n\t\t *\n\t\t * WHY THIS EXISTS\n\t\t * ---------------\n\t\t * `CnAppRoot` renders `<CnAiCompanion :chatAppId=\"chatAppId\" />` and\n\t\t * passes NO `:context`, because it already provides `cnAiContext` for\n\t\t * descendants to inject. This component injected it for `isChatPage`\n\t\t * but handed the panel the raw `context` PROP, which is `null` in that\n\t\t * arrangement — so inside every app shell the panel ran with no\n\t\t * context at all.\n\t\t *\n\t\t * The panel's `relevantAgentOptions` treats an empty context as \"no\n\t\t * page to be relevant to\" and returns EVERY agent, so it selected\n\t\t * whichever agent the API happened to list first. Measured on the\n\t\t * demo instance 2026-09-20: on `/apps/buildiq/` the companion opened\n\t\t * as \"Hydra Triage\", which holds only `hydra.*` tools, and its first\n\t\t * answer was \"This agent's tool grants resolve to no tools\" — while\n\t\t * \"Buildiq builder\", holding all eight `buildiq_*` tools, sat unused\n\t\t * at the bottom of the same list.\n\t\t *\n\t\t * A standalone mount that passes `context` explicitly still wins, so\n\t\t * Hermiq's cross-app companion is unaffected.\n\t\t *\n\t\t * @return {object|null} The context to reason with.\n\t\t */\n\t\teffectiveContext() {\n\t\t\treturn this.context || this.cnAiContext || null\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.runHealthProbe()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.legacySiblingTimer !== null) {\n\t\t\tclearTimeout(this.legacySiblingTimer)\n\t\t\tthis.legacySiblingTimer = null\n\t\t}\n\n\t\tthis.releaseSingleton()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Stand down if another companion is on the page that cannot stand down.\n\t\t *\n\t\t * 🔴 THE WINDOW FLAG ONLY BINDS INSTANCES THAT CHECK IT. Measured on a\n\t\t * live instance after shipping the flag: still TWO hexes, stacked at the\n\t\t * same coordinates — one from `#openregister` (the host app's own bundle,\n\t\t * built against an older version of this library) and one from\n\t\t * `#hermiq-companion-root` (the standalone mount, built against the new\n\t\t * one). The old bundle never reads the flag, so it renders regardless,\n\t\t * and the guarded instance had already claimed the slot and rendered too.\n\t\t *\n\t\t * The library fix alone therefore does not reach a page until EVERY app\n\t\t * on it has rebuilt — which is not something a user with a double hex\n\t\t * can wait for. So the instance that CAN yield does.\n\t\t *\n\t\t * Deferred rather than checked at mount, because order is against us: a\n\t\t * standalone companion mounts at DOMContentLoaded and the host app's Vue\n\t\t * tree boots after it, so at mount time there is nothing to see.\n\t\t *\n\t\t * When both companions carry this guard the flag has already stopped the\n\t\t * second one, so this never fires — it is strictly a bridge for mixed\n\t\t * versions, and it costs one timeout.\n\t\t */\n\t\tyieldToACompanionThatCannotYield() {\n\t\t\tif (typeof document === 'undefined' || this.isPrimaryCompanion !== true) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.legacySiblingTimer = setTimeout(() => {\n\t\t\t\tthis.legacySiblingTimer = null\n\n\t\t\t\tconst rendered = document.querySelectorAll('[data-testid=\"cn-ai-companion\"]')\n\t\t\t\tif (rendered.length <= 1) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tfor (const el of rendered) {\n\t\t\t\t\tif (el !== this.$el) {\n\t\t\t\t\t\t// Yield to it: the host app's own companion sits inside\n\t\t\t\t\t\t// that app's tree and carries its page context, which a\n\t\t\t\t\t\t// standalone mount can only approximate.\n\t\t\t\t\t\tthis.releaseSingleton()\n\t\t\t\t\t\tthis.isPrimaryCompanion = false\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, LEGACY_SIBLING_DELAY)\n\t\t},\n\n\t\t/**\n\t\t * Claim the page's single companion slot, or stand down.\n\t\t *\n\t\t * 🔴 TWO COMPANIONS ON ONE PAGE IS A REACHABLE STATE, and it was reached:\n\t\t * `CnAppRoot` renders a companion for any app that sets `aiCompanion`,\n\t\t * while a host app can also mount one standalone on every page of the\n\t\t * instance (Hermiq does exactly this, so the assistant reaches\n\t\t * third-party office editors that have no CnAppRoot of ours). On any\n\t\t * page that has both, the user saw TWO hexes stacked a few pixels apart\n\t\t * — the two mounts pick up different bundled stylesheets, so they do not\n\t\t * even land in the same place.\n\t\t *\n\t\t * The guard lives HERE, in the component, rather than in each host's\n\t\t * mount script, because only the component knows how many of itself\n\t\t * exist. A host-side check cannot see a sibling that mounts later: the\n\t\t * standalone script runs at DOMContentLoaded and the app's own Vue tree\n\t\t * boots after it, so whichever check runs first sees nothing and both\n\t\t * render.\n\t\t *\n\t\t * First to claim wins and the rest render nothing. That order is stable\n\t\t * in practice — the standalone mount runs first — but the rule does not\n\t\t * depend on it: what matters is that exactly one renders, not which.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclaimSingleton() {\n\t\t\t// 🔴 CLAIMED ONLY WHEN THIS INSTANCE WOULD ACTUALLY RENDER.\n\t\t\t//\n\t\t\t// An earlier version claimed in `created()`, before the health probe\n\t\t\t// had answered — so a companion whose probe FAILED still took the\n\t\t\t// slot and stood the other one down, turning \"one companion\" into\n\t\t\t// NONE. Measured on a slow instance, where the probe's 3×5s budget\n\t\t\t// is genuinely marginal: the slot was claimed and nothing rendered.\n\t\t\t//\n\t\t\t// Claiming late is also why the claim is not in a `watch` on the\n\t\t\t// window key: the slot is contended exactly once per page, at the\n\t\t\t// moment each candidate learns it can render.\n\t\t\tif (this.probeSucceeded !== true || this.isChatPage === true) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst scope = typeof window !== 'undefined' ? window : null\n\t\t\tif (scope === null) {\n\t\t\t\t// No window (SSR, a bare unit test): nothing else can be\n\t\t\t\t// rendering, so this instance is the only one by definition.\n\t\t\t\tthis.isPrimaryCompanion = true\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (scope[COMPANION_SINGLETON_KEY] === null || scope[COMPANION_SINGLETON_KEY] === undefined) {\n\t\t\t\tscope[COMPANION_SINGLETON_KEY] = this\n\t\t\t\tthis.isPrimaryCompanion = true\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.isPrimaryCompanion = scope[COMPANION_SINGLETON_KEY] === this\n\t\t},\n\n\t\t/**\n\t\t * Give the slot back, but only if this instance is holding it.\n\t\t *\n\t\t * Checked rather than cleared unconditionally: a second, standing-down\n\t\t * instance unmounting must not release the slot the FIRST one is still\n\t\t * using, which would let a third mount render alongside it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\treleaseSingleton() {\n\t\t\tconst scope = typeof window !== 'undefined' ? window : null\n\t\t\tif (scope !== null && scope[COMPANION_SINGLETON_KEY] === this) {\n\t\t\t\tscope[COMPANION_SINGLETON_KEY] = null\n\t\t\t}\n\t\t},\n\n\t\tasync runHealthProbe() {\n\t\t\t// RETRY before hiding. A single probe makes the whole companion\n\t\t\t// disappear on one slow response, and \"slow\" is normal: measured on a\n\t\t\t// busy instance (load 48) the health endpoint answered in well under a\n\t\t\t// second, but the request still lost its 5s budget to contention. The\n\t\t\t// failure mode is the worst kind — the widget is simply absent, with\n\t\t\t// nothing on screen saying why, and a reload usually \"fixes\" it, which\n\t\t\t// is what makes it read as flakiness rather than as a probe result.\n\t\t\t//\n\t\t\t// A backend that is genuinely down fails all attempts and is still\n\t\t\t// reported; this only stops one unlucky request from deciding.\n\t\t\tfor (let attempt = 1; attempt <= HEALTH_PROBE_ATTEMPTS; attempt++) {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await axios.get(chatHealthUrl(this.chatAppId), {\n\t\t\t\t\t\ttimeout: HEALTH_TIMEOUT,\n\t\t\t\t\t\tvalidateStatus: (status) => status >= 200 && status < 300,\n\t\t\t\t\t})\n\t\t\t\t\tthis.probeSucceeded = response.status >= 200 && response.status < 300\n\t\t\t\t\t// Contend for the page's single slot only now, with a\n\t\t\t\t\t// backend that answered. See claimSingleton().\n\t\t\t\t\tthis.claimSingleton()\n\t\t\t\t\t// A companion built against an older version of this library\n\t\t\t\t\t// cannot read the flag above, so look for one shortly.\n\t\t\t\t\tthis.yieldToACompanionThatCannotYield()\n\t\t\t\t\treturn\n\t\t\t\t} catch {\n\t\t\t\t\tif (attempt < HEALTH_PROBE_ATTEMPTS) {\n\t\t\t\t\t\tawait new Promise((resolve) => {\n\t\t\t\t\t\t\tsetTimeout(resolve, HEALTH_RETRY_DELAY * attempt)\n\t\t\t\t\t\t})\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.info(`[CnAiCompanion] chat backend \"${this.chatAppId}\" health probe did not return 2xx `\n\t\t\t\t\t\t+ `after ${HEALTH_PROBE_ATTEMPTS} attempts — widget hidden`)\n\t\t\t\t\tthis.probeSucceeded = false\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The hex's own handler: open when closed, close when open.\n\t\t *\n\t\t * The launcher stays visible while the window is open (see the template),\n\t\t * so it has to answer for both directions rather than only opening.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttogglePanel() {\n\t\t\tif (this.isPanelOpen) {\n\t\t\t\tthis.closePanel()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.openPanel()\n\t\t},\n\n\t\topenPanel() {\n\t\t\tthis.isPanelOpen = true\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (this.$refs.panel && this.$refs.panel.$refs.input) {\n\t\t\t\t\tthis.$refs.panel.$refs.input.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\tclosePanel() {\n\t\t\tthis.isPanelOpen = false\n\t\t},\n\n\t\tonSend(text, agentUuid, attachments) {\n\t\t\tthis.stream.send(text, { agentUuid, attachments }).catch((err) => {\n\t\t\t\t// Stream errors are tracked in stream.state.error — no extra handling needed\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.info('[CnAiCompanion] send error:', err?.message)\n\t\t\t})\n\t\t},\n\n\t\tonNewThread() {\n\t\t\tthis.stream.startNewThread()\n\t\t},\n\n\t\tonLoadConversation(uuid) {\n\t\t\tthis.stream.loadConversation(uuid)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n.cn-ai-companion {\n\t/*\n\t * display: contents removes this wrapper from the layout tree so the\n\t * embedded NcAppSidebar inherits NcContent as its flex parent — required\n\t * for the sidebar to slide in from the RIGHT edge. The FAB is itself\n\t * position:fixed so it positions independently of the wrapper's box.\n\t *\n\t * !important is intentional: webpack pulls in BOTH the old (position:\n\t * fixed) and new (display: contents) rules from the package's CJS and\n\t * ESM builds; without !important the old rule wins the cascade and the\n\t * sidebar renders at the left.\n\t */\n\tdisplay: contents !important;\n\tposition: static !important;\n}\n</style>\n"],"names":["CnAiFloatingButton","CnAiChatPanel","axios"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnAiCompanion.vue2.js","sources":["../../../../src/components/CnAiCompanion/CnAiCompanion.vue"],"sourcesContent":["<!--\n CnAiCompanion — top-level mount for the AI Chat Companion widget.\n\n On created():\n - Issues GET /index.php/apps/{chatAppId}/api/chat/health via axios.\n - Renders nothing on non-2xx, network error, or timeout (5s).\n - Probe result cached for component lifetime.\n - Only console.info() on failure (never warn/error).\n\n When probe succeeds:\n - Renders CnAiFloatingButton ALWAYS — it toggles the window rather than only\n opening it, and stays visible while the window is open.\n - Renders CnAiChatPanel (when isPanelOpen) as a floating window anchored to the\n same corner as the hex, offset clear of it so both stay clickable.\n\n FAB and panel both hidden when cnAiContext.pageKind === 'chat'.\n-->\n<template>\n\t<div\n\t\tv-if=\"probeSucceeded && !isChatPage && isPrimaryCompanion\"\n\t\tclass=\"cn-ai-companion\"\n\t\tdata-testid=\"cn-ai-companion\">\n\t\t<!--\n\t\t The hex is rendered UNCONDITIONALLY and toggles the window.\n\n\t\t It used to be `:visible=\"!isPanelOpen\"` — the launcher vanished while\n\t\t the thing it launched was open. Three reasons it now stays, in order of\n\t\t weight:\n\n\t\t 1. It is the one control we can guarantee. The window's own close\n\t\t button lives in markup a host page's stylesheet could hide; the hex\n\t\t is fixed-position with explicit `!important` properties and has\n\t\t survived every host it has been dropped into, including third-party\n\t\t office editors.\n\t\t 2. A launcher that disappears is a dead end — the affordance you used\n\t\t to summon something should dismiss it.\n\t\t 3. It removes a state transition: no show/hide to coordinate with the\n\t\t window's own.\n\n\t\t ⚠️ This is why the window anchors 70px from the edge: the hex is 26x30\n\t\t at a 24px inset, and the two must not overlap or the guarantee above is\n\t\t worth nothing.\n\t\t-->\n\t\t<CnAiFloatingButton\n\t\t\t:visible=\"true\"\n\t\t\t:position=\"position\"\n\t\t\t@click=\"togglePanel\" />\n\t\t<CnAiChatPanel\n\t\t\tref=\"panel\"\n\t\t\t:visible=\"isPanelOpen\"\n\t\t\t:streamState=\"stream.state\"\n\t\t\t:chatAppId=\"chatAppId\"\n\t\t\t:context=\"effectiveContext\"\n\t\t\t:position=\"position\"\n\t\t\t:fabRef=\"$refs.fabButton\"\n\t\t\t@close=\"closePanel\"\n\t\t\t@send=\"onSend\"\n\t\t\t@newThread=\"onNewThread\"\n\t\t\t@loadConversation=\"onLoadConversation\" />\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport CnAiChatPanel from './CnAiChatPanel.vue'\nimport CnAiFloatingButton from './CnAiFloatingButton.vue'\nimport { chatHealthUrl, DEFAULT_CHAT_APP_ID } from '../../composables/aiChatConfig.js'\nimport { useAiChatStream } from '../../composables/useAiChatStream.js'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\n\nconst HEALTH_TIMEOUT = 5000\n\n/** How many times the health probe may fail before the companion hides. */\nconst HEALTH_PROBE_ATTEMPTS = 3\n\n/** Base backoff between probe attempts, multiplied by the attempt number. */\nconst HEALTH_RETRY_DELAY = 750\n\n/**\n * The window key one companion claims so a second one stands down.\n *\n * On `window` rather than in module scope, because the two companions on a page\n * do not share a module instance: one comes from the host app's bundle and one\n * from a standalone bundle attached to every page. Two copies of this file are\n * loaded, so a module-level flag would be two flags and would guard nothing.\n */\nconst COMPANION_SINGLETON_KEY = '__cnAiCompanionPrimary'\n\n/**\n * How long to wait before looking for a companion that cannot stand down.\n *\n * Long enough for a host app's Vue tree to boot after a standalone mount at\n * DOMContentLoaded, short enough that a user never sees two hexes settle.\n */\nconst LEGACY_SIBLING_DELAY = 1200\n\nexport default {\n\tname: 'CnAiCompanion',\n\n\tcomponents: {\n\t\tCnAiFloatingButton,\n\t\tCnAiChatPanel,\n\t},\n\n\tinject: {\n\t\tcnAiContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Default position for the FAB.\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/health/conversation HTTP calls\n\t\t * (`/index.php/apps/{chatAppId}/api/...`). Single configuration point for\n\t\t * the AI Chat Companion's backend — see composables/aiChatConfig.js.\n\t\t * Defaults to `hermiq` (the agent engine's home per hydra ADR-034\n\t\t * \"Amendment 2026-07-05\"); `CnAppRoot` forwards its own `chatAppId`\n\t\t * prop here so a consuming app can point the widget at another backend\n\t\t * (e.g. `openregister` during its compat window) in one place.\n\t\t *\n\t\t * @type {string}\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 * What the user is looking at, stated by whoever mounted the companion.\n\t\t *\n\t\t * Inside a Conduction app, CnAppRoot provides this and the prop is\n\t\t * unnecessary. Mounted standalone on a page belonging to another app —\n\t\t * an office editor, the Files list — there is no provider, and the\n\t\t * injected fallback reports `appId: 'unknown'`. The agent then has no\n\t\t * idea what \"this document\" refers to and says so.\n\t\t *\n\t\t * Shape mirrors the injected context: `{ appId, pageKind, fileId,\n\t\t * objectUuid, registerSlug, schemaSlug, route }`. All optional; whatever\n\t\t * the host knows is better than 'unknown'.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tprobeSucceeded: false,\n\t\t\tisPanelOpen: false,\n\t\t\tisPrimaryCompanion: false,\n\t\t\t// Cleared on unmount so a torn-down companion cannot stand itself\n\t\t\t// down after the fact.\n\t\t\tlegacySiblingTimer: null,\n\t\t\tstream: useAiChatStream(this, { chatAppId: this.chatAppId, context: this.context }),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tisChatPage() {\n\t\t\tconst ctx = this.cnAiContext\n\t\t\treturn ctx && ctx.pageKind === 'chat'\n\t\t},\n\n\t\t/**\n\t\t * The page context the panel reasons with: the prop when a standalone\n\t\t * mount passed one, otherwise the context CnAppRoot provides.\n\t\t *\n\t\t * WHY THIS EXISTS\n\t\t * ---------------\n\t\t * `CnAppRoot` renders `<CnAiCompanion :chatAppId=\"chatAppId\" />` and\n\t\t * passes NO `:context`, because it already provides `cnAiContext` for\n\t\t * descendants to inject. This component injected it for `isChatPage`\n\t\t * but handed the panel the raw `context` PROP, which is `null` in that\n\t\t * arrangement — so inside every app shell the panel ran with no\n\t\t * context at all.\n\t\t *\n\t\t * The panel's `relevantAgentOptions` treats an empty context as \"no\n\t\t * page to be relevant to\" and returns EVERY agent, so it selected\n\t\t * whichever agent the API happened to list first. Measured on the\n\t\t * demo instance 2026-09-20: on `/apps/buildiq/` the companion opened\n\t\t * as \"Hydra Triage\", which holds only `hydra.*` tools, and its first\n\t\t * answer was \"This agent's tool grants resolve to no tools\" — while\n\t\t * \"Buildiq builder\", holding all eight `buildiq_*` tools, sat unused\n\t\t * at the bottom of the same list.\n\t\t *\n\t\t * A standalone mount that passes `context` explicitly still wins, so\n\t\t * Hermiq's cross-app companion is unaffected.\n\t\t *\n\t\t * @return {object|null} The context to reason with.\n\t\t */\n\t\teffectiveContext() {\n\t\t\treturn this.context || this.cnAiContext || null\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.runHealthProbe()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.legacySiblingTimer !== null) {\n\t\t\tclearTimeout(this.legacySiblingTimer)\n\t\t\tthis.legacySiblingTimer = null\n\t\t}\n\n\t\tthis.releaseSingleton()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Stand down if another companion is on the page that cannot stand down.\n\t\t *\n\t\t * 🔴 THE WINDOW FLAG ONLY BINDS INSTANCES THAT CHECK IT. Measured on a\n\t\t * live instance after shipping the flag: still TWO hexes, stacked at the\n\t\t * same coordinates — one from `#openregister` (the host app's own bundle,\n\t\t * built against an older version of this library) and one from\n\t\t * `#hermiq-companion-root` (the standalone mount, built against the new\n\t\t * one). The old bundle never reads the flag, so it renders regardless,\n\t\t * and the guarded instance had already claimed the slot and rendered too.\n\t\t *\n\t\t * The library fix alone therefore does not reach a page until EVERY app\n\t\t * on it has rebuilt — which is not something a user with a double hex\n\t\t * can wait for. So the instance that CAN yield does.\n\t\t *\n\t\t * Deferred rather than checked at mount, because order is against us: a\n\t\t * standalone companion mounts at DOMContentLoaded and the host app's Vue\n\t\t * tree boots after it, so at mount time there is nothing to see.\n\t\t *\n\t\t * When both companions carry this guard the flag has already stopped the\n\t\t * second one, so this never fires — it is strictly a bridge for mixed\n\t\t * versions, and it costs one timeout.\n\t\t */\n\t\tyieldToACompanionThatCannotYield() {\n\t\t\tif (typeof document === 'undefined' || this.isPrimaryCompanion !== true) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.legacySiblingTimer = setTimeout(() => {\n\t\t\t\tthis.legacySiblingTimer = null\n\n\t\t\t\tconst rendered = document.querySelectorAll('[data-testid=\"cn-ai-companion\"]')\n\t\t\t\tif (rendered.length <= 1) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tfor (const el of rendered) {\n\t\t\t\t\tif (el !== this.$el) {\n\t\t\t\t\t\t// Yield to it: the host app's own companion sits inside\n\t\t\t\t\t\t// that app's tree and carries its page context, which a\n\t\t\t\t\t\t// standalone mount can only approximate.\n\t\t\t\t\t\tthis.releaseSingleton()\n\t\t\t\t\t\tthis.isPrimaryCompanion = false\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, LEGACY_SIBLING_DELAY)\n\t\t},\n\n\t\t/**\n\t\t * Claim the page's single companion slot, or stand down.\n\t\t *\n\t\t * 🔴 TWO COMPANIONS ON ONE PAGE IS A REACHABLE STATE, and it was reached:\n\t\t * `CnAppRoot` renders a companion for any app that sets `aiCompanion`,\n\t\t * while a host app can also mount one standalone on every page of the\n\t\t * instance (Hermiq does exactly this, so the assistant reaches\n\t\t * third-party office editors that have no CnAppRoot of ours). On any\n\t\t * page that has both, the user saw TWO hexes stacked a few pixels apart\n\t\t * — the two mounts pick up different bundled stylesheets, so they do not\n\t\t * even land in the same place.\n\t\t *\n\t\t * The guard lives HERE, in the component, rather than in each host's\n\t\t * mount script, because only the component knows how many of itself\n\t\t * exist. A host-side check cannot see a sibling that mounts later: the\n\t\t * standalone script runs at DOMContentLoaded and the app's own Vue tree\n\t\t * boots after it, so whichever check runs first sees nothing and both\n\t\t * render.\n\t\t *\n\t\t * First to claim wins and the rest render nothing. That order is stable\n\t\t * in practice — the standalone mount runs first — but the rule does not\n\t\t * depend on it: what matters is that exactly one renders, not which.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclaimSingleton() {\n\t\t\t// 🔴 CLAIMED ONLY WHEN THIS INSTANCE WOULD ACTUALLY RENDER.\n\t\t\t//\n\t\t\t// An earlier version claimed in `created()`, before the health probe\n\t\t\t// had answered — so a companion whose probe FAILED still took the\n\t\t\t// slot and stood the other one down, turning \"one companion\" into\n\t\t\t// NONE. Measured on a slow instance, where the probe's 3×5s budget\n\t\t\t// is genuinely marginal: the slot was claimed and nothing rendered.\n\t\t\t//\n\t\t\t// Claiming late is also why the claim is not in a `watch` on the\n\t\t\t// window key: the slot is contended exactly once per page, at the\n\t\t\t// moment each candidate learns it can render.\n\t\t\tif (this.probeSucceeded !== true || this.isChatPage === true) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst scope = typeof window !== 'undefined' ? window : null\n\t\t\tif (scope === null) {\n\t\t\t\t// No window (SSR, a bare unit test): nothing else can be\n\t\t\t\t// rendering, so this instance is the only one by definition.\n\t\t\t\tthis.isPrimaryCompanion = true\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (scope[COMPANION_SINGLETON_KEY] === null || scope[COMPANION_SINGLETON_KEY] === undefined) {\n\t\t\t\tscope[COMPANION_SINGLETON_KEY] = this\n\t\t\t\tthis.isPrimaryCompanion = true\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.isPrimaryCompanion = scope[COMPANION_SINGLETON_KEY] === this\n\t\t},\n\n\t\t/**\n\t\t * Give the slot back, but only if this instance is holding it.\n\t\t *\n\t\t * Checked rather than cleared unconditionally: a second, standing-down\n\t\t * instance unmounting must not release the slot the FIRST one is still\n\t\t * using, which would let a third mount render alongside it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\treleaseSingleton() {\n\t\t\tconst scope = typeof window !== 'undefined' ? window : null\n\t\t\tif (scope !== null && scope[COMPANION_SINGLETON_KEY] === this) {\n\t\t\t\tscope[COMPANION_SINGLETON_KEY] = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Ask the chat backend whether it can answer, and render only if so.\n\t\t *\n\t\t * Skipped without a request when the backend app is not installed for\n\t\t * this user.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/no-calls-to-missing-apps-and-presence-beacon/specs/ai-companion/spec.md#requirement-the-companion-does-not-probe-a-backend-that-is-not-installed\n\t\t */\n\t\tasync runHealthProbe() {\n\t\t\t// RETRY before hiding. A single probe makes the whole companion\n\t\t\t// disappear on one slow response, and \"slow\" is normal: measured on a\n\t\t\t// busy instance (load 48) the health endpoint answered in well under a\n\t\t\t// second, but the request still lost its 5s budget to contention. The\n\t\t\t// failure mode is the worst kind — the widget is simply absent, with\n\t\t\t// nothing on screen saying why, and a reload usually \"fixes\" it, which\n\t\t\t// is what makes it read as flakiness rather than as a probe result.\n\t\t\t//\n\t\t\t// A backend that is genuinely down fails all attempts and is still\n\t\t\t// reported; this only stops one unlucky request from deciding.\n\t\t\t//\n\t\t\t// 🔴 NO PROBE AT AN APP THAT IS NOT THERE. Nextcloud already tells\n\t\t\t// the page which apps the user has (`OC.appswebroots`), so asking a\n\t\t\t// missing backend for its health is a guaranteed 404, three times\n\t\t\t// per page, in every console. Measured on cloud.conduction.nl\n\t\t\t// (round-5 check): hermiq not installed, three\n\t\t\t// `/apps/hermiq/api/chat/health` 404s on every pipelinq and dossiq\n\t\t\t// page. Hidden either way; now hidden without the requests.\n\t\t\tif (!isAppInstalled(this.chatAppId || DEFAULT_CHAT_APP_ID)) {\n\t\t\t\tthis.probeSucceeded = false\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor (let attempt = 1; attempt <= HEALTH_PROBE_ATTEMPTS; attempt++) {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await axios.get(chatHealthUrl(this.chatAppId), {\n\t\t\t\t\t\ttimeout: HEALTH_TIMEOUT,\n\t\t\t\t\t\tvalidateStatus: (status) => status >= 200 && status < 300,\n\t\t\t\t\t})\n\t\t\t\t\tthis.probeSucceeded = response.status >= 200 && response.status < 300\n\t\t\t\t\t// Contend for the page's single slot only now, with a\n\t\t\t\t\t// backend that answered. See claimSingleton().\n\t\t\t\t\tthis.claimSingleton()\n\t\t\t\t\t// A companion built against an older version of this library\n\t\t\t\t\t// cannot read the flag above, so look for one shortly.\n\t\t\t\t\tthis.yieldToACompanionThatCannotYield()\n\t\t\t\t\treturn\n\t\t\t\t} catch {\n\t\t\t\t\tif (attempt < HEALTH_PROBE_ATTEMPTS) {\n\t\t\t\t\t\tawait new Promise((resolve) => {\n\t\t\t\t\t\t\tsetTimeout(resolve, HEALTH_RETRY_DELAY * attempt)\n\t\t\t\t\t\t})\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.info(`[CnAiCompanion] chat backend \"${this.chatAppId}\" health probe did not return 2xx `\n\t\t\t\t\t\t+ `after ${HEALTH_PROBE_ATTEMPTS} attempts — widget hidden`)\n\t\t\t\t\tthis.probeSucceeded = false\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The hex's own handler: open when closed, close when open.\n\t\t *\n\t\t * The launcher stays visible while the window is open (see the template),\n\t\t * so it has to answer for both directions rather than only opening.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttogglePanel() {\n\t\t\tif (this.isPanelOpen) {\n\t\t\t\tthis.closePanel()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.openPanel()\n\t\t},\n\n\t\topenPanel() {\n\t\t\tthis.isPanelOpen = true\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (this.$refs.panel && this.$refs.panel.$refs.input) {\n\t\t\t\t\tthis.$refs.panel.$refs.input.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\tclosePanel() {\n\t\t\tthis.isPanelOpen = false\n\t\t},\n\n\t\tonSend(text, agentUuid, attachments) {\n\t\t\tthis.stream.send(text, { agentUuid, attachments }).catch((err) => {\n\t\t\t\t// Stream errors are tracked in stream.state.error — no extra handling needed\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.info('[CnAiCompanion] send error:', err?.message)\n\t\t\t})\n\t\t},\n\n\t\tonNewThread() {\n\t\t\tthis.stream.startNewThread()\n\t\t},\n\n\t\tonLoadConversation(uuid) {\n\t\t\tthis.stream.loadConversation(uuid)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n.cn-ai-companion {\n\t/*\n\t * display: contents removes this wrapper from the layout tree so the\n\t * embedded NcAppSidebar inherits NcContent as its flex parent — required\n\t * for the sidebar to slide in from the RIGHT edge. The FAB is itself\n\t * position:fixed so it positions independently of the wrapper's box.\n\t *\n\t * !important is intentional: webpack pulls in BOTH the old (position:\n\t * fixed) and new (display: contents) rules from the package's CJS and\n\t * ESM builds; without !important the old rule wins the cascade and the\n\t * sidebar renders at the left.\n\t */\n\tdisplay: contents !important;\n\tposition: static !important;\n}\n</style>\n"],"names":["CnAiFloatingButton","CnAiChatPanel","axios"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnAiCompanion.vue3.js","sources":["../../../../src/components/CnAiCompanion/CnAiCompanion.vue?vue&type=template&id=13429583&lang.js"],"sourcesContent":["<!--\n CnAiCompanion — top-level mount for the AI Chat Companion widget.\n\n On created():\n - Issues GET /index.php/apps/{chatAppId}/api/chat/health via axios.\n - Renders nothing on non-2xx, network error, or timeout (5s).\n - Probe result cached for component lifetime.\n - Only console.info() on failure (never warn/error).\n\n When probe succeeds:\n - Renders CnAiFloatingButton ALWAYS — it toggles the window rather than only\n opening it, and stays visible while the window is open.\n - Renders CnAiChatPanel (when isPanelOpen) as a floating window anchored to the\n same corner as the hex, offset clear of it so both stay clickable.\n\n FAB and panel both hidden when cnAiContext.pageKind === 'chat'.\n-->\n<template>\n\t<div\n\t\tv-if=\"probeSucceeded && !isChatPage && isPrimaryCompanion\"\n\t\tclass=\"cn-ai-companion\"\n\t\tdata-testid=\"cn-ai-companion\">\n\t\t<!--\n\t\t The hex is rendered UNCONDITIONALLY and toggles the window.\n\n\t\t It used to be `:visible=\"!isPanelOpen\"` — the launcher vanished while\n\t\t the thing it launched was open. Three reasons it now stays, in order of\n\t\t weight:\n\n\t\t 1. It is the one control we can guarantee. The window's own close\n\t\t button lives in markup a host page's stylesheet could hide; the hex\n\t\t is fixed-position with explicit `!important` properties and has\n\t\t survived every host it has been dropped into, including third-party\n\t\t office editors.\n\t\t 2. A launcher that disappears is a dead end — the affordance you used\n\t\t to summon something should dismiss it.\n\t\t 3. It removes a state transition: no show/hide to coordinate with the\n\t\t window's own.\n\n\t\t ⚠️ This is why the window anchors 70px from the edge: the hex is 26x30\n\t\t at a 24px inset, and the two must not overlap or the guarantee above is\n\t\t worth nothing.\n\t\t-->\n\t\t<CnAiFloatingButton\n\t\t\t:visible=\"true\"\n\t\t\t:position=\"position\"\n\t\t\t@click=\"togglePanel\" />\n\t\t<CnAiChatPanel\n\t\t\tref=\"panel\"\n\t\t\t:visible=\"isPanelOpen\"\n\t\t\t:streamState=\"stream.state\"\n\t\t\t:chatAppId=\"chatAppId\"\n\t\t\t:context=\"effectiveContext\"\n\t\t\t:position=\"position\"\n\t\t\t:fabRef=\"$refs.fabButton\"\n\t\t\t@close=\"closePanel\"\n\t\t\t@send=\"onSend\"\n\t\t\t@newThread=\"onNewThread\"\n\t\t\t@loadConversation=\"onLoadConversation\" />\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport CnAiChatPanel from './CnAiChatPanel.vue'\nimport CnAiFloatingButton from './CnAiFloatingButton.vue'\nimport { chatHealthUrl, DEFAULT_CHAT_APP_ID } from '../../composables/aiChatConfig.js'\nimport { useAiChatStream } from '../../composables/useAiChatStream.js'\n\nconst HEALTH_TIMEOUT = 5000\n\n/** How many times the health probe may fail before the companion hides. */\nconst HEALTH_PROBE_ATTEMPTS = 3\n\n/** Base backoff between probe attempts, multiplied by the attempt number. */\nconst HEALTH_RETRY_DELAY = 750\n\n/**\n * The window key one companion claims so a second one stands down.\n *\n * On `window` rather than in module scope, because the two companions on a page\n * do not share a module instance: one comes from the host app's bundle and one\n * from a standalone bundle attached to every page. Two copies of this file are\n * loaded, so a module-level flag would be two flags and would guard nothing.\n */\nconst COMPANION_SINGLETON_KEY = '__cnAiCompanionPrimary'\n\n/**\n * How long to wait before looking for a companion that cannot stand down.\n *\n * Long enough for a host app's Vue tree to boot after a standalone mount at\n * DOMContentLoaded, short enough that a user never sees two hexes settle.\n */\nconst LEGACY_SIBLING_DELAY = 1200\n\nexport default {\n\tname: 'CnAiCompanion',\n\n\tcomponents: {\n\t\tCnAiFloatingButton,\n\t\tCnAiChatPanel,\n\t},\n\n\tinject: {\n\t\tcnAiContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Default position for the FAB.\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/health/conversation HTTP calls\n\t\t * (`/index.php/apps/{chatAppId}/api/...`). Single configuration point for\n\t\t * the AI Chat Companion's backend — see composables/aiChatConfig.js.\n\t\t * Defaults to `hermiq` (the agent engine's home per hydra ADR-034\n\t\t * \"Amendment 2026-07-05\"); `CnAppRoot` forwards its own `chatAppId`\n\t\t * prop here so a consuming app can point the widget at another backend\n\t\t * (e.g. `openregister` during its compat window) in one place.\n\t\t *\n\t\t * @type {string}\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 * What the user is looking at, stated by whoever mounted the companion.\n\t\t *\n\t\t * Inside a Conduction app, CnAppRoot provides this and the prop is\n\t\t * unnecessary. Mounted standalone on a page belonging to another app —\n\t\t * an office editor, the Files list — there is no provider, and the\n\t\t * injected fallback reports `appId: 'unknown'`. The agent then has no\n\t\t * idea what \"this document\" refers to and says so.\n\t\t *\n\t\t * Shape mirrors the injected context: `{ appId, pageKind, fileId,\n\t\t * objectUuid, registerSlug, schemaSlug, route }`. All optional; whatever\n\t\t * the host knows is better than 'unknown'.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tprobeSucceeded: false,\n\t\t\tisPanelOpen: false,\n\t\t\tisPrimaryCompanion: false,\n\t\t\t// Cleared on unmount so a torn-down companion cannot stand itself\n\t\t\t// down after the fact.\n\t\t\tlegacySiblingTimer: null,\n\t\t\tstream: useAiChatStream(this, { chatAppId: this.chatAppId, context: this.context }),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tisChatPage() {\n\t\t\tconst ctx = this.cnAiContext\n\t\t\treturn ctx && ctx.pageKind === 'chat'\n\t\t},\n\n\t\t/**\n\t\t * The page context the panel reasons with: the prop when a standalone\n\t\t * mount passed one, otherwise the context CnAppRoot provides.\n\t\t *\n\t\t * WHY THIS EXISTS\n\t\t * ---------------\n\t\t * `CnAppRoot` renders `<CnAiCompanion :chatAppId=\"chatAppId\" />` and\n\t\t * passes NO `:context`, because it already provides `cnAiContext` for\n\t\t * descendants to inject. This component injected it for `isChatPage`\n\t\t * but handed the panel the raw `context` PROP, which is `null` in that\n\t\t * arrangement — so inside every app shell the panel ran with no\n\t\t * context at all.\n\t\t *\n\t\t * The panel's `relevantAgentOptions` treats an empty context as \"no\n\t\t * page to be relevant to\" and returns EVERY agent, so it selected\n\t\t * whichever agent the API happened to list first. Measured on the\n\t\t * demo instance 2026-09-20: on `/apps/buildiq/` the companion opened\n\t\t * as \"Hydra Triage\", which holds only `hydra.*` tools, and its first\n\t\t * answer was \"This agent's tool grants resolve to no tools\" — while\n\t\t * \"Buildiq builder\", holding all eight `buildiq_*` tools, sat unused\n\t\t * at the bottom of the same list.\n\t\t *\n\t\t * A standalone mount that passes `context` explicitly still wins, so\n\t\t * Hermiq's cross-app companion is unaffected.\n\t\t *\n\t\t * @return {object|null} The context to reason with.\n\t\t */\n\t\teffectiveContext() {\n\t\t\treturn this.context || this.cnAiContext || null\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.runHealthProbe()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.legacySiblingTimer !== null) {\n\t\t\tclearTimeout(this.legacySiblingTimer)\n\t\t\tthis.legacySiblingTimer = null\n\t\t}\n\n\t\tthis.releaseSingleton()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Stand down if another companion is on the page that cannot stand down.\n\t\t *\n\t\t * 🔴 THE WINDOW FLAG ONLY BINDS INSTANCES THAT CHECK IT. Measured on a\n\t\t * live instance after shipping the flag: still TWO hexes, stacked at the\n\t\t * same coordinates — one from `#openregister` (the host app's own bundle,\n\t\t * built against an older version of this library) and one from\n\t\t * `#hermiq-companion-root` (the standalone mount, built against the new\n\t\t * one). The old bundle never reads the flag, so it renders regardless,\n\t\t * and the guarded instance had already claimed the slot and rendered too.\n\t\t *\n\t\t * The library fix alone therefore does not reach a page until EVERY app\n\t\t * on it has rebuilt — which is not something a user with a double hex\n\t\t * can wait for. So the instance that CAN yield does.\n\t\t *\n\t\t * Deferred rather than checked at mount, because order is against us: a\n\t\t * standalone companion mounts at DOMContentLoaded and the host app's Vue\n\t\t * tree boots after it, so at mount time there is nothing to see.\n\t\t *\n\t\t * When both companions carry this guard the flag has already stopped the\n\t\t * second one, so this never fires — it is strictly a bridge for mixed\n\t\t * versions, and it costs one timeout.\n\t\t */\n\t\tyieldToACompanionThatCannotYield() {\n\t\t\tif (typeof document === 'undefined' || this.isPrimaryCompanion !== true) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.legacySiblingTimer = setTimeout(() => {\n\t\t\t\tthis.legacySiblingTimer = null\n\n\t\t\t\tconst rendered = document.querySelectorAll('[data-testid=\"cn-ai-companion\"]')\n\t\t\t\tif (rendered.length <= 1) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tfor (const el of rendered) {\n\t\t\t\t\tif (el !== this.$el) {\n\t\t\t\t\t\t// Yield to it: the host app's own companion sits inside\n\t\t\t\t\t\t// that app's tree and carries its page context, which a\n\t\t\t\t\t\t// standalone mount can only approximate.\n\t\t\t\t\t\tthis.releaseSingleton()\n\t\t\t\t\t\tthis.isPrimaryCompanion = false\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, LEGACY_SIBLING_DELAY)\n\t\t},\n\n\t\t/**\n\t\t * Claim the page's single companion slot, or stand down.\n\t\t *\n\t\t * 🔴 TWO COMPANIONS ON ONE PAGE IS A REACHABLE STATE, and it was reached:\n\t\t * `CnAppRoot` renders a companion for any app that sets `aiCompanion`,\n\t\t * while a host app can also mount one standalone on every page of the\n\t\t * instance (Hermiq does exactly this, so the assistant reaches\n\t\t * third-party office editors that have no CnAppRoot of ours). On any\n\t\t * page that has both, the user saw TWO hexes stacked a few pixels apart\n\t\t * — the two mounts pick up different bundled stylesheets, so they do not\n\t\t * even land in the same place.\n\t\t *\n\t\t * The guard lives HERE, in the component, rather than in each host's\n\t\t * mount script, because only the component knows how many of itself\n\t\t * exist. A host-side check cannot see a sibling that mounts later: the\n\t\t * standalone script runs at DOMContentLoaded and the app's own Vue tree\n\t\t * boots after it, so whichever check runs first sees nothing and both\n\t\t * render.\n\t\t *\n\t\t * First to claim wins and the rest render nothing. That order is stable\n\t\t * in practice — the standalone mount runs first — but the rule does not\n\t\t * depend on it: what matters is that exactly one renders, not which.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclaimSingleton() {\n\t\t\t// 🔴 CLAIMED ONLY WHEN THIS INSTANCE WOULD ACTUALLY RENDER.\n\t\t\t//\n\t\t\t// An earlier version claimed in `created()`, before the health probe\n\t\t\t// had answered — so a companion whose probe FAILED still took the\n\t\t\t// slot and stood the other one down, turning \"one companion\" into\n\t\t\t// NONE. Measured on a slow instance, where the probe's 3×5s budget\n\t\t\t// is genuinely marginal: the slot was claimed and nothing rendered.\n\t\t\t//\n\t\t\t// Claiming late is also why the claim is not in a `watch` on the\n\t\t\t// window key: the slot is contended exactly once per page, at the\n\t\t\t// moment each candidate learns it can render.\n\t\t\tif (this.probeSucceeded !== true || this.isChatPage === true) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst scope = typeof window !== 'undefined' ? window : null\n\t\t\tif (scope === null) {\n\t\t\t\t// No window (SSR, a bare unit test): nothing else can be\n\t\t\t\t// rendering, so this instance is the only one by definition.\n\t\t\t\tthis.isPrimaryCompanion = true\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (scope[COMPANION_SINGLETON_KEY] === null || scope[COMPANION_SINGLETON_KEY] === undefined) {\n\t\t\t\tscope[COMPANION_SINGLETON_KEY] = this\n\t\t\t\tthis.isPrimaryCompanion = true\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.isPrimaryCompanion = scope[COMPANION_SINGLETON_KEY] === this\n\t\t},\n\n\t\t/**\n\t\t * Give the slot back, but only if this instance is holding it.\n\t\t *\n\t\t * Checked rather than cleared unconditionally: a second, standing-down\n\t\t * instance unmounting must not release the slot the FIRST one is still\n\t\t * using, which would let a third mount render alongside it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\treleaseSingleton() {\n\t\t\tconst scope = typeof window !== 'undefined' ? window : null\n\t\t\tif (scope !== null && scope[COMPANION_SINGLETON_KEY] === this) {\n\t\t\t\tscope[COMPANION_SINGLETON_KEY] = null\n\t\t\t}\n\t\t},\n\n\t\tasync runHealthProbe() {\n\t\t\t// RETRY before hiding. A single probe makes the whole companion\n\t\t\t// disappear on one slow response, and \"slow\" is normal: measured on a\n\t\t\t// busy instance (load 48) the health endpoint answered in well under a\n\t\t\t// second, but the request still lost its 5s budget to contention. The\n\t\t\t// failure mode is the worst kind — the widget is simply absent, with\n\t\t\t// nothing on screen saying why, and a reload usually \"fixes\" it, which\n\t\t\t// is what makes it read as flakiness rather than as a probe result.\n\t\t\t//\n\t\t\t// A backend that is genuinely down fails all attempts and is still\n\t\t\t// reported; this only stops one unlucky request from deciding.\n\t\t\tfor (let attempt = 1; attempt <= HEALTH_PROBE_ATTEMPTS; attempt++) {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await axios.get(chatHealthUrl(this.chatAppId), {\n\t\t\t\t\t\ttimeout: HEALTH_TIMEOUT,\n\t\t\t\t\t\tvalidateStatus: (status) => status >= 200 && status < 300,\n\t\t\t\t\t})\n\t\t\t\t\tthis.probeSucceeded = response.status >= 200 && response.status < 300\n\t\t\t\t\t// Contend for the page's single slot only now, with a\n\t\t\t\t\t// backend that answered. See claimSingleton().\n\t\t\t\t\tthis.claimSingleton()\n\t\t\t\t\t// A companion built against an older version of this library\n\t\t\t\t\t// cannot read the flag above, so look for one shortly.\n\t\t\t\t\tthis.yieldToACompanionThatCannotYield()\n\t\t\t\t\treturn\n\t\t\t\t} catch {\n\t\t\t\t\tif (attempt < HEALTH_PROBE_ATTEMPTS) {\n\t\t\t\t\t\tawait new Promise((resolve) => {\n\t\t\t\t\t\t\tsetTimeout(resolve, HEALTH_RETRY_DELAY * attempt)\n\t\t\t\t\t\t})\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.info(`[CnAiCompanion] chat backend \"${this.chatAppId}\" health probe did not return 2xx `\n\t\t\t\t\t\t+ `after ${HEALTH_PROBE_ATTEMPTS} attempts — widget hidden`)\n\t\t\t\t\tthis.probeSucceeded = false\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The hex's own handler: open when closed, close when open.\n\t\t *\n\t\t * The launcher stays visible while the window is open (see the template),\n\t\t * so it has to answer for both directions rather than only opening.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttogglePanel() {\n\t\t\tif (this.isPanelOpen) {\n\t\t\t\tthis.closePanel()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.openPanel()\n\t\t},\n\n\t\topenPanel() {\n\t\t\tthis.isPanelOpen = true\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (this.$refs.panel && this.$refs.panel.$refs.input) {\n\t\t\t\t\tthis.$refs.panel.$refs.input.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\tclosePanel() {\n\t\t\tthis.isPanelOpen = false\n\t\t},\n\n\t\tonSend(text, agentUuid, attachments) {\n\t\t\tthis.stream.send(text, { agentUuid, attachments }).catch((err) => {\n\t\t\t\t// Stream errors are tracked in stream.state.error — no extra handling needed\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.info('[CnAiCompanion] send error:', err?.message)\n\t\t\t})\n\t\t},\n\n\t\tonNewThread() {\n\t\t\tthis.stream.startNewThread()\n\t\t},\n\n\t\tonLoadConversation(uuid) {\n\t\t\tthis.stream.loadConversation(uuid)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n.cn-ai-companion {\n\t/*\n\t * display: contents removes this wrapper from the layout tree so the\n\t * embedded NcAppSidebar inherits NcContent as its flex parent — required\n\t * for the sidebar to slide in from the RIGHT edge. The FAB is itself\n\t * position:fixed so it positions independently of the wrapper's box.\n\t *\n\t * !important is intentional: webpack pulls in BOTH the old (position:\n\t * fixed) and new (display: contents) rules from the package's CJS and\n\t * ESM builds; without !important the old rule wins the cascade and the\n\t * sidebar renders at the left.\n\t */\n\tdisplay: contents !important;\n\tposition: static !important;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock"],"mappings":";;;;OAqCO,EAAC,iBAAW;;;;;;;;UADX,KAAc,CAAA,cAAA,IAAA,CAAK,QAAS,CAAA,UAAA,IAAK,KAAkB,CAAA,kBAAA;AADpD,OAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnAiCompanion.vue3.js","sources":["../../../../src/components/CnAiCompanion/CnAiCompanion.vue?vue&type=template&id=13429583&lang.js"],"sourcesContent":["<!--\n CnAiCompanion — top-level mount for the AI Chat Companion widget.\n\n On created():\n - Issues GET /index.php/apps/{chatAppId}/api/chat/health via axios.\n - Renders nothing on non-2xx, network error, or timeout (5s).\n - Probe result cached for component lifetime.\n - Only console.info() on failure (never warn/error).\n\n When probe succeeds:\n - Renders CnAiFloatingButton ALWAYS — it toggles the window rather than only\n opening it, and stays visible while the window is open.\n - Renders CnAiChatPanel (when isPanelOpen) as a floating window anchored to the\n same corner as the hex, offset clear of it so both stay clickable.\n\n FAB and panel both hidden when cnAiContext.pageKind === 'chat'.\n-->\n<template>\n\t<div\n\t\tv-if=\"probeSucceeded && !isChatPage && isPrimaryCompanion\"\n\t\tclass=\"cn-ai-companion\"\n\t\tdata-testid=\"cn-ai-companion\">\n\t\t<!--\n\t\t The hex is rendered UNCONDITIONALLY and toggles the window.\n\n\t\t It used to be `:visible=\"!isPanelOpen\"` — the launcher vanished while\n\t\t the thing it launched was open. Three reasons it now stays, in order of\n\t\t weight:\n\n\t\t 1. It is the one control we can guarantee. The window's own close\n\t\t button lives in markup a host page's stylesheet could hide; the hex\n\t\t is fixed-position with explicit `!important` properties and has\n\t\t survived every host it has been dropped into, including third-party\n\t\t office editors.\n\t\t 2. A launcher that disappears is a dead end — the affordance you used\n\t\t to summon something should dismiss it.\n\t\t 3. It removes a state transition: no show/hide to coordinate with the\n\t\t window's own.\n\n\t\t ⚠️ This is why the window anchors 70px from the edge: the hex is 26x30\n\t\t at a 24px inset, and the two must not overlap or the guarantee above is\n\t\t worth nothing.\n\t\t-->\n\t\t<CnAiFloatingButton\n\t\t\t:visible=\"true\"\n\t\t\t:position=\"position\"\n\t\t\t@click=\"togglePanel\" />\n\t\t<CnAiChatPanel\n\t\t\tref=\"panel\"\n\t\t\t:visible=\"isPanelOpen\"\n\t\t\t:streamState=\"stream.state\"\n\t\t\t:chatAppId=\"chatAppId\"\n\t\t\t:context=\"effectiveContext\"\n\t\t\t:position=\"position\"\n\t\t\t:fabRef=\"$refs.fabButton\"\n\t\t\t@close=\"closePanel\"\n\t\t\t@send=\"onSend\"\n\t\t\t@newThread=\"onNewThread\"\n\t\t\t@loadConversation=\"onLoadConversation\" />\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport CnAiChatPanel from './CnAiChatPanel.vue'\nimport CnAiFloatingButton from './CnAiFloatingButton.vue'\nimport { chatHealthUrl, DEFAULT_CHAT_APP_ID } from '../../composables/aiChatConfig.js'\nimport { useAiChatStream } from '../../composables/useAiChatStream.js'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\n\nconst HEALTH_TIMEOUT = 5000\n\n/** How many times the health probe may fail before the companion hides. */\nconst HEALTH_PROBE_ATTEMPTS = 3\n\n/** Base backoff between probe attempts, multiplied by the attempt number. */\nconst HEALTH_RETRY_DELAY = 750\n\n/**\n * The window key one companion claims so a second one stands down.\n *\n * On `window` rather than in module scope, because the two companions on a page\n * do not share a module instance: one comes from the host app's bundle and one\n * from a standalone bundle attached to every page. Two copies of this file are\n * loaded, so a module-level flag would be two flags and would guard nothing.\n */\nconst COMPANION_SINGLETON_KEY = '__cnAiCompanionPrimary'\n\n/**\n * How long to wait before looking for a companion that cannot stand down.\n *\n * Long enough for a host app's Vue tree to boot after a standalone mount at\n * DOMContentLoaded, short enough that a user never sees two hexes settle.\n */\nconst LEGACY_SIBLING_DELAY = 1200\n\nexport default {\n\tname: 'CnAiCompanion',\n\n\tcomponents: {\n\t\tCnAiFloatingButton,\n\t\tCnAiChatPanel,\n\t},\n\n\tinject: {\n\t\tcnAiContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Default position for the FAB.\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/health/conversation HTTP calls\n\t\t * (`/index.php/apps/{chatAppId}/api/...`). Single configuration point for\n\t\t * the AI Chat Companion's backend — see composables/aiChatConfig.js.\n\t\t * Defaults to `hermiq` (the agent engine's home per hydra ADR-034\n\t\t * \"Amendment 2026-07-05\"); `CnAppRoot` forwards its own `chatAppId`\n\t\t * prop here so a consuming app can point the widget at another backend\n\t\t * (e.g. `openregister` during its compat window) in one place.\n\t\t *\n\t\t * @type {string}\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 * What the user is looking at, stated by whoever mounted the companion.\n\t\t *\n\t\t * Inside a Conduction app, CnAppRoot provides this and the prop is\n\t\t * unnecessary. Mounted standalone on a page belonging to another app —\n\t\t * an office editor, the Files list — there is no provider, and the\n\t\t * injected fallback reports `appId: 'unknown'`. The agent then has no\n\t\t * idea what \"this document\" refers to and says so.\n\t\t *\n\t\t * Shape mirrors the injected context: `{ appId, pageKind, fileId,\n\t\t * objectUuid, registerSlug, schemaSlug, route }`. All optional; whatever\n\t\t * the host knows is better than 'unknown'.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcontext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tprobeSucceeded: false,\n\t\t\tisPanelOpen: false,\n\t\t\tisPrimaryCompanion: false,\n\t\t\t// Cleared on unmount so a torn-down companion cannot stand itself\n\t\t\t// down after the fact.\n\t\t\tlegacySiblingTimer: null,\n\t\t\tstream: useAiChatStream(this, { chatAppId: this.chatAppId, context: this.context }),\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tisChatPage() {\n\t\t\tconst ctx = this.cnAiContext\n\t\t\treturn ctx && ctx.pageKind === 'chat'\n\t\t},\n\n\t\t/**\n\t\t * The page context the panel reasons with: the prop when a standalone\n\t\t * mount passed one, otherwise the context CnAppRoot provides.\n\t\t *\n\t\t * WHY THIS EXISTS\n\t\t * ---------------\n\t\t * `CnAppRoot` renders `<CnAiCompanion :chatAppId=\"chatAppId\" />` and\n\t\t * passes NO `:context`, because it already provides `cnAiContext` for\n\t\t * descendants to inject. This component injected it for `isChatPage`\n\t\t * but handed the panel the raw `context` PROP, which is `null` in that\n\t\t * arrangement — so inside every app shell the panel ran with no\n\t\t * context at all.\n\t\t *\n\t\t * The panel's `relevantAgentOptions` treats an empty context as \"no\n\t\t * page to be relevant to\" and returns EVERY agent, so it selected\n\t\t * whichever agent the API happened to list first. Measured on the\n\t\t * demo instance 2026-09-20: on `/apps/buildiq/` the companion opened\n\t\t * as \"Hydra Triage\", which holds only `hydra.*` tools, and its first\n\t\t * answer was \"This agent's tool grants resolve to no tools\" — while\n\t\t * \"Buildiq builder\", holding all eight `buildiq_*` tools, sat unused\n\t\t * at the bottom of the same list.\n\t\t *\n\t\t * A standalone mount that passes `context` explicitly still wins, so\n\t\t * Hermiq's cross-app companion is unaffected.\n\t\t *\n\t\t * @return {object|null} The context to reason with.\n\t\t */\n\t\teffectiveContext() {\n\t\t\treturn this.context || this.cnAiContext || null\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.runHealthProbe()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.legacySiblingTimer !== null) {\n\t\t\tclearTimeout(this.legacySiblingTimer)\n\t\t\tthis.legacySiblingTimer = null\n\t\t}\n\n\t\tthis.releaseSingleton()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Stand down if another companion is on the page that cannot stand down.\n\t\t *\n\t\t * 🔴 THE WINDOW FLAG ONLY BINDS INSTANCES THAT CHECK IT. Measured on a\n\t\t * live instance after shipping the flag: still TWO hexes, stacked at the\n\t\t * same coordinates — one from `#openregister` (the host app's own bundle,\n\t\t * built against an older version of this library) and one from\n\t\t * `#hermiq-companion-root` (the standalone mount, built against the new\n\t\t * one). The old bundle never reads the flag, so it renders regardless,\n\t\t * and the guarded instance had already claimed the slot and rendered too.\n\t\t *\n\t\t * The library fix alone therefore does not reach a page until EVERY app\n\t\t * on it has rebuilt — which is not something a user with a double hex\n\t\t * can wait for. So the instance that CAN yield does.\n\t\t *\n\t\t * Deferred rather than checked at mount, because order is against us: a\n\t\t * standalone companion mounts at DOMContentLoaded and the host app's Vue\n\t\t * tree boots after it, so at mount time there is nothing to see.\n\t\t *\n\t\t * When both companions carry this guard the flag has already stopped the\n\t\t * second one, so this never fires — it is strictly a bridge for mixed\n\t\t * versions, and it costs one timeout.\n\t\t */\n\t\tyieldToACompanionThatCannotYield() {\n\t\t\tif (typeof document === 'undefined' || this.isPrimaryCompanion !== true) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.legacySiblingTimer = setTimeout(() => {\n\t\t\t\tthis.legacySiblingTimer = null\n\n\t\t\t\tconst rendered = document.querySelectorAll('[data-testid=\"cn-ai-companion\"]')\n\t\t\t\tif (rendered.length <= 1) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tfor (const el of rendered) {\n\t\t\t\t\tif (el !== this.$el) {\n\t\t\t\t\t\t// Yield to it: the host app's own companion sits inside\n\t\t\t\t\t\t// that app's tree and carries its page context, which a\n\t\t\t\t\t\t// standalone mount can only approximate.\n\t\t\t\t\t\tthis.releaseSingleton()\n\t\t\t\t\t\tthis.isPrimaryCompanion = false\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, LEGACY_SIBLING_DELAY)\n\t\t},\n\n\t\t/**\n\t\t * Claim the page's single companion slot, or stand down.\n\t\t *\n\t\t * 🔴 TWO COMPANIONS ON ONE PAGE IS A REACHABLE STATE, and it was reached:\n\t\t * `CnAppRoot` renders a companion for any app that sets `aiCompanion`,\n\t\t * while a host app can also mount one standalone on every page of the\n\t\t * instance (Hermiq does exactly this, so the assistant reaches\n\t\t * third-party office editors that have no CnAppRoot of ours). On any\n\t\t * page that has both, the user saw TWO hexes stacked a few pixels apart\n\t\t * — the two mounts pick up different bundled stylesheets, so they do not\n\t\t * even land in the same place.\n\t\t *\n\t\t * The guard lives HERE, in the component, rather than in each host's\n\t\t * mount script, because only the component knows how many of itself\n\t\t * exist. A host-side check cannot see a sibling that mounts later: the\n\t\t * standalone script runs at DOMContentLoaded and the app's own Vue tree\n\t\t * boots after it, so whichever check runs first sees nothing and both\n\t\t * render.\n\t\t *\n\t\t * First to claim wins and the rest render nothing. That order is stable\n\t\t * in practice — the standalone mount runs first — but the rule does not\n\t\t * depend on it: what matters is that exactly one renders, not which.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclaimSingleton() {\n\t\t\t// 🔴 CLAIMED ONLY WHEN THIS INSTANCE WOULD ACTUALLY RENDER.\n\t\t\t//\n\t\t\t// An earlier version claimed in `created()`, before the health probe\n\t\t\t// had answered — so a companion whose probe FAILED still took the\n\t\t\t// slot and stood the other one down, turning \"one companion\" into\n\t\t\t// NONE. Measured on a slow instance, where the probe's 3×5s budget\n\t\t\t// is genuinely marginal: the slot was claimed and nothing rendered.\n\t\t\t//\n\t\t\t// Claiming late is also why the claim is not in a `watch` on the\n\t\t\t// window key: the slot is contended exactly once per page, at the\n\t\t\t// moment each candidate learns it can render.\n\t\t\tif (this.probeSucceeded !== true || this.isChatPage === true) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst scope = typeof window !== 'undefined' ? window : null\n\t\t\tif (scope === null) {\n\t\t\t\t// No window (SSR, a bare unit test): nothing else can be\n\t\t\t\t// rendering, so this instance is the only one by definition.\n\t\t\t\tthis.isPrimaryCompanion = true\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (scope[COMPANION_SINGLETON_KEY] === null || scope[COMPANION_SINGLETON_KEY] === undefined) {\n\t\t\t\tscope[COMPANION_SINGLETON_KEY] = this\n\t\t\t\tthis.isPrimaryCompanion = true\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.isPrimaryCompanion = scope[COMPANION_SINGLETON_KEY] === this\n\t\t},\n\n\t\t/**\n\t\t * Give the slot back, but only if this instance is holding it.\n\t\t *\n\t\t * Checked rather than cleared unconditionally: a second, standing-down\n\t\t * instance unmounting must not release the slot the FIRST one is still\n\t\t * using, which would let a third mount render alongside it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\treleaseSingleton() {\n\t\t\tconst scope = typeof window !== 'undefined' ? window : null\n\t\t\tif (scope !== null && scope[COMPANION_SINGLETON_KEY] === this) {\n\t\t\t\tscope[COMPANION_SINGLETON_KEY] = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Ask the chat backend whether it can answer, and render only if so.\n\t\t *\n\t\t * Skipped without a request when the backend app is not installed for\n\t\t * this user.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/no-calls-to-missing-apps-and-presence-beacon/specs/ai-companion/spec.md#requirement-the-companion-does-not-probe-a-backend-that-is-not-installed\n\t\t */\n\t\tasync runHealthProbe() {\n\t\t\t// RETRY before hiding. A single probe makes the whole companion\n\t\t\t// disappear on one slow response, and \"slow\" is normal: measured on a\n\t\t\t// busy instance (load 48) the health endpoint answered in well under a\n\t\t\t// second, but the request still lost its 5s budget to contention. The\n\t\t\t// failure mode is the worst kind — the widget is simply absent, with\n\t\t\t// nothing on screen saying why, and a reload usually \"fixes\" it, which\n\t\t\t// is what makes it read as flakiness rather than as a probe result.\n\t\t\t//\n\t\t\t// A backend that is genuinely down fails all attempts and is still\n\t\t\t// reported; this only stops one unlucky request from deciding.\n\t\t\t//\n\t\t\t// 🔴 NO PROBE AT AN APP THAT IS NOT THERE. Nextcloud already tells\n\t\t\t// the page which apps the user has (`OC.appswebroots`), so asking a\n\t\t\t// missing backend for its health is a guaranteed 404, three times\n\t\t\t// per page, in every console. Measured on cloud.conduction.nl\n\t\t\t// (round-5 check): hermiq not installed, three\n\t\t\t// `/apps/hermiq/api/chat/health` 404s on every pipelinq and dossiq\n\t\t\t// page. Hidden either way; now hidden without the requests.\n\t\t\tif (!isAppInstalled(this.chatAppId || DEFAULT_CHAT_APP_ID)) {\n\t\t\t\tthis.probeSucceeded = false\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor (let attempt = 1; attempt <= HEALTH_PROBE_ATTEMPTS; attempt++) {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await axios.get(chatHealthUrl(this.chatAppId), {\n\t\t\t\t\t\ttimeout: HEALTH_TIMEOUT,\n\t\t\t\t\t\tvalidateStatus: (status) => status >= 200 && status < 300,\n\t\t\t\t\t})\n\t\t\t\t\tthis.probeSucceeded = response.status >= 200 && response.status < 300\n\t\t\t\t\t// Contend for the page's single slot only now, with a\n\t\t\t\t\t// backend that answered. See claimSingleton().\n\t\t\t\t\tthis.claimSingleton()\n\t\t\t\t\t// A companion built against an older version of this library\n\t\t\t\t\t// cannot read the flag above, so look for one shortly.\n\t\t\t\t\tthis.yieldToACompanionThatCannotYield()\n\t\t\t\t\treturn\n\t\t\t\t} catch {\n\t\t\t\t\tif (attempt < HEALTH_PROBE_ATTEMPTS) {\n\t\t\t\t\t\tawait new Promise((resolve) => {\n\t\t\t\t\t\t\tsetTimeout(resolve, HEALTH_RETRY_DELAY * attempt)\n\t\t\t\t\t\t})\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.info(`[CnAiCompanion] chat backend \"${this.chatAppId}\" health probe did not return 2xx `\n\t\t\t\t\t\t+ `after ${HEALTH_PROBE_ATTEMPTS} attempts — widget hidden`)\n\t\t\t\t\tthis.probeSucceeded = false\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The hex's own handler: open when closed, close when open.\n\t\t *\n\t\t * The launcher stays visible while the window is open (see the template),\n\t\t * so it has to answer for both directions rather than only opening.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttogglePanel() {\n\t\t\tif (this.isPanelOpen) {\n\t\t\t\tthis.closePanel()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.openPanel()\n\t\t},\n\n\t\topenPanel() {\n\t\t\tthis.isPanelOpen = true\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (this.$refs.panel && this.$refs.panel.$refs.input) {\n\t\t\t\t\tthis.$refs.panel.$refs.input.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\tclosePanel() {\n\t\t\tthis.isPanelOpen = false\n\t\t},\n\n\t\tonSend(text, agentUuid, attachments) {\n\t\t\tthis.stream.send(text, { agentUuid, attachments }).catch((err) => {\n\t\t\t\t// Stream errors are tracked in stream.state.error — no extra handling needed\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.info('[CnAiCompanion] send error:', err?.message)\n\t\t\t})\n\t\t},\n\n\t\tonNewThread() {\n\t\t\tthis.stream.startNewThread()\n\t\t},\n\n\t\tonLoadConversation(uuid) {\n\t\t\tthis.stream.loadConversation(uuid)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n.cn-ai-companion {\n\t/*\n\t * display: contents removes this wrapper from the layout tree so the\n\t * embedded NcAppSidebar inherits NcContent as its flex parent — required\n\t * for the sidebar to slide in from the RIGHT edge. The FAB is itself\n\t * position:fixed so it positions independently of the wrapper's box.\n\t *\n\t * !important is intentional: webpack pulls in BOTH the old (position:\n\t * fixed) and new (display: contents) rules from the package's CJS and\n\t * ESM builds; without !important the old rule wins the cascade and the\n\t * sidebar renders at the left.\n\t */\n\tdisplay: contents !important;\n\tposition: static !important;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock"],"mappings":";;;;OAqCO,EAAC,iBAAW;;;;;;;;UADX,KAAc,CAAA,cAAA,IAAA,CAAK,QAAS,CAAA,UAAA,IAAK,KAAkB,CAAA,kBAAA;AADpD,OAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { translate } from '@nextcloud/l10n';
|
|
2
2
|
import { generateUrl } from '@nextcloud/router';
|
|
3
|
-
import { NcAppNavigation, NcAppNavigationCaption, NcAppNavigationItem, NcAppNavigationList, NcAppNavigationNew, NcAppNavigationSettings, NcButton, NcCounterBubble } from '@nextcloud/vue';
|
|
3
|
+
import { NcActionLink, NcAppNavigation, NcAppNavigationCaption, NcAppNavigationItem, NcAppNavigationList, NcAppNavigationNew, NcAppNavigationSettings, NcButton, NcCounterBubble } from '@nextcloud/vue';
|
|
4
4
|
import { toRaw } from 'vue';
|
|
5
5
|
import BookOpenVariant from 'vue-material-design-icons/BookOpenVariant.vue';
|
|
6
6
|
import Cog from 'vue-material-design-icons/Cog.vue';
|
|
7
7
|
import HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue';
|
|
8
8
|
import MapMarkerPathIcon from 'vue-material-design-icons/MapMarkerPath.vue';
|
|
9
9
|
import OpenInNew from 'vue-material-design-icons/OpenInNew.vue';
|
|
10
|
+
import Pencil from 'vue-material-design-icons/Pencil.vue';
|
|
10
11
|
import Plus from 'vue-material-design-icons/Plus.vue';
|
|
11
12
|
import ShieldAccountOutline from 'vue-material-design-icons/ShieldAccountOutline.vue';
|
|
12
13
|
import '../CnIcon/CnIcon.vue.js';
|
|
@@ -90,6 +91,8 @@ var script = {
|
|
|
90
91
|
name: 'CnAppNav',
|
|
91
92
|
|
|
92
93
|
components: {
|
|
94
|
+
NcActionLink,
|
|
95
|
+
Pencil,
|
|
93
96
|
NcAppNavigation,
|
|
94
97
|
NcAppNavigationCaption,
|
|
95
98
|
NcAppNavigationItem,
|
|
@@ -1451,8 +1454,10 @@ var script = {
|
|
|
1451
1454
|
/**
|
|
1452
1455
|
* Whether a menu entry renders as a `NcAppNavigationCaption`
|
|
1453
1456
|
* (`type: "caption"`) rather than a clickable
|
|
1454
|
-
* `NcAppNavigationItem`. Caption entries ignore `route`, `
|
|
1455
|
-
* `
|
|
1457
|
+
* `NcAppNavigationItem`. Caption entries ignore `route`, `action`,
|
|
1458
|
+
* `icon`, `count`, `children`, and `pinned`. A caption's `href` is
|
|
1459
|
+
* drawn as a pencil link beside the heading: the place where what
|
|
1460
|
+
* stands under it is changed (a user's own choice of entries, say).
|
|
1456
1461
|
*
|
|
1457
1462
|
* @param {{ type?: string }} item Menu entry descriptor.
|
|
1458
1463
|
* @return {boolean}
|
|
@@ -1461,6 +1466,16 @@ var script = {
|
|
|
1461
1466
|
return item?.type === 'caption'
|
|
1462
1467
|
},
|
|
1463
1468
|
|
|
1469
|
+
/**
|
|
1470
|
+
* The accessible name of a caption's pencil link.
|
|
1471
|
+
*
|
|
1472
|
+
* @param {object} item The caption entry.
|
|
1473
|
+
* @return {string} "Change {caption}", translated.
|
|
1474
|
+
*/
|
|
1475
|
+
captionEditLabel(item) {
|
|
1476
|
+
return translate('nextcloud-vue', 'Change {name}', { name: this.resolveLabel(item) })
|
|
1477
|
+
},
|
|
1478
|
+
|
|
1464
1479
|
/**
|
|
1465
1480
|
* Whether the host has registered a scoped slot named
|
|
1466
1481
|
* `item-${id}-actions` for this menu item. Used to gate template
|