@antelopejs/dms-ai 0.1.4 → 0.1.5
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/README.md +48 -29
- package/dist/channels/bridge.d.ts +2 -8
- package/dist/channels/bridge.js +26 -42
- package/dist/channels/bridge.js.map +1 -1
- package/dist/channels/channel-service.d.ts +2 -2
- package/dist/channels/channel-service.js +12 -31
- package/dist/channels/channel-service.js.map +1 -1
- package/dist/channels/registry.d.ts +2 -2
- package/dist/channels/registry.js +7 -7
- package/dist/channels/registry.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/constants/channels.d.ts +5 -10
- package/dist/constants/channels.js +6 -10
- package/dist/constants/channels.js.map +1 -1
- package/dist/constants/sidecar.d.ts +2 -1
- package/dist/constants/sidecar.js +3 -2
- package/dist/constants/sidecar.js.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/lifecycle/build-id.d.ts +1 -0
- package/dist/lifecycle/build-id.js +32 -0
- package/dist/lifecycle/build-id.js.map +1 -0
- package/dist/lifecycle/sidecar-socket.d.ts +1 -1
- package/dist/lifecycle/sidecar-socket.js +2 -2
- package/dist/lifecycle/sidecar-socket.js.map +1 -1
- package/dist/lifecycle/spawn-sidecar.d.ts +0 -1
- package/dist/lifecycle/spawn-sidecar.js +5 -27
- package/dist/lifecycle/spawn-sidecar.js.map +1 -1
- package/dist/routes/channels.d.ts +3 -3
- package/dist/routes/channels.js +11 -12
- package/dist/routes/channels.js.map +1 -1
- package/dist/routes/index.d.ts +0 -1
- package/dist/routes/index.js +0 -1
- package/dist/routes/index.js.map +1 -1
- package/frontend-vue/.prettierignore +3 -0
- package/frontend-vue/.prettierrc +10 -0
- package/frontend-vue/app/chat/components/ChatView.vue +582 -0
- package/frontend-vue/app/chat/components/ComposerInput.vue +742 -0
- package/frontend-vue/app/chat/components/ConnectionBanner.vue +105 -0
- package/frontend-vue/app/chat/components/ConversationDrawer.vue +266 -0
- package/frontend-vue/app/chat/components/MarkdownContent.vue +196 -0
- package/frontend-vue/app/chat/components/MessageList.vue +474 -0
- package/frontend-vue/app/chat/components/PermissionRequests.vue +323 -0
- package/frontend-vue/app/chat/components/QuestionPrompt.vue +275 -0
- package/frontend-vue/app/chat/components/QueuedMessages.vue +109 -0
- package/frontend-vue/app/chat/components/TodoList.vue +291 -0
- package/frontend-vue/app/chat/components/ToolCallEntry.vue +218 -0
- package/frontend-vue/app/chat/components/ToolCluster.vue +282 -0
- package/frontend-vue/app/chat/composables/useChatChannel.ts +116 -0
- package/frontend-vue/app/chat/composables/useConversation.ts +838 -0
- package/frontend-vue/app/chat/composables/useConversationId.ts +34 -0
- package/frontend-vue/app/chat/composables/useConversationList.ts +56 -0
- package/frontend-vue/app/chat/composables/usePermissionQueue.ts +59 -0
- package/frontend-vue/app/chat/composables/useQuestionQueue.ts +50 -0
- package/frontend-vue/app/chat/composables/useRunClock.ts +28 -0
- package/frontend-vue/app/chat/composables/useSettings.ts +67 -0
- package/frontend-vue/app/chat/constants/attachments.ts +16 -0
- package/frontend-vue/app/chat/constants/connection-banner.ts +7 -0
- package/frontend-vue/app/chat/constants/conversation-drawer.ts +13 -0
- package/frontend-vue/app/chat/constants/conversation.ts +75 -0
- package/frontend-vue/app/chat/constants/permissions.ts +50 -0
- package/frontend-vue/app/chat/constants/protocol.ts +48 -0
- package/frontend-vue/app/chat/constants/run-status.ts +50 -0
- package/frontend-vue/app/chat/constants/settings.ts +60 -0
- package/frontend-vue/app/chat/types/conversation.ts +87 -0
- package/frontend-vue/app/chat/types/permission.ts +7 -0
- package/frontend-vue/app/chat/types/question.ts +17 -0
- package/frontend-vue/app/chat/types/settings.ts +27 -0
- package/frontend-vue/app/chat/utils/attachments.ts +62 -0
- package/frontend-vue/app/chat/utils/format.ts +14 -0
- package/frontend-vue/app/chat/utils/ids.ts +34 -0
- package/frontend-vue/app/chat/utils/markdown.ts +188 -0
- package/frontend-vue/app/chat/utils/run-status.ts +90 -0
- package/frontend-vue/app/chat/utils/todos.ts +64 -0
- package/frontend-vue/app/chat/utils/tool-summary.ts +53 -0
- package/frontend-vue/app/components/ActivityView.vue +34 -34
- package/frontend-vue/app/components/ChatPanel.vue +276 -0
- package/frontend-vue/app/components/SettingsView.vue +120 -112
- package/frontend-vue/app/components/SkillsView.vue +55 -53
- package/frontend-vue/app/plugins/ai.client.ts +145 -129
- package/frontend-vue/app/runtime/assistant-session.ts +21 -0
- package/frontend-vue/app/runtime/backoff.ts +11 -14
- package/frontend-vue/app/runtime/channel-client.ts +130 -141
- package/frontend-vue/app/runtime/chat-transport.ts +25 -55
- package/frontend-vue/app/runtime/constants.ts +69 -59
- package/frontend-vue/app/runtime/current-page.ts +49 -15
- package/frontend-vue/app/runtime/event-stream.ts +35 -35
- package/frontend-vue/app/runtime/frame-router.ts +30 -0
- package/frontend-vue/app/runtime/header-action.ts +18 -18
- package/frontend-vue/app/runtime/host-commands.ts +57 -56
- package/frontend-vue/app/runtime/listeners.ts +31 -0
- package/frontend-vue/app/runtime/navigation-complete.ts +7 -7
- package/frontend-vue/app/runtime/overlay-prefs.ts +36 -36
- package/frontend-vue/app/runtime/panel-state.ts +88 -0
- package/frontend-vue/app/runtime/session-gate.ts +8 -8
- package/frontend-vue/app/runtime/shortcuts.ts +12 -12
- package/frontend-vue/app/runtime/sidecar-status.ts +61 -61
- package/frontend-vue/app/runtime/typed-message.ts +11 -0
- package/frontend-vue/dms.frontend.ts +12 -12
- package/frontend-vue/eslint.config.mjs +13 -13
- package/frontend-vue/package.json +11 -4
- package/frontend-vue/pnpm-lock.yaml +298 -96
- package/frontend-vue/tests/backoff.test.ts +26 -30
- package/frontend-vue/tests/channel-client.test.ts +187 -188
- package/frontend-vue/tests/chat/markdown.test.ts +271 -0
- package/frontend-vue/tests/chat/useChatChannel.test.ts +124 -0
- package/frontend-vue/tests/chat/useConversation.test.ts +283 -0
- package/frontend-vue/tests/chat-panel.test.ts +354 -0
- package/frontend-vue/tests/chat-transport.test.ts +84 -57
- package/frontend-vue/tests/host-commands.test.ts +126 -87
- package/frontend-vue/tests/navigation.test.ts +65 -34
- package/frontend-vue/tests/overlay-prefs.test.ts +49 -49
- package/frontend-vue/tests/panel-state.test.ts +58 -0
- package/frontend-vue/tests/session-gate.test.ts +33 -33
- package/frontend-vue/tests/sidecar-guest.test.ts +46 -46
- package/frontend-vue/tests/sidecar-status.test.ts +29 -29
- package/frontend-vue/tsconfig.chat.json +16 -0
- package/frontend-vue/vitest.config.ts +6 -4
- package/package.json +5 -5
- package/sidecar/dist/agent/effective-mode.d.ts +2 -2
- package/sidecar/dist/agent/effective-mode.js +1 -1
- package/sidecar/dist/agent/effective-mode.js.map +1 -1
- package/sidecar/dist/agent/host-context.js +7 -9
- package/sidecar/dist/agent/host-context.js.map +1 -1
- package/sidecar/dist/agent/permission-bus.d.ts +1 -1
- package/sidecar/dist/agent/permission-bus.js +2 -2
- package/sidecar/dist/agent/permission-bus.js.map +1 -1
- package/sidecar/dist/agent/provider.d.ts +2 -2
- package/sidecar/dist/agent/question-bus.d.ts +1 -1
- package/sidecar/dist/agent/question-bus.js +2 -2
- package/sidecar/dist/agent/question-bus.js.map +1 -1
- package/sidecar/dist/agent/runner.d.ts +2 -2
- package/sidecar/dist/agent/runner.js +24 -8
- package/sidecar/dist/agent/runner.js.map +1 -1
- package/sidecar/dist/agent/session.d.ts +2 -2
- package/sidecar/dist/agent/session.js +49 -9
- package/sidecar/dist/agent/session.js.map +1 -1
- package/sidecar/dist/agent/switching-runner.js +10 -5
- package/sidecar/dist/agent/switching-runner.js.map +1 -1
- package/sidecar/dist/agent/turn-end-reasons.d.ts +2 -0
- package/sidecar/dist/agent/turn-end-reasons.js +6 -1
- package/sidecar/dist/agent/turn-end-reasons.js.map +1 -1
- package/sidecar/dist/constants/agent.d.ts +4 -1
- package/sidecar/dist/constants/agent.js +4 -1
- package/sidecar/dist/constants/agent.js.map +1 -1
- package/sidecar/dist/constants/cli.d.ts +0 -1
- package/sidecar/dist/constants/cli.js +0 -1
- package/sidecar/dist/constants/cli.js.map +1 -1
- package/sidecar/dist/constants/codex.d.ts +5 -2
- package/sidecar/dist/constants/codex.js +5 -2
- package/sidecar/dist/constants/codex.js.map +1 -1
- package/sidecar/dist/constants/http.d.ts +0 -9
- package/sidecar/dist/constants/http.js +0 -19
- package/sidecar/dist/constants/http.js.map +1 -1
- package/sidecar/dist/constants/mcp.d.ts +1 -1
- package/sidecar/dist/constants/mcp.js +1 -1
- package/sidecar/dist/constants/mcp.js.map +1 -1
- package/sidecar/dist/constants/paths.d.ts +0 -1
- package/sidecar/dist/constants/paths.js +0 -1
- package/sidecar/dist/constants/paths.js.map +1 -1
- package/sidecar/dist/constants/ws.d.ts +3 -4
- package/sidecar/dist/constants/ws.js +3 -4
- package/sidecar/dist/constants/ws.js.map +1 -1
- package/sidecar/dist/index.d.ts +0 -1
- package/sidecar/dist/index.js +6 -10
- package/sidecar/dist/index.js.map +1 -1
- package/sidecar/dist/mcp/tools/find-pages-using.js +5 -10
- package/sidecar/dist/mcp/tools/find-pages-using.js.map +1 -1
- package/sidecar/dist/mcp/types.d.ts +1 -1
- package/sidecar/dist/pages/page-filepath.d.ts +4 -0
- package/sidecar/dist/pages/page-filepath.js +28 -0
- package/sidecar/dist/pages/page-filepath.js.map +1 -0
- package/sidecar/dist/prompts/agent.d.ts +1 -1
- package/sidecar/dist/prompts/agent.js +1 -1
- package/sidecar/dist/prompts/agent.js.map +1 -1
- package/sidecar/dist/protocol/events.js +2 -2
- package/sidecar/dist/protocol/events.js.map +1 -1
- package/sidecar/dist/protocol/messages.d.ts +4 -15
- package/sidecar/dist/protocol/messages.js +4 -11
- package/sidecar/dist/protocol/messages.js.map +1 -1
- package/sidecar/dist/providers/claude/config.d.ts +2 -2
- package/sidecar/dist/providers/claude/config.js +2 -2
- package/sidecar/dist/providers/claude/config.js.map +1 -1
- package/sidecar/dist/providers/claude/provider.js +1 -1
- package/sidecar/dist/providers/claude/provider.js.map +1 -1
- package/sidecar/dist/providers/codex/config.d.ts +3 -2
- package/sidecar/dist/providers/codex/config.js +3 -3
- package/sidecar/dist/providers/codex/config.js.map +1 -1
- package/sidecar/dist/providers/codex/process.js +107 -74
- package/sidecar/dist/providers/codex/process.js.map +1 -1
- package/sidecar/dist/providers/codex/provider.js +68 -31
- package/sidecar/dist/providers/codex/provider.js.map +1 -1
- package/sidecar/dist/providers/codex/skills.d.ts +1 -0
- package/sidecar/dist/providers/codex/skills.js +4 -1
- package/sidecar/dist/providers/codex/skills.js.map +1 -1
- package/sidecar/dist/providers/codex/stop.d.ts +4 -0
- package/sidecar/dist/providers/codex/stop.js +71 -0
- package/sidecar/dist/providers/codex/stop.js.map +1 -0
- package/sidecar/dist/server/{iframe-socket-registry.d.ts → chat-socket-registry.d.ts} +3 -2
- package/sidecar/dist/server/{iframe-socket-registry.js → chat-socket-registry.js} +4 -3
- package/sidecar/dist/server/chat-socket-registry.js.map +1 -0
- package/sidecar/dist/server/client-auth.js +2 -14
- package/sidecar/dist/server/client-auth.js.map +1 -1
- package/sidecar/dist/server/host-command-router.d.ts +5 -0
- package/sidecar/dist/server/host-command-router.js +4 -0
- package/sidecar/dist/server/host-command-router.js.map +1 -0
- package/sidecar/dist/server/host-socket-registry.d.ts +1 -1
- package/sidecar/dist/server/host-socket-registry.js +16 -13
- package/sidecar/dist/server/host-socket-registry.js.map +1 -1
- package/sidecar/dist/server/http.d.ts +0 -1
- package/sidecar/dist/server/http.js +1 -44
- package/sidecar/dist/server/http.js.map +1 -1
- package/sidecar/dist/server/routing.d.ts +5 -6
- package/sidecar/dist/server/routing.js +18 -28
- package/sidecar/dist/server/routing.js.map +1 -1
- package/sidecar/dist/server/ws.d.ts +2 -2
- package/sidecar/dist/server/ws.js +41 -39
- package/sidecar/dist/server/ws.js.map +1 -1
- package/sidecar/dist/state/host-state.d.ts +10 -2
- package/sidecar/dist/state/host-state.js +13 -3
- package/sidecar/dist/state/host-state.js.map +1 -1
- package/sidecar/dist/state/settings-store.js +2 -2
- package/sidecar/dist/state/settings-store.js.map +1 -1
- package/sidecar/dist/state/settings-types.d.ts +3 -3
- package/sidecar/dist/state/settings-types.js +1 -1
- package/sidecar/dist/state/settings-types.js.map +1 -1
- package/sidecar/package.json +3 -5
- package/dist/chatbox/passthrough.d.ts +0 -3
- package/dist/chatbox/passthrough.js +0 -53
- package/dist/chatbox/passthrough.js.map +0 -1
- package/dist/constants/chatbox.d.ts +0 -10
- package/dist/constants/chatbox.js +0 -35
- package/dist/constants/chatbox.js.map +0 -1
- package/dist/routes/chatbox.d.ts +0 -7
- package/dist/routes/chatbox.js +0 -47
- package/dist/routes/chatbox.js.map +0 -1
- package/frontend-vue/app/runtime/overlay.ts +0 -400
- package/frontend-vue/app/runtime/theme-bridge.ts +0 -189
- package/sidecar/chatbox/dist/assets/index-C8u7mkHn.css +0 -1
- package/sidecar/chatbox/dist/assets/index-tdZvY7ee.js +0 -52
- package/sidecar/chatbox/dist/index.html +0 -13
- package/sidecar/dist/server/iframe-socket-registry.js.map +0 -1
package/dist/routes/chatbox.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
-
};
|
|
8
|
-
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
-
};
|
|
11
|
-
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
-
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
-
};
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.AIChatboxController = void 0;
|
|
16
|
-
const interface_api_1 = require("@antelopejs/interface-api");
|
|
17
|
-
const auth_1 = require("@antelopejs/interface-dms/auth");
|
|
18
|
-
const passthrough_1 = require("../chatbox/passthrough");
|
|
19
|
-
const chatbox_1 = require("../constants/chatbox");
|
|
20
|
-
let AIChatboxController = class AIChatboxController extends (0, interface_api_1.Controller)(chatbox_1.CHATBOX_ROUTE_PREFIX) {
|
|
21
|
-
document(ctx) {
|
|
22
|
-
return (0, passthrough_1.relayChatboxFile)(ctx, chatbox_1.CHATBOX_INDEX_PATH);
|
|
23
|
-
}
|
|
24
|
-
asset(ctx, path) {
|
|
25
|
-
return (0, passthrough_1.relayChatboxFile)(ctx, `/${path}`);
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
exports.AIChatboxController = AIChatboxController;
|
|
29
|
-
__decorate([
|
|
30
|
-
(0, interface_api_1.Get)(chatbox_1.CHATBOX_INDEX_ROUTE),
|
|
31
|
-
__param(0, (0, interface_api_1.Context)()),
|
|
32
|
-
__metadata("design:type", Function),
|
|
33
|
-
__metadata("design:paramtypes", [Object]),
|
|
34
|
-
__metadata("design:returntype", Promise)
|
|
35
|
-
], AIChatboxController.prototype, "document", null);
|
|
36
|
-
__decorate([
|
|
37
|
-
(0, interface_api_1.Get)(chatbox_1.CHATBOX_ASSET_ROUTE),
|
|
38
|
-
__param(0, (0, interface_api_1.Context)()),
|
|
39
|
-
__param(1, (0, interface_api_1.Parameter)(chatbox_1.CHATBOX_PATH_PARAM, "param")),
|
|
40
|
-
__metadata("design:type", Function),
|
|
41
|
-
__metadata("design:paramtypes", [Object, String]),
|
|
42
|
-
__metadata("design:returntype", Promise)
|
|
43
|
-
], AIChatboxController.prototype, "asset", null);
|
|
44
|
-
exports.AIChatboxController = AIChatboxController = __decorate([
|
|
45
|
-
(0, auth_1.AuthOwnerOnly)()
|
|
46
|
-
], AIChatboxController);
|
|
47
|
-
//# sourceMappingURL=chatbox.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chatbox.js","sourceRoot":"","sources":["../../src/routes/chatbox.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,6DAOmC;AACnC,yDAA+D;AAC/D,wDAA0D;AAC1D,kDAM8B;AAOvB,IAAM,mBAAmB,GAAzB,MAAM,mBAAoB,SAAQ,IAAA,0BAAU,EAAC,8BAAoB,CAAC;IAEvE,QAAQ,CAAY,GAAmB;QACrC,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,4BAAkB,CAAC,CAAC;IACnD,CAAC;IAGD,KAAK,CACQ,GAAmB,EACU,IAAY;QAEpD,OAAO,IAAA,8BAAgB,EAAC,GAAG,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;CACF,CAAA;AAbY,kDAAmB;AAE9B;IADC,IAAA,mBAAG,EAAC,6BAAmB,CAAC;IACf,WAAA,IAAA,uBAAO,GAAE,CAAA;;;;mDAElB;AAGD;IADC,IAAA,mBAAG,EAAC,6BAAmB,CAAC;IAEtB,WAAA,IAAA,uBAAO,GAAE,CAAA;IACT,WAAA,IAAA,yBAAS,EAAC,4BAAkB,EAAE,OAAO,CAAC,CAAA;;;;gDAGxC;8BAZU,mBAAmB;IAD/B,IAAA,oBAAa,GAAE;GACH,mBAAmB,CAa/B"}
|
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
OVERLAY_BOX_SHADOW,
|
|
3
|
-
OVERLAY_DEFAULT_WIDTH_PX,
|
|
4
|
-
OVERLAY_DOM_ID,
|
|
5
|
-
OVERLAY_IFRAME_ID,
|
|
6
|
-
OVERLAY_MIN_WIDTH_PX,
|
|
7
|
-
OVERLAY_OPEN_CHANGE_EVENT,
|
|
8
|
-
OVERLAY_OUTSIDE_TOGGLE_SUPPRESS_MS,
|
|
9
|
-
OVERLAY_PLACEHOLDER_ID,
|
|
10
|
-
OVERLAY_RESIZE_HANDLE_SIZE_PX,
|
|
11
|
-
OVERLAY_Z_INDEX,
|
|
12
|
-
PLACEHOLDER_CONNECTING_TEXT,
|
|
13
|
-
PLACEHOLDER_REVIVING_TEXT,
|
|
14
|
-
PLACEHOLDER_UNAVAILABLE_TEXT,
|
|
15
|
-
PLACEHOLDER_UNAVAILABLE_TITLE,
|
|
16
|
-
} from './constants'
|
|
17
|
-
import {
|
|
18
|
-
type OverlayPrefs,
|
|
19
|
-
readPrefs,
|
|
20
|
-
writePrefsDebounced,
|
|
21
|
-
} from './overlay-prefs'
|
|
22
|
-
import {
|
|
23
|
-
type HostTheme,
|
|
24
|
-
installThemeBridge,
|
|
25
|
-
isFromFrame,
|
|
26
|
-
readHostMode,
|
|
27
|
-
} from './theme-bridge'
|
|
28
|
-
|
|
29
|
-
// What the host-origin placeholder shows while the sidecar frame can't be shown.
|
|
30
|
-
// `connecting`/`reviving` are transient (spinner); `unavailable` is terminal.
|
|
31
|
-
export type PlaceholderKind = 'connecting' | 'reviving' | 'unavailable'
|
|
32
|
-
|
|
33
|
-
export interface OverlayHandle {
|
|
34
|
-
// Toggle the docked panel's slide-in open/closed state.
|
|
35
|
-
toggleOpen: () => void
|
|
36
|
-
// Launcher-specific toggle: ignores an open request that immediately follows
|
|
37
|
-
// an outside-click close, since the launcher's `click` fires after the same
|
|
38
|
-
// `pointerdown` that already closed the panel. The keyboard shortcut must
|
|
39
|
-
// use `toggleOpen` so it is never suppressed.
|
|
40
|
-
toggleFromLauncher: () => void
|
|
41
|
-
isOpen: () => boolean
|
|
42
|
-
/** Called on every open or close, however the panel was toggled. */
|
|
43
|
-
onOpenChange: (listener: (isOpen: boolean) => void) => () => void
|
|
44
|
-
// Point the frame at the chat document. A no-op when the URL is
|
|
45
|
-
// unchanged and already loaded, so a redundant `connected` notify won't reload
|
|
46
|
-
// the chatbox; otherwise it reloads and shows a connecting placeholder until
|
|
47
|
-
// the frame paints.
|
|
48
|
-
repoint: (url: string) => void
|
|
49
|
-
// Cover the frame with the host-origin status panel (never the browser's raw
|
|
50
|
-
// error page) while the sidecar is unreachable.
|
|
51
|
-
showPlaceholder: (kind: PlaceholderKind) => void
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface OverlayElements {
|
|
55
|
-
container: HTMLDivElement
|
|
56
|
-
iframe: HTMLIFrameElement
|
|
57
|
-
resizeHandle: HTMLDivElement
|
|
58
|
-
placeholder: HTMLDivElement
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface OverlayPrefsRef {
|
|
62
|
-
value: OverlayPrefs
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface MessageData {
|
|
66
|
-
type?: unknown
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const MAX_WIDTH_VW_RATIO = 0.94
|
|
70
|
-
// Message types accepted from the chatbox iframe to close the panel.
|
|
71
|
-
const CLOSE_MESSAGE_TYPES = new Set(['dms-ai:close'])
|
|
72
|
-
|
|
73
|
-
// Fallback backdrop shown behind the iframe while it slides in, before the
|
|
74
|
-
// bridge reports the host's real `--ui-bg`. Only briefly visible.
|
|
75
|
-
const BACKDROP_DARK = '#0a0b12'
|
|
76
|
-
const BACKDROP_LIGHT = '#ffffff'
|
|
77
|
-
|
|
78
|
-
function backdropFor(mode: 'dark' | 'light'): string {
|
|
79
|
-
return mode === 'dark' ? BACKDROP_DARK : BACKDROP_LIGHT
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function maxWidth(): number {
|
|
83
|
-
const vw = globalThis.innerWidth ?? OVERLAY_DEFAULT_WIDTH_PX * 3
|
|
84
|
-
return Math.max(OVERLAY_MIN_WIDTH_PX, Math.round(vw * MAX_WIDTH_VW_RATIO))
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function clampWidth(width: number): number {
|
|
88
|
-
return Math.min(Math.max(width, OVERLAY_MIN_WIDTH_PX), maxWidth())
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function buildContainer(): HTMLDivElement {
|
|
92
|
-
const container = document.createElement('div')
|
|
93
|
-
container.id = OVERLAY_DOM_ID
|
|
94
|
-
const style = container.style
|
|
95
|
-
style.position = 'fixed'
|
|
96
|
-
style.top = '0'
|
|
97
|
-
style.right = '0'
|
|
98
|
-
style.height = '100vh'
|
|
99
|
-
style.zIndex = String(OVERLAY_Z_INDEX)
|
|
100
|
-
style.boxShadow = OVERLAY_BOX_SHADOW
|
|
101
|
-
// Backdrop behind the iframe while it slides in — follow the host mode so a
|
|
102
|
-
// light-mode host doesn't flash a dark sheet. The iframe fills the container
|
|
103
|
-
// once painted, so this is only ever briefly visible.
|
|
104
|
-
style.background = backdropFor(readHostMode())
|
|
105
|
-
style.borderLeft = '1px solid rgba(140,160,220,0.20)'
|
|
106
|
-
style.overflow = 'hidden'
|
|
107
|
-
style.display = 'flex'
|
|
108
|
-
style.transition = 'transform 220ms cubic-bezier(0.16, 1, 0.3, 1)'
|
|
109
|
-
style.willChange = 'transform'
|
|
110
|
-
return container
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Built with no `src`: the frame is only pointed at the chat via `repoint` once
|
|
114
|
-
// the status controller confirms the sidecar runs, so it never renders the
|
|
115
|
-
// error page of a sidecar that is down.
|
|
116
|
-
function buildIframe(): HTMLIFrameElement {
|
|
117
|
-
const iframe = document.createElement('iframe')
|
|
118
|
-
iframe.id = OVERLAY_IFRAME_ID
|
|
119
|
-
iframe.style.flex = '1 1 auto'
|
|
120
|
-
iframe.style.width = '100%'
|
|
121
|
-
iframe.style.height = '100%'
|
|
122
|
-
iframe.style.border = '0'
|
|
123
|
-
iframe.style.colorScheme = readHostMode()
|
|
124
|
-
return iframe
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const SPINNER_STYLE_ID = 'dms-ai-overlay-spinner-style'
|
|
128
|
-
|
|
129
|
-
function ensureSpinnerKeyframes(): void {
|
|
130
|
-
if (document.getElementById(SPINNER_STYLE_ID) !== null) return
|
|
131
|
-
const style = document.createElement('style')
|
|
132
|
-
style.id = SPINNER_STYLE_ID
|
|
133
|
-
style.textContent =
|
|
134
|
-
'@keyframes dms-ai-spin{to{transform:rotate(360deg)}}'
|
|
135
|
-
document.head.appendChild(style)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function buildPlaceholder(): HTMLDivElement {
|
|
139
|
-
const placeholder = document.createElement('div')
|
|
140
|
-
placeholder.id = OVERLAY_PLACEHOLDER_ID
|
|
141
|
-
const style = placeholder.style
|
|
142
|
-
style.position = 'absolute'
|
|
143
|
-
style.inset = '0'
|
|
144
|
-
// Above the iframe, but transparent to pointers so the resize handle and the
|
|
145
|
-
// (harmless) dead frame beneath stay reachable.
|
|
146
|
-
style.zIndex = '2'
|
|
147
|
-
style.pointerEvents = 'none'
|
|
148
|
-
style.display = 'none'
|
|
149
|
-
style.flexDirection = 'column'
|
|
150
|
-
style.alignItems = 'center'
|
|
151
|
-
style.justifyContent = 'center'
|
|
152
|
-
style.gap = '14px'
|
|
153
|
-
style.padding = '24px'
|
|
154
|
-
style.textAlign = 'center'
|
|
155
|
-
style.font =
|
|
156
|
-
'500 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif'
|
|
157
|
-
style.background = backdropFor(readHostMode())
|
|
158
|
-
return placeholder
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function spinnerHtml(color: string): string {
|
|
162
|
-
return `<span style="width:22px;height:22px;border:2.5px solid ${color}33;border-top-color:${color};border-radius:50%;animation:dms-ai-spin 0.7s linear infinite;"></span>`
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function placeholderContent(kind: PlaceholderKind): string {
|
|
166
|
-
if (kind === 'unavailable') {
|
|
167
|
-
return `<div style="font-size:15px;font-weight:600;">${PLACEHOLDER_UNAVAILABLE_TITLE}</div><div style="opacity:0.7;">${PLACEHOLDER_UNAVAILABLE_TEXT}</div>`
|
|
168
|
-
}
|
|
169
|
-
const text =
|
|
170
|
-
kind === 'reviving'
|
|
171
|
-
? PLACEHOLDER_REVIVING_TEXT
|
|
172
|
-
: PLACEHOLDER_CONNECTING_TEXT
|
|
173
|
-
return `${spinnerHtml('currentColor')}<div style="opacity:0.75;">${text}</div>`
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function buildResizeHandle(): HTMLDivElement {
|
|
177
|
-
const handle = document.createElement('div')
|
|
178
|
-
const style = handle.style
|
|
179
|
-
style.position = 'absolute'
|
|
180
|
-
style.left = '0'
|
|
181
|
-
style.top = '0'
|
|
182
|
-
style.width = `${OVERLAY_RESIZE_HANDLE_SIZE_PX}px`
|
|
183
|
-
style.height = '100%'
|
|
184
|
-
style.cursor = 'ew-resize'
|
|
185
|
-
style.touchAction = 'none'
|
|
186
|
-
style.zIndex = '1'
|
|
187
|
-
return handle
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function applyPrefs(elements: OverlayElements, prefs: OverlayPrefs): void {
|
|
191
|
-
const width = clampWidth(prefs.width)
|
|
192
|
-
elements.container.style.width = `${width}px`
|
|
193
|
-
elements.container.style.transform = prefs.isOpen
|
|
194
|
-
? 'translateX(0)'
|
|
195
|
-
: 'translateX(100%)'
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function resolveInitialPrefs(): OverlayPrefs {
|
|
199
|
-
const stored = readPrefs()
|
|
200
|
-
if (stored !== null)
|
|
201
|
-
return { width: clampWidth(stored.width), isOpen: stored.isOpen }
|
|
202
|
-
return { width: OVERLAY_DEFAULT_WIDTH_PX, isOpen: false }
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
interface InteractionContext {
|
|
206
|
-
elements: OverlayElements
|
|
207
|
-
stateRef: OverlayPrefsRef
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function installWidthResize(ctx: InteractionContext): void {
|
|
211
|
-
const { resizeHandle, container, iframe } = ctx.elements
|
|
212
|
-
let startX = 0
|
|
213
|
-
let startWidth = 0
|
|
214
|
-
|
|
215
|
-
const onMove = (event: PointerEvent): void => {
|
|
216
|
-
// Panel is pinned right, so dragging left (smaller clientX) grows it.
|
|
217
|
-
const next = clampWidth(startWidth + (startX - event.clientX))
|
|
218
|
-
ctx.stateRef.value = { ...ctx.stateRef.value, width: next }
|
|
219
|
-
container.style.width = `${next}px`
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const onUp = (): void => {
|
|
223
|
-
globalThis.removeEventListener('pointermove', onMove)
|
|
224
|
-
globalThis.removeEventListener('pointerup', onUp)
|
|
225
|
-
// Restore the iframe's pointer capture now the drag is over.
|
|
226
|
-
iframe.style.pointerEvents = ''
|
|
227
|
-
writePrefsDebounced(ctx.stateRef.value)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
resizeHandle.addEventListener('pointerdown', (event: PointerEvent) => {
|
|
231
|
-
event.preventDefault()
|
|
232
|
-
startX = event.clientX
|
|
233
|
-
startWidth = ctx.stateRef.value.width
|
|
234
|
-
// While dragging, dragging over the iframe would let it swallow the
|
|
235
|
-
// pointer stream, so the parent never sees pointermove/up and the drag
|
|
236
|
-
// sticks. Make the iframe transparent to pointers for the drag's duration.
|
|
237
|
-
iframe.style.pointerEvents = 'none'
|
|
238
|
-
globalThis.addEventListener('pointermove', onMove)
|
|
239
|
-
globalThis.addEventListener('pointerup', onUp)
|
|
240
|
-
})
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Close the panel when the user clicks anywhere in the host page outside it.
|
|
244
|
-
// Clicks inside the iframe never reach the host document, so they can't close
|
|
245
|
-
// the panel. Returns a predicate telling whether an outside click just closed
|
|
246
|
-
// the panel: the header launcher toggles on `click`, which follows the same
|
|
247
|
-
// `pointerdown` that closed the panel — without this guard, clicking the
|
|
248
|
-
// launcher while open would close then immediately re-open it.
|
|
249
|
-
function installOutsideClickClose(ctx: InteractionContext): () => boolean {
|
|
250
|
-
let lastCloseAt = Number.NEGATIVE_INFINITY
|
|
251
|
-
document.addEventListener(
|
|
252
|
-
'pointerdown',
|
|
253
|
-
(event: PointerEvent) => {
|
|
254
|
-
if (!ctx.stateRef.value.isOpen) return
|
|
255
|
-
if (event.composedPath().includes(ctx.elements.container)) return
|
|
256
|
-
lastCloseAt = event.timeStamp
|
|
257
|
-
setOpen(ctx.elements, ctx.stateRef, false)
|
|
258
|
-
},
|
|
259
|
-
{ capture: true },
|
|
260
|
-
)
|
|
261
|
-
return (): boolean =>
|
|
262
|
-
performance.now() - lastCloseAt < OVERLAY_OUTSIDE_TOGGLE_SUPPRESS_MS
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function appendElements(elements: OverlayElements): void {
|
|
266
|
-
elements.container.appendChild(elements.iframe)
|
|
267
|
-
elements.container.appendChild(elements.placeholder)
|
|
268
|
-
elements.container.appendChild(elements.resizeHandle)
|
|
269
|
-
document.body.appendChild(elements.container)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
interface PlaceholderController {
|
|
273
|
-
show: (kind: PlaceholderKind) => void
|
|
274
|
-
hide: () => void
|
|
275
|
-
recolor: (theme: HostTheme) => void
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function createPlaceholderController(
|
|
279
|
-
placeholder: HTMLDivElement,
|
|
280
|
-
): PlaceholderController {
|
|
281
|
-
return {
|
|
282
|
-
show: (kind) => {
|
|
283
|
-
placeholder.innerHTML = placeholderContent(kind)
|
|
284
|
-
placeholder.style.display = 'flex'
|
|
285
|
-
},
|
|
286
|
-
hide: () => {
|
|
287
|
-
placeholder.style.display = 'none'
|
|
288
|
-
},
|
|
289
|
-
recolor: (theme) => {
|
|
290
|
-
placeholder.style.background =
|
|
291
|
-
theme.tokens['--ui-bg'] || backdropFor(theme.mode)
|
|
292
|
-
placeholder.style.color = theme.tokens['--ui-text'] || ''
|
|
293
|
-
},
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Repoint the frame at `url`, deferring the placeholder-hide until it paints so a
|
|
298
|
-
// reload of the chatbox never flashes the previous frame. A no-op reload when
|
|
299
|
-
// the URL is already loaded.
|
|
300
|
-
function createRepointer(
|
|
301
|
-
iframe: HTMLIFrameElement,
|
|
302
|
-
placeholder: PlaceholderController,
|
|
303
|
-
): (url: string) => void {
|
|
304
|
-
let currentUrl = ''
|
|
305
|
-
let loaded = false
|
|
306
|
-
iframe.addEventListener('load', () => {
|
|
307
|
-
if (iframe.src === '') return
|
|
308
|
-
loaded = true
|
|
309
|
-
placeholder.hide()
|
|
310
|
-
})
|
|
311
|
-
return (url: string): void => {
|
|
312
|
-
if (url === currentUrl && loaded) {
|
|
313
|
-
placeholder.hide()
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
currentUrl = url
|
|
317
|
-
loaded = false
|
|
318
|
-
placeholder.show('connecting')
|
|
319
|
-
iframe.src = url
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function setOpen(
|
|
324
|
-
elements: OverlayElements,
|
|
325
|
-
stateRef: OverlayPrefsRef,
|
|
326
|
-
isOpen: boolean,
|
|
327
|
-
): void {
|
|
328
|
-
const hasChanged = stateRef.value.isOpen !== isOpen
|
|
329
|
-
stateRef.value = { ...stateRef.value, isOpen }
|
|
330
|
-
applyPrefs(elements, stateRef.value)
|
|
331
|
-
writePrefsDebounced(stateRef.value)
|
|
332
|
-
if (!hasChanged) return
|
|
333
|
-
elements.container.dispatchEvent(
|
|
334
|
-
new CustomEvent(OVERLAY_OPEN_CHANGE_EVENT, { detail: isOpen }),
|
|
335
|
-
)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export function injectOverlay(): OverlayHandle | null {
|
|
339
|
-
const isAlreadyInjected = document.getElementById(OVERLAY_DOM_ID) !== null
|
|
340
|
-
if (isAlreadyInjected) return null
|
|
341
|
-
ensureSpinnerKeyframes()
|
|
342
|
-
const elements: OverlayElements = {
|
|
343
|
-
container: buildContainer(),
|
|
344
|
-
iframe: buildIframe(),
|
|
345
|
-
resizeHandle: buildResizeHandle(),
|
|
346
|
-
placeholder: buildPlaceholder(),
|
|
347
|
-
}
|
|
348
|
-
appendElements(elements)
|
|
349
|
-
const placeholder = createPlaceholderController(elements.placeholder)
|
|
350
|
-
const repoint = createRepointer(elements.iframe, placeholder)
|
|
351
|
-
const stateRef = { value: resolveInitialPrefs() }
|
|
352
|
-
applyPrefs(elements, stateRef.value)
|
|
353
|
-
installWidthResize({ elements, stateRef })
|
|
354
|
-
const wasJustClosedByOutsideClick = installOutsideClickClose({
|
|
355
|
-
elements,
|
|
356
|
-
stateRef,
|
|
357
|
-
})
|
|
358
|
-
// Mirror the host's primary palette + light/dark mode into the iframe, and
|
|
359
|
-
// keep the panel backdrop / iframe color-scheme (and the status placeholder)
|
|
360
|
-
// following live mode changes (e.g. an OS-driven dark switch) so no light edge
|
|
361
|
-
// lingers until a reload.
|
|
362
|
-
installThemeBridge(elements.iframe, (theme) => {
|
|
363
|
-
elements.container.style.background =
|
|
364
|
-
theme.tokens['--ui-bg'] || backdropFor(theme.mode)
|
|
365
|
-
elements.iframe.style.colorScheme = theme.mode
|
|
366
|
-
placeholder.recolor(theme)
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
// The chatbox header's close button lives inside the iframe; it asks the
|
|
370
|
-
// host to slide the panel away via postMessage.
|
|
371
|
-
globalThis.addEventListener('message', (event: MessageEvent) => {
|
|
372
|
-
if (!isFromFrame(event, elements.iframe)) return
|
|
373
|
-
const type = (event.data as MessageData)?.type
|
|
374
|
-
if (typeof type === 'string' && CLOSE_MESSAGE_TYPES.has(type)) {
|
|
375
|
-
setOpen(elements, stateRef, false)
|
|
376
|
-
}
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
const toggle = (): void =>
|
|
380
|
-
setOpen(elements, stateRef, !stateRef.value.isOpen)
|
|
381
|
-
|
|
382
|
-
return {
|
|
383
|
-
toggleOpen: toggle,
|
|
384
|
-
toggleFromLauncher: (): void => {
|
|
385
|
-
const wouldOpen = !stateRef.value.isOpen
|
|
386
|
-
if (wouldOpen && wasJustClosedByOutsideClick()) return
|
|
387
|
-
toggle()
|
|
388
|
-
},
|
|
389
|
-
isOpen: (): boolean => stateRef.value.isOpen,
|
|
390
|
-
onOpenChange: (listener) => {
|
|
391
|
-
const onChange = (event: Event): void =>
|
|
392
|
-
listener((event as CustomEvent<boolean>).detail)
|
|
393
|
-
elements.container.addEventListener(OVERLAY_OPEN_CHANGE_EVENT, onChange)
|
|
394
|
-
return () =>
|
|
395
|
-
elements.container.removeEventListener(OVERLAY_OPEN_CHANGE_EVENT, onChange)
|
|
396
|
-
},
|
|
397
|
-
repoint,
|
|
398
|
-
showPlaceholder: (kind) => placeholder.show(kind),
|
|
399
|
-
}
|
|
400
|
-
}
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
// Theme bridge (host → chatbox iframe). The host DMS is the source of truth for
|
|
2
|
-
// the active Nuxt UI primary palette and the light/dark mode; the chatbox iframe
|
|
3
|
-
// is a separate document, so it cannot inherit them through CSS — we forward
|
|
4
|
-
// them explicitly here. Both documents share the dashboard's origin, which is
|
|
5
|
-
// the only one either side posts to or accepts messages from.
|
|
6
|
-
//
|
|
7
|
-
// Protocol:
|
|
8
|
-
// iframe → host : { type: "dms-ai:ready" } (chatbox announces mount)
|
|
9
|
-
// host → iframe : { type: "dms-ai:theme", mode, primary }
|
|
10
|
-
// See sidecar/chatbox/src/composables/useHostTheme.ts for the receiver.
|
|
11
|
-
|
|
12
|
-
const THEME_MESSAGE_TYPE = 'dms-ai:theme'
|
|
13
|
-
const READY_MESSAGE_TYPE = 'dms-ai:ready'
|
|
14
|
-
|
|
15
|
-
const PRIMARY_STEPS = [
|
|
16
|
-
'50',
|
|
17
|
-
'100',
|
|
18
|
-
'200',
|
|
19
|
-
'300',
|
|
20
|
-
'400',
|
|
21
|
-
'500',
|
|
22
|
-
'600',
|
|
23
|
-
'700',
|
|
24
|
-
'800',
|
|
25
|
-
'900',
|
|
26
|
-
'950',
|
|
27
|
-
] as const
|
|
28
|
-
|
|
29
|
-
// Nuxt UI semantic surface/text/border tokens. The host DMS may override these
|
|
30
|
-
// with bespoke values (e.g. the DMS dark canvas is #0a0b12, not the default
|
|
31
|
-
// neutral-900), so the chatbox can't just rely on the default neutral palette —
|
|
32
|
-
// it has to mirror the host's resolved values for a faithful match in both modes.
|
|
33
|
-
const SEMANTIC_TOKENS = [
|
|
34
|
-
'--ui-bg',
|
|
35
|
-
'--ui-bg-muted',
|
|
36
|
-
'--ui-bg-elevated',
|
|
37
|
-
'--ui-bg-accented',
|
|
38
|
-
'--ui-bg-inverted',
|
|
39
|
-
'--ui-text-dimmed',
|
|
40
|
-
'--ui-text-muted',
|
|
41
|
-
'--ui-text-toned',
|
|
42
|
-
'--ui-text',
|
|
43
|
-
'--ui-text-highlighted',
|
|
44
|
-
'--ui-text-inverted',
|
|
45
|
-
'--ui-border',
|
|
46
|
-
'--ui-border-muted',
|
|
47
|
-
'--ui-border-accented',
|
|
48
|
-
'--ui-border-inverted',
|
|
49
|
-
] as const
|
|
50
|
-
|
|
51
|
-
export interface HostTheme {
|
|
52
|
-
mode: 'dark' | 'light'
|
|
53
|
-
primary: Record<string, string>
|
|
54
|
-
tokens: Record<string, string>
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function readHostMode(): 'dark' | 'light' {
|
|
58
|
-
return document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface ResolvedColors {
|
|
62
|
-
primary: Record<string, string>
|
|
63
|
-
tokens: Record<string, string>
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Resolve CSS custom properties to concrete color strings. They are authored as
|
|
67
|
-
// `var(--color-…)` chains, so we read them through a throwaway probe whose
|
|
68
|
-
// computed `color` forces full var() resolution (getComputedStyle on the custom
|
|
69
|
-
// property itself may return the unresolved `var(...)` token in some browsers).
|
|
70
|
-
// A variable the host does not define is skipped entirely: `color: var(--undef)`
|
|
71
|
-
// falls back to the *inherited* color, so the probe would otherwise report a
|
|
72
|
-
// bogus concrete value (e.g. black) instead of "missing" — leaving downstream
|
|
73
|
-
// guards/fallbacks ineffective.
|
|
74
|
-
function readResolvedColors(): ResolvedColors {
|
|
75
|
-
const rootStyle = getComputedStyle(document.documentElement)
|
|
76
|
-
const probe = document.createElement('span')
|
|
77
|
-
probe.style.cssText =
|
|
78
|
-
'position:absolute;width:0;height:0;overflow:hidden;visibility:hidden;pointer-events:none;'
|
|
79
|
-
document.body.appendChild(probe)
|
|
80
|
-
const probeStyle = getComputedStyle(probe)
|
|
81
|
-
const resolve = (name: string): string => {
|
|
82
|
-
if (rootStyle.getPropertyValue(name).trim() === '') return ''
|
|
83
|
-
probe.style.color = `var(${name})`
|
|
84
|
-
return probeStyle.color
|
|
85
|
-
}
|
|
86
|
-
const primary: Record<string, string> = {}
|
|
87
|
-
for (const step of PRIMARY_STEPS) {
|
|
88
|
-
const value = resolve(`--ui-color-primary-${step}`)
|
|
89
|
-
if (value !== '') primary[step] = value
|
|
90
|
-
}
|
|
91
|
-
const tokens: Record<string, string> = {}
|
|
92
|
-
for (const name of SEMANTIC_TOKENS) {
|
|
93
|
-
const value = resolve(name)
|
|
94
|
-
if (value !== '') tokens[name] = value
|
|
95
|
-
}
|
|
96
|
-
probe.remove()
|
|
97
|
-
return { primary, tokens }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function readHostTheme(): HostTheme {
|
|
101
|
-
const { primary, tokens } = readResolvedColors()
|
|
102
|
-
return { mode: readHostMode(), primary, tokens }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function themeSignature(theme: HostTheme): string {
|
|
106
|
-
const primary = PRIMARY_STEPS.map((s) => theme.primary[s]).join(',')
|
|
107
|
-
const tokens = SEMANTIC_TOKENS.map((t) => theme.tokens[t]).join(',')
|
|
108
|
-
return `${theme.mode}|${primary}|${tokens}`
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
interface MessageData {
|
|
112
|
-
type?: unknown
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
type ThemePush = (force: boolean) => void
|
|
116
|
-
|
|
117
|
-
/** True for a message the chatbox frame itself posted, from our own origin. */
|
|
118
|
-
export function isFromFrame(
|
|
119
|
-
event: MessageEvent,
|
|
120
|
-
iframe: HTMLIFrameElement,
|
|
121
|
-
): boolean {
|
|
122
|
-
return (
|
|
123
|
-
event.source === iframe.contentWindow &&
|
|
124
|
-
event.origin === globalThis.location.origin
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Only answer the handshake from our own iframe, never an arbitrary frame.
|
|
129
|
-
function answerReadyHandshake(
|
|
130
|
-
iframe: HTMLIFrameElement,
|
|
131
|
-
post: ThemePush,
|
|
132
|
-
): void {
|
|
133
|
-
globalThis.addEventListener('message', (event: MessageEvent) => {
|
|
134
|
-
const type = (event.data as MessageData)?.type
|
|
135
|
-
if (type === READY_MESSAGE_TYPE && isFromFrame(event, iframe)) {
|
|
136
|
-
post(true)
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// @nuxtjs/color-mode toggles `.dark` on <html>; mirror those changes live.
|
|
142
|
-
function watchHostModeChanges(post: ThemePush): void {
|
|
143
|
-
const observer = new MutationObserver(() => post(false))
|
|
144
|
-
observer.observe(document.documentElement, {
|
|
145
|
-
attributes: true,
|
|
146
|
-
attributeFilter: ['class'],
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Wire the bridge to a chatbox iframe: reply to its `ready` handshake and
|
|
151
|
-
// re-push the theme whenever the host flips light/dark. Primary/token swaps
|
|
152
|
-
// alone are not mirrored live — they only reach the iframe on the forced
|
|
153
|
-
// pushes (`ready` handshake and iframe `load`).
|
|
154
|
-
// `onTheme` fires on every (re)push so the caller can keep host-side chrome
|
|
155
|
-
// (the overlay backdrop / iframe color-scheme) in sync with live mode changes —
|
|
156
|
-
// without it, an OS-driven dark switch leaves the panel edge on its old shade
|
|
157
|
-
// until the page is reloaded.
|
|
158
|
-
export function installThemeBridge(
|
|
159
|
-
iframe: HTMLIFrameElement,
|
|
160
|
-
onTheme?: (theme: HostTheme) => void,
|
|
161
|
-
): void {
|
|
162
|
-
let lastMode: 'dark' | 'light' | '' = ''
|
|
163
|
-
let lastSignature = ''
|
|
164
|
-
|
|
165
|
-
const post: ThemePush = (force) => {
|
|
166
|
-
// The observer fires on any <html> class mutation (scroll locks, modal or
|
|
167
|
-
// route classes…). Gate the expensive probe resolution behind a cheap mode
|
|
168
|
-
// check: only a light/dark flip changes what this bridge mirrors live, and
|
|
169
|
-
// primary/token swaps are re-pushed on the `force` paths (ready/load).
|
|
170
|
-
const mode = readHostMode()
|
|
171
|
-
if (!force && mode === lastMode) return
|
|
172
|
-
lastMode = mode
|
|
173
|
-
const theme = readHostTheme()
|
|
174
|
-
const signature = themeSignature(theme)
|
|
175
|
-
if (!force && signature === lastSignature) return
|
|
176
|
-
lastSignature = signature
|
|
177
|
-
onTheme?.(theme)
|
|
178
|
-
iframe.contentWindow?.postMessage(
|
|
179
|
-
{ type: THEME_MESSAGE_TYPE, ...theme },
|
|
180
|
-
globalThis.location.origin,
|
|
181
|
-
)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
answerReadyHandshake(iframe, post)
|
|
185
|
-
watchHostModeChanges(post)
|
|
186
|
-
// Belt and suspenders for the case where the iframe loaded before its
|
|
187
|
-
// message listener was ready and the `ready` handshake was missed.
|
|
188
|
-
iframe.addEventListener('load', () => post(true))
|
|
189
|
-
}
|