@antelopejs/dms-ai 0.1.4 → 0.1.6

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 (250) hide show
  1. package/README.md +55 -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 +5 -1
  18. package/dist/constants/sidecar.js +6 -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-owner.d.ts +13 -0
  26. package/dist/lifecycle/sidecar-owner.js +89 -0
  27. package/dist/lifecycle/sidecar-owner.js.map +1 -0
  28. package/dist/lifecycle/sidecar-socket.d.ts +1 -1
  29. package/dist/lifecycle/sidecar-socket.js +2 -2
  30. package/dist/lifecycle/sidecar-socket.js.map +1 -1
  31. package/dist/lifecycle/spawn-sidecar.d.ts +0 -1
  32. package/dist/lifecycle/spawn-sidecar.js +51 -159
  33. package/dist/lifecycle/spawn-sidecar.js.map +1 -1
  34. package/dist/lifecycle/terminate-process.d.ts +1 -0
  35. package/dist/lifecycle/terminate-process.js +34 -0
  36. package/dist/lifecycle/terminate-process.js.map +1 -0
  37. package/dist/routes/channels.d.ts +3 -3
  38. package/dist/routes/channels.js +11 -12
  39. package/dist/routes/channels.js.map +1 -1
  40. package/dist/routes/index.d.ts +0 -1
  41. package/dist/routes/index.js +0 -1
  42. package/dist/routes/index.js.map +1 -1
  43. package/frontend-vue/.prettierignore +3 -0
  44. package/frontend-vue/.prettierrc +10 -0
  45. package/frontend-vue/app/chat/components/ChatView.vue +582 -0
  46. package/frontend-vue/app/chat/components/ComposerInput.vue +742 -0
  47. package/frontend-vue/app/chat/components/ConnectionBanner.vue +105 -0
  48. package/frontend-vue/app/chat/components/ConversationDrawer.vue +266 -0
  49. package/frontend-vue/app/chat/components/MarkdownContent.vue +196 -0
  50. package/frontend-vue/app/chat/components/MessageList.vue +474 -0
  51. package/frontend-vue/app/chat/components/PermissionRequests.vue +323 -0
  52. package/frontend-vue/app/chat/components/QuestionPrompt.vue +275 -0
  53. package/frontend-vue/app/chat/components/QueuedMessages.vue +109 -0
  54. package/frontend-vue/app/chat/components/TodoList.vue +291 -0
  55. package/frontend-vue/app/chat/components/ToolCallEntry.vue +218 -0
  56. package/frontend-vue/app/chat/components/ToolCluster.vue +282 -0
  57. package/frontend-vue/app/chat/composables/useChatChannel.ts +116 -0
  58. package/frontend-vue/app/chat/composables/useConversation.ts +838 -0
  59. package/frontend-vue/app/chat/composables/useConversationId.ts +34 -0
  60. package/frontend-vue/app/chat/composables/useConversationList.ts +56 -0
  61. package/frontend-vue/app/chat/composables/usePermissionQueue.ts +59 -0
  62. package/frontend-vue/app/chat/composables/useQuestionQueue.ts +50 -0
  63. package/frontend-vue/app/chat/composables/useRunClock.ts +28 -0
  64. package/frontend-vue/app/chat/composables/useSettings.ts +67 -0
  65. package/frontend-vue/app/chat/constants/attachments.ts +16 -0
  66. package/frontend-vue/app/chat/constants/connection-banner.ts +7 -0
  67. package/frontend-vue/app/chat/constants/conversation-drawer.ts +13 -0
  68. package/frontend-vue/app/chat/constants/conversation.ts +75 -0
  69. package/frontend-vue/app/chat/constants/permissions.ts +50 -0
  70. package/frontend-vue/app/chat/constants/protocol.ts +48 -0
  71. package/frontend-vue/app/chat/constants/run-status.ts +50 -0
  72. package/frontend-vue/app/chat/constants/settings.ts +60 -0
  73. package/frontend-vue/app/chat/types/conversation.ts +87 -0
  74. package/frontend-vue/app/chat/types/permission.ts +7 -0
  75. package/frontend-vue/app/chat/types/question.ts +17 -0
  76. package/frontend-vue/app/chat/types/settings.ts +27 -0
  77. package/frontend-vue/app/chat/utils/attachments.ts +62 -0
  78. package/frontend-vue/app/chat/utils/format.ts +14 -0
  79. package/frontend-vue/app/chat/utils/ids.ts +34 -0
  80. package/frontend-vue/app/chat/utils/markdown.ts +188 -0
  81. package/frontend-vue/app/chat/utils/run-status.ts +90 -0
  82. package/frontend-vue/app/chat/utils/todos.ts +64 -0
  83. package/frontend-vue/app/chat/utils/tool-summary.ts +53 -0
  84. package/frontend-vue/app/components/ActivityView.vue +34 -34
  85. package/frontend-vue/app/components/ChatPanel.vue +276 -0
  86. package/frontend-vue/app/components/SettingsView.vue +120 -112
  87. package/frontend-vue/app/components/SkillsView.vue +55 -53
  88. package/frontend-vue/app/plugins/ai.client.ts +145 -129
  89. package/frontend-vue/app/runtime/assistant-session.ts +21 -0
  90. package/frontend-vue/app/runtime/backoff.ts +11 -14
  91. package/frontend-vue/app/runtime/channel-client.ts +130 -141
  92. package/frontend-vue/app/runtime/chat-transport.ts +25 -55
  93. package/frontend-vue/app/runtime/constants.ts +69 -59
  94. package/frontend-vue/app/runtime/current-page.ts +49 -15
  95. package/frontend-vue/app/runtime/event-stream.ts +35 -35
  96. package/frontend-vue/app/runtime/frame-router.ts +30 -0
  97. package/frontend-vue/app/runtime/header-action.ts +18 -18
  98. package/frontend-vue/app/runtime/host-commands.ts +57 -56
  99. package/frontend-vue/app/runtime/listeners.ts +31 -0
  100. package/frontend-vue/app/runtime/navigation-complete.ts +7 -7
  101. package/frontend-vue/app/runtime/overlay-prefs.ts +36 -36
  102. package/frontend-vue/app/runtime/panel-state.ts +88 -0
  103. package/frontend-vue/app/runtime/session-gate.ts +8 -8
  104. package/frontend-vue/app/runtime/shortcuts.ts +12 -12
  105. package/frontend-vue/app/runtime/sidecar-status.ts +61 -61
  106. package/frontend-vue/app/runtime/typed-message.ts +11 -0
  107. package/frontend-vue/dms.frontend.ts +12 -12
  108. package/frontend-vue/eslint.config.mjs +13 -13
  109. package/frontend-vue/package.json +11 -4
  110. package/frontend-vue/pnpm-lock.yaml +298 -96
  111. package/frontend-vue/tests/backoff.test.ts +26 -30
  112. package/frontend-vue/tests/channel-client.test.ts +187 -188
  113. package/frontend-vue/tests/chat/markdown.test.ts +271 -0
  114. package/frontend-vue/tests/chat/useChatChannel.test.ts +124 -0
  115. package/frontend-vue/tests/chat/useConversation.test.ts +283 -0
  116. package/frontend-vue/tests/chat-panel.test.ts +354 -0
  117. package/frontend-vue/tests/chat-transport.test.ts +84 -57
  118. package/frontend-vue/tests/host-commands.test.ts +126 -87
  119. package/frontend-vue/tests/navigation.test.ts +65 -34
  120. package/frontend-vue/tests/overlay-prefs.test.ts +49 -49
  121. package/frontend-vue/tests/panel-state.test.ts +58 -0
  122. package/frontend-vue/tests/session-gate.test.ts +33 -33
  123. package/frontend-vue/tests/sidecar-guest.test.ts +46 -46
  124. package/frontend-vue/tests/sidecar-status.test.ts +29 -29
  125. package/frontend-vue/tsconfig.chat.json +16 -0
  126. package/frontend-vue/vitest.config.ts +6 -4
  127. package/package.json +6 -5
  128. package/sidecar/dist/agent/effective-mode.d.ts +2 -2
  129. package/sidecar/dist/agent/effective-mode.js +1 -1
  130. package/sidecar/dist/agent/effective-mode.js.map +1 -1
  131. package/sidecar/dist/agent/host-context.js +7 -9
  132. package/sidecar/dist/agent/host-context.js.map +1 -1
  133. package/sidecar/dist/agent/permission-bus.d.ts +1 -1
  134. package/sidecar/dist/agent/permission-bus.js +2 -2
  135. package/sidecar/dist/agent/permission-bus.js.map +1 -1
  136. package/sidecar/dist/agent/provider.d.ts +2 -2
  137. package/sidecar/dist/agent/question-bus.d.ts +1 -1
  138. package/sidecar/dist/agent/question-bus.js +2 -2
  139. package/sidecar/dist/agent/question-bus.js.map +1 -1
  140. package/sidecar/dist/agent/runner.d.ts +2 -2
  141. package/sidecar/dist/agent/runner.js +24 -8
  142. package/sidecar/dist/agent/runner.js.map +1 -1
  143. package/sidecar/dist/agent/session.d.ts +2 -2
  144. package/sidecar/dist/agent/session.js +49 -9
  145. package/sidecar/dist/agent/session.js.map +1 -1
  146. package/sidecar/dist/agent/switching-runner.js +10 -5
  147. package/sidecar/dist/agent/switching-runner.js.map +1 -1
  148. package/sidecar/dist/agent/turn-end-reasons.d.ts +2 -0
  149. package/sidecar/dist/agent/turn-end-reasons.js +6 -1
  150. package/sidecar/dist/agent/turn-end-reasons.js.map +1 -1
  151. package/sidecar/dist/constants/agent.d.ts +4 -1
  152. package/sidecar/dist/constants/agent.js +4 -1
  153. package/sidecar/dist/constants/agent.js.map +1 -1
  154. package/sidecar/dist/constants/cli.d.ts +0 -1
  155. package/sidecar/dist/constants/cli.js +0 -1
  156. package/sidecar/dist/constants/cli.js.map +1 -1
  157. package/sidecar/dist/constants/codex.d.ts +5 -2
  158. package/sidecar/dist/constants/codex.js +5 -2
  159. package/sidecar/dist/constants/codex.js.map +1 -1
  160. package/sidecar/dist/constants/http.d.ts +0 -9
  161. package/sidecar/dist/constants/http.js +0 -19
  162. package/sidecar/dist/constants/http.js.map +1 -1
  163. package/sidecar/dist/constants/mcp.d.ts +1 -1
  164. package/sidecar/dist/constants/mcp.js +1 -1
  165. package/sidecar/dist/constants/mcp.js.map +1 -1
  166. package/sidecar/dist/constants/paths.d.ts +0 -1
  167. package/sidecar/dist/constants/paths.js +0 -1
  168. package/sidecar/dist/constants/paths.js.map +1 -1
  169. package/sidecar/dist/constants/ws.d.ts +3 -4
  170. package/sidecar/dist/constants/ws.js +3 -4
  171. package/sidecar/dist/constants/ws.js.map +1 -1
  172. package/sidecar/dist/index.d.ts +4 -1
  173. package/sidecar/dist/index.js +13 -13
  174. package/sidecar/dist/index.js.map +1 -1
  175. package/sidecar/dist/mcp/tools/find-pages-using.js +5 -10
  176. package/sidecar/dist/mcp/tools/find-pages-using.js.map +1 -1
  177. package/sidecar/dist/mcp/types.d.ts +1 -1
  178. package/sidecar/dist/pages/page-filepath.d.ts +4 -0
  179. package/sidecar/dist/pages/page-filepath.js +28 -0
  180. package/sidecar/dist/pages/page-filepath.js.map +1 -0
  181. package/sidecar/dist/prompts/agent.d.ts +1 -1
  182. package/sidecar/dist/prompts/agent.js +1 -1
  183. package/sidecar/dist/prompts/agent.js.map +1 -1
  184. package/sidecar/dist/protocol/events.js +2 -2
  185. package/sidecar/dist/protocol/events.js.map +1 -1
  186. package/sidecar/dist/protocol/messages.d.ts +4 -15
  187. package/sidecar/dist/protocol/messages.js +4 -11
  188. package/sidecar/dist/protocol/messages.js.map +1 -1
  189. package/sidecar/dist/providers/claude/config.d.ts +2 -2
  190. package/sidecar/dist/providers/claude/config.js +2 -2
  191. package/sidecar/dist/providers/claude/config.js.map +1 -1
  192. package/sidecar/dist/providers/claude/provider.js +1 -1
  193. package/sidecar/dist/providers/claude/provider.js.map +1 -1
  194. package/sidecar/dist/providers/codex/config.d.ts +3 -2
  195. package/sidecar/dist/providers/codex/config.js +3 -3
  196. package/sidecar/dist/providers/codex/config.js.map +1 -1
  197. package/sidecar/dist/providers/codex/process.js +107 -74
  198. package/sidecar/dist/providers/codex/process.js.map +1 -1
  199. package/sidecar/dist/providers/codex/provider.js +68 -31
  200. package/sidecar/dist/providers/codex/provider.js.map +1 -1
  201. package/sidecar/dist/providers/codex/skills.d.ts +1 -0
  202. package/sidecar/dist/providers/codex/skills.js +4 -1
  203. package/sidecar/dist/providers/codex/skills.js.map +1 -1
  204. package/sidecar/dist/providers/codex/stop.d.ts +4 -0
  205. package/sidecar/dist/providers/codex/stop.js +71 -0
  206. package/sidecar/dist/providers/codex/stop.js.map +1 -0
  207. package/sidecar/dist/server/{iframe-socket-registry.d.ts → chat-socket-registry.d.ts} +3 -2
  208. package/sidecar/dist/server/{iframe-socket-registry.js → chat-socket-registry.js} +4 -3
  209. package/sidecar/dist/server/chat-socket-registry.js.map +1 -0
  210. package/sidecar/dist/server/client-auth.js +2 -14
  211. package/sidecar/dist/server/client-auth.js.map +1 -1
  212. package/sidecar/dist/server/host-command-router.d.ts +5 -0
  213. package/sidecar/dist/server/host-command-router.js +4 -0
  214. package/sidecar/dist/server/host-command-router.js.map +1 -0
  215. package/sidecar/dist/server/host-socket-registry.d.ts +1 -1
  216. package/sidecar/dist/server/host-socket-registry.js +16 -13
  217. package/sidecar/dist/server/host-socket-registry.js.map +1 -1
  218. package/sidecar/dist/server/http.d.ts +0 -1
  219. package/sidecar/dist/server/http.js +1 -44
  220. package/sidecar/dist/server/http.js.map +1 -1
  221. package/sidecar/dist/server/routing.d.ts +5 -6
  222. package/sidecar/dist/server/routing.js +18 -28
  223. package/sidecar/dist/server/routing.js.map +1 -1
  224. package/sidecar/dist/server/ws.d.ts +2 -2
  225. package/sidecar/dist/server/ws.js +41 -39
  226. package/sidecar/dist/server/ws.js.map +1 -1
  227. package/sidecar/dist/state/host-state.d.ts +10 -2
  228. package/sidecar/dist/state/host-state.js +13 -3
  229. package/sidecar/dist/state/host-state.js.map +1 -1
  230. package/sidecar/dist/state/settings-store.js +2 -2
  231. package/sidecar/dist/state/settings-store.js.map +1 -1
  232. package/sidecar/dist/state/settings-types.d.ts +3 -3
  233. package/sidecar/dist/state/settings-types.js +1 -1
  234. package/sidecar/dist/state/settings-types.js.map +1 -1
  235. package/sidecar/package.json +3 -5
  236. package/dist/chatbox/passthrough.d.ts +0 -3
  237. package/dist/chatbox/passthrough.js +0 -53
  238. package/dist/chatbox/passthrough.js.map +0 -1
  239. package/dist/constants/chatbox.d.ts +0 -10
  240. package/dist/constants/chatbox.js +0 -35
  241. package/dist/constants/chatbox.js.map +0 -1
  242. package/dist/routes/chatbox.d.ts +0 -7
  243. package/dist/routes/chatbox.js +0 -47
  244. package/dist/routes/chatbox.js.map +0 -1
  245. package/frontend-vue/app/runtime/overlay.ts +0 -400
  246. package/frontend-vue/app/runtime/theme-bridge.ts +0 -189
  247. package/sidecar/chatbox/dist/assets/index-C8u7mkHn.css +0 -1
  248. package/sidecar/chatbox/dist/assets/index-tdZvY7ee.js +0 -52
  249. package/sidecar/chatbox/dist/index.html +0 -13
  250. package/sidecar/dist/server/iframe-socket-registry.js.map +0 -1
