@antelopejs/dms-ai 0.9.0 → 0.9.2

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 (293) hide show
  1. package/README.md +32 -34
  2. package/dist/constants/http.d.ts +1 -0
  3. package/dist/constants/http.js +1 -0
  4. package/dist/constants/http.js.map +1 -1
  5. package/dist/database/catalog.js +21 -3
  6. package/dist/database/catalog.js.map +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/lifecycle/sidecar-owner.js.map +1 -1
  9. package/dist/lifecycle/skill-sources.js +19 -146
  10. package/dist/lifecycle/skill-sources.js.map +1 -1
  11. package/dist/lifecycle/spawn-sidecar.js +2 -2
  12. package/dist/lifecycle/spawn-sidecar.js.map +1 -1
  13. package/dist/lifecycle/terminate-process.d.ts +1 -0
  14. package/dist/lifecycle/terminate-process.js +8 -4
  15. package/dist/lifecycle/terminate-process.js.map +1 -1
  16. package/dist/logging/log-buffer.js +22 -14
  17. package/dist/logging/log-buffer.js.map +1 -1
  18. package/dist/pages/settings/availability.js +41 -5
  19. package/dist/pages/settings/availability.js.map +1 -1
  20. package/dist/pages/settings/options.d.ts +2 -1
  21. package/dist/pages/settings/options.js +29 -8
  22. package/dist/pages/settings/options.js.map +1 -1
  23. package/dist/pages/settings/sections.js +15 -4
  24. package/dist/pages/settings/sections.js.map +1 -1
  25. package/dist/pages-registry.js +2 -6
  26. package/dist/pages-registry.js.map +1 -1
  27. package/dist/routes/activity.js +4 -3
  28. package/dist/routes/activity.js.map +1 -1
  29. package/dist/routes/banner.d.ts +2 -1
  30. package/dist/routes/banner.js +4 -1
  31. package/dist/routes/banner.js.map +1 -1
  32. package/dist/routes/builder.d.ts +5 -5
  33. package/dist/routes/builder.js +62 -44
  34. package/dist/routes/builder.js.map +1 -1
  35. package/dist/routes/changes.js +8 -7
  36. package/dist/routes/changes.js.map +1 -1
  37. package/dist/routes/codex-account.js +11 -5
  38. package/dist/routes/codex-account.js.map +1 -1
  39. package/dist/routes/content-json.d.ts +6 -0
  40. package/dist/routes/content-json.js +12 -0
  41. package/dist/routes/content-json.js.map +1 -0
  42. package/dist/routes/metrics.js +8 -8
  43. package/dist/routes/metrics.js.map +1 -1
  44. package/dist/routes/settings.js +4 -3
  45. package/dist/routes/settings.js.map +1 -1
  46. package/dist/routes/sidecar-answers.d.ts +10 -0
  47. package/dist/routes/sidecar-answers.js +90 -0
  48. package/dist/routes/sidecar-answers.js.map +1 -0
  49. package/dist/routes/sidecar-results.d.ts +2 -1
  50. package/dist/routes/sidecar-results.js +8 -2
  51. package/dist/routes/sidecar-results.js.map +1 -1
  52. package/dist/routes/skills.d.ts +2 -2
  53. package/dist/routes/skills.js +3 -2
  54. package/dist/routes/skills.js.map +1 -1
  55. package/dist/routes/status.js +23 -4
  56. package/dist/routes/status.js.map +1 -1
  57. package/dist/sidecar/answer-schemas.d.ts +8 -0
  58. package/dist/sidecar/answer-schemas.js +60 -0
  59. package/dist/sidecar/answer-schemas.js.map +1 -0
  60. package/dist/sidecar/http-client.d.ts +4 -2
  61. package/dist/sidecar/http-client.js +5 -2
  62. package/dist/sidecar/http-client.js.map +1 -1
  63. package/dist/sidecar/index.d.ts +1 -0
  64. package/dist/sidecar/index.js +1 -0
  65. package/dist/sidecar/index.js.map +1 -1
  66. package/dist/sidecar/settings-values.d.ts +3 -0
  67. package/dist/sidecar/settings-values.js +33 -1
  68. package/dist/sidecar/settings-values.js.map +1 -1
  69. package/dist/types/status.d.ts +2 -2
  70. package/dist/vocabulary/enumerations.js +2 -0
  71. package/dist/vocabulary/enumerations.js.map +1 -1
  72. package/frontend-vue/app/chat/components/ChatView.vue +16 -49
  73. package/frontend-vue/app/chat/components/Composer.vue +23 -39
  74. package/frontend-vue/app/chat/components/NoticeCard.vue +9 -0
  75. package/frontend-vue/app/chat/components/ProviderNotice.vue +14 -3
  76. package/frontend-vue/app/chat/composables/conversation-events.ts +0 -3
  77. package/frontend-vue/app/chat/composables/useConversationMode.ts +8 -11
  78. package/frontend-vue/app/chat/composables/useFollowScroll.ts +77 -0
  79. package/frontend-vue/app/chat/constants/modes.ts +15 -0
  80. package/frontend-vue/app/chat/constants/protocol.ts +0 -1
  81. package/frontend-vue/app/chat/constants/settings.ts +1 -1
  82. package/frontend-vue/app/chat/types/protocol.ts +11 -9
  83. package/frontend-vue/app/chat/types/settings.ts +7 -2
  84. package/frontend-vue/app/chat/utils/permission-view.ts +1 -0
  85. package/frontend-vue/app/views/constants.ts +2 -0
  86. package/frontend-vue/app/views/types.ts +3 -1
  87. package/frontend-vue/i18n/locales/dms-ai-pages-en-GB.json +21 -11
  88. package/frontend-vue/i18n/locales/dms-ai-pages-fr-FR.json +21 -11
  89. package/frontend-vue/i18n/locales/dms-ai-panel-en-GB.json +11 -21
  90. package/frontend-vue/i18n/locales/dms-ai-panel-fr-FR.json +11 -21
  91. package/frontend-vue/i18n/locales/dms-ai-views-en-GB.json +2 -0
  92. package/frontend-vue/i18n/locales/dms-ai-views-fr-FR.json +2 -0
  93. package/frontend-vue/package.json +1 -1
  94. package/frontend-vue/tests/chat/chat-scroll.test.ts +251 -0
  95. package/frontend-vue/tests/chat/conversation-events.test.ts +1 -3
  96. package/frontend-vue/tests/chat/provider-notice.test.ts +33 -0
  97. package/frontend-vue/tests/chat-panel.test.ts +2 -4
  98. package/frontend-vue/tests/panel-modes.test.ts +146 -0
  99. package/frontend-vue/tests/panel-questions.test.ts +1 -19
  100. package/frontend-vue/tests/panel-states.test.ts +12 -16
  101. package/frontend-vue/tests/support/fake-layout.ts +119 -0
  102. package/frontend-vue/tests/support/panel-harness.ts +14 -6
  103. package/frontend-vue/tests/support/setup-layout.ts +3 -0
  104. package/frontend-vue/tests/support/stubs.ts +2 -0
  105. package/frontend-vue/vitest.config.ts +1 -0
  106. package/package.json +16 -10
  107. package/sidecar/dist/agent/always-ask-files.d.ts +2 -0
  108. package/sidecar/dist/agent/always-ask-files.js +34 -0
  109. package/sidecar/dist/agent/always-ask-files.js.map +1 -0
  110. package/sidecar/dist/agent/conversation-modes.d.ts +3 -12
  111. package/sidecar/dist/agent/conversation-modes.js +16 -69
  112. package/sidecar/dist/agent/conversation-modes.js.map +1 -1
  113. package/sidecar/dist/agent/effective-mode.d.ts +2 -5
  114. package/sidecar/dist/agent/effective-mode.js +1 -15
  115. package/sidecar/dist/agent/effective-mode.js.map +1 -1
  116. package/sidecar/dist/agent/permission-bus.d.ts +1 -2
  117. package/sidecar/dist/agent/permission-bus.js +1 -3
  118. package/sidecar/dist/agent/permission-bus.js.map +1 -1
  119. package/sidecar/dist/agent/permission-preview.d.ts +6 -0
  120. package/sidecar/dist/agent/permission-preview.js +35 -10
  121. package/sidecar/dist/agent/permission-preview.js.map +1 -1
  122. package/sidecar/dist/agent/permission-rules.js +2 -2
  123. package/sidecar/dist/agent/permission-rules.js.map +1 -1
  124. package/sidecar/dist/agent/project-secrets.d.ts +7 -0
  125. package/sidecar/dist/agent/project-secrets.js +61 -0
  126. package/sidecar/dist/agent/project-secrets.js.map +1 -0
  127. package/sidecar/dist/agent/provider.d.ts +5 -4
  128. package/sidecar/dist/agent/runner.d.ts +3 -2
  129. package/sidecar/dist/agent/runner.js +9 -3
  130. package/sidecar/dist/agent/runner.js.map +1 -1
  131. package/sidecar/dist/agent/session.d.ts +3 -3
  132. package/sidecar/dist/agent/session.js.map +1 -1
  133. package/sidecar/dist/agent/tool-kinds.d.ts +7 -1
  134. package/sidecar/dist/agent/tool-kinds.js +24 -5
  135. package/sidecar/dist/agent/tool-kinds.js.map +1 -1
  136. package/sidecar/dist/agent/typecheck.js +14 -1
  137. package/sidecar/dist/agent/typecheck.js.map +1 -1
  138. package/sidecar/dist/audit/activity-query.js +2 -0
  139. package/sidecar/dist/audit/activity-query.js.map +1 -1
  140. package/sidecar/dist/audit/activity.js +1 -0
  141. package/sidecar/dist/audit/activity.js.map +1 -1
  142. package/sidecar/dist/audit/call-audit.js.map +1 -1
  143. package/sidecar/dist/builder/builder-client.d.ts +3 -1
  144. package/sidecar/dist/builder/builder-client.js +13 -1
  145. package/sidecar/dist/builder/builder-client.js.map +1 -1
  146. package/sidecar/dist/constants/audit.d.ts +1 -3
  147. package/sidecar/dist/constants/audit.js +2 -1
  148. package/sidecar/dist/constants/audit.js.map +1 -1
  149. package/sidecar/dist/constants/builder.d.ts +3 -0
  150. package/sidecar/dist/constants/builder.js +3 -0
  151. package/sidecar/dist/constants/builder.js.map +1 -1
  152. package/sidecar/dist/constants/claude.d.ts +6 -0
  153. package/sidecar/dist/constants/claude.js +6 -0
  154. package/sidecar/dist/constants/claude.js.map +1 -1
  155. package/sidecar/dist/constants/codex.d.ts +4 -0
  156. package/sidecar/dist/constants/codex.js +4 -0
  157. package/sidecar/dist/constants/codex.js.map +1 -1
  158. package/sidecar/dist/constants/permissions.d.ts +2 -0
  159. package/sidecar/dist/constants/permissions.js +23 -0
  160. package/sidecar/dist/constants/permissions.js.map +1 -1
  161. package/sidecar/dist/constants/project-secrets.d.ts +5 -0
  162. package/sidecar/dist/constants/project-secrets.js +20 -0
  163. package/sidecar/dist/constants/project-secrets.js.map +1 -0
  164. package/sidecar/dist/constants/providers.d.ts +2 -0
  165. package/sidecar/dist/constants/providers.js +1 -0
  166. package/sidecar/dist/constants/providers.js.map +1 -1
  167. package/sidecar/dist/constants/settings.d.ts +1 -2
  168. package/sidecar/dist/constants/settings.js +2 -9
  169. package/sidecar/dist/constants/settings.js.map +1 -1
  170. package/sidecar/dist/constants/tool-kinds.d.ts +67 -2
  171. package/sidecar/dist/constants/tool-kinds.js +111 -34
  172. package/sidecar/dist/constants/tool-kinds.js.map +1 -1
  173. package/sidecar/dist/index.js.map +1 -1
  174. package/sidecar/dist/mcp/http-binding.js +1 -1
  175. package/sidecar/dist/mcp/http-binding.js.map +1 -1
  176. package/sidecar/dist/mcp/tools/builder-compose.d.ts +4 -4
  177. package/sidecar/dist/mcp/tools/builder-compose.js +4 -3
  178. package/sidecar/dist/mcp/tools/builder-compose.js.map +1 -1
  179. package/sidecar/dist/mcp/tools/builder-expr.d.ts +5 -2
  180. package/sidecar/dist/mcp/tools/builder-expr.js +8 -11
  181. package/sidecar/dist/mcp/tools/builder-expr.js.map +1 -1
  182. package/sidecar/dist/mcp/tools/builder-gate.js +3 -1
  183. package/sidecar/dist/mcp/tools/builder-gate.js.map +1 -1
  184. package/sidecar/dist/mcp/tools/builder-project.d.ts +2 -1
  185. package/sidecar/dist/mcp/tools/builder-project.js +94 -16
  186. package/sidecar/dist/mcp/tools/builder-project.js.map +1 -1
  187. package/sidecar/dist/mcp/tools/builder-queries.d.ts +6 -6
  188. package/sidecar/dist/mcp/tools/builder-queries.js +9 -10
  189. package/sidecar/dist/mcp/tools/builder-queries.js.map +1 -1
  190. package/sidecar/dist/mcp/tools/builder-table-view.js +10 -8
  191. package/sidecar/dist/mcp/tools/builder-table-view.js.map +1 -1
  192. package/sidecar/dist/mcp/tools/builder.d.ts +14 -14
  193. package/sidecar/dist/mcp/tools/builder.js +17 -12
  194. package/sidecar/dist/mcp/tools/builder.js.map +1 -1
  195. package/sidecar/dist/protocol/events.d.ts +97 -63
  196. package/sidecar/dist/protocol/events.js +14 -8
  197. package/sidecar/dist/protocol/events.js.map +1 -1
  198. package/sidecar/dist/protocol/messages.d.ts +54 -49
  199. package/sidecar/dist/protocol/messages.js +8 -13
  200. package/sidecar/dist/protocol/messages.js.map +1 -1
  201. package/sidecar/dist/providers/claude/config.d.ts +3 -3
  202. package/sidecar/dist/providers/claude/config.js +4 -2
  203. package/sidecar/dist/providers/claude/config.js.map +1 -1
  204. package/sidecar/dist/providers/claude/provider.js +20 -26
  205. package/sidecar/dist/providers/claude/provider.js.map +1 -1
  206. package/sidecar/dist/providers/claude/safe-mode.d.ts +5 -5
  207. package/sidecar/dist/providers/claude/safe-mode.js +49 -30
  208. package/sidecar/dist/providers/claude/safe-mode.js.map +1 -1
  209. package/sidecar/dist/providers/claude/sdk-loader.js +2 -5
  210. package/sidecar/dist/providers/claude/sdk-loader.js.map +1 -1
  211. package/sidecar/dist/providers/claude/secret-files.d.ts +2 -0
  212. package/sidecar/dist/providers/claude/secret-files.js +50 -0
  213. package/sidecar/dist/providers/claude/secret-files.js.map +1 -0
  214. package/sidecar/dist/providers/claude/session-stream.d.ts +4 -9
  215. package/sidecar/dist/providers/claude/session-stream.js +22 -0
  216. package/sidecar/dist/providers/claude/session-stream.js.map +1 -1
  217. package/sidecar/dist/providers/claude/tool-guard.d.ts +14 -0
  218. package/sidecar/dist/providers/claude/tool-guard.js +112 -0
  219. package/sidecar/dist/providers/claude/tool-guard.js.map +1 -0
  220. package/sidecar/dist/providers/codex/adapter.d.ts +10 -0
  221. package/sidecar/dist/providers/codex/adapter.js +20 -5
  222. package/sidecar/dist/providers/codex/adapter.js.map +1 -1
  223. package/sidecar/dist/providers/codex/client.d.ts +3 -1
  224. package/sidecar/dist/providers/codex/client.js +26 -14
  225. package/sidecar/dist/providers/codex/client.js.map +1 -1
  226. package/sidecar/dist/providers/codex/config.d.ts +6 -17
  227. package/sidecar/dist/providers/codex/config.js +9 -56
  228. package/sidecar/dist/providers/codex/config.js.map +1 -1
  229. package/sidecar/dist/providers/codex/mcp-elicitation.d.ts +21 -0
  230. package/sidecar/dist/providers/codex/mcp-elicitation.js +66 -0
  231. package/sidecar/dist/providers/codex/mcp-elicitation.js.map +1 -0
  232. package/sidecar/dist/providers/codex/module.js +1 -1
  233. package/sidecar/dist/providers/codex/module.js.map +1 -1
  234. package/sidecar/dist/providers/codex/patch-directories.d.ts +6 -0
  235. package/sidecar/dist/providers/codex/patch-directories.js +53 -0
  236. package/sidecar/dist/providers/codex/patch-directories.js.map +1 -0
  237. package/sidecar/dist/providers/codex/permission-profiles.d.ts +11 -0
  238. package/sidecar/dist/providers/codex/permission-profiles.js +37 -0
  239. package/sidecar/dist/providers/codex/permission-profiles.js.map +1 -0
  240. package/sidecar/dist/providers/codex/permissions.d.ts +13 -4
  241. package/sidecar/dist/providers/codex/permissions.js +28 -2
  242. package/sidecar/dist/providers/codex/permissions.js.map +1 -1
  243. package/sidecar/dist/providers/codex/process.d.ts +1 -0
  244. package/sidecar/dist/providers/codex/process.js +3 -1
  245. package/sidecar/dist/providers/codex/process.js.map +1 -1
  246. package/sidecar/dist/providers/codex/protocol/common.d.ts +16 -4
  247. package/sidecar/dist/providers/codex/protocol/index.d.ts +2 -2
  248. package/sidecar/dist/providers/codex/protocol/index.js +1 -1
  249. package/sidecar/dist/providers/codex/protocol/index.js.map +1 -1
  250. package/sidecar/dist/providers/codex/protocol/v2.d.ts +23 -32
  251. package/sidecar/dist/providers/codex/provider.js +16 -15
  252. package/sidecar/dist/providers/codex/provider.js.map +1 -1
  253. package/sidecar/dist/providers/codex/secret-file-globs.d.ts +2 -0
  254. package/sidecar/dist/providers/codex/secret-file-globs.js +45 -0
  255. package/sidecar/dist/providers/codex/secret-file-globs.js.map +1 -0
  256. package/sidecar/dist/providers/codex/skills.d.ts +36 -3
  257. package/sidecar/dist/providers/codex/skills.js +16 -0
  258. package/sidecar/dist/providers/codex/skills.js.map +1 -1
  259. package/sidecar/dist/providers/types.d.ts +3 -1
  260. package/sidecar/dist/providers/types.js +2 -2
  261. package/sidecar/dist/providers/types.js.map +1 -1
  262. package/sidecar/dist/server/http-routes.js +1 -1
  263. package/sidecar/dist/server/http-routes.js.map +1 -1
  264. package/sidecar/dist/server/permission-events.d.ts +1 -1
  265. package/sidecar/dist/server/permission-events.js +5 -8
  266. package/sidecar/dist/server/permission-events.js.map +1 -1
  267. package/sidecar/dist/server/routing.js +0 -5
  268. package/sidecar/dist/server/routing.js.map +1 -1
  269. package/sidecar/dist/server/tool-calls.d.ts +0 -2
  270. package/sidecar/dist/server/tool-calls.js +20 -20
  271. package/sidecar/dist/server/tool-calls.js.map +1 -1
  272. package/sidecar/dist/server/turns.js +9 -1
  273. package/sidecar/dist/server/turns.js.map +1 -1
  274. package/sidecar/dist/server/ws.js +4 -15
  275. package/sidecar/dist/server/ws.js.map +1 -1
  276. package/sidecar/dist/skills/skill-usage.js +2 -2
  277. package/sidecar/dist/skills/skill-usage.js.map +1 -1
  278. package/sidecar/dist/state/change-sets.js.map +1 -1
  279. package/sidecar/dist/state/settings-store.d.ts +13 -5
  280. package/sidecar/dist/state/settings-store.js +33 -8
  281. package/sidecar/dist/state/settings-store.js.map +1 -1
  282. package/sidecar/dist/state/settings-types.d.ts +15 -2
  283. package/sidecar/dist/state/settings-types.js +14 -1
  284. package/sidecar/dist/state/settings-types.js.map +1 -1
  285. package/sidecar/dist/state/store.js +1 -4
  286. package/sidecar/dist/state/store.js.map +1 -1
  287. package/sidecar/dist/state/types.d.ts +7 -6
  288. package/sidecar/dist/state/types.js.map +1 -1
  289. package/sidecar/package.json +2 -1
  290. package/skills/dms-ai/SKILL.md +2 -1
  291. package/skills/dms-external-database/SKILL.md +5 -2
  292. package/frontend-vue/app/chat/components/FullAutoBanner.vue +0 -76
  293. package/frontend-vue/app/chat/components/FullAutoDialog.vue +0 -103
