@antelopejs/dms-ai 0.1.3 → 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 (245) 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/pages/settings/page.js.map +1 -1
  32. package/dist/routes/channels.d.ts +3 -3
  33. package/dist/routes/channels.js +11 -12
  34. package/dist/routes/channels.js.map +1 -1
  35. package/dist/routes/index.d.ts +0 -1
  36. package/dist/routes/index.js +0 -1
  37. package/dist/routes/index.js.map +1 -1
  38. package/frontend-vue/.prettierignore +3 -0
  39. package/frontend-vue/.prettierrc +10 -0
  40. package/frontend-vue/app/chat/components/ChatView.vue +582 -0
  41. package/frontend-vue/app/chat/components/ComposerInput.vue +742 -0
  42. package/frontend-vue/app/chat/components/ConnectionBanner.vue +105 -0
  43. package/frontend-vue/app/chat/components/ConversationDrawer.vue +266 -0
  44. package/frontend-vue/app/chat/components/MarkdownContent.vue +196 -0
  45. package/frontend-vue/app/chat/components/MessageList.vue +474 -0
  46. package/frontend-vue/app/chat/components/PermissionRequests.vue +323 -0
  47. package/frontend-vue/app/chat/components/QuestionPrompt.vue +275 -0
  48. package/frontend-vue/app/chat/components/QueuedMessages.vue +109 -0
  49. package/frontend-vue/app/chat/components/TodoList.vue +291 -0
  50. package/frontend-vue/app/chat/components/ToolCallEntry.vue +218 -0
  51. package/frontend-vue/app/chat/components/ToolCluster.vue +282 -0
  52. package/frontend-vue/app/chat/composables/useChatChannel.ts +116 -0
  53. package/frontend-vue/app/chat/composables/useConversation.ts +838 -0
  54. package/frontend-vue/app/chat/composables/useConversationId.ts +34 -0
  55. package/frontend-vue/app/chat/composables/useConversationList.ts +56 -0
  56. package/frontend-vue/app/chat/composables/usePermissionQueue.ts +59 -0
  57. package/frontend-vue/app/chat/composables/useQuestionQueue.ts +50 -0
  58. package/frontend-vue/app/chat/composables/useRunClock.ts +28 -0
  59. package/frontend-vue/app/chat/composables/useSettings.ts +67 -0
  60. package/frontend-vue/app/chat/constants/attachments.ts +16 -0
  61. package/frontend-vue/app/chat/constants/connection-banner.ts +7 -0
  62. package/frontend-vue/app/chat/constants/conversation-drawer.ts +13 -0
  63. package/frontend-vue/app/chat/constants/conversation.ts +75 -0
  64. package/frontend-vue/app/chat/constants/permissions.ts +50 -0
  65. package/frontend-vue/app/chat/constants/protocol.ts +48 -0
  66. package/frontend-vue/app/chat/constants/run-status.ts +50 -0
  67. package/frontend-vue/app/chat/constants/settings.ts +60 -0
  68. package/frontend-vue/app/chat/types/conversation.ts +87 -0
  69. package/frontend-vue/app/chat/types/permission.ts +7 -0
  70. package/frontend-vue/app/chat/types/question.ts +17 -0
  71. package/frontend-vue/app/chat/types/settings.ts +27 -0
  72. package/frontend-vue/app/chat/utils/attachments.ts +62 -0
  73. package/frontend-vue/app/chat/utils/format.ts +14 -0
  74. package/frontend-vue/app/chat/utils/ids.ts +34 -0
  75. package/frontend-vue/app/chat/utils/markdown.ts +188 -0
  76. package/frontend-vue/app/chat/utils/run-status.ts +90 -0
  77. package/frontend-vue/app/chat/utils/todos.ts +64 -0
  78. package/frontend-vue/app/chat/utils/tool-summary.ts +53 -0
  79. package/frontend-vue/app/components/ActivityView.vue +34 -34
  80. package/frontend-vue/app/components/ChatPanel.vue +276 -0
  81. package/frontend-vue/app/components/SettingsView.vue +127 -130
  82. package/frontend-vue/app/components/SkillsView.vue +55 -53
  83. package/frontend-vue/app/plugins/ai.client.ts +145 -129
  84. package/frontend-vue/app/runtime/assistant-session.ts +21 -0
  85. package/frontend-vue/app/runtime/backoff.ts +11 -14
  86. package/frontend-vue/app/runtime/channel-client.ts +130 -141
  87. package/frontend-vue/app/runtime/chat-transport.ts +25 -55
  88. package/frontend-vue/app/runtime/constants.ts +70 -60
  89. package/frontend-vue/app/runtime/current-page.ts +49 -15
  90. package/frontend-vue/app/runtime/event-stream.ts +35 -35
  91. package/frontend-vue/app/runtime/frame-router.ts +30 -0
  92. package/frontend-vue/app/runtime/header-action.ts +18 -18
  93. package/frontend-vue/app/runtime/host-commands.ts +57 -56
  94. package/frontend-vue/app/runtime/listeners.ts +31 -0
  95. package/frontend-vue/app/runtime/navigation-complete.ts +7 -7
  96. package/frontend-vue/app/runtime/overlay-prefs.ts +36 -36
  97. package/frontend-vue/app/runtime/panel-state.ts +88 -0
  98. package/frontend-vue/app/runtime/session-gate.ts +8 -8
  99. package/frontend-vue/app/runtime/shortcuts.ts +12 -12
  100. package/frontend-vue/app/runtime/sidecar-status.ts +61 -61
  101. package/frontend-vue/app/runtime/typed-message.ts +11 -0
  102. package/frontend-vue/dms.frontend.ts +12 -12
  103. package/frontend-vue/eslint.config.mjs +13 -13
  104. package/frontend-vue/package.json +11 -4
  105. package/frontend-vue/pnpm-lock.yaml +298 -96
  106. package/frontend-vue/tests/backoff.test.ts +26 -30
  107. package/frontend-vue/tests/channel-client.test.ts +187 -188
  108. package/frontend-vue/tests/chat/markdown.test.ts +271 -0
  109. package/frontend-vue/tests/chat/useChatChannel.test.ts +124 -0
  110. package/frontend-vue/tests/chat/useConversation.test.ts +283 -0
  111. package/frontend-vue/tests/chat-panel.test.ts +354 -0
  112. package/frontend-vue/tests/chat-transport.test.ts +84 -57
  113. package/frontend-vue/tests/host-commands.test.ts +126 -87
  114. package/frontend-vue/tests/navigation.test.ts +65 -34
  115. package/frontend-vue/tests/overlay-prefs.test.ts +49 -49
  116. package/frontend-vue/tests/panel-state.test.ts +58 -0
  117. package/frontend-vue/tests/session-gate.test.ts +33 -33
  118. package/frontend-vue/tests/sidecar-guest.test.ts +46 -46
  119. package/frontend-vue/tests/sidecar-status.test.ts +29 -29
  120. package/frontend-vue/tsconfig.chat.json +16 -0
  121. package/frontend-vue/vitest.config.ts +6 -4
  122. package/package.json +5 -5
  123. package/sidecar/dist/agent/effective-mode.d.ts +2 -2
  124. package/sidecar/dist/agent/effective-mode.js +1 -1
  125. package/sidecar/dist/agent/effective-mode.js.map +1 -1
  126. package/sidecar/dist/agent/host-context.js +7 -9
  127. package/sidecar/dist/agent/host-context.js.map +1 -1
  128. package/sidecar/dist/agent/permission-bus.d.ts +1 -1
  129. package/sidecar/dist/agent/permission-bus.js +2 -2
  130. package/sidecar/dist/agent/permission-bus.js.map +1 -1
  131. package/sidecar/dist/agent/provider.d.ts +2 -2
  132. package/sidecar/dist/agent/question-bus.d.ts +1 -1
  133. package/sidecar/dist/agent/question-bus.js +2 -2
  134. package/sidecar/dist/agent/question-bus.js.map +1 -1
  135. package/sidecar/dist/agent/runner.d.ts +2 -2
  136. package/sidecar/dist/agent/runner.js +24 -8
  137. package/sidecar/dist/agent/runner.js.map +1 -1
  138. package/sidecar/dist/agent/session.d.ts +2 -2
  139. package/sidecar/dist/agent/session.js +49 -9
  140. package/sidecar/dist/agent/session.js.map +1 -1
  141. package/sidecar/dist/agent/switching-runner.js +10 -5
  142. package/sidecar/dist/agent/switching-runner.js.map +1 -1
  143. package/sidecar/dist/agent/turn-end-reasons.d.ts +2 -0
  144. package/sidecar/dist/agent/turn-end-reasons.js +6 -1
  145. package/sidecar/dist/agent/turn-end-reasons.js.map +1 -1
  146. package/sidecar/dist/constants/agent.d.ts +4 -1
  147. package/sidecar/dist/constants/agent.js +4 -1
  148. package/sidecar/dist/constants/agent.js.map +1 -1
  149. package/sidecar/dist/constants/cli.d.ts +0 -1
  150. package/sidecar/dist/constants/cli.js +0 -1
  151. package/sidecar/dist/constants/cli.js.map +1 -1
  152. package/sidecar/dist/constants/codex.d.ts +5 -2
  153. package/sidecar/dist/constants/codex.js +5 -2
  154. package/sidecar/dist/constants/codex.js.map +1 -1
  155. package/sidecar/dist/constants/http.d.ts +0 -9
  156. package/sidecar/dist/constants/http.js +0 -19
  157. package/sidecar/dist/constants/http.js.map +1 -1
  158. package/sidecar/dist/constants/mcp.d.ts +1 -1
  159. package/sidecar/dist/constants/mcp.js +1 -1
  160. package/sidecar/dist/constants/mcp.js.map +1 -1
  161. package/sidecar/dist/constants/paths.d.ts +0 -1
  162. package/sidecar/dist/constants/paths.js +0 -1
  163. package/sidecar/dist/constants/paths.js.map +1 -1
  164. package/sidecar/dist/constants/ws.d.ts +3 -4
  165. package/sidecar/dist/constants/ws.js +3 -4
  166. package/sidecar/dist/constants/ws.js.map +1 -1
  167. package/sidecar/dist/index.d.ts +0 -1
  168. package/sidecar/dist/index.js +6 -10
  169. package/sidecar/dist/index.js.map +1 -1
  170. package/sidecar/dist/mcp/tools/find-pages-using.js +5 -10
  171. package/sidecar/dist/mcp/tools/find-pages-using.js.map +1 -1
  172. package/sidecar/dist/mcp/types.d.ts +1 -1
  173. package/sidecar/dist/pages/page-filepath.d.ts +4 -0
  174. package/sidecar/dist/pages/page-filepath.js +28 -0
  175. package/sidecar/dist/pages/page-filepath.js.map +1 -0
  176. package/sidecar/dist/prompts/agent.d.ts +1 -1
  177. package/sidecar/dist/prompts/agent.js +1 -1
  178. package/sidecar/dist/prompts/agent.js.map +1 -1
  179. package/sidecar/dist/protocol/events.js +2 -2
  180. package/sidecar/dist/protocol/events.js.map +1 -1
  181. package/sidecar/dist/protocol/messages.d.ts +4 -15
  182. package/sidecar/dist/protocol/messages.js +4 -11
  183. package/sidecar/dist/protocol/messages.js.map +1 -1
  184. package/sidecar/dist/providers/claude/config.d.ts +2 -2
  185. package/sidecar/dist/providers/claude/config.js +2 -2
  186. package/sidecar/dist/providers/claude/config.js.map +1 -1
  187. package/sidecar/dist/providers/claude/provider.js +1 -1
  188. package/sidecar/dist/providers/claude/provider.js.map +1 -1
  189. package/sidecar/dist/providers/codex/config.d.ts +3 -2
  190. package/sidecar/dist/providers/codex/config.js +3 -3
  191. package/sidecar/dist/providers/codex/config.js.map +1 -1
  192. package/sidecar/dist/providers/codex/process.js +107 -74
  193. package/sidecar/dist/providers/codex/process.js.map +1 -1
  194. package/sidecar/dist/providers/codex/provider.js +68 -31
  195. package/sidecar/dist/providers/codex/provider.js.map +1 -1
  196. package/sidecar/dist/providers/codex/skills.d.ts +1 -0
  197. package/sidecar/dist/providers/codex/skills.js +4 -1
  198. package/sidecar/dist/providers/codex/skills.js.map +1 -1
  199. package/sidecar/dist/providers/codex/stop.d.ts +4 -0
  200. package/sidecar/dist/providers/codex/stop.js +71 -0
  201. package/sidecar/dist/providers/codex/stop.js.map +1 -0
  202. package/sidecar/dist/server/{iframe-socket-registry.d.ts → chat-socket-registry.d.ts} +3 -2
  203. package/sidecar/dist/server/{iframe-socket-registry.js → chat-socket-registry.js} +4 -3
  204. package/sidecar/dist/server/chat-socket-registry.js.map +1 -0
  205. package/sidecar/dist/server/client-auth.js +2 -14
  206. package/sidecar/dist/server/client-auth.js.map +1 -1
  207. package/sidecar/dist/server/host-command-router.d.ts +5 -0
  208. package/sidecar/dist/server/host-command-router.js +4 -0
  209. package/sidecar/dist/server/host-command-router.js.map +1 -0
  210. package/sidecar/dist/server/host-socket-registry.d.ts +1 -1
  211. package/sidecar/dist/server/host-socket-registry.js +16 -13
  212. package/sidecar/dist/server/host-socket-registry.js.map +1 -1
  213. package/sidecar/dist/server/http.d.ts +0 -1
  214. package/sidecar/dist/server/http.js +1 -44
  215. package/sidecar/dist/server/http.js.map +1 -1
  216. package/sidecar/dist/server/routing.d.ts +5 -6
  217. package/sidecar/dist/server/routing.js +18 -28
  218. package/sidecar/dist/server/routing.js.map +1 -1
  219. package/sidecar/dist/server/ws.d.ts +2 -2
  220. package/sidecar/dist/server/ws.js +41 -39
  221. package/sidecar/dist/server/ws.js.map +1 -1
  222. package/sidecar/dist/state/host-state.d.ts +10 -2
  223. package/sidecar/dist/state/host-state.js +13 -3
  224. package/sidecar/dist/state/host-state.js.map +1 -1
  225. package/sidecar/dist/state/settings-store.js +2 -2
  226. package/sidecar/dist/state/settings-store.js.map +1 -1
  227. package/sidecar/dist/state/settings-types.d.ts +3 -3
  228. package/sidecar/dist/state/settings-types.js +1 -1
  229. package/sidecar/dist/state/settings-types.js.map +1 -1
  230. package/sidecar/package.json +3 -5
  231. package/dist/chatbox/passthrough.d.ts +0 -3
  232. package/dist/chatbox/passthrough.js +0 -53
  233. package/dist/chatbox/passthrough.js.map +0 -1
  234. package/dist/constants/chatbox.d.ts +0 -10
  235. package/dist/constants/chatbox.js +0 -35
  236. package/dist/constants/chatbox.js.map +0 -1
  237. package/dist/routes/chatbox.d.ts +0 -7
  238. package/dist/routes/chatbox.js +0 -47
  239. package/dist/routes/chatbox.js.map +0 -1
  240. package/frontend-vue/app/runtime/overlay.ts +0 -400
  241. package/frontend-vue/app/runtime/theme-bridge.ts +0 -189
  242. package/sidecar/chatbox/dist/assets/index-C8u7mkHn.css +0 -1
  243. package/sidecar/chatbox/dist/assets/index-tdZvY7ee.js +0 -52
  244. package/sidecar/chatbox/dist/index.html +0 -13
  245. package/sidecar/dist/server/iframe-socket-registry.js.map +0 -1
