@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
@@ -1,8 +1,8 @@
1
1
  <script setup lang="ts">
2
- import { computed, watch } from "vue";
2
+ import { computed } from "vue";
3
3
  import DiffView from "./DiffView.vue";
4
4
  import UnavailableState from "./UnavailableState.vue";
5
- import { useAiRequest } from "./composables/useAiRequest";
5
+ import { useAiData } from "./composables/useAiData";
6
6
  import { useAssistant } from "./composables/useAssistant";
7
7
  import { useChangeSetActions } from "./composables/useChangeSetActions";
8
8
  import { useViewFormat } from "./composables/useViewFormat";
@@ -20,28 +20,23 @@ interface Props {
20
20
  }
21
21
 
22
22
  const props = defineProps<Props>();
23
- const emit = defineEmits<{ changed: [] }>();
24
23
 
25
24
  const { t } = useI18n();
26
- const { $authFetch } = useAuthFetch();
27
25
  const { moment } = useViewFormat();
28
26
  const assistant = useAssistant();
29
- const detail = useAiRequest<ChangeSetDetail>();
30
- const actions = useChangeSetActions(() => {
31
- void load();
32
- emit("changed");
33
- });
27
+ // A set picked again shows what was read before and reads again: it may have
28
+ // been undone or redone since.
29
+ const detail = useAiData<ChangeSetDetail>(
30
+ () => `${AI_ROUTES.changes}/${encodeURIComponent(props.changeSetId)}`,
31
+ );
32
+ // Once a set is undone or redone, the page reads its data again: this panel,
33
+ // the list and the assistant's status.
34
+ const actions = useChangeSetActions();
34
35
 
