@antelopejs/dms-ai 0.2.1 → 0.4.0

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 (87) hide show
  1. package/README.md +64 -205
  2. package/dist/constants/frontend-module.d.ts +2 -0
  3. package/dist/constants/frontend-module.js +6 -1
  4. package/dist/constants/frontend-module.js.map +1 -1
  5. package/dist/index.js +4 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/lifecycle/skill-sources.d.ts +1 -1
  8. package/dist/lifecycle/skill-sources.js +29 -13
  9. package/dist/lifecycle/skill-sources.js.map +1 -1
  10. package/frontend-vue/app/chat/components/BlockPreviewCard.vue +172 -0
  11. package/frontend-vue/app/chat/components/BlockPreviewModal.vue +324 -0
  12. package/frontend-vue/app/chat/components/BlockPreviewThumb.vue +141 -0
  13. package/frontend-vue/app/chat/components/MessageList.vue +20 -0
  14. package/frontend-vue/app/chat/components/SketchFrame.vue +59 -0
  15. package/frontend-vue/app/chat/composables/conversation-events.ts +23 -1
  16. package/frontend-vue/app/chat/composables/useBlockPreviewComponent.ts +15 -0
  17. package/frontend-vue/app/chat/composables/useDashboardScheme.ts +42 -0
  18. package/frontend-vue/app/chat/constants/block-preview.ts +27 -0
  19. package/frontend-vue/app/chat/constants/conversation.ts +1 -0
  20. package/frontend-vue/app/chat/constants/protocol.ts +1 -0
  21. package/frontend-vue/app/chat/constants/sketch.ts +105 -0
  22. package/frontend-vue/app/chat/types/conversation.ts +11 -1
  23. package/frontend-vue/app/chat/types/protocol.ts +56 -0
  24. package/frontend-vue/app/chat/utils/block-preview.ts +145 -0
  25. package/frontend-vue/app/chat/utils/conversation-messages.ts +23 -1
  26. package/frontend-vue/app/chat/utils/render-items.ts +13 -2
  27. package/frontend-vue/app/chat/utils/sketch.ts +56 -0
  28. package/frontend-vue/app/chat/utils/tool-lexicon.ts +7 -0
  29. package/frontend-vue/app/components/ActivityDetail.vue +11 -13
  30. package/frontend-vue/app/components/ChangesView.vue +56 -49
  31. package/frontend-vue/app/plugins/ai.client.ts +16 -6
  32. package/frontend-vue/app/runtime/assistant-activity.ts +38 -21
  33. package/frontend-vue/app/runtime/sidecar-status.ts +78 -56
  34. package/frontend-vue/app/views/ChangeSetPanel.vue +13 -18
  35. package/frontend-vue/app/views/UnavailableState.vue +1 -1
  36. package/frontend-vue/app/views/composables/useAiData.ts +116 -0
  37. package/frontend-vue/app/views/composables/useAssistant.ts +8 -2
  38. package/frontend-vue/app/views/composables/useChangeSetActions.ts +7 -6
  39. package/frontend-vue/app/views/utils/change-sets.ts +0 -7
  40. package/frontend-vue/i18n/locales/dms-ai-panel-en-GB.json +23 -1
  41. package/frontend-vue/i18n/locales/dms-ai-panel-fr-FR.json +23 -1
  42. package/frontend-vue/package.json +1 -1
  43. package/frontend-vue/tests/assistant-activity.test.ts +69 -0
  44. package/frontend-vue/tests/chat/block-preview.test.ts +448 -0
  45. package/frontend-vue/tests/sidecar-guest.test.ts +1 -1
  46. package/frontend-vue/tests/sidecar-status.test.ts +93 -15
  47. package/frontend-vue/tests/support/dms-frontend-module.ts +9 -0
  48. package/frontend-vue/tests/views/api-data-reads.test.ts +445 -0
  49. package/frontend-vue/tests/views/use-assistant.test.ts +53 -0
  50. package/frontend-vue/vitest.config.ts +10 -0
  51. package/package.json +20 -6
  52. package/sidecar/dist/constants/mcp.js +2 -0
  53. package/sidecar/dist/constants/mcp.js.map +1 -1
  54. package/sidecar/dist/constants/preview.d.ts +39 -0
  55. package/sidecar/dist/constants/preview.js +55 -0
  56. package/sidecar/dist/constants/preview.js.map +1 -0
  57. package/sidecar/dist/constants/tool-kinds.js +2 -0
  58. package/sidecar/dist/constants/tool-kinds.js.map +1 -1
  59. package/sidecar/dist/mcp/tool-definitions.js +5 -0
  60. package/sidecar/dist/mcp/tool-definitions.js.map +1 -1
  61. package/sidecar/dist/mcp/tools/preview-blocks.d.ts +65 -0
  62. package/sidecar/dist/mcp/tools/preview-blocks.js +176 -0
  63. package/sidecar/dist/mcp/tools/preview-blocks.js.map +1 -0
  64. package/sidecar/dist/mcp/types.d.ts +3 -0
  65. package/sidecar/dist/prompts/agent.d.ts +1 -1
  66. package/sidecar/dist/prompts/agent.js +3 -2
  67. package/sidecar/dist/prompts/agent.js.map +1 -1
  68. package/sidecar/dist/protocol/events.d.ts +313 -0
  69. package/sidecar/dist/protocol/events.js +9 -0
  70. package/sidecar/dist/protocol/events.js.map +1 -1
  71. package/sidecar/dist/protocol/preview.d.ts +130 -0
  72. package/sidecar/dist/protocol/preview.js +91 -0
  73. package/sidecar/dist/protocol/preview.js.map +1 -0
  74. package/sidecar/dist/server/chat-events.d.ts +3 -0
  75. package/sidecar/dist/server/chat-events.js +21 -0
  76. package/sidecar/dist/server/chat-events.js.map +1 -1
  77. package/sidecar/dist/server/ws.js +3 -1
  78. package/sidecar/dist/server/ws.js.map +1 -1
  79. package/sidecar/dist/state/types.d.ts +3 -1
  80. package/sidecar/dist/state/types.js +1 -0
  81. package/sidecar/dist/state/types.js.map +1 -1
  82. package/sidecar/package.json +1 -1
  83. package/skills/dms-ai/SKILL.md +30 -0
  84. package/skills/dms-ai-code-mode/SKILL.md +51 -0
  85. package/skills/dms-builder-safe/SKILL.md +9 -2
  86. package/frontend-vue/app/views/composables/useAiRequest.ts +0 -89
  87. package/skills/dms-builder-page/SKILL.md +0 -80