@@ -0,0 +1,251 @@
1
+ // @vitest-environment jsdom
2
+ import { flushPromises, type VueWrapper } from "@vue/test-utils";
3
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
4
+ import { nextTick } from "vue";
5
+ import {
6
+ CONVERSATION_ID,
7
+ createHarness,
8
+ deliver,
9
+ type Harness,
10
+ openPanel,
11
+ sendMessage,
12
+ unmountPanel,
13
+ } from "../support/panel-harness";
14
+ import {
15
+ type FakeScroller,
16
+ fakeScroller,
17
+ settleLayout,
18
+ } from "../support/fake-layout";
19
+
20
+ const BOTTOM_TOLERANCE_PX = 4;
21
+
22
+ interface OpenChat {
23
+ harness: Harness;
24
+ panel: VueWrapper;
25
+ scroller: FakeScroller;
26
+ }
27
+
28
+ function transcript(rows: number) {
29
+ return Array.from({ length: rows }, (_, index) => ({
30
+ role: index % 2 === 0 ? "user" : "assistant",
31
+ content: `message ${index}`,
32
+ timestampMs: 1_700_000_000_000 + index,
33
+ }));
34
+ }
35
+
36
+ /** One browser frame: Vue renders, then layout is reported. */
37
+ async function frame(): Promise<void> {
38
+ await nextTick();
39
+ await flushPromises();
40
+ settleLayout();
41
+ }
42
+
43
+ async function openChat(rows = 30): Promise<OpenChat> {
44
+ const harness = createHarness();
45
+ const panel = await openPanel(harness);
46
+ const scroller = fakeScroller(
47
+ panel.find<HTMLElement>(".chat-view-body").element,
48
+ );
49
+ deliver(harness, {
50
+ type: "conversation_snapshot",
51
+ messages: transcript(rows),
52
+ });
53
+ await frame();
54
+ return { harness, panel, scroller };
55
+ }
56
+
57
+ async function growTranscript(chat: OpenChat, rows: number): Promise<void> {
58
+ deliver(chat.harness, {
59
+ type: "conversation_snapshot",
60
+ messages: transcript(rows),
61
+ });
62
+ await frame();
63
+ }
64
+
65
+ beforeEach(() => {
66
+ localStorage.setItem("dms-ai-conversation-id", CONVERSATION_ID);
67
+ });
68
+
69
+ afterEach(() => {
70
+ unmountPanel();
71
+ localStorage.clear();
72
+ });
73
+
74
+ describe("the chat follows the conversation", () => {
75
+ it("shows the end of a chat opened from its snapshot", async () => {
76
+ const { scroller } = await openChat(60);
77
+ expect(scroller.distanceFromBottom()).toBeLessThanOrEqual(
78
+ BOTTOM_TOLERANCE_PX,
79
+ );
80
+ });
81
+
82
+ it("stays at the bottom when the view loses height without any message arriving", async () => {
83
+ const { scroller } = await openChat();
84
+ scroller.viewportPx -= 250;
85
+ await frame();
86
+ expect(scroller.distanceFromBottom()).toBeLessThanOrEqual(
87
+ BOTTOM_TOLERANCE_PX,
88
+ );
89
+ });
90
+
91
+ it("stays at the bottom when content grows without any message arriving", async () => {
92
+ const { scroller } = await openChat();
93
+ scroller.extraContentPx += 600;
94
+ await frame();
95
+ expect(scroller.distanceFromBottom()).toBeLessThanOrEqual(
96
+ BOTTOM_TOLERANCE_PX,
97
+ );
98
+ });
99
+
100
+ it("stays at the bottom through one message taller than the old 80px margin", async () => {
101
+ const { scroller, harness } = await openChat();
102
+ deliver(harness, { type: "assistant_message_chunk", text: "answer" });
103
+ scroller.extraContentPx += 900;
104
+ await frame();
105
+ expect(scroller.distanceFromBottom()).toBeLessThanOrEqual(
106
+ BOTTOM_TOLERANCE_PX,
107
+ );
108
+ });
109
+
110
+ it("does not pull the person back down once they scroll up", async () => {
111
+ const chat = await openChat();
112
+ chat.scroller.scrollTo(500);
113
+ await growTranscript(chat, 40);
114
+ chat.scroller.extraContentPx += 300;
115
+ await frame();
116
+ expect(
117
+ chat.panel.find<HTMLElement>(".chat-view-body").element.scrollTop,
118
+ ).toBe(500);
119
+ });
120
+
121
+ it("jumps to the bottom when a message is sent from far up", async () => {
122
+ const chat = await openChat();
123
+ chat.scroller.scrollTo(200);
124
+ await sendMessage(chat.panel, "and now this");
125
+ await frame();
126
+ expect(chat.scroller.distanceFromBottom()).toBeLessThanOrEqual(
127
+ BOTTOM_TOLERANCE_PX,
128
+ );
129
+ });
130
+
131
+ it("follows again once the person scrolls back to the bottom", async () => {
132
+ const chat = await openChat();
133
+ chat.scroller.scrollTo(500);
134
+ const bottom = chat.scroller.viewportPx;
135
+ chat.scroller.scrollTo(
136
+ chat.panel.find<HTMLElement>(".chat-view-body").element.scrollHeight -
137
+ bottom,
138
+ );
139
+ await growTranscript(chat, 40);
140
+ expect(chat.scroller.distanceFromBottom()).toBeLessThanOrEqual(
141
+ BOTTOM_TOLERANCE_PX,
142
+ );
143
+ });
144
+
145
+ it("stops following on an upward scroll that carries no input", async () => {
146
+ const chat = await openChat();
147
+ chat.scroller.scrollTo(500);
148
+ await growTranscript(chat, 40);
149
+ chat.scroller.extraContentPx += 300;
150
+ await frame();
151
+ expect(
152
+ chat.panel.find<HTMLElement>(".chat-view-body").element.scrollTop,
153
+ ).toBe(500);
154
+ });
155
+
156
+ it("keeps following when content above the view collapses and scroll anchoring moves the view up", async () => {
157
+ const chat = await openChat();
158
+ chat.scroller.extraContentPx += 800;
159
+ await frame();
160
+ chat.scroller.collapseAbove(600);
161
+ await frame();
162
+ await frame();
163
+ expect(chat.scroller.distanceFromBottom()).toBeLessThanOrEqual(
164
+ BOTTOM_TOLERANCE_PX,
165
+ );
166
+ chat.scroller.extraContentPx += 400;
167
+ await frame();
168
+ expect(chat.scroller.distanceFromBottom()).toBeLessThanOrEqual(
169
+ BOTTOM_TOLERANCE_PX,
170
+ );
171
+ });
172
+
173
+ it("keeps following when a collapse above the view comes with growth below it", async () => {
174
+ const chat = await openChat();
175
+ chat.scroller.extraContentPx += 800;
176
+ await frame();
177
+ chat.scroller.collapseAbove(600);
178
+ chat.scroller.extraContentPx += 700;
179
+ await frame();
180
+ await frame();
181
+ expect(chat.scroller.distanceFromBottom()).toBeLessThanOrEqual(
182
+ BOTTOM_TOLERANCE_PX,
183
+ );
184
+ });
185
+
186
+ it("keeps following when the view gains height and the browser clamps the scroll", async () => {
187
+ const chat = await openChat();
188
+ chat.scroller.viewportPx += 250;
189
+ await frame();
190
+ await frame();
191
+ chat.scroller.extraContentPx += 500;
192
+ await frame();
193
+ expect(chat.scroller.distanceFromBottom()).toBeLessThanOrEqual(
194
+ BOTTOM_TOLERANCE_PX,
195
+ );
196
+ });
197
+
198
+ it("keeps following after the content shrinks under the view", async () => {
199
+ const chat = await openChat(60);
200
+ await growTranscript(chat, 20);
201
+ await growTranscript(chat, 30);
202
+ expect(chat.scroller.distanceFromBottom()).toBeLessThanOrEqual(
203
+ BOTTOM_TOLERANCE_PX,
204
+ );
205
+ });
206
+
207
+ it("ends at the bottom for a chat started from outside that sends its prompt", async () => {
208
+ const chat = await openChat();
209
+ chat.scroller.scrollTo(200);
210
+ chat.harness.session.startConversation("build me a page", undefined, true);
211
+ await frame();
212
+ await frame();
213
+ expect(chat.scroller.distanceFromBottom()).toBeLessThanOrEqual(
214
+ BOTTOM_TOLERANCE_PX,
215
+ );
216
+ });
217
+
218
+ it("opens another chat at its bottom, even after scrolling up in this one", async () => {
219
+ const chat = await openChat();
220
+ chat.scroller.scrollTo(200);
221
+ chat.harness.session.openConversation("conv-other");
222
+ await frame();
223
+ deliver(chat.harness, {
224
+ conversationId: "conv-other",
225
+ type: "conversation_snapshot",
226
+ messages: transcript(50),
227
+ });
228
+ await frame();
229
+ expect(chat.scroller.distanceFromBottom()).toBeLessThanOrEqual(
230
+ BOTTOM_TOLERANCE_PX,
231
+ );
232
+ });
233
+
234
+ it("keeps following in a shorter chat opened from the bottom of a long one", async () => {
235
+ const chat = await openChat(60);
236
+ chat.harness.session.openConversation("conv-other");
237
+ await frame();
238
+ deliver(chat.harness, {
239
+ conversationId: "conv-other",
240
+ type: "conversation_snapshot",
241
+ messages: transcript(8),
242
+ });
243
+ await frame();
244
+ await frame();
245
+ chat.scroller.extraContentPx += 500;
246
+ await frame();
247
+ expect(chat.scroller.distanceFromBottom()).toBeLessThanOrEqual(
248
+ BOTTOM_TOLERANCE_PX,
249
+ );
250
+ });
251
+ });
@@ -134,7 +134,6 @@ describe("the conversation folds the redesign's events", () => {
134
134
  type: "conversation_mode",
135
135
  mode: "plan",
136
136
  generationMode: "vibe",
137
- fullAuto: { duration: "turn" },
138
137
  });