@@ -1,32 +1,66 @@
1
- import { router } from '@inertiajs/vue3'
2
- import { HOST_STATE_UPDATE_TYPE } from './constants'
1
+ import { router } from "@inertiajs/vue3";
2
+ import { HOST_STATE_UPDATE_TYPE } from "./constants";
3
3
 
4
4
  interface CurrentPagePayload {
5
- path: string
5
+ path: string;
6
+ title?: string;
6
7
  }
7
8
 
8
9
  interface HostStateUpdateMessage {
9
- type: typeof HOST_STATE_UPDATE_TYPE
10
- currentPage: CurrentPagePayload
10
+ type: typeof HOST_STATE_UPDATE_TYPE;
11
+ currentPage: CurrentPagePayload;
11
12
  }
12
13
 
13
14
  interface InstallCurrentPageTrackerOptions {
14
- send: (msg: HostStateUpdateMessage) => void
15
+ send: (msg: HostStateUpdateMessage) => void;
15
16
  }
16
17
 
17
- /** The state update announcing the page at `url`. */
18
+ const TITLE_OBSERVER_OPTIONS: MutationObserverInit = {
19
+ subtree: true,
20
+ childList: true,
21
+ characterData: true,
22
+ };
23
+
24
+ function readTitle(): string | undefined {
25
+ const title = document.title.trim();
26
+ return title === "" ? undefined : title;
27
+ }
28
+
29
+ function buildCurrentPage(url: string): CurrentPagePayload {
30
+ const path = new URL(url, window.location.origin).pathname;
31
+ const title = readTitle();
32
+ return title === undefined ? { path } : { path, title };
33
+ }
34
+
35
+ /** The state update announcing the page at `url`, titled after the document. */
18
36
  export function buildCurrentPageUpdate(url: string): HostStateUpdateMessage {
19
- return {
20
- type: HOST_STATE_UPDATE_TYPE,
21
- currentPage: { path: new URL(url, window.location.origin).pathname },
22
- }
37
+ return { type: HOST_STATE_UPDATE_TYPE, currentPage: buildCurrentPage(url) };
23
38
  }