@@ -1,142 +1,181 @@
1
- import { describe, expect, it, vi } from 'vitest'
1
+ import { describe, expect, it, vi } from "vitest";
2
2
  import {
3
3
  createHostCommandDispatcher,
4
4
  type DevReloadWaiter,
5
- } from '../app/runtime/host-commands'
5
+ } from "../app/runtime/host-commands";
6
6
 
7
- function navigateCommand(path: string): string {
8
- return JSON.stringify({ type: 'host_command_navigate', path })
7
+ function navigateCommand(path: string): object {
8
+ return { type: "host_command_navigate", path };
9
9
  }
10
10
 
11
11
  interface ControlledWaiter {
12
- waiter: DevReloadWaiter
13
- calls: string[]
14
- release: (path: string, served?: boolean) => void
15
- reject: (path: string, err: Error) => void
12
+ waiter: DevReloadWaiter;
13
+ calls: string[];
14
+ release: (path: string, served?: boolean) => void;
15
+ reject: (path: string, err: Error) => void;
16
16
  }
17
17
 
18
18
  function controlledWaiter(): ControlledWaiter {
19
- const calls: string[] = []
19
+ const calls: string[] = [];
20
20
  const settlers = new Map<
21
21
  string,
22
22
  { resolve: (served: boolean) => void; reject: (err: Error) => void }
23
- >()
23
+ >();
24
24
  return {
25
25
  calls,
26
26
  release: (path, served = true) => settlers.get(path)?.resolve(served),
27
27
  reject: (path, err) => settlers.get(path)?.reject(err),
28
28
  waiter: {
29
29
  awaitRoute: (path) => {
30
- calls.push(path)
30
+ calls.push(path);
31
31
  return new Promise<boolean>((resolve, reject) => {
32
- settlers.set(path, { resolve, reject })
33
- })
32
+ settlers.set(path, { resolve, reject });
33
+ });
34
34
  },
35
35
  },
36
- }
36
+ };
37
37
  }
