@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.
Files changed (244) hide show
  1. package/README.md +48 -29
  2. package/dist/channels/bridge.d.ts +2 -8
  3. package/dist/channels/bridge.js +26 -42
  4. package/dist/channels/bridge.js.map +1 -1
  5. package/dist/channels/channel-service.d.ts +2 -2
  6. package/dist/channels/channel-service.js +12 -31
  7. package/dist/channels/channel-service.js.map +1 -1
  8. package/dist/channels/registry.d.ts +2 -2
  9. package/dist/channels/registry.js +7 -7
  10. package/dist/channels/registry.js.map +1 -1
  11. package/dist/config.d.ts +1 -0
  12. package/dist/config.js +8 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/constants/channels.d.ts +5 -10
  15. package/dist/constants/channels.js +6 -10
  16. package/dist/constants/channels.js.map +1 -1
  17. package/dist/constants/sidecar.d.ts +2 -1
  18. package/dist/constants/sidecar.js +3 -2
  19. package/dist/constants/sidecar.js.map +1 -1
  20. package/dist/index.js +8 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/lifecycle/build-id.d.ts +1 -0
  23. package/dist/lifecycle/build-id.js +32 -0
  24. package/dist/lifecycle/build-id.js.map +1 -0
  25. package/dist/lifecycle/sidecar-socket.d.ts +1 -1
  26. package/dist/lifecycle/sidecar-socket.js +2 -2
  27. package/dist/lifecycle/sidecar-socket.js.map +1 -1
  28. package/dist/lifecycle/spawn-sidecar.d.ts +0 -1
  29. package/dist/lifecycle/spawn-sidecar.js +5 -27
  30. package/dist/lifecycle/spawn-sidecar.js.map +1 -1
  31. package/dist/routes/channels.d.ts +3 -3
  32. package/dist/routes/channels.js +11 -12
  33. package/dist/routes/channels.js.map +1 -1
  34. package/dist/routes/index.d.ts +0 -1
  35. package/dist/routes/index.js +0 -1
  36. package/dist/routes/index.js.map +1 -1
  37. package/frontend-vue/.prettierignore +3 -0
  38. package/frontend-vue/.prettierrc +10 -0
  39. package/frontend-vue/app/chat/components/ChatView.vue +582 -0
  40. package/frontend-vue/app/chat/components/ComposerInput.vue +742 -0
  41. package/frontend-vue/app/chat/components/ConnectionBanner.vue +105 -0
  42. package/frontend-vue/app/chat/components/ConversationDrawer.vue +266 -0
  43. package/frontend-vue/app/chat/components/MarkdownContent.vue +196 -0
  44. package/frontend-vue/app/chat/components/MessageList.vue +474 -0
  45. package/frontend-vue/app/chat/components/PermissionRequests.vue +323 -0
  46. package/frontend-vue/app/chat/components/QuestionPrompt.vue +275 -0
  47. package/frontend-vue/app/chat/components/QueuedMessages.vue +109 -0
  48. package/frontend-vue/app/chat/components/TodoList.vue +291 -0
  49. package/frontend-vue/app/chat/components/ToolCallEntry.vue +218 -0
  50. package/frontend-vue/app/chat/components/ToolCluster.vue +282 -0
  51. package/frontend-vue/app/chat/composables/useChatChannel.ts +116 -0
  52. package/frontend-vue/app/chat/composables/useConversation.ts +838 -0
  53. package/frontend-vue/app/chat/composables/useConversationId.ts +34 -0
  54. package/frontend-vue/app/chat/composables/useConversationList.ts +56 -0
  55. package/frontend-vue/app/chat/composables/usePermissionQueue.ts +59 -0
  56. package/frontend-vue/app/chat/composables/useQuestionQueue.ts +50 -0
  57. package/frontend-vue/app/chat/composables/useRunClock.ts +28 -0
  58. package/frontend-vue/app/chat/composables/useSettings.ts +67 -0
  59. package/frontend-vue/app/chat/constants/attachments.ts +16 -0
  60. package/frontend-vue/app/chat/constants/connection-banner.ts +7 -0
  61. package/frontend-vue/app/chat/constants/conversation-drawer.ts +13 -0
  62. package/frontend-vue/app/chat/constants/conversation.ts +75 -0
  63. package/frontend-vue/app/chat/constants/permissions.ts +50 -0
  64. package/frontend-vue/app/chat/constants/protocol.ts +48 -0
  65. package/frontend-vue/app/chat/constants/run-status.ts +50 -0
  66. package/frontend-vue/app/chat/constants/settings.ts +60 -0
  67. package/frontend-vue/app/chat/types/conversation.ts +87 -0
  68. package/frontend-vue/app/chat/types/permission.ts +7 -0
  69. package/frontend-vue/app/chat/types/question.ts +17 -0
  70. package/frontend-vue/app/chat/types/settings.ts +27 -0
  71. package/frontend-vue/app/chat/utils/attachments.ts +62 -0
  72. package/frontend-vue/app/chat/utils/format.ts +14 -0
  73. package/frontend-vue/app/chat/utils/ids.ts +34 -0
  74. package/frontend-vue/app/chat/utils/markdown.ts +188 -0
  75. package/frontend-vue/app/chat/utils/run-status.ts +90 -0
  76. package/frontend-vue/app/chat/utils/todos.ts +64 -0
  77. package/frontend-vue/app/chat/utils/tool-summary.ts +53 -0
  78. package/frontend-vue/app/components/ActivityView.vue +34 -34
  79. package/frontend-vue/app/components/ChatPanel.vue +276 -0
  80. package/frontend-vue/app/components/SettingsView.vue +120 -112
  81. package/frontend-vue/app/components/SkillsView.vue +55 -53
  82. package/frontend-vue/app/plugins/ai.client.ts +145 -129
  83. package/frontend-vue/app/runtime/assistant-session.ts +21 -0
  84. package/frontend-vue/app/runtime/backoff.ts +11 -14
  85. package/frontend-vue/app/runtime/channel-client.ts +130 -141
  86. package/frontend-vue/app/runtime/chat-transport.ts +25 -55
  87. package/frontend-vue/app/runtime/constants.ts +69 -59
  88. package/frontend-vue/app/runtime/current-page.ts +49 -15
  89. package/frontend-vue/app/runtime/event-stream.ts +35 -35
  90. package/frontend-vue/app/runtime/frame-router.ts +30 -0
  91. package/frontend-vue/app/runtime/header-action.ts +18 -18
  92. package/frontend-vue/app/runtime/host-commands.ts +57 -56
  93. package/frontend-vue/app/runtime/listeners.ts +31 -0
  94. package/frontend-vue/app/runtime/navigation-complete.ts +7 -7
  95. package/frontend-vue/app/runtime/overlay-prefs.ts +36 -36
  96. package/frontend-vue/app/runtime/panel-state.ts +88 -0
  97. package/frontend-vue/app/runtime/session-gate.ts +8 -8
  98. package/frontend-vue/app/runtime/shortcuts.ts +12 -12
  99. package/frontend-vue/app/runtime/sidecar-status.ts +61 -61
  100. package/frontend-vue/app/runtime/typed-message.ts +11 -0
  101. package/frontend-vue/dms.frontend.ts +12 -12
  102. package/frontend-vue/eslint.config.mjs +13 -13
  103. package/frontend-vue/package.json +11 -4
  104. package/frontend-vue/pnpm-lock.yaml +298 -96
  105. package/frontend-vue/tests/backoff.test.ts +26 -30
  106. package/frontend-vue/tests/channel-client.test.ts +187 -188
  107. package/frontend-vue/tests/chat/markdown.test.ts +271 -0
  108. package/frontend-vue/tests/chat/useChatChannel.test.ts +124 -0
  109. package/frontend-vue/tests/chat/useConversation.test.ts +283 -0
  110. package/frontend-vue/tests/chat-panel.test.ts +354 -0
  111. package/frontend-vue/tests/chat-transport.test.ts +84 -57
  112. package/frontend-vue/tests/host-commands.test.ts +126 -87
  113. package/frontend-vue/tests/navigation.test.ts +65 -34
  114. package/frontend-vue/tests/overlay-prefs.test.ts +49 -49
  115. package/frontend-vue/tests/panel-state.test.ts +58 -0
  116. package/frontend-vue/tests/session-gate.test.ts +33 -33
  117. package/frontend-vue/tests/sidecar-guest.test.ts +46 -46
  118. package/frontend-vue/tests/sidecar-status.test.ts +29 -29
  119. package/frontend-vue/tsconfig.chat.json +16 -0
  120. package/frontend-vue/vitest.config.ts +6 -4
  121. package/package.json +5 -5
  122. package/sidecar/dist/agent/effective-mode.d.ts +2 -2
  123. package/sidecar/dist/agent/effective-mode.js +1 -1
  124. package/sidecar/dist/agent/effective-mode.js.map +1 -1
  125. package/sidecar/dist/agent/host-context.js +7 -9
  126. package/sidecar/dist/agent/host-context.js.map +1 -1
  127. package/sidecar/dist/agent/permission-bus.d.ts +1 -1
  128. package/sidecar/dist/agent/permission-bus.js +2 -2
  129. package/sidecar/dist/agent/permission-bus.js.map +1 -1
  130. package/sidecar/dist/agent/provider.d.ts +2 -2
  131. package/sidecar/dist/agent/question-bus.d.ts +1 -1
  132. package/sidecar/dist/agent/question-bus.js +2 -2
  133. package/sidecar/dist/agent/question-bus.js.map +1 -1
  134. package/sidecar/dist/agent/runner.d.ts +2 -2
  135. package/sidecar/dist/agent/runner.js +24 -8
  136. package/sidecar/dist/agent/runner.js.map +1 -1
  137. package/sidecar/dist/agent/session.d.ts +2 -2
  138. package/sidecar/dist/agent/session.js +49 -9
  139. package/sidecar/dist/agent/session.js.map +1 -1
  140. package/sidecar/dist/agent/switching-runner.js +10 -5
  141. package/sidecar/dist/agent/switching-runner.js.map +1 -1
  142. package/sidecar/dist/agent/turn-end-reasons.d.ts +2 -0
  143. package/sidecar/dist/agent/turn-end-reasons.js +6 -1
  144. package/sidecar/dist/agent/turn-end-reasons.js.map +1 -1
  145. package/sidecar/dist/constants/agent.d.ts +4 -1
  146. package/sidecar/dist/constants/agent.js +4 -1
  147. package/sidecar/dist/constants/agent.js.map +1 -1
  148. package/sidecar/dist/constants/cli.d.ts +0 -1
  149. package/sidecar/dist/constants/cli.js +0 -1
  150. package/sidecar/dist/constants/cli.js.map +1 -1
  151. package/sidecar/dist/constants/codex.d.ts +5 -2
  152. package/sidecar/dist/constants/codex.js +5 -2
  153. package/sidecar/dist/constants/codex.js.map +1 -1
  154. package/sidecar/dist/constants/http.d.ts +0 -9
  155. package/sidecar/dist/constants/http.js +0 -19
  156. package/sidecar/dist/constants/http.js.map +1 -1
  157. package/sidecar/dist/constants/mcp.d.ts +1 -1
  158. package/sidecar/dist/constants/mcp.js +1 -1
  159. package/sidecar/dist/constants/mcp.js.map +1 -1
  160. package/sidecar/dist/constants/paths.d.ts +0 -1
  161. package/sidecar/dist/constants/paths.js +0 -1
  162. package/sidecar/dist/constants/paths.js.map +1 -1
  163. package/sidecar/dist/constants/ws.d.ts +3 -4
  164. package/sidecar/dist/constants/ws.js +3 -4
  165. package/sidecar/dist/constants/ws.js.map +1 -1
  166. package/sidecar/dist/index.d.ts +0 -1
  167. package/sidecar/dist/index.js +6 -10
  168. package/sidecar/dist/index.js.map +1 -1
  169. package/sidecar/dist/mcp/tools/find-pages-using.js +5 -10
  170. package/sidecar/dist/mcp/tools/find-pages-using.js.map +1 -1
  171. package/sidecar/dist/mcp/types.d.ts +1 -1
  172. package/sidecar/dist/pages/page-filepath.d.ts +4 -0
  173. package/sidecar/dist/pages/page-filepath.js +28 -0
  174. package/sidecar/dist/pages/page-filepath.js.map +1 -0
  175. package/sidecar/dist/prompts/agent.d.ts +1 -1
  176. package/sidecar/dist/prompts/agent.js +1 -1
  177. package/sidecar/dist/prompts/agent.js.map +1 -1
  178. package/sidecar/dist/protocol/events.js +2 -2
  179. package/sidecar/dist/protocol/events.js.map +1 -1
  180. package/sidecar/dist/protocol/messages.d.ts +4 -15
  181. package/sidecar/dist/protocol/messages.js +4 -11
  182. package/sidecar/dist/protocol/messages.js.map +1 -1
  183. package/sidecar/dist/providers/claude/config.d.ts +2 -2
  184. package/sidecar/dist/providers/claude/config.js +2 -2
  185. package/sidecar/dist/providers/claude/config.js.map +1 -1
  186. package/sidecar/dist/providers/claude/provider.js +1 -1
  187. package/sidecar/dist/providers/claude/provider.js.map +1 -1
  188. package/sidecar/dist/providers/codex/config.d.ts +3 -2
  189. package/sidecar/dist/providers/codex/config.js +3 -3
  190. package/sidecar/dist/providers/codex/config.js.map +1 -1
  191. package/sidecar/dist/providers/codex/process.js +107 -74
  192. package/sidecar/dist/providers/codex/process.js.map +1 -1
  193. package/sidecar/dist/providers/codex/provider.js +68 -31
  194. package/sidecar/dist/providers/codex/provider.js.map +1 -1
  195. package/sidecar/dist/providers/codex/skills.d.ts +1 -0
  196. package/sidecar/dist/providers/codex/skills.js +4 -1
  197. package/sidecar/dist/providers/codex/skills.js.map +1 -1
  198. package/sidecar/dist/providers/codex/stop.d.ts +4 -0
  199. package/sidecar/dist/providers/codex/stop.js +71 -0
  200. package/sidecar/dist/providers/codex/stop.js.map +1 -0
  201. package/sidecar/dist/server/{iframe-socket-registry.d.ts → chat-socket-registry.d.ts} +3 -2
  202. package/sidecar/dist/server/{iframe-socket-registry.js → chat-socket-registry.js} +4 -3
  203. package/sidecar/dist/server/chat-socket-registry.js.map +1 -0
  204. package/sidecar/dist/server/client-auth.js +2 -14
  205. package/sidecar/dist/server/client-auth.js.map +1 -1
  206. package/sidecar/dist/server/host-command-router.d.ts +5 -0
  207. package/sidecar/dist/server/host-command-router.js +4 -0
  208. package/sidecar/dist/server/host-command-router.js.map +1 -0
  209. package/sidecar/dist/server/host-socket-registry.d.ts +1 -1
  210. package/sidecar/dist/server/host-socket-registry.js +16 -13
  211. package/sidecar/dist/server/host-socket-registry.js.map +1 -1
  212. package/sidecar/dist/server/http.d.ts +0 -1
  213. package/sidecar/dist/server/http.js +1 -44
  214. package/sidecar/dist/server/http.js.map +1 -1
  215. package/sidecar/dist/server/routing.d.ts +5 -6
  216. package/sidecar/dist/server/routing.js +18 -28
  217. package/sidecar/dist/server/routing.js.map +1 -1
  218. package/sidecar/dist/server/ws.d.ts +2 -2
  219. package/sidecar/dist/server/ws.js +41 -39
  220. package/sidecar/dist/server/ws.js.map +1 -1
  221. package/sidecar/dist/state/host-state.d.ts +10 -2
  222. package/sidecar/dist/state/host-state.js +13 -3
  223. package/sidecar/dist/state/host-state.js.map +1 -1
  224. package/sidecar/dist/state/settings-store.js +2 -2
  225. package/sidecar/dist/state/settings-store.js.map +1 -1
  226. package/sidecar/dist/state/settings-types.d.ts +3 -3
  227. package/sidecar/dist/state/settings-types.js +1 -1
  228. package/sidecar/dist/state/settings-types.js.map +1 -1
  229. package/sidecar/package.json +3 -5
  230. package/dist/chatbox/passthrough.d.ts +0 -3
  231. package/dist/chatbox/passthrough.js +0 -53
  232. package/dist/chatbox/passthrough.js.map +0 -1
  233. package/dist/constants/chatbox.d.ts +0 -10
  234. package/dist/constants/chatbox.js +0 -35
  235. package/dist/constants/chatbox.js.map +0 -1
  236. package/dist/routes/chatbox.d.ts +0 -7
  237. package/dist/routes/chatbox.js +0 -47
  238. package/dist/routes/chatbox.js.map +0 -1
  239. package/frontend-vue/app/runtime/overlay.ts +0 -400
  240. package/frontend-vue/app/runtime/theme-bridge.ts +0 -189
  241. package/sidecar/chatbox/dist/assets/index-C8u7mkHn.css +0 -1
  242. package/sidecar/chatbox/dist/assets/index-tdZvY7ee.js +0 -52
  243. package/sidecar/chatbox/dist/index.html +0 -13
  244. package/sidecar/dist/server/iframe-socket-registry.js.map +0 -1
@@ -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
- }