@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
@@ -0,0 +1,354 @@
1
+ import { flushPromises, mount, type VueWrapper } from "@vue/test-utils";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { nextTick, ref } from "vue";
4
+ import ChatPanel from "../app/components/ChatPanel.vue";
5
+ import {
6
+ ASSISTANT_SESSION_KEY,
7
+ type AssistantSession,
8
+ } from "../app/runtime/assistant-session";
9
+ import type { ChannelStatus } from "../app/runtime/channel-client";
10
+ import {
11
+ type ChatTransportHub,
12
+ createChatTransport,
13
+ } from "../app/runtime/chat-transport";
14
+ import { resetPrefsForTesting } from "../app/runtime/overlay-prefs";
15
+ import { createChatPanelState } from "../app/runtime/panel-state";
16
+ import type { SidecarStatus } from "../app/runtime/sidecar-status";
17
+
18
+ const CONVERSATION_ID = "conv-native-1";
19
+ const STALL_AFTER_MS = 20_000;
20
+
21
+ interface Harness {
22
+ session: AssistantSession;
23
+ hub: ChatTransportHub;
24
+ sent: Array<Record<string, unknown>>;
25
+ reconnects: number;
26
+ sidecarStatus: ReturnType<typeof ref<SidecarStatus>>;
27
+ setChannelStatus: (status: ChannelStatus) => void;
28
+ navigate: ReturnType<typeof vi.fn>;
29
+ }
30
+
31
+ function createHarness(): Harness {
32
+ let channelStatus: ChannelStatus = "connected";
33
+ const harness = {
34
+ sent: [] as Array<Record<string, unknown>>,
35
+ reconnects: 0,
36
+ sidecarStatus: ref<SidecarStatus>("connected"),
37
+ navigate: vi.fn(),
38
+ } as Harness;
39
+ harness.hub = createChatTransport({
40
+ send: (msg) => {
41
+ if (channelStatus !== "connected") return false;
42
+ harness.sent.push(msg as Record<string, unknown>);
43
+ return true;
44
+ },
45
+ reconnect: () => {
46
+ harness.reconnects += 1;
47
+ },
48
+ getStatus: () => channelStatus,
49
+ });
50
+ harness.setChannelStatus = (status) => {
51
+ channelStatus = status;
52
+ harness.hub.announceStatus(status);
53
+ };
54
+ harness.session = {
55
+ status: harness.sidecarStatus,
56
+ chat: harness.hub.transport,
57
+ panel: createChatPanelState(),
58
+ navigate: harness.navigate,
59
+ };
60
+ return harness;
61
+ }
62
+
63
+ let wrapper: VueWrapper | null = null;
64
+
65
+ function mountPanel(harness: Harness): VueWrapper {
66
+ wrapper = mount(ChatPanel, {
67
+ attachTo: document.body,
68
+ global: {
69
+ provide: { [ASSISTANT_SESSION_KEY as symbol]: harness.session },
70
+ stubs: { UIcon: true, USelect: true },
71
+ },
72
+ });
73
+ return wrapper;
74
+ }
75
+
76
+ async function openPanel(harness: Harness): Promise<VueWrapper> {
77
+ const mounted = mountPanel(harness);
78
+ harness.session.panel.toggle();
79
+ await nextTick();
80
+ await flushPromises();
81
+ return mounted;
82
+ }
83
+
84
+ function deliver(harness: Harness, event: Record<string, unknown>): void {
85
+ harness.hub.deliver({ conversationId: CONVERSATION_ID, ...event });
86
+ }
87
+
88
+ async function sendMessage(panel: VueWrapper, text: string): Promise<void> {
89
+ await panel.find("textarea").setValue(text);
90
+ await panel.find("form.composer").trigger("submit");
91
+ }
92
+
93
+ function sentOfType(harness: Harness, type: string) {
94
+ return harness.sent.filter((msg) => msg.type === type);
95
+ }
96
+
97
+ beforeEach(() => {
98
+ localStorage.setItem("dms-ai-conversation-id", CONVERSATION_ID);
99
+ });
100
+
101
+ afterEach(() => {
102
+ wrapper?.unmount();
103
+ wrapper = null;
104
+ resetPrefsForTesting();
105
+ localStorage.clear();
106
+ vi.useRealTimers();
107
+ });
108
+
109
+ describe("the chat as a component of the dashboard", () => {
110
+ it("renders in the dashboard document itself: no iframe, no second document", async () => {
111
+ const harness = createHarness();
112
+ const panel = await openPanel(harness);
113
+ expect(document.querySelector("iframe")).toBeNull();
114
+ expect(document.getElementById("dms-ai-overlay-root")).not.toBeNull();
115
+ expect(panel.find(".chat-view").exists()).toBe(true);
116
+ expect(Reflect.has(globalThis, "dmsAiChatTransport")).toBe(false);
117
+ });
118
+
119
+ it("says hello with its conversation on the shared stream when it opens", async () => {
120
+ const harness = createHarness();
121
+ await openPanel(harness);
122
+ expect(sentOfType(harness, "hello")).toEqual([
123
+ { type: "hello", role: "chat", conversationId: CONVERSATION_ID },
124
+ ]);
125
+ });
126
+
127
+ it("keeps its conversation when the panel is closed and opened again", async () => {
128
+ const harness = createHarness();
129
+ const panel = await openPanel(harness);
130
+ await sendMessage(panel, "build me a page");
131
+ harness.session.panel.close();
132
+ await nextTick();
133
+ harness.session.panel.toggle();
134
+ await nextTick();
135
+ expect(panel.text()).toContain("build me a page");
136
+ });
137
+
138
+ it("opens the settings page through the dashboard router, not the sidecar", async () => {
139
+ const harness = createHarness();
140
+ const panel = await openPanel(harness);
141
+ await panel.find('button[aria-label="Settings"]').trigger("click");
142
+ expect(harness.navigate).toHaveBeenCalledExactlyOnceWith(
143
+ "/modules/ai/settings",
144
+ );
145
+ expect(sentOfType(harness, "request_host_navigate")).toEqual([]);
146
+ });
147
+
148
+ it("closes from its own button, and on a click elsewhere in the dashboard", async () => {
149
+ const harness = createHarness();
150
+ const panel = await openPanel(harness);
151
+ panel
152
+ .find(".chat-view")
153
+ .element.dispatchEvent(
154
+ new PointerEvent("pointerdown", { bubbles: true, composed: true }),
155
+ );
156
+ expect(harness.session.panel.isOpen.value).toBe(true);
157
+ document.body.dispatchEvent(
158
+ new PointerEvent("pointerdown", { bubbles: true, composed: true }),
159
+ );
160
+ expect(harness.session.panel.isOpen.value).toBe(false);
161
+ harness.session.panel.toggle();
162
+ await nextTick();
163
+ await panel.find('button[aria-label="Close"]').trigger("click");
164
+ expect(harness.session.panel.isOpen.value).toBe(false);
165
+ });
166
+
167
+ it("stays open on a click in what the dashboard portals out of the app", async () => {
168
+ const harness = createHarness();
169
+ await openPanel(harness);
170
+ const overlays = document.createElement("div");
171
+ overlays.id = "dms-overlays";
172
+ const toast = document.createElement("button");
173
+ overlays.append(toast);
174
+ document.body.append(overlays);
175
+ toast.dispatchEvent(
176
+ new PointerEvent("pointerdown", { bubbles: true, composed: true }),
177
+ );
178
+ expect(harness.session.panel.isOpen.value).toBe(true);
179
+ overlays.remove();
180
+ });
181
+
182
+ it("covers the chat with the sidecar status while it is not reachable, and can still be closed", async () => {
183
+ const harness = createHarness();
184
+ const panel = await openPanel(harness);
185
+ harness.sidecarStatus.value = "reviving";
186
+ await nextTick();
187
+ expect(panel.find(".dms-ai-panel-status").text()).toContain(
188
+ "Reconnecting the assistant",
189
+ );
190
+ harness.sidecarStatus.value = "unavailable";
191
+ await nextTick();
192
+ expect(panel.find(".dms-ai-panel-status").text()).toContain(
193
+ "Assistant unavailable",
194
+ );
195
+ await panel.find(".dms-ai-panel-status button").trigger("click");
196
+ expect(harness.session.panel.isOpen.value).toBe(false);
197
+ harness.sidecarStatus.value = "connected";
198
+ await nextTick();
199
+ expect(panel.find(".dms-ai-panel-status").exists()).toBe(false);
200
+ });
201
+
202
+ it("renders the answer as markup, with nothing the model wrote running in the dashboard", async () => {
203
+ vi.useFakeTimers();
204
+ const harness = createHarness();
205
+ const panel = await openPanel(harness);
206
+ await sendMessage(panel, "go");
207
+ deliver(harness, {
208
+ type: "assistant_message_chunk",
209
+ text: '**Done** <img src=x onerror="window.pwned=1"> [x](javascript:alert(1))',
210
+ });
211
+ await vi.advanceTimersByTimeAsync(100);
212
+ deliver(harness, { type: "run_done" });
213
+ await nextTick();
214
+ const answer = panel.find(".chat-markdown");
215
+ expect(answer.find("strong").text()).toBe("Done");
216
+ expect(answer.find("img").exists()).toBe(false);
217
+ expect(answer.find("a").exists()).toBe(false);
218
+ expect(Reflect.get(globalThis, "pwned")).toBeUndefined();
219
+ });
220
+ });
221
+
222
+ describe("a running turn shows what it is doing, and never spins forever", () => {
223
+ it("shows the activity the sidecar reports, with the elapsed time", async () => {
224
+ const harness = createHarness();
225
+ const panel = await openPanel(harness);
226
+ await sendMessage(panel, "reproduce this page");
227
+ deliver(harness, {
228
+ type: "run_progress",
229
+ activity: "writing",
230
+ detail: "Write",
231
+ elapsedMs: 72_000,
232
+ idleMs: 0,
233
+ });
234
+ await nextTick();
235
+ expect(panel.find(".thinking").text()).toContain("Preparing · Write file…");
236
+ expect(panel.find(".thinking-clock").text()).toBe("1:12");
237
+ });
238
+
239
+ it("says so after 20 s without news about the turn, and reconnects or stops from there", async () => {
240
+ vi.useFakeTimers();
241
+ const harness = createHarness();
242
+ const panel = await openPanel(harness);
243
+ await sendMessage(panel, "reproduce this page");
244
+ await vi.advanceTimersByTimeAsync(STALL_AFTER_MS + 1_000);
245
+ const notice = panel.find(".run-stalled");
246
+ expect(notice.text()).toContain("No news from the assistant for 0:2");
247
+ const [reconnect, stop] = notice.findAll("button");
248
+ await reconnect?.trigger("click");
249
+ expect(harness.reconnects).toBe(1);
250
+ await stop?.trigger("click");
251
+ expect(sentOfType(harness, "interrupt_turn")).toHaveLength(1);
252
+ });
253
+
254
+ it("stays quiet while the sidecar keeps reporting a long step", async () => {
255
+ vi.useFakeTimers();
256
+ const harness = createHarness();
257
+ const panel = await openPanel(harness);
258
+ await sendMessage(panel, "reproduce this page");
259
+ for (let beat = 0; beat < 8; beat += 1) {
260
+ await vi.advanceTimersByTimeAsync(5_000);
261
+ deliver(harness, {
262
+ type: "run_progress",
263
+ activity: "tool",
264
+ detail: "Write",
265
+ elapsedMs: (beat + 1) * 5_000,
266
+ idleMs: 0,
267
+ });
268
+ }
269
+ await nextTick();
270
+ expect(panel.find(".run-stalled").exists()).toBe(false);
271
+ expect(panel.find(".thinking").exists()).toBe(true);
272
+ });
273
+
274
+ it("shows a run error with Retry, which sends the message again", async () => {
275
+ const harness = createHarness();
276
+ const panel = await openPanel(harness);
277
+ await sendMessage(panel, "reproduce this page");
278
+ deliver(harness, { type: "run_error", error: "API Error: 529 overloaded" });
279
+ await nextTick();
280
+ const error = panel.find(".message-bubble-error");
281
+ expect(error.text()).toContain("API Error: 529 overloaded");
282
+ await error.find("button").trigger("click");
283
+ expect(sentOfType(harness, "user_message")).toHaveLength(2);
284
+ });
285
+
286
+ it("offers no Retry for a run error that sending again cannot fix", async () => {
287
+ const harness = createHarness();
288
+ const panel = await openPanel(harness);
289
+ await sendMessage(panel, "reproduce this page");
290
+ deliver(harness, {
291
+ type: "run_error",
292
+ error: "Prompt is too long",
293
+ isRetryable: false,
294
+ });
295
+ await nextTick();
296
+ const error = panel.find(".message-bubble-error");
297
+ expect(error.text()).toContain("Prompt is too long");
298
+ expect(error.find("button").exists()).toBe(false);
299
+ });
300
+
301
+ it("says a message was not sent when the stream dropped as it left", async () => {
302
+ const harness = createHarness();
303
+ const panel = await openPanel(harness);
304
+ await panel.find("textarea").setValue("reproduce this page");
305
+ harness.setChannelStatus("reconnecting");
306
+ await panel.find("form.composer").trigger("submit");
307
+ expect(panel.find(".message-bubble-error").text()).toContain(
308
+ "Your message was not sent",
309
+ );
310
+ expect(sentOfType(harness, "user_message")).toEqual([]);
311
+ });
312
+
313
+ it("shows the error a run left in the stored transcript after a reload", async () => {
314
+ const harness = createHarness();
315
+ const panel = await openPanel(harness);
316
+ deliver(harness, {
317
+ type: "conversation_snapshot",
318
+ messages: [
319
+ { role: "user", content: "go", timestampMs: 1 },
320
+ { role: "error", content: "The run was stopped.", timestampMs: 2 },
321
+ ],
322
+ });
323
+ await nextTick();
324
+ expect(panel.find(".message-bubble-error").text()).toContain(
325
+ "The run was stopped.",
326
+ );
327
+ });
328
+ });
329
+
330
+ describe("the shared stream going away and coming back", () => {
331
+ it("says reconnecting and holds the input, then says hello again and picks the transcript up, as after a dms-ai hot reload", async () => {
332
+ const harness = createHarness();
333
+ const panel = await openPanel(harness);
334
+ harness.setChannelStatus("reconnecting");
335
+ await nextTick();
336
+ expect(panel.find(".connection-banner").text()).toContain("Reconnecting");
337
+ expect(panel.find("textarea").attributes("disabled")).toBeDefined();
338
+ harness.setChannelStatus("connected");
339
+ harness.hub.announceReady();
340
+ deliver(harness, {
341
+ type: "conversation_snapshot",
342
+ messages: [
343
+ { role: "user", content: "before the reload", timestampMs: 1 },
344
+ ],
345
+ });
346
+ deliver(harness, { type: "run_resumed" });
347
+ await nextTick();
348
+ expect(sentOfType(harness, "hello")).toHaveLength(2);
349
+ expect(panel.find(".connection-banner").exists()).toBe(false);
350
+ expect(panel.find("textarea").attributes("disabled")).toBeUndefined();
351
+ expect(panel.text()).toContain("before the reload");
352
+ expect(panel.find(".thinking").exists()).toBe(true);
353
+ });
354
+ });
@@ -1,67 +1,94 @@
1
- import { describe, expect, it, vi } from 'vitest'
2
- import {
3
- createChatTransport,
4
- exposeChatTransport,
5
- } from '../app/runtime/chat-transport'
6
- import { CHAT_TRANSPORT_KEY } from '../app/runtime/constants'
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createChatTransport } from "../app/runtime/chat-transport";
3
+ import { createFrameRouter } from "../app/runtime/frame-router";
7
4
 