38
38
 
39
- describe('host command dispatch', () => {
40
- it('navigates only once the dev reload serves the target route', async () => {
41
- const push = vi.fn()
42
- const { waiter, calls, release } = controlledWaiter()
39
+ describe("host command dispatch", () => {
40
+ it("navigates only once the dev reload serves the target route", async () => {
41
+ const push = vi.fn();
42
+ const { waiter, calls, release } = controlledWaiter();
43
43
  const dispatch = createHostCommandDispatcher({
44
44
  router: { push },
45
45
  devReload: waiter,
46
- })
47
- dispatch(navigateCommand('/new-page'))
48
- await vi.waitFor(() => expect(calls).toEqual(['/new-page']))
49
- expect(push).not.toHaveBeenCalled()
50
- release('/new-page')
46
+ });
47
+ dispatch(navigateCommand("/new-page"));
48
+ await vi.waitFor(() => expect(calls).toEqual(["/new-page"]));
49
+ expect(push).not.toHaveBeenCalled();
50
+ release("/new-page");
51
51
  await vi.waitFor(() =>
52
- expect(push).toHaveBeenCalledExactlyOnceWith('/new-page'),
53
- )
54
- })
52
+ expect(push).toHaveBeenCalledExactlyOnceWith("/new-page"),
53
+ );
54
+ });
55
55
 
56
- it('navigates anyway when the route never comes back', async () => {
57
- const push = vi.fn()
56
+ it("navigates anyway when the route never comes back", async () => {
57
+ const push = vi.fn();
58
58
  const dispatch = createHostCommandDispatcher({
59
59
  router: { push },
60
60
  devReload: { awaitRoute: async () => false },
61
- })
62
- dispatch(navigateCommand('/deleted'))
61
+ });
62
+ dispatch(navigateCommand("/deleted"));
63
63
  await vi.waitFor(() =>
64
- expect(push).toHaveBeenCalledExactlyOnceWith('/deleted'),
65
- )
66
- })
64
+ expect(push).toHaveBeenCalledExactlyOnceWith("/deleted"),
65
+ );
66
+ });
67
67
 