139
138
  expect(conversation.messages.value[0]).toMatchObject({
140
139
  role: "notice",
@@ -144,7 +143,6 @@ describe("the conversation folds the redesign's events", () => {
144
143
  expect(conversation.mode.value).toEqual({
145
144
  mode: "plan",
146
145
  generationMode: "vibe",
147
- fullAuto: { duration: "turn" },
148
146
  });
149
147
  });
150
148
 
@@ -200,7 +198,7 @@ describe("the conversation folds the redesign's events", () => {
200
198
  },
201
199
  ],
202
200
  changeSets: [changeSet()],
203
- mode: { mode: "acceptEdits", generationMode: "safe", fullAuto: null },
201
+ mode: { mode: "acceptEdits", generationMode: "safe" },
204
202
  totalTokens: 1_200,
205
203
  });
206
204
  expect(conversation.messages.value.map((m) => m.role)).toEqual([
@@ -0,0 +1,33 @@
1
+ // @vitest-environment jsdom
2
+ import { mount } from "@vue/test-utils";
3
+ import { describe, expect, it } from "vitest";
4
+ import ProviderNotice from "../../app/chat/components/ProviderNotice.vue";
5
+ import { CHAT_I18N_KEY } from "../../app/chat/composables/useChatI18n";
6
+ import fr from "../../i18n/locales/dms-ai-panel-fr-FR.json";
7
+ import { createTestI18n } from "../support/i18n";
8
+ import { PANEL_STUBS } from "../support/stubs";
9
+
10
+ const ENGLISH_REASON =
11
+ "Sign in with ChatGPT in Assistant › Settings to run Codex on your ChatGPT plan.";
12
+
13
+ function noticeText(code?: "codex_not_signed_in"): string {
14
+ return mount(ProviderNotice, {
15
+ props: { agentName: "Codex", reason: ENGLISH_REASON, code },
16
+ global: {
17
+ provide: { [CHAT_I18N_KEY as symbol]: createTestI18n(fr) },
18
+ stubs: PANEL_STUBS,
19
+ },
20
+ }).text();
21
+ }
22
+
23
+ describe("the agent can't run notice", () => {
24
+ it("words the ChatGPT sign-in hint in the panel's language", () => {
25
+ const text = noticeText("codex_not_signed_in");
26
+ expect(text).toContain("Connectez-vous avec ChatGPT");
27
+ expect(text).not.toContain(ENGLISH_REASON);
28
+ });
29
+
30
+ it("shows the sidecar's own reason when the panel has no wording for it", () => {
31
+ expect(noticeText()).toContain(ENGLISH_REASON);
32
+ });
33
+ });
@@ -47,16 +47,14 @@ describe("the chat as a component of the dashboard", () => {
47
47
  ]);
48
48
  });