24
39
 
40
+ /**
41
+ * Reports the displayed page on every Inertia navigation, and again whenever
42
+ * the document title changes after the page has rendered.
43
+ */
25
44
  export function installCurrentPageTracker(
26
45
  options: InstallCurrentPageTrackerOptions,
27
46
  ): () => void {
28
- const pushFor = (url: string): void => options.send(buildCurrentPageUpdate(url))
29
- const stop = router.on('navigate', (event) => pushFor(event.detail.page.url))
30
- pushFor(window.location.href)
31
- return stop
47
+ let currentUrl = window.location.href;
48
+ let sentTitle = readTitle();
49
+ const push = (): void => {
50
+ sentTitle = readTitle();
51
+ options.send(buildCurrentPageUpdate(currentUrl));
52
+ };
53
+ const stopNavigation = router.on("navigate", (event) => {
54
+ currentUrl = event.detail.page.url;
55
+ push();
56
+ });
57
+ const titleObserver = new MutationObserver(() => {
58
+ if (readTitle() !== sentTitle) push();
59
+ });
60
+ titleObserver.observe(document.head, TITLE_OBSERVER_OPTIONS);
61
+ push();
62
+ return () => {
63
+ stopNavigation();
64
+ titleObserver.disconnect();
65
+ };
32
66
  }