35
- function load(): Promise<ChangeSetDetail | null> {
36
- return detail.run(() =>
37
- $authFetch<ChangeSetDetail>(
38
- `${AI_ROUTES.changes}/${encodeURIComponent(props.changeSetId)}`,
39
- ),
40
- );
36
+ function load(): Promise<void> {
37
+ return detail.refresh();
41
38
  }
42
39
 
43
- watch(() => props.changeSetId, load, { immediate: true });
44
-
45
40
  const changeSet = computed<ChangeSetSummary | null>(
46
41
  () => detail.data.value?.changeSet ?? null,
47
42
  );
@@ -128,7 +123,7 @@ function openPage(): void {
128
123
  <USkeleton class="mt-4 h-24 w-full" />
129
124
  <USkeleton class="h-40 w-full" />
130
125
  </div>
131
- <article v-else :aria-busy="detail.isLoading.value">
126
+ <article v-else :aria-busy="detail.pending.value">
132
127
  <header class="flex min-h-16 flex-wrap items-start gap-3 px-5 py-3.5">
133
128
  <div class="min-w-0 flex-1">
134
129
  <p
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from "vue";
3
- import type { RequestFailure } from "./composables/useAiRequest";
3
+ import type { RequestFailure } from "./composables/useAiData";
4
4
  import { useViewFormat } from "./composables/useViewFormat";
5
5
 
6
6
  type UnavailableStateSize = "sm" | "md" | "lg";
@@ -0,0 +1,116 @@
1
+ import { computed, type ComputedRef, type MaybeRefOrGetter } from "vue";
2
+ import type {
3
+ ApiDataRequest,
4
+ ApiDataResult,
5
+ ApiPagedDataHandler,
6
+ ApiPagedDataResult,
7
+ UseApiDataOptions,
8
+ } from "#dms-core/app/composables/auth/useApiData";
9
+ import type {
10
+ DmsAsyncDataKey,
11
+ UseDmsPagedDataOptions,
12
+ } from "#dms/frontend-module";
13
+ import { HTTP_SERVICE_UNAVAILABLE } from "../constants";
14
+
15
+ /** Why a request failed: the sidecar is down (503), or anything else. */
16
+ export type RequestFailure = "unavailable" | "error";
17
+
18
+ /** Why a read failed, and the server's message for it. */
19
+ interface FailureDescription {
20
+ /** Why the last read failed; none while a retry is in flight. */
21
+ failure: ComputedRef<RequestFailure | null>;
22
+ /** The server's message for the failure. */
23
+ message: ComputedRef<string>;
24
+ }
25
+
26
+ /** A view's read, and why it failed. */
27
+ export type AiData<T> = ApiDataResult<T> & FailureDescription;
28
+
29
+ /** A view's list read page by page, and why it failed. */
30
+ export type AiPagedData<TPage, TCursor> = ApiPagedDataResult<TPage, TCursor> &
31
+ FailureDescription;
32
+
33
+ /** What a failure description needs of a read. */
34
+ interface FailingRead {
35
+ error: { value: unknown };
36
+ status: { value: string };
37
+ }
38
+
39
+ interface FetchErrorResponse {
40
+ status?: number;
41
+ }
42
+
43
+ interface FetchErrorBody {
44
+ message?: unknown;
45
+ }
46
+
47
+ interface FetchErrorLike {
48
+ status?: number;
49
+ statusCode?: number;
50
+ response?: FetchErrorResponse;
51
+ data?: FetchErrorBody;
52
+ message?: string;
53
+ }
54
+
55
+ /** The HTTP status of a `$authFetch` rejection, if it got an answer. */
56
+ function errorStatus(error: unknown): number | undefined {
57
+ const candidate = error as FetchErrorLike | null;
58
+ return (
59
+ candidate?.status ?? candidate?.statusCode ?? candidate?.response?.status
60
+ );
61
+ }
62
+
63
+ /** The server's `{ message }`, else the error's own text. */
64
+ export function errorMessage(error: unknown): string {
65
+ const candidate = error as FetchErrorLike | null;
66
+ const serverMessage = candidate?.data?.message;
67
+ if (typeof serverMessage === "string") return serverMessage;
68
+ return candidate?.message ?? String(error);
69
+ }
70
+
71
+ export function classifyFailure(error: unknown): RequestFailure {
72
+ return errorStatus(error) === HTTP_SERVICE_UNAVAILABLE
73
+ ? "unavailable"
74
+ : "error";
75
+ }
76
+
77
+ /**
78
+ * A view's read of the assistant's data, with the page (`useApiData`): a
79
+ * server render reads it and hands it to the browser. Its failure says
80
+ * whether the assistant isn't running (503) rather than an error.
81
+ */
82
+ function describeFailure(result: FailingRead): FailureDescription {
83
+ return {
84
+ failure: computed(() =>
85
+ result.error.value && result.status.value !== "pending"
86
+ ? classifyFailure(result.error.value)
87
+ : null,
88
+ ),
89
+ message: computed(() =>
90
+ result.error.value ? errorMessage(result.error.value) : "",
91
+ ),
92
+ };
93
+ }
94
+
95
+ export function useAiData<T>(
96
+ url: ApiDataRequest,
97
+ options?: UseApiDataOptions<T>,
98
+ ): AiData<T> {
99
+ const result = useApiData<T>(url, options);
100
+ return Object.assign(result, describeFailure(result));
101
+ }
102
+
103
+ /**
104
+ * A view's list read page by page ("Load more"), with the page
105
+ * (`useApiPagedData`): a server render reads the first page, and the page's
106
+ * refreshes read every page loaded again. Its failure says whether the
107
+ * assistant isn't running (503) rather than an error.
108
+ */
109
+ export function useAiPagedData<TPage, TCursor>(
110
+ key: MaybeRefOrGetter<DmsAsyncDataKey>,
111
+ handler: ApiPagedDataHandler<TPage, TCursor>,
112
+ options: UseDmsPagedDataOptions<TPage, TCursor>,
113
+ ): AiPagedData<TPage, TCursor> {
114
+ const result = useApiPagedData<TPage, TCursor>(key, handler, options);
115
+ return Object.assign(result, describeFailure(result));
116
+ }
@@ -1,4 +1,4 @@
1
- import { inject } from "vue";
1
+ import { getCurrentInstance, inject, onMounted, ref } from "vue";
2
2
  import { useDmsRouter } from "#dms/frontend-module";
3
3
  import { ASSISTANT_SESSION_KEY } from "../../runtime/assistant-session";
4
4
 
@@ -16,13 +16,19 @@ export interface AssistantActions {
16
16
  * The tab's assistant session as the workspace views use it. Until it runs
17
17
  * (its first probe pending) and without one (production, sidecar disabled) the
18
18
  * actions do nothing and `isAvailable` hides the buttons that need them.
19
+ *
20
+ * A view only offers them once mounted: the server never has a session, and a
21
+ * probe that answered before the view hydrated would otherwise make the
22
+ * browser render buttons the server's HTML lacks.
19
23
  */
20
24
  export function useAssistant(): AssistantActions {
21
25
  const session = inject(ASSISTANT_SESSION_KEY, null);
22
26
  const router = useDmsRouter();
27
+ const isMounted = ref(!getCurrentInstance());
28
+ if (!isMounted.value) onMounted(() => (isMounted.value = true));
23
29
  return {
24
30
  get isAvailable() {
25
- return (session?.value ?? null) !== null;
31
+ return isMounted.value && (session?.value ?? null) !== null;
26
32
  },
27
33
  openPanel: () => session?.value?.panel.open(),
28
34
  openConversation: (conversationId) =>
@@ -1,7 +1,7 @@
1
1
  import { ref, type Ref } from "vue";
2
2
  import { AI_ROUTES } from "../constants";
3
3
  import type { ChangeSetSummary } from "../types";
4
- import { classifyFailure, errorMessage } from "./useAiRequest";
4
+ import { classifyFailure, errorMessage } from "./useAiData";
5
5
 
6
6
  export interface ChangeSetActions {
7
7
  isBusy: Ref<boolean>;
@@ -21,10 +21,11 @@ function changeSetPath(changeSetId: string, action: string): string {
21
21
  }
22
22
 
23
23
  /**
24
- * Undo and redo of a change set, each confirmed by a toast; `onChanged` runs
25
- * once the sidecar has restored the files.
24
+ * Undo and redo of a change set, each confirmed by a toast; the page is read
25
+ * again once the sidecar has restored the files, and the action stays busy
26
+ * until it is: the set shows its new state before it can be acted on again.
26
27
  */
27
- export function useChangeSetActions(onChanged: () => void): ChangeSetActions {
28
+ export function useChangeSetActions(): ChangeSetActions {
28
29
  const { t } = useI18n();
29
30
  const toast = useToast();
30
31
  const { $authFetch } = useAuthFetch();
@@ -56,7 +57,7 @@ export function useChangeSetActions(onChanged: () => void): ChangeSetActions {
56
57
  color: AI_COLOR,
57
58
  icon: "i-ph-arrow-clockwise",
58
59
  });
59
- onChanged();
60
+ await refreshPageBlocks();
60
61
  } catch (error) {
61
62
  failureToast(error);
62
63
  } finally {
@@ -88,7 +89,7 @@ export function useChangeSetActions(onChanged: () => void): ChangeSetActions {
88
89
  },
89
90
  ],
90
91
  });
91
- onChanged();
92
+ await refreshPageBlocks();
92
93
  }
93
94
 
94
95
  async function undo(changeSet: ChangeSetSummary): Promise<void> {
@@ -26,13 +26,6 @@ export function matchesSearch(
26
26
  return haystack.some((text) => text.toLowerCase().includes(needle));
27
27
  }
28
28
 
29
- /** The `?set=<id>` deep link of the page, if any. */
30
- export function readSelectedSet(): string | undefined {
31
- if (typeof window === "undefined") return undefined;
32
- const params = new URLSearchParams(window.location.search);
33
- return params.get(CHANGE_SET_QUERY_KEY) ?? undefined;
34
- }
35
-
36
29
  /**
37
30
  * Keeps the selected set in the URL without a visit, so a reload or a shared
38
31
  * link opens it again.
@@ -368,6 +368,27 @@
368
368
  "close": "Close",
369
369
  "not_running": "The assistant isn't running in this tab."
370
370
  }
371
+ },
372
+ "preview": {
373
+ "eyebrow": "Block preview",
374
+ "open": "Preview",
375
+ "open_aria": "Open the block preview: {title}",
376
+ "blocks": "{count} block | {count} blocks",
377
+ "variants": "{count} variant | {count} variants",
378
+ "with_mockup": "vs your mockup",
379
+ "proposed": "Proposed",
380
+ "variants_aria": "Variants",
381
+ "mockup": "Your mockup",
382
+ "dms_blocks": "DMS blocks",
383
+ "mockup_missing": "{name} was attached before the chat reloaded, so it can no longer be shown. Attach it again to compare side by side.",
384
+ "mockup_external": "External image, not loaded in the dashboard.",
385
+ "mockup_open": "Open the mockup",
386
+ "fallback_title": "This DMS version cannot draw block previews",
387
+ "fallback_body": "Update the DMS to see the real blocks. Here is the tree the assistant sent.",
388
+ "custom_sketch": "Custom sketch — not a DMS block",
389
+ "sketch_badge": "Custom sketch",
390
+ "with_sketch": "custom sketch",
391
+ "sketch_frame": "Static sketch of a custom component: {caption}"
371
392
  }
372
393
  },
373
394
  "tools": {
@@ -419,7 +440,8 @@
419
440
  "add_query": "Add query",
420
441
  "configure_query": "Configure query",
421
442
  "remove_query": "Remove query",
422
- "search_tools": "Load tools"
443
+ "search_tools": "Load tools",
444
+ "show_preview": "Show a preview"
423
445
  }
424
446
  }
425
447
  }
@@ -368,6 +368,27 @@
368
368
  "close": "Fermer",
369
369
  "not_running": "L'assistant ne tourne pas dans cet onglet."
370
370
  }
371
+ },
372
+ "preview": {
373
+ "eyebrow": "Aperçu de blocs",
374
+ "open": "Aperçu",
375
+ "open_aria": "Ouvrir l'aperçu des blocs : {title}",
376
+ "blocks": "{count} bloc | {count} blocs",
377
+ "variants": "{count} variante | {count} variantes",
378
+ "with_mockup": "face à votre maquette",
379
+ "proposed": "Proposé",
380
+ "variants_aria": "Variantes",
381
+ "mockup": "Votre maquette",
382
+ "dms_blocks": "Blocs DMS",
383
+ "mockup_missing": "{name} a été jointe avant le rechargement du chat et ne peut plus être affichée. Joignez-la à nouveau pour comparer côte à côte.",
384
+ "mockup_external": "Image externe, non chargée dans le tableau de bord.",
385
+ "mockup_open": "Ouvrir la maquette",
386
+ "fallback_title": "Cette version du DMS ne sait pas afficher d'aperçu de blocs",
387
+ "fallback_body": "Mettez à jour le DMS pour voir les vrais blocs. Voici l'arbre envoyé par l'assistant.",
388
+ "custom_sketch": "Esquisse sur mesure — pas un bloc DMS",
389
+ "sketch_badge": "Esquisse",
390
+ "with_sketch": "esquisse sur mesure",
391
+ "sketch_frame": "Esquisse statique d'un composant sur mesure : {caption}"
371
392
  }
372
393
  },
373
394
  "tools": {
@@ -419,7 +440,8 @@
419
440
  "add_query": "Ajouter une requête",
420
441
  "configure_query": "Configurer la requête",
421
442
  "remove_query": "Retirer la requête",
422
- "search_tools": "Charger des outils"
443
+ "search_tools": "Charger des outils",
444
+ "show_preview": "Montrer un aperçu"
423
445
  }
424
446
  }
425
447
  }
@@ -6,7 +6,7 @@
6
6
  "license": "Apache-2.0",
7
7
  "packageManager": "pnpm@10.6.5",
8
8
  "engines": {
9
- "@antelopejs/dms-frontend": ">=0.5.5 <0.6.0"
9
+ "@antelopejs/dms-frontend": ">=0.6.3 <0.7.0"
10
10
  },
11
11
  "scripts": {
12
12
  "build": "pnpm --dir \"${DMS_FRONTEND_WORKSPACE:?Set DMS_FRONTEND_WORKSPACE to the generated Inertia workspace}\" build",
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { nextTick, ref } from "vue";
3
+ import {
4
+ type AssistantStatusPayload,
5
+ createAssistantActivity,
6
+ } from "../app/runtime/assistant-activity";
7
+ import type { ChatTransport } from "../app/runtime/chat-transport";
8
+
9
+ type Listener = (msg: object) => void;
10
+
11
+ function fakeChat() {
12
+ const listeners = new Set<Listener>();
13
+ const chat = {
14
+ onMessage: (listener: Listener) => {
15
+ listeners.add(listener);
16
+ return () => listeners.delete(listener);
17
+ },
18
+ } as unknown as ChatTransport;
19
+ return { chat, emit: (msg: object) => listeners.forEach((l) => l(msg)) };
20
+ }
21
+
22
+ describe("assistant activity", () => {
23
+ it("polls the status through the read's interval while it is useful, and stops on teardown", async () => {
24
+ const data = ref<AssistantStatusPayload | null>(null);
25
+ let interval: () => number = () => -1;
26
+ const isPanelOpen = ref(false);
27
+ const { chat } = fakeChat();
28
+ const activity = createAssistantActivity({
29
+ readStatus: (given) => {
30
+ interval = given;
31
+ return { data, refresh: async () => undefined };
32
+ },
33
+ chat,
34
+ shouldPoll: () => !isPanelOpen.value,
35
+ });
36
+ expect(interval()).toBe(15_000);
37
+ isPanelOpen.value = true;
38
+ expect(interval()).toBe(0);
39
+ isPanelOpen.value = false;
40
+ activity.stop();
41
+ expect(interval()).toBe(0);
42
+ });
43
+
44
+ it("takes its counts from each status answer and from the stream's lists", async () => {
45
+ const data = ref<AssistantStatusPayload | null>(null);
46
+ const { chat, emit } = fakeChat();
47
+ const activity = createAssistantActivity({
48
+ readStatus: () => ({ data, refresh: async () => undefined }),
49
+ chat,
50
+ shouldPoll: () => true,
51
+ });
52
+ data.value = { pendingApprovals: 2, workingConversations: 0 };
53
+ await nextTick();
54
+ expect(activity.pendingApprovals.value).toBe(2);
55
+ expect(activity.isBusy.value).toBe(true);
56
+ emit({ type: "conversation_list", conversations: [{ isRunning: true }] });
57
+ expect(activity.pendingApprovals.value).toBe(0);
58
+ expect(activity.workingConversations.value).toBe(1);
59
+ data.value = {
60
+ pendingApprovals: 0,
61
+ workingConversations: 0,
62
+ lastError: "boom",
63
+ };
64
+ await nextTick();
65
+ expect(activity.workingConversations.value).toBe(0);
66
+ expect(activity.lastError.value).toBe("boom");
67
+ activity.stop();
68
+ });
69
+ });