68
- it('navigates anyway when the dev reload wait rejects', async () => {
69
- const push = vi.fn()
70
- const error = new Error('dev reload exploded')
71
- const { waiter, calls, reject } = controlledWaiter()
72
- const logged = vi.spyOn(console, 'error').mockImplementation(() => {})
68
+ it("navigates anyway when the dev reload wait rejects", async () => {
69
+ const push = vi.fn();
70
+ const error = new Error("dev reload exploded");
71
+ const { waiter, calls, reject } = controlledWaiter();
72
+ const logged = vi.spyOn(console, "error").mockImplementation(() => {});
73
73
  const dispatch = createHostCommandDispatcher({
74
74
  router: { push },
75
75
  devReload: waiter,
76
- })
77
- dispatch(navigateCommand('/broken-wait'))
78
- await vi.waitFor(() => expect(calls).toEqual(['/broken-wait']))
79
- reject('/broken-wait', error)
76
+ });
77
+ dispatch(navigateCommand("/broken-wait"));
78
+ await vi.waitFor(() => expect(calls).toEqual(["/broken-wait"]));
79
+ reject("/broken-wait", error);
80
80
  await vi.waitFor(() =>
81
- expect(push).toHaveBeenCalledExactlyOnceWith('/broken-wait'),
82
- )
81
+ expect(push).toHaveBeenCalledExactlyOnceWith("/broken-wait"),
82
+ );
83
83
  expect(logged).toHaveBeenCalledWith(
84
- expect.stringContaining('/broken-wait'),
84
+ expect.stringContaining("/broken-wait"),
85
85
  error,
86
- )
87
- logged.mockRestore()
88
- })
86
+ );
87
+ logged.mockRestore();
88
+ });
89
89
 