@@ -0,0 +1,445 @@
1
+ // @vitest-environment jsdom
2
+ import { flushPromises, shallowMount } from "@vue/test-utils";
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import {
5
+ computed,
6
+ defineComponent,
7
+ h,
8
+ reactive,
9
+ ref,
10
+ toValue,
11
+ watch,
12
+ type MaybeRefOrGetter,
13
+ } from "vue";
14
+ import ActivityDetail from "../../app/components/ActivityDetail.vue";
15
+ import ChangesView from "../../app/components/ChangesView.vue";
16
+ import ChangeSetPanel from "../../app/views/ChangeSetPanel.vue";
17
+ import UnavailableState from "../../app/views/UnavailableState.vue";
18
+
19
+ // The views read what they render with the DMS's `useApiData`, which a server
20
+ // render hands to the browser (this repository has no renderer to run it:
21
+ // the playground proves that part), and the Changes list page by page with
22
+ // `useApiPagedData`; `$authFetch` is left to the writes. These fakes read as
23
+ // the browser does.
24
+
25
+ const route = reactive({ query: {} as Record<string, string> });
26
+
27
+ vi.mock("#dms/frontend-module", () => ({
28
+ useDmsRoute: () => route,
29
+ useDmsRouter: () => ({ push: vi.fn() }),
30
+ }));
31
+
32
+ interface Read {
33
+ url: string;
34
+ query?: Record<string, unknown>;
35
+ }
36
+
37
+ type Backend = (read: Read) => unknown;
38
+
39
+ let backend: Backend;
40
+ const reads: Read[] = [];
41
+ const authFetch = vi.fn();
42
+ /** The reads the page's refreshes read again, as the DMS's `refreshPageBlocks()` does. */
43
+ const pageReads = new Set<() => Promise<void>>();
44
+ const refreshPageBlocks = vi.fn(async () => {
45
+ await Promise.all([...pageReads].map((refresh) => refresh()));
46
+ });
47
+
48
+ interface FakeApiDataOptions {
49
+ query?: MaybeRefOrGetter<Record<string, unknown>>;
50
+ refreshOnPageRefresh?: boolean;
51
+ }
52
+
53
+ function useApiData<T>(
54
+ url: MaybeRefOrGetter<string | null | undefined>,
55
+ options: FakeApiDataOptions = {},
56
+ ) {
57
+ const data = ref<T | null>(null);
58
+ const error = ref<unknown>(null);
59
+ const status = ref<"idle" | "pending" | "success" | "error">("idle");
60
+ const request = () => ({ url: toValue(url), query: toValue(options.query) });
61
+ async function refresh(): Promise<void> {
62
+ const { url: target, query } = request();
63
+ if (target == null) return;
64
+ const read = query ? { url: target, query } : { url: target };
65
+ reads.push(read);
66
+ status.value = "pending";
67
+ try {
68
+ // Each answer is a new value, as a parsed response is.
69
+ data.value = structuredClone(await backend(read)) as T;
70
+ error.value = null;
71
+ status.value = "success";
72
+ } catch (failure) {
73
+ error.value = failure;
74
+ status.value = "error";
75
+ }
76
+ }
77
+ watch(request, () => void refresh(), { deep: true, immediate: true });
78
+ if (options.refreshOnPageRefresh !== false) pageReads.add(refresh);
79
+ return {
80
+ data,
81
+ error,
82
+ status,
83
+ pending: computed(() => status.value === "pending"),
84
+ placeholder: ref(false),
85
+ refresh,
86
+ execute: refresh,
87
+ clear: () => undefined,
88
+ };
89
+ }
90
+
91
+ interface FakePagedOptions<TPage> {
92
+ nextCursor: (page: TPage, pages: TPage[]) => number | null | undefined;
93
+ }
94
+
95
+ type PagedContext = {
96
+ $authFetch: typeof authFetch;
97
+ cursor: number | undefined;
98
+ signal: AbortSignal;
99
+ };
100
+
101
+ /** `useApiPagedData` as the renderer runs it: one key, the pages it loaded, all read again on a refresh. */
102
+ function useApiPagedData<TPage>(
103
+ key: MaybeRefOrGetter<string>,
104
+ handler: (context: PagedContext) => Promise<TPage>,
105
+ options: FakePagedOptions<TPage>,
106
+ ) {
107
+ const pages = ref<TPage[]>([]) as { value: TPage[] };
108
+ const next = ref<number | null>(null);
109
+ const error = ref<unknown>(null);
110
+ const status = ref<"idle" | "pending" | "success" | "error">("idle");
111
+ const loadingMore = ref(false);
112
+ const read = (cursor: number | undefined) =>
113
+ handler({
114
+ $authFetch: authFetch,
115
+ cursor,
116
+ signal: new AbortController().signal,
117
+ });
118
+ async function readPages(count: number): Promise<void> {
119
+ status.value = "pending";
120
+ const loaded: TPage[] = [];
121
+ let cursor: number | undefined;
122
+ try {
123
+ for (let index = 0; index < count; index++) {
124
+ const page = structuredClone(await read(cursor)) as TPage;
125
+ loaded.push(page);
126
+ const after = options.nextCursor(page, loaded) ?? null;
127
+ next.value = after;
128
+ if (after === null) break;
129
+ cursor = after;
130
+ }
131
+ pages.value = loaded;
132
+ error.value = null;
133
+ status.value = "success";
134
+ } catch (failure) {
135
+ error.value = failure;
136
+ status.value = "error";
137
+ }
138
+ }
139
+ const refresh = () => readPages(Math.max(pages.value.length, 1));
140
+ watch(
141
+ () => toValue(key),
142
+ () => void readPages(1),
143
+ { immediate: true },
144
+ );
145
+ pageReads.add(refresh);
146
+ async function loadMore(): Promise<void> {
147
+ if (next.value === null) return;
148
+ loadingMore.value = true;
149
+ try {
150
+ const page = structuredClone(await read(next.value)) as TPage;
151
+ pages.value = [...pages.value, page];
152
+ next.value = options.nextCursor(page, pages.value) ?? null;
153
+ } catch (failure) {
154
+ error.value = failure;
155
+ } finally {
156
+ loadingMore.value = false;
157
+ }
158
+ }
159
+ return {
160
+ pages,
161
+ error,
162
+ status,
163
+ pending: computed(() => status.value === "pending"),
164
+ hasMore: computed(() => next.value !== null),
165
+ loadingMore,
166
+ loadMore,
167
+ refresh,
168
+ };
169
+ }
170
+
171
+ const Stub = (name: string) =>
172
+ defineComponent({
173
+ name,
174
+ inheritAttrs: false,
175
+ setup:
176
+ (_, { slots }) =>
177
+ () =>
178
+ h("div", { "data-stub": name }, slots.default?.()),
179
+ });
180
+
181
+ const GLOBAL_STUBS = Object.fromEntries(
182
+ [
183
+ "DmsCard",
184
+ "DmsEmptyState",
185
+ "DmsMasterDetail",
186
+ "DmsSegmented",
187
+ "DmsKeyValueList",
188
+ "DmsIconWell",
189
+ "DmsEyebrow",
190
+ "DmsCodeSnippet",
191
+ "UButton",
192
+ "UInput",
193
+ "USkeleton",
194
+ ].map((name) => [name, Stub(name)]),
195
+ );
196
+
197
+ const unavailable = () =>
198
+ Object.assign(new Error("Service Unavailable"), {
199
+ statusCode: 503,
200
+ data: { message: "The assistant is not running" },
201
+ });
202
+
203
+ const ROW = {
204
+ id: "act-1",
205
+ timestampMs: Date.UTC(2026, 9, 15, 10),
206
+ tool: "Edit",
207
+ target: "src/pages/home.ts",
208
+ agent: "claude",
209
+ allowedBy: "approved",
210
+ result: "done",
211
+ conversationId: "conv-1",
212
+ conversationTitle: "Rename the home page",
213
+ isReadOnly: false,
214
+ isAutoFix: false,
215
+ };
216
+
217
+ const summary = (id: string, number: number) => ({
218
+ id,
219
+ number,
220
+ title: `Change ${number}`,
221
+ state: "applied",
222
+ createdAtMs: Date.UTC(2026, 9, 15, 10),
223
+ added: 1,
224
+ removed: 0,
225
+ scope: "safe",
226
+ agent: "claude",
227
+ files: [],
228
+ isAutoFix: false,
229
+ approvalsNeeded: 0,
230
+ builderOps: 0,
231
+ typecheck: "passed",
232
+ });
233
+
234
+ beforeEach(() => {
235
+ reads.length = 0;
236
+ authFetch.mockReset();
237
+ route.query = {};
238
+ pageReads.clear();
239
+ refreshPageBlocks.mockClear();
240
+ vi.stubGlobal("useApiData", useApiData);
241
+ vi.stubGlobal("useApiPagedData", useApiPagedData);
242
+ vi.stubGlobal("refreshPageBlocks", refreshPageBlocks);
243
+ vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
244
+ vi.stubGlobal("useI18n", () => ({
245
+ t: (key: string) => key,
246
+ te: () => false,
247
+ }));
248
+ vi.stubGlobal("useRegionalFormat", () => ({
249
+ dateTimeFormat: (options: Intl.DateTimeFormatOptions) =>
250
+ new Intl.DateTimeFormat("en-GB", { ...options, timeZone: "UTC" }),
251
+ }));
252
+ vi.stubGlobal("useToast", () => ({ add: vi.fn() }));
253
+ vi.stubGlobal("useConfirmFrom", () => ({ confirmFrom: vi.fn() }));
254
+ });
255
+
256
+ afterEach(() => {
257
+ vi.unstubAllGlobals();
258
+ });
259
+
260
+ const mount = (component: Parameters<typeof shallowMount>[0], props = {}) =>
261
+ shallowMount(component, {
262
+ props,
263
+ global: { stubs: { ...GLOBAL_STUBS, UnavailableState: false } },
264
+ });
265
+
266
+ describe("activity detail", () => {
267
+ it("reads the entry its row names with the page, not with $authFetch", async () => {
268
+ backend = () => ({ ...ROW, args: { file_path: "src/pages/home.ts" } });
269
+ const wrapper = mount(ActivityDetail, { rowData: ROW });
270
+ await flushPromises();
271
+ expect(reads).toEqual([{ url: "/ai/activity/act-1" }]);
272
+ expect(authFetch).not.toHaveBeenCalled();
273
+ expect(wrapper.findComponent(UnavailableState).exists()).toBe(false);
274
+ expect(wrapper.find('[data-stub="USkeleton"]').exists()).toBe(false);
275
+ });
276
+
277
+ it("says the assistant is not running on a 503, and reads again on retry", async () => {
278
+ backend = () => {
279
+ throw unavailable();
280
+ };
281
+ const wrapper = mount(ActivityDetail, { rowData: ROW });
282
+ await flushPromises();
283
+ const state = wrapper.findComponent(UnavailableState);
284
+ expect(state.props("failure")).toBe("unavailable");
285
+ backend = () => ROW;
286
+ state.vm.$emit("retry");
287
+ await flushPromises();
288
+ expect(reads).toHaveLength(2);
289
+ expect(wrapper.findComponent(UnavailableState).exists()).toBe(false);
290
+ });
291
+ });
292
+
293
+ describe("changes view", () => {
294
+ const PAGE = { results: [summary("cs-1", 1), summary("cs-2", 2)], total: 3 };
295
+ const NEXT_PAGE = { results: [summary("cs-3", 3)], total: 3 };
296
+
297
+ function changesBackend({ url, query }: Read): unknown {
298
+ if (url === "/ai/status") return { checkpointsAvailable: true };
299
+ if (url === "/ai/changes") return query?.offset ? NEXT_PAGE : PAGE;
300
+ return { changeSet: summary("cs-1", 1), files: [] };
301
+ }
302
+
303
+ /** The paged list's requests, which go through the handler's `$authFetch`. */
304
+ function listReads(): unknown[] {
305
+ return authFetch.mock.calls.map(([url, options]) => [
306
+ url,
307
+ (options as { query: unknown }).query,
308
+ ]);
309
+ }
310
+
311
+ beforeEach(() => {
312
+ authFetch.mockImplementation(async (url: string, options: Read) =>
313
+ changesBackend({ url, query: options.query }),
314
+ );
315
+ });
316
+
317
+ it("reads the status and the first page with the page, and opens the set the URL names", async () => {
318
+ backend = changesBackend;
319
+ route.query = { set: "cs-2" };
320
+ const wrapper = mount(ChangesView);
321
+ await flushPromises();
322
+ expect(reads).toEqual([{ url: "/ai/status" }]);
323
+ expect(listReads()).toEqual([["/ai/changes", { offset: 0, limit: 30 }]]);
324
+ expect(wrapper.findComponent(ChangeSetPanel).props("changeSetId")).toBe(
325
+ "cs-2",
326
+ );
327
+ });
328
+
329
+ it("selects the first set when the URL names none", async () => {
330
+ backend = changesBackend;
331
+ const wrapper = mount(ChangesView);
332
+ await flushPromises();
333
+ expect(wrapper.findComponent(ChangeSetPanel).props("changeSetId")).toBe(
334
+ "cs-1",
335
+ );
336
+ });
337
+
338
+ it("appends the next page on Load more, and reads every loaded page again on a page refresh", async () => {
339
+ backend = changesBackend;
340
+ const wrapper = mount(ChangesView);
341
+ await flushPromises();
342
+ const loadMoreButton = () =>
343
+ wrapper
344
+ .findAllComponents({ name: "UButton" })
345
+ .find(
346
+ (button) =>
347
+ button.vm.$attrs.label === "dms_ai.views.common.load_more",
348
+ );
349
+ loadMoreButton()!.vm.$emit("click");
350
+ await flushPromises();
351
+ expect(listReads().at(-1)).toEqual([
352
+ "/ai/changes",
353
+ { offset: 2, limit: 30 },
354
+ ]);
355
+ const items = () =>
356
+ (
357
+ wrapper.findComponent({ name: "DmsMasterDetail" }).vm.$attrs.items as {
358
+ value: string;
359
+ }[]
360
+ ).map((item) => item.value);
361
+ expect(items()).toEqual(["cs-1", "cs-2", "cs-3"]);
362
+ expect(loadMoreButton()).toBeUndefined();
363
+
364
+ // A change set undone in the panel refreshes the page, which reads the
365
+ // pages loaded again rather than dropping the second one.
366
+ authFetch.mockClear();
367
+ await refreshPageBlocks();
368
+ await flushPromises();
369
+ expect(listReads()).toEqual([
370
+ ["/ai/changes", { offset: 0, limit: 30 }],
371
+ ["/ai/changes", { offset: 2, limit: 30 }],
372
+ ]);
373
+ expect(items()).toEqual(["cs-1", "cs-2", "cs-3"]);
374
+ });
375
+
376
+ it("reads the filtered first page when the scope changes", async () => {
377
+ backend = changesBackend;
378
+ const wrapper = mount(ChangesView);
379
+ await flushPromises();
380
+ wrapper
381
+ .findComponent({ name: "DmsSegmented" })
382
+ .vm.$emit("update:modelValue", "vibe");
383
+ await flushPromises();
384
+ expect(listReads().at(-1)).toEqual([
385
+ "/ai/changes",
386
+ { offset: 0, limit: 30, scope: "vibe", filter_scope: "is:vibe" },
387
+ ]);
388
+ });
389
+ });
390
+
391
+ describe("change set panel", () => {
392
+ it("refreshes the page once a set is redone, instead of reading on its own", async () => {
393
+ backend = () => ({
394
+ changeSet: { ...summary("cs-1", 1), state: "undone" },
395
+ files: [],
396
+ });
397
+ authFetch.mockResolvedValue({});
398
+ const wrapper = mount(ChangeSetPanel, { changeSetId: "cs-1" });
399
+ await flushPromises();
400
+ expect(reads).toEqual([{ url: "/ai/changes/cs-1" }]);
401
+ const redo = wrapper
402
+ .findAllComponents({ name: "UButton" })
403
+ .find((button) => button.vm.$attrs.label === "dms_ai.views.changes.redo");
404
+ redo!.vm.$emit("click");
405
+ await flushPromises();
406
+ expect(authFetch).toHaveBeenCalledWith("/ai/changes/cs-1/redo", {
407
+ method: "POST",
408
+ body: {},
409
+ });
410
+ expect(refreshPageBlocks).toHaveBeenCalledTimes(1);
411
+ expect(reads).toEqual([
412
+ { url: "/ai/changes/cs-1" },
413
+ { url: "/ai/changes/cs-1" },
414
+ ]);
415
+ });
416
+ });
417
+
418
+ describe("change set actions", () => {
419
+ it("stay busy until the page refresh a redo asks for is done", async () => {
420
+ backend = () => ({
421
+ changeSet: { ...summary("cs-1", 1), state: "undone" },
422
+ files: [],
423
+ });
424
+ authFetch.mockResolvedValue({});
425
+ let finishRefresh: () => void = () => undefined;
426
+ refreshPageBlocks.mockImplementationOnce(
427
+ () => new Promise<void>((resolve) => (finishRefresh = resolve)),
428
+ );
429
+ const wrapper = mount(ChangeSetPanel, { changeSetId: "cs-1" });
430
+ await flushPromises();
431
+ const redo = () =>
432
+ wrapper
433
+ .findAllComponents({ name: "UButton" })
434
+ .find(
435
+ (button) => button.vm.$attrs.label === "dms_ai.views.changes.redo",
436
+ )!;
437
+ redo().vm.$emit("click");
438
+ await flushPromises();
439
+ expect(refreshPageBlocks).toHaveBeenCalledTimes(1);
440
+ expect(redo().vm.$attrs.loading).toBe(true);
441
+ finishRefresh();
442
+ await flushPromises();
443
+ expect(redo().vm.$attrs.loading).toBe(false);
444
+ });
445
+ });
@@ -0,0 +1,53 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { createSSRApp, defineComponent, h, nextTick, shallowRef } from "vue";
4
+ import { renderToString } from "vue/server-renderer";
5
+ import type { AssistantSession } from "../../app/runtime/assistant-session";
6
+ import { ASSISTANT_SESSION_KEY } from "../../app/runtime/assistant-session";
7
+ import { useAssistant } from "../../app/views/composables/useAssistant";
8
+
9
+ vi.mock("#dms/frontend-module", () => ({
10
+ useDmsRouter: () => ({ push: vi.fn() }),
11
+ }));
12
+
13
+ // A view offering an assistant button, as the Changes empty state does.
14
+ const View = defineComponent({
15
+ setup() {
16
+ const assistant = useAssistant();
17
+ return () =>
18
+ h("div", [assistant.isAvailable ? h("button", "Open assistant") : null]);
19
+ },
20
+ });
21
+
22
+ function appWith(session: AssistantSession | null) {
23
+ const app = createSSRApp(View);
24
+ app.provide(ASSISTANT_SESSION_KEY, shallowRef(session));
25
+ return app;
26
+ }
27
+
28
+ afterEach(() => {
29
+ document.body.innerHTML = "";
30
+ });
31
+
32
+ describe("useAssistant", () => {
33
+ it("hydrates what the server rendered when the session started first, then offers the assistant", async () => {
34
+ const html = await renderToString(appWith(null));
35
+ const root = document.createElement("div");
36
+ root.innerHTML = html;
37
+ document.body.append(root);
38
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
39
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
40
+
41
+ // The tab's probe answered before this block hydrated.
42
+ appWith({} as AssistantSession).mount(root);
43
+ const mismatches = [...warn.mock.calls, ...error.mock.calls]
44
+ .flat()
45
+ .filter((entry) => String(entry).includes("Hydration"));
46
+ warn.mockRestore();
47
+ error.mockRestore();
48
+ expect(mismatches).toEqual([]);
49
+
50
+ await nextTick();
51
+ expect(root.querySelector("button")?.textContent).toBe("Open assistant");
52
+ });
53
+ });
@@ -1,8 +1,18 @@
1
+ import { resolve } from "node:path";
1
2
  import vue from "@vitejs/plugin-vue";