8
5
  function hub() {
9
6
  const actions = {
10
7
  send: vi.fn(() => true),
11
8
  reconnect: vi.fn(),
12
- getStatus: vi.fn(() => 'connected' as const),
13
- }
14
- return { actions, chat: createChatTransport(actions) }
9
+ getStatus: vi.fn(() => "connected" as const),
10
+ };
11
+ return { actions, chat: createChatTransport(actions) };
15
12
  }
16
13
 
17
- describe('chat transport', () => {
18
- it('lends the chat its channel: frames, readiness and status in, messages out', () => {
19
- const { actions, chat } = hub()
20
- const frames: string[] = []
21
- const statuses: string[] = []
22
- const ready = vi.fn()
23
- chat.transport.onFrame((raw) => frames.push(raw))
24
- chat.transport.onStatus((status) => statuses.push(status))
25
- chat.transport.onReady(ready)
26
- chat.deliverFrame('{"type":"run_done"}')
27
- chat.announceStatus('reconnecting')
28
- chat.announceReady()
29
- expect(chat.transport.send({ type: 'hello' })).toBe(true)
30
- chat.transport.reconnect()
31
- expect(frames).toEqual(['{"type":"run_done"}'])
32
- expect(statuses).toEqual(['reconnecting'])
33
- expect(ready).toHaveBeenCalledTimes(1)
34
- expect(actions.send).toHaveBeenCalledWith({ type: 'hello' })
35
- expect(actions.reconnect).toHaveBeenCalledTimes(1)
36
- expect(chat.transport.getStatus()).toBe('connected')
37
- })
14
+ describe("chat transport", () => {
15
+ it("gives the chat its messages, readiness and status, and sends through the stream", () => {
16
+ const { actions, chat } = hub();
17
+ const messages: object[] = [];
18
+ const statuses: string[] = [];
19
+ const ready = vi.fn();
20
+ chat.transport.onMessage((msg) => messages.push(msg));
21
+ chat.transport.onStatus((status) => statuses.push(status));
22
+ chat.transport.onReady(ready);
23
+ chat.deliver({ type: "run_done" });
24
+ chat.announceStatus("reconnecting");
25
+ chat.announceReady();
26
+ expect(chat.transport.send({ type: "hello" })).toBe(true);
27
+ chat.transport.reconnect();
28
+ expect(messages).toEqual([{ type: "run_done" }]);
29
+ expect(statuses).toEqual(["reconnecting"]);
30
+ expect(ready).toHaveBeenCalledTimes(1);
31
+ expect(actions.send).toHaveBeenCalledWith({ type: "hello" });
32
+ expect(actions.reconnect).toHaveBeenCalledTimes(1);
33
+ expect(chat.transport.getStatus()).toBe("connected");
34
+ });
38
35
 
39
- it('stops delivering to a listener once it unsubscribed', () => {
40
- const { chat } = hub()
41
- const frames: string[] = []
42
- const unsubscribe = chat.transport.onFrame((raw) => frames.push(raw))
43
- unsubscribe()
44
- chat.deliverFrame('{}')
45
- expect(frames).toEqual([])
46
- })
36
+ it("stops delivering to a listener once it unsubscribed", () => {
37
+ const { chat } = hub();
38
+ const messages: object[] = [];
39
+ const unsubscribe = chat.transport.onMessage((msg) => messages.push(msg));
40
+ unsubscribe();
41
+ chat.deliver({});
42
+ expect(messages).toEqual([]);
43
+ });
47
44
 
48
- it('keeps delivering when one listener throws, as a gone chat document would', () => {
49
- const { chat } = hub()
50
- const frames: string[] = []
51
- const error = vi.spyOn(console, 'error').mockImplementation(() => undefined)
52
- chat.transport.onFrame(() => {
53
- throw new Error('dead document')
54
- })
55
- chat.transport.onFrame((raw) => frames.push(raw))
56
- chat.deliverFrame('{}')
57
- expect(frames).toEqual(['{}'])
58
- error.mockRestore()
59
- })
45
+ it("keeps delivering when one listener throws", () => {
46
+ const { chat } = hub();
47
+ const messages: object[] = [];
48
+ const error = vi
49
+ .spyOn(console, "error")
50
+ .mockImplementation(() => undefined);
51
+ chat.transport.onMessage(() => {
52
+ throw new Error("broken listener");
53
+ });
54
+ chat.transport.onMessage((msg) => messages.push(msg));
55
+ chat.deliver({ type: "run_done" });
56
+ expect(messages).toEqual([{ type: "run_done" }]);
57
+ error.mockRestore();
58
+ });
60
59
 
61
- it('publishes itself where the same-origin chat document looks', () => {
62
- const { chat } = hub()
63
- exposeChatTransport(chat.transport)
64
- expect(Reflect.get(globalThis, CHAT_TRANSPORT_KEY)).toBe(chat.transport)
65
- Reflect.deleteProperty(globalThis, CHAT_TRANSPORT_KEY)
66
- })
67
- })
60
+ it("publishes nothing on the window: no other document borrows it any more", () => {
61
+ hub();
62
+ expect(Reflect.has(globalThis, "dmsAiChatTransport")).toBe(false);
63
+ });
64
+ });
65
+
66
+ describe("frame routing on the tab's one stream", () => {
67
+ it("hands host commands to the dashboard and everything else to the chat, parsed once", () => {
68
+ const host = vi.fn();
69
+ const chat = vi.fn();
70
+ const route = createFrameRouter({ host, chat });
71
+ route('{"type":"host_command_navigate","path":"/x"}');
72
+ route(
73
+ '{"type":"assistant_message_chunk","conversationId":"c","text":"hi"}',
74
+ );
75
+ route('{"type":"settings_update","settings":{}}');
76
+ expect(host.mock.calls).toEqual([
77
+ [{ type: "host_command_navigate", path: "/x" }],
78
+ ]);
79
+ expect(chat.mock.calls.map(([msg]) => msg.type)).toEqual([
80
+ "assistant_message_chunk",
81
+ "settings_update",
82
+ ]);
83
+ });
84
+
85
+ it("drops what is not a typed message", () => {
86
+ const host = vi.fn();
87
+ const chat = vi.fn();
88
+ const route = createFrameRouter({ host, chat });
89
+ for (const raw of ["not json", "42", "null", '{"type":7}', "[]"])
90
+ route(raw);
91
+ expect(host).not.toHaveBeenCalled();
92
+ expect(chat).not.toHaveBeenCalled();
93
+ });
94
+ });