@antelopejs/dms-ai 0.9.0 → 0.9.1

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 (208) hide show
  1. package/README.md +2 -1
  2. package/dist/lifecycle/skill-sources.js +19 -146
  3. package/dist/lifecycle/skill-sources.js.map +1 -1
  4. package/dist/pages/settings/availability.js +8 -1
  5. package/dist/pages/settings/availability.js.map +1 -1
  6. package/dist/pages/settings/options.d.ts +2 -1
  7. package/dist/pages/settings/options.js +29 -8
  8. package/dist/pages/settings/options.js.map +1 -1
  9. package/dist/pages/settings/sections.js +15 -4
  10. package/dist/pages/settings/sections.js.map +1 -1
  11. package/dist/routes/builder.d.ts +3 -3
  12. package/dist/routes/builder.js +7 -2
  13. package/dist/routes/builder.js.map +1 -1
  14. package/dist/routes/settings.js +1 -1
  15. package/dist/routes/settings.js.map +1 -1
  16. package/dist/sidecar/settings-values.d.ts +3 -0
  17. package/dist/sidecar/settings-values.js +33 -1
  18. package/dist/sidecar/settings-values.js.map +1 -1
  19. package/dist/vocabulary/enumerations.js +2 -0
  20. package/dist/vocabulary/enumerations.js.map +1 -1
  21. package/frontend-vue/app/chat/components/ChatView.vue +16 -49
  22. package/frontend-vue/app/chat/components/Composer.vue +23 -39
  23. package/frontend-vue/app/chat/components/NoticeCard.vue +9 -0
  24. package/frontend-vue/app/chat/components/ProviderNotice.vue +14 -3
  25. package/frontend-vue/app/chat/composables/conversation-events.ts +0 -3
  26. package/frontend-vue/app/chat/composables/useConversationMode.ts +8 -11
  27. package/frontend-vue/app/chat/composables/useFollowScroll.ts +77 -0
  28. package/frontend-vue/app/chat/constants/modes.ts +15 -0
  29. package/frontend-vue/app/chat/constants/protocol.ts +0 -1
  30. package/frontend-vue/app/chat/constants/settings.ts +1 -1
  31. package/frontend-vue/app/chat/types/protocol.ts +11 -9
  32. package/frontend-vue/app/chat/types/settings.ts +7 -2
  33. package/frontend-vue/app/chat/utils/permission-view.ts +1 -0
  34. package/frontend-vue/app/views/constants.ts +2 -0
  35. package/frontend-vue/app/views/types.ts +3 -1
  36. package/frontend-vue/i18n/locales/dms-ai-pages-en-GB.json +21 -11
  37. package/frontend-vue/i18n/locales/dms-ai-pages-fr-FR.json +21 -11
  38. package/frontend-vue/i18n/locales/dms-ai-panel-en-GB.json +11 -21
  39. package/frontend-vue/i18n/locales/dms-ai-panel-fr-FR.json +11 -21
  40. package/frontend-vue/i18n/locales/dms-ai-views-en-GB.json +2 -0
  41. package/frontend-vue/i18n/locales/dms-ai-views-fr-FR.json +2 -0
  42. package/frontend-vue/tests/chat/chat-scroll.test.ts +251 -0
  43. package/frontend-vue/tests/chat/conversation-events.test.ts +1 -3
  44. package/frontend-vue/tests/chat/provider-notice.test.ts +33 -0
  45. package/frontend-vue/tests/chat-panel.test.ts +2 -4
  46. package/frontend-vue/tests/panel-modes.test.ts +146 -0
  47. package/frontend-vue/tests/panel-questions.test.ts +1 -19
  48. package/frontend-vue/tests/panel-states.test.ts +3 -15
  49. package/frontend-vue/tests/support/fake-layout.ts +119 -0
  50. package/frontend-vue/tests/support/panel-harness.ts +14 -6
  51. package/frontend-vue/tests/support/setup-layout.ts +3 -0
  52. package/frontend-vue/tests/support/stubs.ts +2 -0
  53. package/frontend-vue/vitest.config.ts +1 -0
  54. package/package.json +4 -3
  55. package/sidecar/dist/agent/always-ask-files.d.ts +2 -0
  56. package/sidecar/dist/agent/always-ask-files.js +34 -0
  57. package/sidecar/dist/agent/always-ask-files.js.map +1 -0
  58. package/sidecar/dist/agent/conversation-modes.d.ts +3 -12
  59. package/sidecar/dist/agent/conversation-modes.js +16 -69
  60. package/sidecar/dist/agent/conversation-modes.js.map +1 -1
  61. package/sidecar/dist/agent/effective-mode.d.ts +2 -5
  62. package/sidecar/dist/agent/effective-mode.js +1 -15
  63. package/sidecar/dist/agent/effective-mode.js.map +1 -1
  64. package/sidecar/dist/agent/permission-bus.d.ts +1 -2
  65. package/sidecar/dist/agent/permission-bus.js +1 -3
  66. package/sidecar/dist/agent/permission-bus.js.map +1 -1
  67. package/sidecar/dist/agent/permission-preview.d.ts +6 -0
  68. package/sidecar/dist/agent/permission-preview.js +35 -10
  69. package/sidecar/dist/agent/permission-preview.js.map +1 -1
  70. package/sidecar/dist/agent/permission-rules.js +2 -2
  71. package/sidecar/dist/agent/permission-rules.js.map +1 -1
  72. package/sidecar/dist/agent/project-secrets.d.ts +7 -0
  73. package/sidecar/dist/agent/project-secrets.js +61 -0
  74. package/sidecar/dist/agent/project-secrets.js.map +1 -0
  75. package/sidecar/dist/agent/provider.d.ts +5 -4
  76. package/sidecar/dist/agent/runner.d.ts +3 -2
  77. package/sidecar/dist/agent/runner.js +9 -3
  78. package/sidecar/dist/agent/runner.js.map +1 -1
  79. package/sidecar/dist/agent/session.d.ts +3 -3
  80. package/sidecar/dist/agent/session.js.map +1 -1
  81. package/sidecar/dist/agent/tool-kinds.d.ts +7 -1
  82. package/sidecar/dist/agent/tool-kinds.js +24 -5
  83. package/sidecar/dist/agent/tool-kinds.js.map +1 -1
  84. package/sidecar/dist/audit/activity-query.js +2 -0
  85. package/sidecar/dist/audit/activity-query.js.map +1 -1
  86. package/sidecar/dist/audit/activity.js +1 -0
  87. package/sidecar/dist/audit/activity.js.map +1 -1
  88. package/sidecar/dist/builder/builder-client.js +13 -1
  89. package/sidecar/dist/builder/builder-client.js.map +1 -1
  90. package/sidecar/dist/constants/audit.d.ts +1 -3
  91. package/sidecar/dist/constants/audit.js +2 -1
  92. package/sidecar/dist/constants/audit.js.map +1 -1
  93. package/sidecar/dist/constants/builder.d.ts +3 -0
  94. package/sidecar/dist/constants/builder.js +3 -0
  95. package/sidecar/dist/constants/builder.js.map +1 -1
  96. package/sidecar/dist/constants/claude.d.ts +6 -0
  97. package/sidecar/dist/constants/claude.js +6 -0
  98. package/sidecar/dist/constants/claude.js.map +1 -1
  99. package/sidecar/dist/constants/codex.d.ts +4 -0
  100. package/sidecar/dist/constants/codex.js +4 -0
  101. package/sidecar/dist/constants/codex.js.map +1 -1
  102. package/sidecar/dist/constants/permissions.d.ts +2 -0
  103. package/sidecar/dist/constants/permissions.js +23 -0
  104. package/sidecar/dist/constants/permissions.js.map +1 -1
  105. package/sidecar/dist/constants/project-secrets.d.ts +5 -0
  106. package/sidecar/dist/constants/project-secrets.js +20 -0
  107. package/sidecar/dist/constants/project-secrets.js.map +1 -0
  108. package/sidecar/dist/constants/providers.d.ts +2 -0
  109. package/sidecar/dist/constants/providers.js +1 -0
  110. package/sidecar/dist/constants/providers.js.map +1 -1
  111. package/sidecar/dist/constants/settings.d.ts +1 -2
  112. package/sidecar/dist/constants/settings.js +2 -9
  113. package/sidecar/dist/constants/settings.js.map +1 -1
  114. package/sidecar/dist/constants/tool-kinds.d.ts +67 -2
  115. package/sidecar/dist/constants/tool-kinds.js +111 -34
  116. package/sidecar/dist/constants/tool-kinds.js.map +1 -1
  117. package/sidecar/dist/mcp/tools/builder-gate.js +3 -1
  118. package/sidecar/dist/mcp/tools/builder-gate.js.map +1 -1
  119. package/sidecar/dist/mcp/tools/builder-project.js +65 -3
  120. package/sidecar/dist/mcp/tools/builder-project.js.map +1 -1
  121. package/sidecar/dist/protocol/events.d.ts +97 -63
  122. package/sidecar/dist/protocol/events.js +14 -8
  123. package/sidecar/dist/protocol/events.js.map +1 -1
  124. package/sidecar/dist/protocol/messages.d.ts +54 -49
  125. package/sidecar/dist/protocol/messages.js +8 -13
  126. package/sidecar/dist/protocol/messages.js.map +1 -1
  127. package/sidecar/dist/providers/claude/config.d.ts +3 -3
  128. package/sidecar/dist/providers/claude/config.js +4 -2
  129. package/sidecar/dist/providers/claude/config.js.map +1 -1
  130. package/sidecar/dist/providers/claude/provider.js +20 -26
  131. package/sidecar/dist/providers/claude/provider.js.map +1 -1
  132. package/sidecar/dist/providers/claude/safe-mode.d.ts +5 -5
  133. package/sidecar/dist/providers/claude/safe-mode.js +49 -30
  134. package/sidecar/dist/providers/claude/safe-mode.js.map +1 -1
  135. package/sidecar/dist/providers/claude/secret-files.d.ts +2 -0
  136. package/sidecar/dist/providers/claude/secret-files.js +50 -0
  137. package/sidecar/dist/providers/claude/secret-files.js.map +1 -0
  138. package/sidecar/dist/providers/claude/session-stream.d.ts +4 -9
  139. package/sidecar/dist/providers/claude/session-stream.js +22 -0
  140. package/sidecar/dist/providers/claude/session-stream.js.map +1 -1
  141. package/sidecar/dist/providers/claude/tool-guard.d.ts +14 -0
  142. package/sidecar/dist/providers/claude/tool-guard.js +112 -0
  143. package/sidecar/dist/providers/claude/tool-guard.js.map +1 -0
  144. package/sidecar/dist/providers/codex/adapter.d.ts +10 -0
  145. package/sidecar/dist/providers/codex/adapter.js +20 -5
  146. package/sidecar/dist/providers/codex/adapter.js.map +1 -1
  147. package/sidecar/dist/providers/codex/config.d.ts +6 -17
  148. package/sidecar/dist/providers/codex/config.js +9 -56
  149. package/sidecar/dist/providers/codex/config.js.map +1 -1
  150. package/sidecar/dist/providers/codex/mcp-elicitation.d.ts +21 -0
  151. package/sidecar/dist/providers/codex/mcp-elicitation.js +66 -0
  152. package/sidecar/dist/providers/codex/mcp-elicitation.js.map +1 -0
  153. package/sidecar/dist/providers/codex/module.js +1 -1
  154. package/sidecar/dist/providers/codex/module.js.map +1 -1
  155. package/sidecar/dist/providers/codex/patch-directories.d.ts +6 -0
  156. package/sidecar/dist/providers/codex/patch-directories.js +53 -0
  157. package/sidecar/dist/providers/codex/patch-directories.js.map +1 -0
  158. package/sidecar/dist/providers/codex/permission-profiles.d.ts +11 -0
  159. package/sidecar/dist/providers/codex/permission-profiles.js +37 -0
  160. package/sidecar/dist/providers/codex/permission-profiles.js.map +1 -0
  161. package/sidecar/dist/providers/codex/permissions.d.ts +13 -4
  162. package/sidecar/dist/providers/codex/permissions.js +28 -2
  163. package/sidecar/dist/providers/codex/permissions.js.map +1 -1
  164. package/sidecar/dist/providers/codex/process.d.ts +1 -0
  165. package/sidecar/dist/providers/codex/process.js +3 -1
  166. package/sidecar/dist/providers/codex/process.js.map +1 -1
  167. package/sidecar/dist/providers/codex/protocol/common.d.ts +16 -4
  168. package/sidecar/dist/providers/codex/protocol/index.d.ts +2 -2
  169. package/sidecar/dist/providers/codex/protocol/index.js +1 -1
  170. package/sidecar/dist/providers/codex/protocol/index.js.map +1 -1
  171. package/sidecar/dist/providers/codex/protocol/v2.d.ts +23 -32
  172. package/sidecar/dist/providers/codex/provider.js +13 -12
  173. package/sidecar/dist/providers/codex/provider.js.map +1 -1
  174. package/sidecar/dist/providers/codex/secret-file-globs.d.ts +2 -0
  175. package/sidecar/dist/providers/codex/secret-file-globs.js +45 -0
  176. package/sidecar/dist/providers/codex/secret-file-globs.js.map +1 -0
  177. package/sidecar/dist/providers/types.d.ts +3 -1
  178. package/sidecar/dist/providers/types.js +2 -2
  179. package/sidecar/dist/providers/types.js.map +1 -1
  180. package/sidecar/dist/server/http-routes.js +1 -1
  181. package/sidecar/dist/server/http-routes.js.map +1 -1
  182. package/sidecar/dist/server/permission-events.d.ts +1 -1
  183. package/sidecar/dist/server/permission-events.js +5 -8
  184. package/sidecar/dist/server/permission-events.js.map +1 -1
  185. package/sidecar/dist/server/routing.js +0 -5
  186. package/sidecar/dist/server/routing.js.map +1 -1
  187. package/sidecar/dist/server/tool-calls.d.ts +0 -2
  188. package/sidecar/dist/server/tool-calls.js +7 -8
  189. package/sidecar/dist/server/tool-calls.js.map +1 -1
  190. package/sidecar/dist/server/turns.js +9 -1
  191. package/sidecar/dist/server/turns.js.map +1 -1
  192. package/sidecar/dist/server/ws.js +4 -15
  193. package/sidecar/dist/server/ws.js.map +1 -1
  194. package/sidecar/dist/skills/skill-usage.js +2 -2
  195. package/sidecar/dist/skills/skill-usage.js.map +1 -1
  196. package/sidecar/dist/state/settings-store.d.ts +13 -5
  197. package/sidecar/dist/state/settings-store.js +33 -8
  198. package/sidecar/dist/state/settings-store.js.map +1 -1
  199. package/sidecar/dist/state/settings-types.d.ts +15 -2
  200. package/sidecar/dist/state/settings-types.js +14 -1
  201. package/sidecar/dist/state/settings-types.js.map +1 -1
  202. package/sidecar/dist/state/types.d.ts +7 -6
  203. package/sidecar/dist/state/types.js.map +1 -1
  204. package/sidecar/package.json +1 -1
  205. package/skills/dms-ai/SKILL.md +2 -1
  206. package/skills/dms-external-database/SKILL.md +5 -2
  207. package/frontend-vue/app/chat/components/FullAutoBanner.vue +0 -76
  208. package/frontend-vue/app/chat/components/FullAutoDialog.vue +0 -103