49
49
 
50
- it("leaves its conversation when the panel closes, and picks it up again when it opens", async () => {
50
+ it("picks its conversation up again when the panel reopens, without announcing that it left", async () => {
51
51
  const harness = createHarness();
52
52
  const panel = await openPanel(harness);
53
53
  await sendMessage(panel, "build me a page");
54
54
  harness.session.panel.close();
55
55
  await nextTick();
56
56
  expect(panel.find(".chat-view").exists()).toBe(false);
57
- expect(sentOfType(harness, "leave_conversation")).toEqual([
58
- { type: "leave_conversation", conversationId: CONVERSATION_ID },
59
- ]);
57
+ expect(sentOfType(harness, "leave_conversation")).toEqual([]);
60
58
  harness.session.panel.toggle();
61
59
  await flushPromises();
62
60
  expect(sentOfType(harness, "hello")).toEqual([
@@ -0,0 +1,146 @@
1
+ // @vitest-environment jsdom
2
+ import type { VueWrapper } from "@vue/test-utils";
3
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
4
+ import { nextTick } from "vue";
5
+ import {
6
+ CONVERSATION_ID,
7
+ createHarness,
8
+ deliver,
9
+ openPanel,
10
+ unmountPanel,
11
+ } from "./support/panel-harness";
12
+
13
+ beforeEach(() => {
14
+ localStorage.setItem("dms-ai-conversation-id", CONVERSATION_ID);
15
+ });
16
+
17
+ afterEach(() => {
18
+ unmountPanel();
19
+ localStorage.clear();
20
+ });
21
+
22
+ const SETTINGS = {
23
+ defaultModes: { claude: "auto", codex: "normal" },
24
+ thinking: "medium",
25
+ generationMode: "safe",
26
+ allowLocalSkills: false,
27
+ };
28
+
29
+ const PROVIDERS = {
30
+ claude: { available: true },
31
+ codex: { available: true },
32
+ };
33
+
34
+ function modePill(panel: VueWrapper) {
35
+ const pills = panel.findAll(".cb-mode-btn");
36
+ return pills[pills.length - 1];
37
+ }
38
+
39
+ function pickerItems(panel: VueWrapper): string[] {
40
+ const menu = modePill(panel).element.closest(".u-dropdown");
41
+ return Array.from(menu?.querySelectorAll(".u-dropdown-item") ?? []).map(
42
+ (item) => item.textContent?.trim() ?? "",
43
+ );
44
+ }
45
+
46
+ async function useProvider(
47
+ harness: ReturnType<typeof createHarness>,
48
+ provider: "claude" | "codex",
49
+ ): Promise<void> {
50
+ deliver(harness, {
51
+ type: "settings_update",
52
+ settings: { ...SETTINGS, provider },
53
+ providers: PROVIDERS,
54
+ });
55
+ await nextTick();
56
+ }
57
+
58
+ describe("the approval mode pill", () => {
59
+ it("shows Auto review on a fresh Claude chat, without danger styling", async () => {
60
+ const harness = createHarness();
61
+ const panel = await openPanel(harness);
62
+ await useProvider(harness, "claude");
63
+ const pill = modePill(panel);
64
+ expect(pill.text()).toBe("Auto review");
65
+ expect(pickerItems(panel)).toContain("Auto review");
66
+ });
67
+
68
+ it("reads Ask first on Codex for a chat holding auto, and offers no Auto review", async () => {
69
+ const harness = createHarness();
70
+ const panel = await openPanel(harness);
71
+ await useProvider(harness, "codex");
72
+ deliver(harness, {
73
+ type: "conversation_mode",
74
+ mode: "auto",
75
+ generationMode: "safe",
76
+ });
77
+ await nextTick();
78
+ expect(modePill(panel).text()).toBe("Ask first");
79
+ expect(pickerItems(panel)).not.toContain("Auto review");
80
+ expect(pickerItems(panel)).toContain("Ask first");
81
+ });
82
+
83
+ it.each(["claude", "codex"] as const)(
84
+ "offers no Full auto in the %s mode menu",
85
+ async (provider) => {
86
+ const harness = createHarness();
87
+ const panel = await openPanel(harness);
88
+ await useProvider(harness, provider);
89
+ const items = pickerItems(panel);
90
+ expect(items.length).toBeGreaterThan(0);
91
+ expect(items.filter((item) => /full auto/i.test(item))).toEqual([]);
92
+ },
93
+ );
94
+
95
+ it.each([
96
+ ["codex", true],
97
+ ["claude", false],
98
+ ] as const)(
99
+ "says Accept edits works as Ask first on %s: %s",
100
+ async (provider, worksAsAskFirst) => {
101
+ const harness = createHarness();
102
+ const panel = await openPanel(harness);
103
+ await useProvider(harness, provider);
104
+ const acceptEdits = panel
105
+ .findAll(".u-dropdown-item")
106
+ .find((item) => item.text() === "Accept edits");
107
+ const description = acceptEdits?.attributes("data-description") ?? "";
108
+ expect(description).not.toBe("");
109
+ expect(description.includes("works as Ask first")).toBe(worksAsAskFirst);
110
+ },
111
+ );
112
+
113
+ it("keeps the pill on the chat's own mode when an older sidecar still sends Full auto", async () => {
114
+ const harness = createHarness();
115
+ const panel = await openPanel(harness);
116
+ await useProvider(harness, "claude");
117
+ deliver(harness, {
118
+ type: "conversation_mode",
119
+ mode: "auto",
120
+ generationMode: "safe",
121
+ fullAuto: { duration: "chat" },
122
+ });
123
+ await nextTick();
124
+ expect(modePill(panel).text()).toBe("Auto review");
125
+ expect(panel.find(".cb-banner-auto").exists()).toBe(false);
126
+ });
127
+
128
+ it("says Auto review is not available, naming the model", async () => {
129
+ const harness = createHarness();
130
+ const panel = await openPanel(harness);
131
+ deliver(harness, {
132
+ type: "notice",
133
+ notice: {
134
+ kind: "auto_unavailable",
135
+ model: "claude-haiku-4-5",
136
+ timestampMs: Date.now(),
137
+ },
138
+ });
139
+ await nextTick();
140
+ const notice = panel.find(".cb-notice");
141
+ expect(notice.text()).toContain("Auto review isn't available here.");
142
+ expect(notice.text()).toContain(
143
+ "claude-haiku-4-5, this plan or a setting doesn't allow it.",
144
+ );
145
+ });
146
+ });
@@ -106,35 +106,17 @@ describe("questions, queue and modes", () => {
106
106
  ).toEqual(["queue_update", "queue_move", "queue_clear"]);
107
107
  });
108
108
 
109
- it("switches the chat's mode and scope, and confirms Full auto with a duration", async () => {
109
+ it("sends the chosen mode alone, with nothing that approves for the person", async () => {
110
110
  const harness = createHarness();
111
111
  const panel = await openPanel(harness);
112
112
  await buttonWithText(panel, "Plan only").trigger("click");
113
- await buttonWithText(panel, "Full auto…").trigger("click");
114
- await nextTick();
115
- const modal = panel.find(".u-modal");
116
- expect(modal.text()).toContain("Turn on Full auto?");
117
- await buttonWithText(modal, "30 minutes").trigger("click");
118
- await buttonWithText(modal, "Turn on Full auto").trigger("click");
119
113
  expect(sentOfType(harness, "set_conversation_mode")).toEqual([
120
114
  {
121
115
  type: "set_conversation_mode",
122
116
  conversationId: CONVERSATION_ID,
123
117
  mode: "plan",
124
118
  },
125
- {
126
- type: "set_conversation_mode",
127
- conversationId: CONVERSATION_ID,
128
- fullAuto: { duration: "30m" },
129
- },
130
119
  ]);
131
- await nextTick();
132
- const banner = panel.find(".cb-banner-auto");
133
- expect(banner.text()).toContain("Full auto · for 30 minutes");
134
- await buttonWithText(banner, "Turn off").trigger("click");
135
- expect(sentOfType(harness, "set_conversation_mode").at(-1)).toMatchObject({
136
- fullAuto: null,
137
- });
138
120
  });
139
121
 
140
122
  it("runs a proposed plan in Ask first", async () => {
@@ -60,16 +60,6 @@ describe("the panel's header and empty chat", () => {
60
60
  );
61
61
  });
62
62
 
63
- it("sends leave_conversation when the panel closes", async () => {
64
- const harness = createHarness();
65
- await openPanel(harness);
66
- harness.session.panel.close();
67
- await nextTick();
68
- expect(sentOfType(harness, "leave_conversation")).toEqual([
69
- { type: "leave_conversation", conversationId: CONVERSATION_ID },
70
- ]);
71
- });
72
-
73
63
  it("stamps a message with the page unless its chip was removed", async () => {
74
64
  const harness = createHarness();
75
65
  const panel = await openPanel(harness);
@@ -116,7 +106,7 @@ describe("notices and errors", () => {
116
106
  type: "settings_update",
117
107
  settings: {
118
108
  provider: "codex",
119
- mode: "normal",
109
+ defaultModes: { claude: "auto", codex: "normal" },
120
110
  thinking: "medium",
121
111
  generationMode: "safe",
122
112
  allowLocalSkills: false,
@@ -144,6 +134,8 @@ describe("notices and errors", () => {
144
134
  });
145
135
 
146
136
  describe("the history drawer", () => {
137
+ const HOUR_MS = 3_600_000;
138
+ const NOON_MS = new Date(2026, 5, 15, 12, 0, 0).getTime();
147
139
  const LIST = {
148
140
  type: "conversation_list",
149
141
  conversations: [
@@ -151,7 +143,7 @@ describe("the history drawer", () => {
151
143
  id: "c-run",
152
144
  title: "Add top customers",
153
145
  createdAtMs: 1,
154
- updatedAtMs: Date.now(),
146
+ updatedAtMs: NOON_MS,
155
147
  messageCount: 4,
156
148
  isRunning: true,
157
149
  filesChanged: 2,
@@ -161,7 +153,7 @@ describe("the history drawer", () => {
161
153
  id: "c-old",
162
154
  title: "Why is order pending?",
163
155
  createdAtMs: 1,
164
- updatedAtMs: Date.now() - 3_600_000,
156
+ updatedAtMs: NOON_MS - HOUR_MS,
165
157
  messageCount: 2,
166
158
  filesChanged: 0,
167
159
  filesUndone: 1,
@@ -170,6 +162,11 @@ describe("the history drawer", () => {
170
162
  ],
171
163
  };
172
164
 
165
+ beforeEach(() => {
166
+ vi.useFakeTimers({ toFake: ["Date"] });
167
+ vi.setSystemTime(NOON_MS);
168
+ });
169
+
173
170
  it("opens with ⌘J, pins the running chat, and switches chats", async () => {
174
171
  const harness = createHarness();
175
172
  const panel = await openPanel(harness);
@@ -185,9 +182,7 @@ describe("the history drawer", () => {
185
182
  expect(drawer.text()).toContain("9.6k tok");
186
183
  expect(drawer.text()).toContain("1 file undone");
187
184
  await buttonWithText(drawer, "Why is order pending?").trigger("click");
188
- expect(sentOfType(harness, "leave_conversation")[0]).toMatchObject({
189
- conversationId: CONVERSATION_ID,
190
- });
185
+ expect(sentOfType(harness, "leave_conversation")).toEqual([]);
191
186
  expect(sentOfType(harness, "hello").at(-1)).toMatchObject({
192
187
  conversationId: "c-old",
193
188
  });
@@ -195,6 +190,7 @@ describe("the history drawer", () => {
195
190
 
196
191
  it("deletes after 8 seconds unless undone, and asks first for a running chat", async () => {
197
192
  vi.useFakeTimers();
193
+ vi.setSystemTime(NOON_MS);
198
194
  const harness = createHarness();
199
195
  const panel = await openPanel(harness);
200
196
  document.dispatchEvent(