@@ -1,53 +1,53 @@
1
1
  export interface StreamEvent {
2
- name: string
3
- data: string
2
+ name: string;
3
+ data: string;
4
4
  }
5
5
 
6
- const DEFAULT_EVENT_NAME = 'message'
7
- const FRAME_SEPARATOR = /\r?\n\r?\n/
8
- const LINE_SEPARATOR = /\r?\n/
9
- const FIELD_SEPARATOR = ':'
10
- const EVENT_FIELD = 'event'
11
- const DATA_FIELD = 'data'
6
+ const DEFAULT_EVENT_NAME = "message";
7
+ const FRAME_SEPARATOR = /\r?\n\r?\n/;
8
+ const LINE_SEPARATOR = /\r?\n/;
9
+ const FIELD_SEPARATOR = ":";
10
+ const EVENT_FIELD = "event";
11
+ const DATA_FIELD = "data";
12
12
 
13
13
  interface Field {
14
- name: string
15
- value: string
14
+ name: string;
15
+ value: string;
16
16
  }
17
17
 
18
18
  function parseField(line: string): Field {
19
- const boundary = line.indexOf(FIELD_SEPARATOR)
20
- if (boundary === -1) return { name: line, value: '' }
21
- const raw = line.slice(boundary + FIELD_SEPARATOR.length)
19
+ const boundary = line.indexOf(FIELD_SEPARATOR);
20
+ if (boundary === -1) return { name: line, value: "" };
21
+ const raw = line.slice(boundary + FIELD_SEPARATOR.length);
22
22
  return {
23
23
  name: line.slice(0, boundary),
24
- value: raw.startsWith(' ') ? raw.slice(1) : raw,
25
- }
24
+ value: raw.startsWith(" ") ? raw.slice(1) : raw,
25
+ };
26
26
  }
27
27
 
28
28
  function parseFrame(frame: string): StreamEvent | null {
29
- let name = DEFAULT_EVENT_NAME
30
- const data: string[] = []
29
+ let name = DEFAULT_EVENT_NAME;
30
+ const data: string[] = [];
31
31
  for (const line of frame.split(LINE_SEPARATOR)) {
32
- const field = parseField(line)
33
- if (field.name === EVENT_FIELD) name = field.value
34
- if (field.name === DATA_FIELD) data.push(field.value)
32
+ const field = parseField(line);
33
+ if (field.name === EVENT_FIELD) name = field.value;
34
+ if (field.name === DATA_FIELD) data.push(field.value);
35
35
  }
36
- if (data.length === 0) return null
37
- return { name, data: data.join('\n') }
36
+ if (data.length === 0) return null;
37
+ return { name, data: data.join("\n") };
38
38
  }