2
3
  import { defineConfig } from "vitest/config";
3
4
 
4
5
  export default defineConfig({
5
6
  plugins: [vue()],
7
+ resolve: {
8
+ alias: {
9
+ // The renderer's runtime exists in the generated application only.
10
+ "#dms/frontend-module": resolve(
11
+ __dirname,
12
+ "tests/support/dms-frontend-module.ts",
13
+ ),
14
+ },
15
+ },
6
16
  test: {
7
17
  // Most suites need no DOM, and setting jsdom up per file is the bulk of
8
18
  // the run; the files that mount components opt in with a
package/package.json CHANGED
@@ -1,6 +1,20 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-ai",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
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
+ "keywords": [
6
+ "ai",
7
+ "antelopejs",
8
+ "assistant",
9
+ "claude-code",
10
+ "codex",
11
+ "dms",
12
+ "dms-module"
13
+ ],
14
+ "homepage": "https://dms.antelopejs.com/docs/modules/ai",
15
+ "bugs": {
16
+ "url": "https://github.com/AntelopeJS/dms-ai/issues"
17
+ },
4
18
  "license": "Apache-2.0",
5
19
  "repository": {
6
20
  "type": "git",
@@ -52,9 +66,9 @@
52
66
  "knip:all": "knip"
53
67
  },
54
68
  "dependencies": {
55
- "@antelopejs/interface-api": ">=0.0.14 <1.0.0",
56
- "@antelopejs/interface-core": ">=0.1.1 <1.0.0",
57
- "@antelopejs/interface-dms": ">=0.6.0 <1.0.0",
69
+ "@antelopejs/interface-api": ">=0.0.15 <1.0.0",
70
+ "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
71
+ "@antelopejs/interface-dms": ">=0.7.2 <1.0.0",
58
72
  "@anthropic-ai/claude-agent-sdk": "^0.2.131",
59
73
  "@anthropic-ai/claude-code": "^2.1.131",
60
74
  "@modelcontextprotocol/sdk": "1.29.0",
@@ -62,7 +76,7 @@
62
76
  "zod": "^4.0.0"
63
77
  },
64
78
  "devDependencies": {
65
- "@antelopejs/tooling-configs": ">=0.0.7 <1.0.0",
79
+ "@antelopejs/tooling-configs": ">=0.0.9 <1.0.0",
66
80
  "@types/node": "^24.0.0",
67
81
  "@types/ws": "^8.5.12",
68
82
  "knip": "^6.34.0",
@@ -83,7 +97,7 @@
83
97
  }
84
98
  },
85
99
  "optionalDependencies": {
86
- "@antelopejs/interface-dms-builder": ">=0.1.3 <1.0.0"
100
+ "@antelopejs/interface-dms-builder": ">=0.2.1 <1.0.0"
87
101
  },
88
102
  "packageManager": "pnpm@10.2.0",
89
103
  "antelopeJs": {
@@ -1,6 +1,7 @@
1
1
  import { BUILDER_TOOL_NAMES } from "./builder.js";
2
2
  import { QUERY_LOGS_TOOL_NAME } from "./logs.js";
3
3
  import { FIND_PAGES_TOOL_NAME, LIST_PAGES_TOOL_NAME } from "./pages.js";
4
+ import { PREVIEW_BLOCKS_TOOL_NAME } from "./preview.js";
4
5
  import { REQUEST_TIMEOUT_MINUTES } from "../state/settings-types.js";
5
6
  import { QUESTION_TIMEOUT_MS } from "./questions.js";
6
7
  import { MS_PER_MINUTE } from "./settings.js";
@@ -35,6 +36,7 @@ export const FIRST_PARTY_AUTO_ALLOW_TOOL_NAMES = new Set([
35
36
  FIND_PAGES_TOOL_NAME,
36
37
  LIST_PAGES_TOOL_NAME,
37
38
  ASK_USER_TOOL_NAME,
39
+ PREVIEW_BLOCKS_TOOL_NAME,
38
40
  TYPECHECK_TOOL_NAME,
39
41
  QUERY_LOGS_TOOL_NAME,
40
42
  ...BUILDER_TOOL_NAMES,
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/constants/mcp.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/constants/mcp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ export declare const PREVIEW_BLOCKS_TOOL_NAME = "PreviewBlocks";
2
+ export declare const PREVIEW_BLOCKS_TOOL_DESCRIPTION = "Shows the user a live preview of DMS blocks as an attachment in the chat: a card they open to see the real blocks rendered, next to their own mockup when you pass it as `reference`, with one tab per variant. A view may carry a `sketch` instead of (or beside) its `tree`: static HTML and CSS of a custom component you propose, drawn in a sandbox and labelled as not a DMS block, so the user compares it with the DMS option before any code is written. Use it to settle a visual choice before building (DMS block as is versus custom, or between layouts), then ask the decision with AskUser. It returns at once with the preview id; it does not wait for the user and changes nothing in the project.";
3
+ export declare const PREVIEW_MAX_DEPTH = 8;
4
+ export declare const PREVIEW_MAX_NODES = 200;
5
+ export declare const PREVIEW_MAX_VARIANTS = 4;
6
+ export declare const PREVIEW_MAX_ROOTS = 12;
7
+ export declare const PREVIEW_MAX_TITLE_CHARS = 120;
8
+ export declare const PREVIEW_MAX_LABEL_CHARS = 60;
9
+ export declare const PREVIEW_MAX_DESCRIPTION_CHARS = 2000;
10
+ export declare const PREVIEW_MAX_CAPTION_CHARS = 200;
11
+ export declare const PREVIEW_MAX_URL_CHARS = 2048;
12
+ export declare const PREVIEW_MAX_JSON_BYTES: number;
13
+ export declare const PREVIEW_SKETCH_MIN_HEIGHT_PX = 80;
14
+ export declare const PREVIEW_SKETCH_DEFAULT_HEIGHT_PX = 320;
15
+ export declare const PREVIEW_SKETCH_MAX_HEIGHT_PX = 1200;
16
+ export declare const PREVIEW_LATEST_ATTACHMENT_ID = "latest";
17
+ export declare const PREVIEW_SAME_ORIGIN_PATH: RegExp;
18
+ export declare const PREVIEW_REMOTE_URL: RegExp;
19
+ export declare const PREVIEW_IMAGE_MIME_PREFIX = "image/";
20
+ export declare const PREVIEW_URL_OPTION_KEY: RegExp;
21
+ export declare const PREVIEW_ERROR_CODES: {
22
+ readonly INVALID_TREE: "invalid_tree";
23
+ readonly TOO_LARGE: "too_large";
24
+ readonly UNKNOWN_ATTACHMENT: "unknown_attachment";
25
+ readonly INVALID_URL: "invalid_url";
26
+ readonly INVALID_SKETCH: "invalid_sketch";
27
+ };
28
+ export declare const PREVIEW_SKETCH_FORBIDDEN: ReadonlyArray<readonly [RegExp, string]>;
29
+ export declare const PREVIEW_RESULT_SHOWN = "Preview shown to the user as an attachment. If it settles a choice, ask it now with AskUser.";
30
+ export declare const PREVIEW_ERROR_NO_TYPE = "Every node needs a `type` (a block type from BuilderCatalog) or a `componentName`.";
31
+ export declare const PREVIEW_ERROR_DEPTH = "The tree is nested deeper than 8 levels.";
32
+ export declare const PREVIEW_ERROR_NODES = "The preview has more than 200 nodes in all; show a smaller part of the page.";
33
+ export declare const PREVIEW_ERROR_BYTES = "The preview's trees and sketches are larger than 65536 bytes of JSON; show a smaller part of the page.";
34
+ export declare const PREVIEW_ERROR_NO_ATTACHMENTS = "The user attached no image in this conversation; leave `reference.attachmentId` out.";
35
+ export declare const PREVIEW_ERROR_UNKNOWN_ATTACHMENT = "No image of that name was attached in this conversation. Attached images: ";
36
+ export declare const PREVIEW_ERROR_OPTION_URL = "Options ending in `Url` (and `location`) must be same-origin paths such as `/api/orders`, or the preview refuses the block. Offending option: ";
37
+ export declare const PREVIEW_ERROR_EMPTY_VIEW = "Every view (the preview and each variant) needs a `tree`, a `sketch`, or both.";
38
+ export declare const PREVIEW_ERROR_SKETCH = "A sketch is static HTML and CSS: inline images and fonts as data: URLs, and use no script, event handler, external resource or embedded frame. Refused: ";
39
+ export declare const PREVIEW_ERROR_URL = "`reference.url` must be a same-origin path (`/media/...`) or an http(s) URL.";