@@ -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,
@@ -161,7 +151,7 @@ describe("the history drawer", () => {
161
151
  id: "c-old",
162
152
  title: "Why is order pending?",
163
153
  createdAtMs: 1,
164
- updatedAtMs: Date.now() - 3_600_000,
154
+ updatedAtMs: Date.now() - 1_000,
165
155
  messageCount: 2,
166
156
  filesChanged: 0,
167
157
  filesUndone: 1,
@@ -185,9 +175,7 @@ describe("the history drawer", () => {
185
175
  expect(drawer.text()).toContain("9.6k tok");
186
176
  expect(drawer.text()).toContain("1 file undone");
187
177
  await buttonWithText(drawer, "Why is order pending?").trigger("click");
188
- expect(sentOfType(harness, "leave_conversation")[0]).toMatchObject({
189
- conversationId: CONVERSATION_ID,
190
- });
178
+ expect(sentOfType(harness, "leave_conversation")).toEqual([]);
191
179
  expect(sentOfType(harness, "hello").at(-1)).toMatchObject({
192
180
  conversationId: "c-old",
193
181
  });
@@ -0,0 +1,119 @@
1
+ type ResizeCallback = () => void;
2
+
3
+ const observers = new Set<FakeResizeObserver>();
4
+ const pendingScrollEvents = new Set<() => void>();
5
+ const layoutPasses = new Set<() => void>();
6
+
7
+ /**
8
+ * jsdom has no layout, so no resize ever happens by itself: a test reports
9
+ * one with `settleLayout`, as the browser does once per frame.
10
+ */
11
+ class FakeResizeObserver {
12
+ private readonly targets = new Set<Element>();
13
+
14
+ constructor(private readonly callback: ResizeCallback) {
15
+ observers.add(this);
16
+ }
17
+
18
+ observe(target: Element): void {
19
+ this.targets.add(target);
20
+ }
21
+
22
+ unobserve(target: Element): void {
23
+ this.targets.delete(target);
24
+ }
25
+
26
+ disconnect(): void {
27
+ this.targets.clear();
28
+ }
29
+
30
+ deliver(): void {
31
+ if (this.targets.size > 0) this.callback();
32
+ }
33
+ }
34
+
35
+ export function installResizeObserver(): void {
36
+ Reflect.set(globalThis, "ResizeObserver", FakeResizeObserver);
37
+ }
38
+
39
+ function dispatchPendingScrollEvents(): void {
40
+ for (const dispatch of [...pendingScrollEvents]) dispatch();
41
+ pendingScrollEvents.clear();
42
+ }
43
+
44
+ /**
45
+ * A frame: scroll events fire first, then layout runs and the browser clamps
46
+ * or anchors the scroll position, then resizes are reported, and the scroll
47
+ * events layout caused fire at the start of the next frame.
48
+ */
49
+ export function settleLayout(): void {
50
+ dispatchPendingScrollEvents();
51
+ for (const pass of layoutPasses) pass();
52
+ for (const observer of [...observers]) observer.deliver();
53
+ dispatchPendingScrollEvents();
54
+ }
55
+
56
+ export interface FakeScroller {
57
+ /** Height of what the list draws, beside one row height per message. */
58
+ extraContentPx: number;
59
+ viewportPx: number;
60
+ distanceFromBottom: () => number;
61
+ /** A scroll to `scrollTop` and the event it fires, from any source. */
62
+ scrollTo: (scrollTop: number) => void;
63
+ /**
64
+ * Content above the viewport shrinks by `px` at the next layout, and scroll
65
+ * anchoring moves the scroll position up by as much to keep the view still.
66
+ */
67
+ collapseAbove: (px: number) => void;
68
+ }
69
+
70
+ const ROW_PX = 100;
71
+
72
+ /** Gives a scroll container the geometry a browser would compute. */
73
+ export function fakeScroller(el: HTMLElement): FakeScroller {
74
+ let scrollTop = 0;
75
+ let reportedTop = 0;
76
+ let anchorShiftPx = 0;
77
+ const dispatchScroll = (): void => {
78
+ el.dispatchEvent(new Event("scroll"));
79
+ };
80
+ const scroller: FakeScroller = {
81
+ extraContentPx: 0,
82
+ viewportPx: 400,
83
+ distanceFromBottom: () => el.scrollHeight - el.scrollTop - el.clientHeight,
84
+ scrollTo: (target) => {
85
+ dispatchPendingScrollEvents();
86
+ el.scrollTop = target;
87
+ dispatchPendingScrollEvents();
88
+ },
89
+ collapseAbove: (px) => {
90
+ scroller.extraContentPx -= px;
91
+ anchorShiftPx -= px;
92
+ },
93
+ };
94
+ const maxTop = (): number =>
95
+ Math.max(0, el.scrollHeight - scroller.viewportPx);
96
+ layoutPasses.add(() => {
97
+ scrollTop = Math.min(Math.max(0, el.scrollTop + anchorShiftPx), maxTop());
98
+ anchorShiftPx = 0;
99
+ if (scrollTop === reportedTop) return;
100
+ reportedTop = scrollTop;
101
+ pendingScrollEvents.add(dispatchScroll);
102
+ });
103
+ Object.defineProperties(el, {
104
+ clientHeight: { get: () => scroller.viewportPx },
105
+ scrollHeight: {
106
+ get: () =>
107
+ el.querySelectorAll("li").length * ROW_PX + scroller.extraContentPx,
108
+ },
109
+ scrollTop: {
110
+ get: () => Math.min(scrollTop, maxTop()),
111
+ set: (value: number) => {
112
+ scrollTop = Math.min(Math.max(0, value), maxTop());
113
+ reportedTop = scrollTop;
114
+ pendingScrollEvents.add(dispatchScroll);
115
+ },
116
+ },
117
+ });
118
+ return scroller;
119
+ }
@@ -1,6 +1,14 @@
1
1
  import { flushPromises, mount, type VueWrapper } from "@vue/test-utils";
2
- import { vi } from "vitest";
3
- import { defineComponent, h, nextTick, readonly, ref, shallowRef } from "vue";
2
+ import { type Mock, vi } from "vitest";
3
+ import {
4
+ defineComponent,
5
+ h,
6
+ nextTick,
7
+ readonly,
8
+ ref,
9
+ type Ref,
10
+ shallowRef,
11
+ } from "vue";
4
12
  import ChatPanel from "../../app/components/ChatPanel.vue";
5
13
  import {
6
14
  ASSISTANT_SESSION_KEY,
@@ -26,11 +34,11 @@ export interface Harness {
26
34
  hub: ChatTransportHub;
27
35
  sent: Array<Record<string, unknown>>;
28
36
  reconnects: number;
29
- sidecarStatus: ReturnType<typeof ref<SidecarStatus>>;
37
+ sidecarStatus: Ref<SidecarStatus>;
30
38
  setChannelStatus: (status: ChannelStatus) => void;
31
- navigate: ReturnType<typeof vi.fn>;
32
- restart: ReturnType<typeof vi.fn>;
33
- lastError: ReturnType<typeof ref<string | null>>;
39
+ navigate: Mock<(path: string) => void>;
40
+ restart: Mock<() => Promise<void>>;
41
+ lastError: Ref<string | null>;
34
42
  api: AssistantSession["api"];
35
43
  /** How many holds keep the tab's stream open. */
36
44
  streamHolds: number;
@@ -0,0 +1,3 @@
1
+ import { installResizeObserver } from "./fake-layout";
2
+
3
+ installResizeObserver();
@@ -64,6 +64,7 @@ const UModal = defineComponent({
64
64
 
65
65
  interface MenuItem {
66
66
  label?: string;
67
+ description?: string;
67
68
  onSelect?: () => void;
68
69
  }
69
70
 
@@ -84,6 +85,7 @@ const UDropdownMenu = defineComponent({
84
85
  {
85
86
  class: "u-dropdown-item",
86
87
  type: "button",
88
+ "data-description": item.description,
87
89
  onClick: item.onSelect,
88
90
  },
89
91
  item.label,
@@ -20,5 +20,6 @@ export default defineConfig({
20
20
  // `// @vitest-environment jsdom` header.
21
21
  environment: "node",
22
22
  include: ["tests/**/*.test.ts"],
23
+ setupFiles: ["tests/support/setup-layout.ts"],
23
24
  },
24
25
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-ai",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "AI development assistant for the AntelopeJS DMS: a chat panel next to every page and a coding agent that edits the project in reviewable, undoable change sets.",
5
5
  "keywords": [
6
6
  "ai",
@@ -76,6 +76,7 @@
76
76
  "suppressions:update": "antelopejs-suppressions --update"
77
77
  },
78
78
  "dependencies": {
79
+ "@antelopejs/agent-skills": "^0.1.0",
79
80
  "@antelopejs/interface-api": ">=0.0.15 <1.0.0",
80
81
  "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
81
82
  "@antelopejs/interface-dms": ">=0.9.0 <1.0.0",
@@ -101,7 +102,7 @@
101
102
  "typescript": "^6.0.3"
102
103
  },
103
104
  "peerDependencies": {
104
- "@openai/codex": "0.154.0"
105
+ "@openai/codex": "0.166.0"
105
106
  },
106
107
  "peerDependenciesMeta": {
107
108
  "@openai/codex": {
@@ -110,7 +111,7 @@
110
111
  },
111
112
  "optionalDependencies": {
112
113
  "@antelopejs/interface-database-catalog": ">=0.0.1 <1.0.0",
113
- "@antelopejs/interface-dms-builder": ">=0.3.0 <1.0.0"
114
+ "@antelopejs/interface-dms-builder": ">=0.3.1 <1.0.0"
114
115
  },
115
116
  "packageManager": "pnpm@10.2.0",
116
117
  "antelopeJs": {
@@ -0,0 +1,2 @@
1
+ export declare function isGitHookFile(path: string): boolean;
2
+ export declare function isDependencyFile(path: string): boolean;
@@ -0,0 +1,34 @@
1
+ import { basename } from "node:path";
2
+ import { DEPENDENCY_FILE_NAMES, GIT_HOOK_PATHS, } from "../constants/permissions.js";
3
+ const ANY_TRAILING_SEGMENTS = "**";
4
+ const GIT_HOOK_PATTERNS = GIT_HOOK_PATHS.map((path) => path.split("/"));
5
+ function pathSegments(path) {
6
+ return path
7
+ .toLowerCase()
8
+ .split(/[\\/]/)
9
+ .filter((segment) => segment.length > 0);
10
+ }
11
+ function matchesAt(segments, parts, start) {
12
+ return parts.every((part, index) => segments[start + index] === part);
13
+ }
14
+ function isUnder(segments, parts) {
15
+ return segments.some((_segment, start) => start + parts.length < segments.length &&
16
+ matchesAt(segments, parts, start));
17
+ }
18
+ function endsWith(segments, parts) {
19
+ const start = segments.length - parts.length;
20
+ return start >= 0 && matchesAt(segments, parts, start);
21
+ }
22
+ function matchesPathEnding(segments, pattern) {
23
+ return pattern.at(-1) === ANY_TRAILING_SEGMENTS
24
+ ? isUnder(segments, pattern.slice(0, -1))
25
+ : endsWith(segments, pattern);
26
+ }
27
+ export function isGitHookFile(path) {
28
+ const segments = pathSegments(path);
29
+ return GIT_HOOK_PATTERNS.some((pattern) => matchesPathEnding(segments, pattern));
30
+ }
31
+ export function isDependencyFile(path) {
32
+ return DEPENDENCY_FILE_NAMES.has(basename(path).toLowerCase());
33
+ }
34
+ //# sourceMappingURL=always-ask-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"always-ask-files.js","sourceRoot":"","sources":["../../src/agent/always-ask-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EACL,qBAAqB,EACrB,cAAc,GACf,MAAM,6BAA6B,CAAC;AAErC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC,MAAM,iBAAiB,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;AAExE,SAAS,YAAY,CAAC,IAAY;IAChC,OAAO,IAAI;SACR,WAAW,EAAE;SACb,KAAK,CAAC,OAAO,CAAC;SACd,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,SAAS,CAChB,QAA2B,EAC3B,KAAwB,EACxB,KAAa;IAEb,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,OAAO,CACd,QAA2B,EAC3B,KAAwB;IAExB,OAAO,QAAQ,CAAC,IAAI,CAClB,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,CAClB,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM;QACtC,SAAS,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,CACpC,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CACf,QAA2B,EAC3B,KAAwB;IAExB,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC7C,OAAO,KAAK,IAAI,CAAC,IAAI,SAAS,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,iBAAiB,CACxB,QAA2B,EAC3B,OAA0B;IAE1B,OAAO,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,qBAAqB;QAC7C,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACzC,CAAC,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AAClC,CAAC;AAMD,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACpC,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CACxC,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,CACrC,CAAC;AACJ,CAAC;AAMD,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,qBAAqB,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AACjE,CAAC"}
@@ -1,32 +1,23 @@
1
- import type { FullAutoDuration } from "../constants/audit.js";
2
1
  import type { ConversationModeType } from "../protocol/events.js";
3
2
  import type { ConversationStore } from "../state/conversations.js";
4
3
  import type { SettingsStore } from "../state/settings-store.js";
5
- import type { AppSettings, ChatMode, GenerationMode } from "../state/settings-types.js";
4
+ import { type AppSettings, type ChatMode, type GenerationMode, type TurnSettings } from "../state/settings-types.js";
6
5
  import { type ConversationModeState } from "./effective-mode.js";
7
6
  export interface ConversationModesPatch {
8
7
  mode?: ChatMode;
9
8
  generationMode?: GenerationMode;
10
- fullAuto?: FullAutoRequest | null;
11
- }
12
- interface FullAutoRequest {
13
- duration: FullAutoDuration;
14
9
  }
15
10
  export interface ConversationModesDeps {
16
11
  conversationStore: ConversationStore;
17
12
  settingsStore: SettingsStore;
18
- onChanged: (conversationId: string, hasFullAutoEnded: boolean) => void;
13
+ onChanged: (conversationId: string) => void;
19
14
  }
20
15
  export interface ConversationModes {
21
16
  state(conversationId: string): ConversationModeState;
22
17
  describe(conversationId: string): ConversationModeType;
23
18
  update(conversationId: string, patch: ConversationModesPatch): void;
24
19
  pinDefaults(conversationId: string): void;
25
- settingsFor(conversationId: string, settings: AppSettings): AppSettings;
26
- isFullAuto(conversationId: string): boolean;
27
- endTurn(conversationId: string): void;
28
- leave(conversationId: string): void;
20
+ settingsFor(conversationId: string, settings: AppSettings): TurnSettings;
29
21
  forget(conversationId: string): void;
30
22
  }
31
23
  export declare function createConversationModes(deps: ConversationModesDeps): ConversationModes;
32
- export {};
@@ -1,61 +1,24 @@
1
1
  import { effectiveGenerationMode } from "../builder/capability.js";
2
- import { MS_PER_MINUTE } from "../constants/settings.js";
3
- import { conversationSettings, isFullAutoInForce, } from "./effective-mode.js";
4
- const FULL_AUTO_TIMED_MINUTES = 30;
5
- const FULL_AUTO_CLOCK_MS = {
6
- "30m": FULL_AUTO_TIMED_MINUTES * MS_PER_MINUTE,
7
- };
8
- function readState(state, conversationId) {
2
+ import { modeForProvider, } from "../state/settings-types.js";
3
+ import { conversationSettings, } from "./effective-mode.js";
4
+ function readChosen(state, conversationId) {
9
5
  const stored = state.conversationStore.get(conversationId);
10
6
  const draft = state.drafts.get(conversationId);
11
7
  const defaults = state.settingsStore.get();
12
8
  return {
13
- mode: stored?.mode ?? draft?.mode ?? defaults.mode,
9
+ mode: stored?.mode ?? draft?.mode ?? defaults.defaultModes[defaults.provider],
14
10
  generationMode: stored?.generationMode ??
15
11
  draft?.generationMode ??
16
12
  defaults.generationMode,
17
- fullAuto: state.fullAuto.get(conversationId)?.state ?? null,
18
13
  };
19
14
  }
20
- function clearFullAuto(state, conversationId) {
21
- const entry = state.fullAuto.get(conversationId);
22
- if (entry === undefined)
23
- return false;
24
- clearTimeout(entry.timer);
25
- state.fullAuto.delete(conversationId);
26
- return true;
27
- }
28
- function endFullAuto(state, conversationId, duration) {
29
- const entry = state.fullAuto.get(conversationId);
30
- if (entry?.state.duration !== duration)
31
- return;
32
- clearFullAuto(state, conversationId);
33
- state.onChanged(conversationId, true);
34
- }
35
- function startFullAuto(state, conversationId, request) {
36
- clearFullAuto(state, conversationId);
37
- const clockMs = FULL_AUTO_CLOCK_MS[request.duration];
38
- if (clockMs === undefined) {
39
- state.fullAuto.set(conversationId, {
40
- state: { duration: request.duration },
41
- });
42
- return;
43
- }
44
- const timer = setTimeout(() => endFullAuto(state, conversationId, request.duration), clockMs);
45
- timer.unref();
46
- state.fullAuto.set(conversationId, {
47
- state: { duration: request.duration, untilMs: Date.now() + clockMs },
48
- timer,
49
- });
50
- }
51
- function applyFullAuto(state, conversationId, request) {
52
- if (request === undefined)
53
- return;
54
- if (request === null) {
55
- clearFullAuto(state, conversationId);
56
- return;
57
- }
58
- startFullAuto(state, conversationId, request);
15
+ function readState(state, conversationId) {
16
+ const chosen = readChosen(state, conversationId);
17
+ const { provider } = state.settingsStore.get();
18
+ return {
19
+ mode: modeForProvider(provider, chosen.mode),
20
+ generationMode: chosen.generationMode,
21
+ };
59
22
  }
60
23
  function storeModes(state, conversationId, patch) {
61
24
  if (state.conversationStore.get(conversationId) === null) {
@@ -70,12 +33,8 @@ function storeModes(state, conversationId, patch) {
70
33
  }
71
34
  function update(state, conversationId, patch) {
72
35
  pinDefaults(state, conversationId);
73
- storeModes(state, conversationId, {
74
- mode: patch.mode,
75
- generationMode: patch.generationMode,
76
- });
77
- applyFullAuto(state, conversationId, patch.fullAuto);
78
- state.onChanged(conversationId, false);
36
+ storeModes(state, conversationId, patch);
37
+ state.onChanged(conversationId);
79
38
  }
80
39
  function pinDefaults(state, conversationId) {
81
40
  const stored = state.conversationStore.get(conversationId);
@@ -83,19 +42,12 @@ function pinDefaults(state, conversationId) {
83
42
  return;
84
43
  if (stored.mode !== undefined && stored.generationMode !== undefined)
85
44
  return;
86
- const current = readState(state, conversationId);
45
+ const chosen = readChosen(state, conversationId);
87
46
  state.drafts.delete(conversationId);
88
- state.conversationStore.setModes(conversationId, {
89
- mode: current.mode,
90
- generationMode: current.generationMode,
91
- });
47
+ state.conversationStore.setModes(conversationId, chosen);
92
48
  }
93
49
  export function createConversationModes(deps) {
94
- const state = {
95
- ...deps,
96
- fullAuto: new Map(),
97
- drafts: new Map(),
98
- };
50
+ const state = { ...deps, drafts: new Map() };
99
51
  return {
100
52
  state: (conversationId) => readState(state, conversationId),
101
53
  describe(conversationId) {
@@ -104,17 +56,12 @@ export function createConversationModes(deps) {
104
56
  conversationId,
105
57
  mode: current.mode,
106
58
  generationMode: effectiveGenerationMode(current.generationMode),
107
- fullAuto: current.fullAuto,
108
59
  };
109
60
  },
110
61
  update: (conversationId, patch) => update(state, conversationId, patch),
111
62
  pinDefaults: (conversationId) => pinDefaults(state, conversationId),
112
63
  settingsFor: (conversationId, settings) => conversationSettings(settings, readState(state, conversationId)),
113
- isFullAuto: (conversationId) => isFullAutoInForce(readState(state, conversationId)),
114
- endTurn: (conversationId) => endFullAuto(state, conversationId, "turn"),
115
- leave: (conversationId) => endFullAuto(state, conversationId, "chat"),
116
64
  forget: (conversationId) => {
117
- clearFullAuto(state, conversationId);
118
65
  state.drafts.delete(conversationId);
119
66
  },
120
67
  };