90
- it('collapses a burst into a single trip to the newest path', async () => {
91
- const push = vi.fn()
92
- const { waiter, calls, release } = controlledWaiter()
90
+ it("collapses a burst into a single trip to the newest path", async () => {
91
+ const push = vi.fn();
92
+ const { waiter, calls, release } = controlledWaiter();
93
93
  const dispatch = createHostCommandDispatcher({
94
94
  router: { push },
95
95
  devReload: waiter,
96
- })
97
- dispatch(navigateCommand('/stale'))
98
- dispatch(navigateCommand('/fresh'))
99
- await vi.waitFor(() => expect(calls).toEqual(['/fresh']))
100
- release('/fresh')
96
+ });
97
+ dispatch(navigateCommand("/stale"));
98
+ dispatch(navigateCommand("/fresh"));
99
+ await vi.waitFor(() => expect(calls).toEqual(["/fresh"]));
100
+ release("/fresh");
101
101
  await vi.waitFor(() =>
102
- expect(push).toHaveBeenCalledExactlyOnceWith('/fresh'),
103
- )
104
- })
102
+ expect(push).toHaveBeenCalledExactlyOnceWith("/fresh"),
103
+ );
104
+ });
105
105
 
106
- it('keeps navigations in order when one arrives mid-wait', async () => {
107
- const push = vi.fn()
108
- const { waiter, calls, release } = controlledWaiter()
106
+ it("keeps navigations in order when one arrives mid-wait", async () => {
107
+ const push = vi.fn();
108
+ const { waiter, calls, release } = controlledWaiter();
109
109
  const dispatch = createHostCommandDispatcher({
110
110
  router: { push },
111
111
  devReload: waiter,
112
- })
113
- dispatch(navigateCommand('/first'))
114
- await vi.waitFor(() => expect(calls).toEqual(['/first']))
115
- dispatch(navigateCommand('/second'))
112
+ });
113
+ dispatch(navigateCommand("/first"));
114
+ await vi.waitFor(() => expect(calls).toEqual(["/first"]));
115
+ dispatch(navigateCommand("/second"));
116
116
  // The second command must not overtake the first while it is still waiting.
117
- expect(calls).toEqual(['/first'])
118
- release('/first')
119
- await vi.waitFor(() => expect(calls).toEqual(['/first', '/second']))
120
- expect(push.mock.calls.flat()).toEqual(['/first'])
121
- release('/second')
117
+ expect(calls).toEqual(["/first"]);
118
+ release("/first");
119
+ await vi.waitFor(() => expect(calls).toEqual(["/first", "/second"]));
120
+ expect(push.mock.calls.flat()).toEqual(["/first"]);
121
+ release("/second");
122
122
  await vi.waitFor(() =>
123
- expect(push.mock.calls.flat()).toEqual(['/first', '/second']),
124
- )
125
- })
123
+ expect(push.mock.calls.flat()).toEqual(["/first", "/second"]),
124
+ );
125
+ });
126
126
 
127
- it('ignores malformed payloads and unknown command types', async () => {
128
- const push = vi.fn()
129
- const awaitRoute = vi.fn(async () => true)
127
+ it("ignores malformed payloads and unknown command types", async () => {
128
+ const push = vi.fn();
129
+ const awaitRoute = vi.fn(async () => true);
130
130
  const dispatch = createHostCommandDispatcher({
131
131
  router: { push },
132
132
  devReload: { awaitRoute },
133
- })
134
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
135
- dispatch('not json')
136
- dispatch(JSON.stringify({ type: 'host_command_unknown' }))
137
- expect(push).not.toHaveBeenCalled()
138
- expect(awaitRoute).not.toHaveBeenCalled()
139
- expect(warn).toHaveBeenCalledOnce()
140
- warn.mockRestore()
141
- })
142
- })
133
+ });
134
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
135
+ dispatch("not an object");
136
+ dispatch({ type: "host_command_unknown" });
137
+ expect(push).not.toHaveBeenCalled();
138
+ expect(awaitRoute).not.toHaveBeenCalled();
139
+ expect(warn).toHaveBeenCalledOnce();
140
+ warn.mockRestore();
141
+ });
142
+
143
+ it("never sends the dashboard off its own origin, whatever the agent asks", async () => {
144
+ const push = vi.fn();
145
+ const awaitRoute = vi.fn(async () => true);
146
+ const dispatch = createHostCommandDispatcher({
147
+ router: { push },
148
+ devReload: { awaitRoute },
149
+ });
150
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
151
+ for (const path of [
152
+ "https://attacker.example/steal",
153
+ "//attacker.example/steal",
154
+ "/\\attacker.example/steal",
155
+ "javascript:alert(document.cookie)",
156
+ "relative/path",
157
+ "",
158
+ ]) {
159
+ dispatch(navigateCommand(path));
160
+ }
161
+ dispatch({ type: "host_command_navigate", path: 42 });
162
+ await Promise.resolve();
163
+ expect(push).not.toHaveBeenCalled();
164
+ expect(awaitRoute).not.toHaveBeenCalled();
165
+ warn.mockRestore();
166
+ });
167
+
168
+ it("follows a path of the dashboard, query and hash included", async () => {
169
+ const push = vi.fn();
170
+ const dispatch = createHostCommandDispatcher({
171
+ router: { push },
172
+ devReload: { awaitRoute: async () => true },
173
+ });
174
+ dispatch(navigateCommand("/modules/ai/settings?tab=1#top"));
175
+ await vi.waitFor(() =>
176
+ expect(push).toHaveBeenCalledExactlyOnceWith(
177
+ "/modules/ai/settings?tab=1#top",
178
+ ),
179
+ );
180
+ });
181
+ });
@@ -1,45 +1,76 @@
1
- import { describe, expect, it, vi } from 'vitest'
2
- import { installCurrentPageTracker } from '../app/runtime/current-page'
3
- import { installNavigationCompleteEmitter } from '../app/runtime/navigation-complete'
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { installCurrentPageTracker } from "../app/runtime/current-page";
3
+ import { installNavigationCompleteEmitter } from "../app/runtime/navigation-complete";
4
4
 