39
39
 
40
40
  function dispatchFrames(
41
41
  buffer: string,
42
42
  onEvent: (event: StreamEvent) => void,
43
43
  ): string {
44
- const frames = buffer.split(FRAME_SEPARATOR)
45
- const rest = frames.pop() ?? ''
44
+ const frames = buffer.split(FRAME_SEPARATOR);
45
+ const rest = frames.pop() ?? "";
46
46
  for (const frame of frames) {
47
- const event = parseFrame(frame)
48
- if (event !== null) onEvent(event)
47
+ const event = parseFrame(frame);
48
+ if (event !== null) onEvent(event);
49
49
  }
50
- return rest
50
+ return rest;
51
51
  }
52
52
 
53
53
  /**
@@ -59,17 +59,17 @@ export async function readEventStream(
59
59
  body: ReadableStream<Uint8Array>,
60
60
  onEvent: (event: StreamEvent) => void,
61
61
  ): Promise<void> {
62
- const reader = body.getReader()
63
- const decoder = new TextDecoder()
64
- let buffer = ''
62
+ const reader = body.getReader();
63
+ const decoder = new TextDecoder();
64
+ let buffer = "";
65
65
  try {
66
66
  while (true) {
67
- const chunk = await reader.read()
68
- if (chunk.done) return
69
- buffer += decoder.decode(chunk.value, { stream: true })
70
- buffer = dispatchFrames(buffer, onEvent)
67
+ const chunk = await reader.read();
68
+ if (chunk.done) return;
69
+ buffer += decoder.decode(chunk.value, { stream: true });
70
+ buffer = dispatchFrames(buffer, onEvent);
71
71
  }
72
72
  } finally {
73
- reader.releaseLock()
73
+ reader.releaseLock();
74
74
  }
75
75
  }
@@ -0,0 +1,30 @@
1
+ import { HOST_COMMAND_TYPES } from "./constants";
2
+ import { isTypedMessage, type TypedMessage } from "./typed-message";
3
+
4
+ export interface FrameRoutes {
5
+ host: (msg: TypedMessage) => void;
6
+ chat: (msg: TypedMessage) => void;
7
+ }
8
+
9
+ function parseFrame(raw: string): TypedMessage | null {
10
+ try {
11
+ const parsed: unknown = JSON.parse(raw);
12
+ return isTypedMessage(parsed) ? parsed : null;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * The dashboard and its chat share the tab's one sidecar socket: each frame is
20
+ * parsed once, then handed to the dashboard when it is a host command and to
21
+ * the chat otherwise. A frame that is not a typed message reaches neither.
22
+ */
23
+ export function createFrameRouter(routes: FrameRoutes): (raw: string) => void {
24
+ return (raw) => {
25
+ const msg = parseFrame(raw);
26
+ if (msg === null) return;
27
+ const route = HOST_COMMAND_TYPES.has(msg.type) ? routes.host : routes.chat;
28
+ route(msg);
29
+ };
30
+ }
@@ -1,4 +1,4 @@
1
- import { useDmsState as useState } from '#dms/frontend-module'
1
+ import { useDmsState as useState } from "#dms/frontend-module";
2
2
  import {
3
3
  HEADER_ACTIONS_STATE_KEY,
4
4
  LAUNCHER_ACTION_ID,
@@ -7,14 +7,14 @@ import {
7
7
  LAUNCHER_ORDER,
8
8
  TOGGLE_SHORTCUT_LABEL_MAC,
9
9
  TOGGLE_SHORTCUT_LABEL_OTHER,
10
- } from './constants'
10
+ } from "./constants";
11
11
 
12
12
  interface UserAgentData {
13
- platform?: string
13
+ platform?: string;
14
14
  }
15
15
 
16
16
  interface NavigatorUserAgentData {
17
- userAgentData?: UserAgentData
17
+ userAgentData?: UserAgentData;
18
18
  }
19
19
 
20
20
  // The shortcut combo accepts meta OR ctrl, so show the modifier each platform