5
5
  function navigate(url: string): void {
6
6
  document.dispatchEvent(
7
- new CustomEvent('inertia:navigate', {
7
+ new CustomEvent("inertia:navigate", {
8
8
  detail: { page: { url } },
9
9
  }),
10
- )
10
+ );
11
11
  }
12
12
 
13
- describe('Inertia host navigation', () => {
14
- it('reports the initial pathname and completed visits, then unsubscribes', () => {
15
- window.history.replaceState({}, '', '/initial?tab=one#section')
16
- const send = vi.fn()
17
- const stop = installCurrentPageTracker({ send })
13
+ async function flushMutations(): Promise<void> {
14
+ await new Promise((resolve) => setTimeout(resolve, 0));
15
+ }
16
+
17
+ describe("Inertia host navigation", () => {
18
+ it("reports the initial pathname and completed visits, then unsubscribes", () => {
19
+ window.history.replaceState({}, "", "/initial?tab=one#section");
20
+ const send = vi.fn();
21
+ const stop = installCurrentPageTracker({ send });
22
+ expect(send).toHaveBeenLastCalledWith({
23
+ type: "host_state_update",
24
+ currentPage: { path: "/initial" },
25
+ });
26
+ navigate("/other/deep?tab=two#anchor");
27
+ expect(send).toHaveBeenLastCalledWith({
28
+ type: "host_state_update",
29
+ currentPage: { path: "/other/deep" },
30
+ });
31
+ stop();
32
+ navigate("/ignored");
33
+ expect(send).toHaveBeenCalledTimes(2);
34
+ });
35
+
36
+ it("carries the document title and reports a title rendered after navigation", async () => {
37
+ window.history.replaceState({}, "", "/orders");
38
+ document.title = "Orders";
39
+ const send = vi.fn();
40
+ const stop = installCurrentPageTracker({ send });
18
41
  expect(send).toHaveBeenLastCalledWith({
19
- type: 'host_state_update',
20
- currentPage: { path: '/initial' },
21
- })
22
- navigate('/other/deep?tab=two#anchor')
42
+ type: "host_state_update",
43
+ currentPage: { path: "/orders", title: "Orders" },
44
+ });
45
+ navigate("/customers");
46
+ document.title = "Customers";
47
+ await flushMutations();
23
48
  expect(send).toHaveBeenLastCalledWith({
24
- type: 'host_state_update',
25
- currentPage: { path: '/other/deep' },
26
- })
27
- stop()
28
- navigate('/ignored')
29
- expect(send).toHaveBeenCalledTimes(2)
30
- })
49
+ type: "host_state_update",
50
+ currentPage: { path: "/customers", title: "Customers" },
51
+ });
52
+ const sentCount = send.mock.calls.length;
53
+ document.title = "Customers";
54
+ await flushMutations();
55
+ expect(send).toHaveBeenCalledTimes(sentCount);
56
+ stop();
57
+ document.title = "Ignored";
58
+ await flushMutations();
59
+ expect(send).toHaveBeenCalledTimes(sentCount);
60
+ document.title = "";
61
+ });
31
62
 
32
- it('emits completion only after navigation and removes its event listener', () => {
33
- const send = vi.fn()
34
- const stop = installNavigationCompleteEmitter({ send })
35
- expect(send).not.toHaveBeenCalled()
36
- navigate('https://example.test/finished?view=graph#node')
63
+ it("emits completion only after navigation and removes its event listener", () => {
64
+ const send = vi.fn();
65
+ const stop = installNavigationCompleteEmitter({ send });
66
+ expect(send).not.toHaveBeenCalled();
67
+ navigate("https://example.test/finished?view=graph#node");
37
68
  expect(send).toHaveBeenCalledExactlyOnceWith({
38
- type: 'host_navigation_complete',
39
- path: '/finished',
40
- })
41
- stop()
42
- navigate('/ignored')
43
- expect(send).toHaveBeenCalledTimes(1)
44
- })
45
- })
69
+ type: "host_navigation_complete",
70
+ path: "/finished",
71
+ });
72
+ stop();
73
+ navigate("/ignored");
74
+ expect(send).toHaveBeenCalledTimes(1);
75
+ });
76
+ });
@@ -1,91 +1,91 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import {
3
3
  OVERLAY_PREFS_DEBOUNCE_MS,
4
4
  OVERLAY_PREFS_STORAGE_KEY,
5
- } from '../app/runtime/constants'
5
+ } from "../app/runtime/constants";
6
6
  import {
7
7
  flushPrefsForTesting,
8
8
  type OverlayPrefs,
9
9
  readPrefs,
10
10
  resetPrefsForTesting,
11
11
  writePrefsDebounced,
12
- } from '../app/runtime/overlay-prefs'
12
+ } from "../app/runtime/overlay-prefs";
13
13
 
14
14
  const SAMPLE_PREFS: OverlayPrefs = {
15
15
  width: 400,
16
16
  isOpen: false,
17
- }
17
+ };
18
18
 
19
19
  const FIRST_PREFS: OverlayPrefs = {
20
20
  width: 300,
21
21
  isOpen: false,
22
- }
22
+ };
23
23
 
24
24
  const SECOND_PREFS: OverlayPrefs = {
25
25
  width: 320,
26
26
  isOpen: false,
27
- }
27
+ };
28
28
 
29
29
  const FINAL_PREFS: OverlayPrefs = {
30
30
  width: 360,
31
31
  isOpen: true,
32
- }
32
+ };
33
33
 
34
- describe('overlay-prefs storage', () => {
34
+ describe("overlay-prefs storage", () => {
35
35
  beforeEach(() => {
36
- resetPrefsForTesting()
37
- })
36
+ resetPrefsForTesting();
37
+ });
38
38
 
39
39
  afterEach(() => {
40
- resetPrefsForTesting()
41
- })
40
+ resetPrefsForTesting();
41
+ });
42
42
 
43
- it('returns null when no prefs are stored', () => {
44
- expect(readPrefs()).toBeNull()
45
- })
43
+ it("returns null when no prefs are stored", () => {
44
+ expect(readPrefs()).toBeNull();
45
+ });
46
46
 
47
- it('round-trips a prefs object via debounced write then read', () => {
48
- writePrefsDebounced(SAMPLE_PREFS)
49
- flushPrefsForTesting()
50
- expect(readPrefs()).toEqual(SAMPLE_PREFS)
51
- })
47
+ it("round-trips a prefs object via debounced write then read", () => {
48
+ writePrefsDebounced(SAMPLE_PREFS);
49
+ flushPrefsForTesting();
50
+ expect(readPrefs()).toEqual(SAMPLE_PREFS);
51
+ });
52
52
 
53
- it('returns null when the stored payload is malformed JSON', () => {
54
- globalThis.localStorage.setItem(OVERLAY_PREFS_STORAGE_KEY, '{not json')
55
- expect(readPrefs()).toBeNull()
56
- })
53
+ it("returns null when the stored payload is malformed JSON", () => {
54
+ globalThis.localStorage.setItem(OVERLAY_PREFS_STORAGE_KEY, "{not json");
55
+ expect(readPrefs()).toBeNull();
56
+ });
57
57
 
58
- it('returns null when the stored payload is missing fields', () => {
58
+ it("returns null when the stored payload is missing fields", () => {
59
59
  globalThis.localStorage.setItem(
60
60
  OVERLAY_PREFS_STORAGE_KEY,
61
61
  JSON.stringify({ x: 0, y: 0 }),
62
- )
63
- expect(readPrefs()).toBeNull()
64
- })
65
- })
62
+ );
63
+ expect(readPrefs()).toBeNull();
64
+ });
65
+ });
66
66
 
67
- describe('writePrefsDebounced timing', () => {
67
+ describe("writePrefsDebounced timing", () => {
68
68
  beforeEach(() => {
69
- vi.useFakeTimers()
70
- resetPrefsForTesting()
71
- })
69
+ vi.useFakeTimers();
70
+ resetPrefsForTesting();
71
+ });
72
72
 
73
73
  afterEach(() => {
74
- vi.useRealTimers()
75
- resetPrefsForTesting()
76
- })
74
+ vi.useRealTimers();
75
+ resetPrefsForTesting();
76
+ });
77
77
 
78
- it('does not persist before the debounce window elapses', () => {
79
- writePrefsDebounced(FIRST_PREFS)
80
- vi.advanceTimersByTime(OVERLAY_PREFS_DEBOUNCE_MS - 1)
81
- expect(readPrefs()).toBeNull()
82
- })
78
+ it("does not persist before the debounce window elapses", () => {
79
+ writePrefsDebounced(FIRST_PREFS);
80
+ vi.advanceTimersByTime(OVERLAY_PREFS_DEBOUNCE_MS - 1);
81
+ expect(readPrefs()).toBeNull();
82
+ });
83
83
 