@@ -22,35 +22,35 @@ interface NavigatorUserAgentData {
22
22
  function isApplePlatform(): boolean {
23
23
  const nav = globalThis.navigator as
24
24
  | (Navigator & NavigatorUserAgentData)
25
- | undefined
26
- if (nav === undefined) return false
27
- const platform = nav.userAgentData?.platform ?? nav.platform ?? ''
28
- return /mac|iphone|ipad|ipod/i.test(platform)
25
+ | undefined;
26
+ if (nav === undefined) return false;
27
+ const platform = nav.userAgentData?.platform ?? nav.platform ?? "";
28
+ return /mac|iphone|ipad|ipod/i.test(platform);
29
29
  }
30
30
 
31
31
  function launcherLabel(): string {
32
32
  const shortcut = isApplePlatform()
33
33
  ? TOGGLE_SHORTCUT_LABEL_MAC
34
- : TOGGLE_SHORTCUT_LABEL_OTHER
35
- return `${LAUNCHER_LABEL} (${shortcut})`
34
+ : TOGGLE_SHORTCUT_LABEL_OTHER;
35
+ return `${LAUNCHER_LABEL} (${shortcut})`;
36
36
  }
37
37
 
38
38
  // Our view of the shared header-action shape the DMS core renders. Declared
39
39
  // locally so this layer has no build-time dependency on the core layer; the
40
40
  // only contract is the state key and these fields.
41
41
  interface HeaderAction {
42
- id: string
43
- icon: string
44
- label: string
45
- onSelect: () => void
46
- order?: number
42
+ id: string;
43
+ icon: string;
44
+ label: string;
45
+ onSelect: () => void;
46
+ order?: number;
47
47
  }
48
48
 
49
49
  // Register the assistant launcher in the core's generic header-action registry.
50
50
  // Idempotent so a re-running plugin can't add a duplicate button.
51
51
  export function registerLauncherAction(onSelect: () => void): void {
52
- const actions = useState<HeaderAction[]>(HEADER_ACTIONS_STATE_KEY, () => [])
53
- if (actions.value.some((action) => action.id === LAUNCHER_ACTION_ID)) return
52
+ const actions = useState<HeaderAction[]>(HEADER_ACTIONS_STATE_KEY, () => []);
53
+ if (actions.value.some((action) => action.id === LAUNCHER_ACTION_ID)) return;
54
54
  actions.value = [
55
55
  ...actions.value,
56
56
  {
@@ -60,5 +60,5 @@ export function registerLauncherAction(onSelect: () => void): void {
60
60
  order: LAUNCHER_ORDER,
61
61
  onSelect,
62
62
  },
63
- ]
63
+ ];
64
64
  }
@@ -1,7 +1,8 @@
1
- import { HOST_COMMAND_NAVIGATE_TYPE, LOG_PREFIX } from './constants'
1
+ import { HOST_COMMAND_NAVIGATE_TYPE, LOG_PREFIX } from "./constants";
2
+ import { isTypedMessage } from "./typed-message";
2
3
 
3
4
  interface RouterLike {
4
- push: (path: string) => unknown
5
+ push: (path: string) => unknown;
5
6
  }
6
7
 
7
8
  /**
@@ -10,22 +11,23 @@ interface RouterLike {
10
11
  * timeout.
11
12
  */
12
13
  export interface DevReloadWaiter {
13
- awaitRoute: (path: string) => Promise<boolean>
14
+ awaitRoute: (path: string) => Promise<boolean>;
14
15
  }
15
16
 
16
17
  export interface HostCommandContext {
17
- router: RouterLike
18
- devReload: DevReloadWaiter
18
+ router: RouterLike;
19
+ devReload: DevReloadWaiter;
19
20
  }
20
21
 
21
22
  export interface HostCommandNavigateMessage {
22
- type: typeof HOST_COMMAND_NAVIGATE_TYPE
23
- path: string
23
+ type: typeof HOST_COMMAND_NAVIGATE_TYPE;
24
+ path: string;
24
25
  }
25
26
 
26
- export type HostCommandMessage = HostCommandNavigateMessage
27
+ export type HostCommandMessage = HostCommandNavigateMessage;
27
28
 
28
- export type HostCommandDispatcher = (raw: unknown) => void
29
+ /** Takes a sidecar message already parsed; anything that is not a host command is ignored. */
30
+ export type HostCommandDispatcher = (msg: unknown) => void;
29
31
 
30
32
  /**
31
33
  * Commands run one at a time on `tail`. `nextPath` holds the newest navigation
@@ -33,20 +35,22 @@ export type HostCommandDispatcher = (raw: unknown) => void
33
35
  * the latest path instead of racing and landing on the oldest one.
34
36
  */
35
37
  interface DispatcherState {
36
- ctx: HostCommandContext
37
- tail: Promise<void>
38
- nextPath: string | null
38
+ ctx: HostCommandContext;
39
+ tail: Promise<void>;
40
+ nextPath: string | null;
39
41
  }
40
42
 
41
43
  type HostCommandHandler = (
42
44
  msg: HostCommandMessage,
43
45
  state: DispatcherState,
44
- ) => void
46
+ ) => void;
47
+
48
+ const IN_APP_PATH = /^\/(?![/\\])/;
45
49
 
46
50
  function enqueue(state: DispatcherState, job: () => Promise<void>): void {
47
51
  state.tail = state.tail.then(job).catch((err: unknown) => {
48
- console.error(`${LOG_PREFIX} host command failed`, err)
49
- })
52
+ console.error(`${LOG_PREFIX} host command failed`, err);
53
+ });
50
54
  }
51
55
 
52
56
  // The agent asks to navigate right after writing the page, while the host is
@@ -60,52 +64,50 @@ async function awaitRouteThenPush(
60
64
  ctx: HostCommandContext,
61
65
  ): Promise<void> {
62
66
  try {
63
- await ctx.devReload.awaitRoute(path)
67
+ await ctx.devReload.awaitRoute(path);
64
68
  } catch (err: unknown) {
65
- console.error(`${LOG_PREFIX} dev reload wait failed for ${path}`, err)
69
+ console.error(`${LOG_PREFIX} dev reload wait failed for ${path}`, err);
66
70
  }
67
- void ctx.router.push(path)
71
+ void ctx.router.push(path);
68
72
  }
69
73
 
70
74
  async function runNavigate(state: DispatcherState): Promise<void> {
71
- const path = state.nextPath
75
+ const path = state.nextPath;
72
76
  // A newer navigate already claimed this slot's target: this one is superseded
73
77
  // and must not drag the user back to the older path.
74
- if (path === null) return
75
- state.nextPath = null
76
- await awaitRouteThenPush(path, state.ctx)
77
- }
78
-
79
- function handleNavigate(
80
- msg: HostCommandMessage,
81
- state: DispatcherState,
82
- ): void {
83
- if (msg.type !== HOST_COMMAND_NAVIGATE_TYPE) return
84
- state.nextPath = msg.path
85
- enqueue(state, () => runNavigate(state))
86
- }
87
-
88
- const COMMAND_HANDLERS: Record<string, HostCommandHandler> = {
89
- [HOST_COMMAND_NAVIGATE_TYPE]: handleNavigate,
90
- }
91
-
92
- function isHostCommand(value: unknown): value is HostCommandMessage {
93
- if (value === null || typeof value !== 'object') return false
94
- const candidate = value as { type?: unknown }
95
- return typeof candidate.type === 'string'
78
+ if (path === null) return;
79
+ state.nextPath = null;
80
+ await awaitRouteThenPush(path, state.ctx);
96
81
  }
97
82
 
98
- function parseRaw(raw: unknown): HostCommandMessage | null {
99
- if (typeof raw !== 'string') return null
83
+ /**
84
+ * Whether the agent may send the dashboard to `path`. What it asks for comes
85
+ * from model output, so it may only ever lead to a page of this origin.
86
+ */
87
+ export function isInAppPath(path: unknown): path is string {
88
+ if (typeof path !== "string" || !IN_APP_PATH.test(path)) return false;
100
89
  try {
101
- const parsed = JSON.parse(raw) as unknown
102
- if (!isHostCommand(parsed)) return null
103
- return parsed
90
+ const origin = globalThis.location.origin;
91
+ return new URL(path, origin).origin === origin;
104
92
  } catch {
105
- return null
93
+ return false;
94
+ }
95
+ }
96
+
97
+ function handleNavigate(msg: HostCommandMessage, state: DispatcherState): void {
98
+ if (msg.type !== HOST_COMMAND_NAVIGATE_TYPE) return;
99
+ if (!isInAppPath(msg.path)) {
100
+ console.warn(`${LOG_PREFIX} refused to navigate off the dashboard`);
101
+ return;
106
102
  }
103
+ state.nextPath = msg.path;
104
+ enqueue(state, () => runNavigate(state));
107
105
  }
108
106
 
107
+ const COMMAND_HANDLERS: Record<string, HostCommandHandler> = {
108
+ [HOST_COMMAND_NAVIGATE_TYPE]: handleNavigate,
109
+ };
110
+
109
111
  export function createHostCommandDispatcher(
110
112
  ctx: HostCommandContext,
111
113
  ): HostCommandDispatcher {
@@ -113,15 +115,14 @@ export function createHostCommandDispatcher(
113
115
  ctx,
114
116
  tail: Promise.resolve(),
115
117
  nextPath: null,
116
- }
117
- return (raw: unknown): void => {
118
- const msg = parseRaw(raw)
119
- if (msg === null) return
120
- const handler = COMMAND_HANDLERS[msg.type]
118
+ };
119
+ return (msg: unknown): void => {
120
+ if (!isTypedMessage<HostCommandMessage>(msg)) return;
121
+ const handler = COMMAND_HANDLERS[msg.type];
121
122
  if (handler === undefined) {
122
- console.warn(`${LOG_PREFIX} unknown host command type=${msg.type}`)
123
- return
123
+ console.warn(`${LOG_PREFIX} unknown host command type=${msg.type}`);
124
+ return;
124
125
  }
125
- handler(msg, state)
126
- }
126
+ handler(msg, state);
127
+ };
127
128
  }
@@ -0,0 +1,31 @@
1
+ import { LOG_PREFIX } from "./constants";
2
+
3
+ export type Listener<T> = (value: T) => void;
4
+
5
+ export interface Listeners<T> {
6
+ add: (listener: Listener<T>) => () => void;
7
+ emit: Listener<T>;
8
+ }
9
+
10
+ /**
11
+ * A set of listeners notified in order. A listener belongs to a component that
12
+ * may already be failing: one throwing must not stop the others.
13
+ */
14
+ export function createListeners<T>(): Listeners<T> {
15
+ const listeners = new Set<Listener<T>>();
16
+ return {
17
+ add: (listener) => {
18
+ listeners.add(listener);
19
+ return () => listeners.delete(listener);
20
+ },
21
+ emit: (value) => {
22
+ for (const listener of listeners) {
23
+ try {
24
+ listener(value);
25
+ } catch (error: unknown) {
26
+ console.error(`${LOG_PREFIX} listener failed`, error);
27
+ }
28
+ }
29
+ },
30
+ };
31
+ }
@@ -1,22 +1,22 @@
1
- import { router } from '@inertiajs/vue3'
2
- import { HOST_NAVIGATION_COMPLETE_TYPE } from './constants'
1
+ import { router } from "@inertiajs/vue3";
2
+ import { HOST_NAVIGATION_COMPLETE_TYPE } from "./constants";
3
3
 
4
4
  export interface HostNavigationCompleteMessage {
5
- type: typeof HOST_NAVIGATION_COMPLETE_TYPE
6
- path: string
5
+ type: typeof HOST_NAVIGATION_COMPLETE_TYPE;
6
+ path: string;
7
7
  }
8
8
 
9
9
  interface InstallNavigationCompleteEmitterOptions {
10
- send: (msg: HostNavigationCompleteMessage) => void
10
+ send: (msg: HostNavigationCompleteMessage) => void;
11
11
  }
12
12
 