84
- it('persists only the last call when invoked rapidly', () => {
85
- writePrefsDebounced(FIRST_PREFS)
86
- writePrefsDebounced(SECOND_PREFS)
87
- writePrefsDebounced(FINAL_PREFS)
88
- vi.advanceTimersByTime(OVERLAY_PREFS_DEBOUNCE_MS)
89
- expect(readPrefs()).toEqual(FINAL_PREFS)
90
- })
91
- })
84
+ it("persists only the last call when invoked rapidly", () => {
85
+ writePrefsDebounced(FIRST_PREFS);
86
+ writePrefsDebounced(SECOND_PREFS);
87
+ writePrefsDebounced(FINAL_PREFS);
88
+ vi.advanceTimersByTime(OVERLAY_PREFS_DEBOUNCE_MS);
89
+ expect(readPrefs()).toEqual(FINAL_PREFS);
90
+ });
91
+ });
@@ -0,0 +1,58 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import {
3
+ OVERLAY_DEFAULT_WIDTH_PX,
4
+ OVERLAY_MIN_WIDTH_PX,
5
+ OVERLAY_OUTSIDE_TOGGLE_SUPPRESS_MS,
6
+ } from "../app/runtime/constants";
7
+ import {
8
+ flushPrefsForTesting,
9
+ readPrefs,
10
+ resetPrefsForTesting,
11
+ } from "../app/runtime/overlay-prefs";
12
+ import { clampWidth, createChatPanelState } from "../app/runtime/panel-state";
13
+
14
+ afterEach(() => {
15
+ resetPrefsForTesting();
16
+ });
17
+
18
+ describe("chat panel state", () => {
19
+ it("starts closed at the default width, then remembers what the user chose", () => {
20
+ const panel = createChatPanelState();
21
+ expect(panel.isOpen.value).toBe(false);
22
+ expect(panel.width.value).toBe(OVERLAY_DEFAULT_WIDTH_PX);
23
+ panel.toggle();
24
+ panel.resize(520);
25
+ panel.commitWidth();
26
+ flushPrefsForTesting();
27
+ expect(readPrefs()).toEqual({ width: 520, isOpen: true });
28
+ const reloaded = createChatPanelState();
29
+ expect(reloaded.isOpen.value).toBe(true);
30
+ expect(reloaded.width.value).toBe(520);
31
+ });
32
+
33
+ it("keeps the width between the minimum and most of the viewport", () => {
34
+ expect(clampWidth(10)).toBe(OVERLAY_MIN_WIDTH_PX);
35
+ expect(clampWidth(100_000)).toBeLessThan(globalThis.innerWidth);
36
+ });
37
+
38
+ it("does not let the launcher reopen the panel an outside click just closed", () => {
39
+ const panel = createChatPanelState();
40
+ panel.toggle();
41
+ panel.closeFromOutside(performance.now());
42
+ panel.toggleFromLauncher();
43
+ expect(panel.isOpen.value).toBe(false);
44
+ });
45
+
46
+ it("lets the launcher open the panel again a moment later, and the shortcut at once", () => {
47
+ const panel = createChatPanelState();
48
+ panel.toggle();
49
+ panel.closeFromOutside(performance.now());
50
+ panel.toggle();
51
+ expect(panel.isOpen.value).toBe(true);
52
+ panel.closeFromOutside(
53
+ performance.now() - OVERLAY_OUTSIDE_TOGGLE_SUPPRESS_MS - 1,
54
+ );
55
+ panel.toggleFromLauncher();
56
+ expect(panel.isOpen.value).toBe(true);
57
+ });
58
+ });
@@ -1,39 +1,39 @@
1
- import { nextTick, ref } from 'vue'
2
- import { describe, expect, it, vi } from 'vitest'
3
- import { runWhenLoggedIn } from '../app/runtime/session-gate'
1
+ import { nextTick, ref } from "vue";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { runWhenLoggedIn } from "../app/runtime/session-gate";
4
4
 
5
- describe('session gate', () => {
6
- it('starts immediately when the session is already hydrated', () => {
7
- const start = vi.fn()
8
- runWhenLoggedIn(() => true, start)
9
- expect(start).toHaveBeenCalledTimes(1)
10
- })
5
+ describe("session gate", () => {
6
+ it("starts immediately when the session is already hydrated", () => {
7
+ const start = vi.fn();
8
+ runWhenLoggedIn(() => true, start);
9
+ expect(start).toHaveBeenCalledTimes(1);
10
+ });
11
11
 
12
- it('starts once the session appears, and only once', async () => {
13
- const loggedIn = ref(false)
14
- const start = vi.fn()
15
- runWhenLoggedIn(() => loggedIn.value, start)
16
- expect(start).not.toHaveBeenCalled()
12
+ it("starts once the session appears, and only once", async () => {
13
+ const loggedIn = ref(false);
14
+ const start = vi.fn();
15
+ runWhenLoggedIn(() => loggedIn.value, start);
16
+ expect(start).not.toHaveBeenCalled();
17
17
 
18
- loggedIn.value = true
19
- await nextTick()
20
- expect(start).toHaveBeenCalledTimes(1)
18
+ loggedIn.value = true;
19
+ await nextTick();
20
+ expect(start).toHaveBeenCalledTimes(1);
21
21
 
22
- loggedIn.value = false
23
- await nextTick()
24
- loggedIn.value = true
25
- await nextTick()
26
- expect(start).toHaveBeenCalledTimes(1)
27
- })
22
+ loggedIn.value = false;
23
+ await nextTick();
24
+ loggedIn.value = true;
25
+ await nextTick();
26
+ expect(start).toHaveBeenCalledTimes(1);
27
+ });
28
28
 
29
- it('never starts once the gate is stopped', async () => {
30
- const loggedIn = ref(false)
31
- const start = vi.fn()
32
- const stop = runWhenLoggedIn(() => loggedIn.value, start)
29
+ it("never starts once the gate is stopped", async () => {
30
+ const loggedIn = ref(false);
31
+ const start = vi.fn();
32
+ const stop = runWhenLoggedIn(() => loggedIn.value, start);
33
33
 
34
- stop()
35
- loggedIn.value = true
36
- await nextTick()
37
- expect(start).not.toHaveBeenCalled()
38
- })
39
- })
34
+ stop();
35
+ loggedIn.value = true;
36
+ await nextTick();
37
+ expect(start).not.toHaveBeenCalled();
38
+ });
39
+ });