13
13
  export function installNavigationCompleteEmitter(
14
14
  options: InstallNavigationCompleteEmitterOptions,
15
15
  ): () => void {
16
- return router.on('navigate', (event) =>
16
+ return router.on("navigate", (event) =>
17
17
  options.send({
18
18
  type: HOST_NAVIGATION_COMPLETE_TYPE,
19
19
  path: new URL(event.detail.page.url, window.location.origin).pathname,
20
20
  }),
21
- )
21
+ );
22
22
  }
@@ -1,89 +1,89 @@
1
1
  import {
2
2
  OVERLAY_PREFS_DEBOUNCE_MS,
3
3
  OVERLAY_PREFS_STORAGE_KEY,
4
- } from './constants'
4
+ } from "./constants";
5
5
 
6
6
  export interface OverlayPrefs {
7
- width: number
8
- isOpen: boolean
7
+ width: number;
8
+ isOpen: boolean;
9
9
  }
10
10
 
11
- let pendingTimer: ReturnType<typeof setTimeout> | null = null
12
- let pendingPrefs: OverlayPrefs | null = null
11
+ let pendingTimer: ReturnType<typeof setTimeout> | null = null;
12
+ let pendingPrefs: OverlayPrefs | null = null;
13
13
 
14
14
  function tryReadStorage(): string | null {
15
15
  try {
16
- return globalThis.localStorage.getItem(OVERLAY_PREFS_STORAGE_KEY)
16
+ return globalThis.localStorage.getItem(OVERLAY_PREFS_STORAGE_KEY);
17
17
  } catch {
18
- return null
18
+ return null;
19
19
  }
20
20
  }
21
21
 
22
22
  function tryWriteStorage(payload: string): void {
23
23
  try {
24
- globalThis.localStorage.setItem(OVERLAY_PREFS_STORAGE_KEY, payload)
24
+ globalThis.localStorage.setItem(OVERLAY_PREFS_STORAGE_KEY, payload);
25
25
  } catch {
26
- return
26
+ return;
27
27
  }
28
28
  }
29
29
 
30
30
  function isFiniteNumber(value: unknown): value is number {
31
- return typeof value === 'number' && Number.isFinite(value)
31
+ return typeof value === "number" && Number.isFinite(value);
32
32
  }
33
33
 
34
34
  function isValidPrefs(value: unknown): value is OverlayPrefs {
35
- if (value === null || typeof value !== 'object') return false
36
- const candidate = value as Record<string, unknown>
37
- if (!isFiniteNumber(candidate.width)) return false
38
- if (typeof candidate.isOpen !== 'boolean') return false
39
- return true
35
+ if (value === null || typeof value !== "object") return false;
36
+ const candidate = value as Record<string, unknown>;
37
+ if (!isFiniteNumber(candidate.width)) return false;
38
+ if (typeof candidate.isOpen !== "boolean") return false;
39
+ return true;
40
40
  }
41
41
 
42
42
  function parsePrefs(raw: string): OverlayPrefs | null {
43
43
  try {
44
- const parsed: unknown = JSON.parse(raw)
45
- return isValidPrefs(parsed) ? parsed : null
44
+ const parsed: unknown = JSON.parse(raw);
45
+ return isValidPrefs(parsed) ? parsed : null;
46
46
  } catch {
47
- return null
47
+ return null;
48
48
  }
49
49
  }
50
50
 
51
51
  export function readPrefs(): OverlayPrefs | null {
52
- const raw = tryReadStorage()
53
- if (raw === null) return null
54
- return parsePrefs(raw)
52
+ const raw = tryReadStorage();
53
+ if (raw === null) return null;
54
+ return parsePrefs(raw);
55
55
  }
56
56
 
57
57
  function flushPending(): void {
58
- if (pendingPrefs === null) return
59
- tryWriteStorage(JSON.stringify(pendingPrefs))
60
- pendingPrefs = null
61
- pendingTimer = null
58
+ if (pendingPrefs === null) return;
59
+ tryWriteStorage(JSON.stringify(pendingPrefs));
60
+ pendingPrefs = null;
61
+ pendingTimer = null;
62
62
  }
63
63
 
64
64
  export function writePrefsDebounced(prefs: OverlayPrefs): void {
65
- pendingPrefs = prefs
66
- if (pendingTimer !== null) clearTimeout(pendingTimer)
67
- pendingTimer = setTimeout(flushPending, OVERLAY_PREFS_DEBOUNCE_MS)
65
+ pendingPrefs = prefs;
66
+ if (pendingTimer !== null) clearTimeout(pendingTimer);
67
+ pendingTimer = setTimeout(flushPending, OVERLAY_PREFS_DEBOUNCE_MS);
68
68
  }
69
69
 
70
70
  export function flushPrefsForTesting(): void {
71
71
  if (pendingTimer !== null) {
72
- clearTimeout(pendingTimer)
73
- pendingTimer = null
72
+ clearTimeout(pendingTimer);
73
+ pendingTimer = null;
74
74
  }
75
- flushPending()
75
+ flushPending();
76
76
  }
77
77
 
78
78
  export function resetPrefsForTesting(): void {
79
79
  if (pendingTimer !== null) {
80
- clearTimeout(pendingTimer)
81
- pendingTimer = null
80
+ clearTimeout(pendingTimer);
81
+ pendingTimer = null;
82
82
  }
83
- pendingPrefs = null
83
+ pendingPrefs = null;
84
84
  try {
85
- globalThis.localStorage.removeItem(OVERLAY_PREFS_STORAGE_KEY)
85
+ globalThis.localStorage.removeItem(OVERLAY_PREFS_STORAGE_KEY);
86
86
  } catch {
87
- return
87
+ return;
88
88
  }
89
89
  }