@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,172 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from "vue";
3
+ import { useChatI18n } from "../composables/useChatI18n";
4
+ import { CARD_TYPE_LIMIT } from "../constants/block-preview";
5
+ import type { BlockPreview } from "../types/protocol";
6
+ import {
7
+ countBlocks,
8
+ hasSketch,
9
+ type ReferenceView,
10
+ topBlockTypes,
11
+ } from "../utils/block-preview";
12
+ import BlockPreviewModal from "./BlockPreviewModal.vue";
13
+ import BlockPreviewThumb from "./BlockPreviewThumb.vue";
14
+
15
+ interface Props {
16
+ preview: BlockPreview;
17
+ reference: ReferenceView | null;
18
+ }
19
+
20
+ const props = defineProps<Props>();
21
+ const { t } = useChatI18n();
22
+ const isOpen = ref(false);
23
+
24
+ const META_SEPARATOR = " · ";
25
+
26
+ const isSketched = computed<boolean>(() => hasSketch(props.preview));
27
+
28
+ const typesLabel = computed<string>(() => {
29
+ const types = topBlockTypes(props.preview.tree);
30
+ const shown = types.slice(0, CARD_TYPE_LIMIT).join(", ");
31
+ const hidden = types.length - CARD_TYPE_LIMIT;
32
+ return hidden > 0 ? `${shown} +${hidden}` : shown;
33
+ });
34
+
35
+ const meta = computed<string>(() => {
36
+ const blocks = countBlocks(props.preview.tree);
37
+ const variants = props.preview.variants?.length ?? 0;
38
+ const parts = [
39
+ typesLabel.value,
40
+ blocks > 0
41
+ ? t("dms_ai.panel.preview.blocks", { count: blocks }, blocks)
42
+ : "",
43
+ variants > 0
44
+ ? t(
45
+ "dms_ai.panel.preview.variants",
46
+ { count: variants + 1 },
47
+ variants + 1,
48
+ )
49
+ : "",
50
+ isSketched.value ? t("dms_ai.panel.preview.with_sketch") : "",
51
+ props.reference ? t("dms_ai.panel.preview.with_mockup") : "",
52
+ ];
53
+ return parts.filter((part) => part !== "").join(META_SEPARATOR);
54
+ });
55
+ </script>
56
+
57
+ <template>
58
+ <div class="bp">
59
+ <span class="bp__who">✦ {{ t("dms_ai.panel.preview.eyebrow") }}</span>
60
+ <button
61
+ type="button"
62
+ class="bp-card"
63
+ aria-haspopup="dialog"
64
+ :aria-expanded="isOpen"
65
+ :aria-label="
66
+ t('dms_ai.panel.preview.open_aria', { title: preview.title })
67
+ "
68
+ @click="isOpen = true"
69
+ >
70
+ <BlockPreviewThumb :view="preview" :is-sketched="isSketched" />
71
+ <span class="bp-card__body">
72
+ <span class="bp-card__title">{{ preview.title }}</span>
73
+ <span class="bp-card__meta">{{ meta }}</span>
74
+ <span class="bp-card__open">
75
+ <UIcon name="i-ph-frame-corners" class="bp-card__open-icon" />
76
+ {{ t("dms_ai.panel.preview.open") }}
77
+ </span>
78
+ </span>
79
+ </button>
80
+ <BlockPreviewModal
81
+ v-model:open="isOpen"
82
+ :preview="preview"
83
+ :reference="reference"
84
+ />
85
+ </div>
86
+ </template>
87
+
88
+ <style scoped>
89
+ .bp {
90
+ display: grid;
91
+ gap: 8px;
92
+ justify-items: start;
93
+ }
94
+
95
+ .bp__who {
96
+ font: 600 10px var(--ai-font-mono);
97
+ letter-spacing: 0.08em;
98
+ text-transform: uppercase;
99
+ color: var(--ai);
100
+ }
101
+
102
+ .bp-card {
103
+ display: flex;
104
+ align-items: stretch;
105
+ gap: 12px;
106
+ width: min(100%, 360px);
107
+ padding: 8px 12px 8px 8px;
108
+ border: 1px solid var(--ui-border);
109
+ border-radius: var(--ai-radius-md);
110
+ background: var(--dms-surface-card, var(--ui-bg));
111
+ color: inherit;
112
+ text-align: left;
113
+ cursor: pointer;
114
+ transition:
115
+ border-color 0.15s ease,
116
+ background-color 0.15s ease;
117
+ }
118
+
119
+ .bp-card:hover {
120
+ border-color: var(--ai-line);
121
+ background: var(--ai-bg-hover);
122
+ }
123
+
124
+ .bp-card:focus-visible {
125
+ outline: 2px solid var(--ai);
126
+ outline-offset: 2px;
127
+ }
128
+
129
+ .bp-card__body {
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: 3px;
133
+ min-width: 0;
134
+ }
135
+
136
+ .bp-card__title {
137
+ overflow: hidden;
138
+ font-size: 13px;
139
+ font-weight: 600;
140
+ line-height: 1.35;
141
+ color: var(--ui-text-highlighted);
142
+ display: -webkit-box;
143
+ -webkit-line-clamp: 2;
144
+ -webkit-box-orient: vertical;
145
+ }
146
+
147
+ .bp-card__meta {
148
+ font: 500 10.5px/1.45 var(--ai-font-mono);
149
+ color: var(--ui-text-dimmed);
150
+ }
151
+
152
+ .bp-card__open {
153
+ display: inline-flex;
154
+ align-items: center;
155
+ gap: 4px;
156
+ margin-top: auto;
157
+ font-size: 12px;
158
+ font-weight: 600;
159
+ color: var(--ai);
160
+ }
161
+
162
+ .bp-card__open-icon {
163
+ width: 13px;
164
+ height: 13px;
165
+ }
166
+
167
+ @media (prefers-reduced-motion: reduce) {
168
+ .bp-card {
169
+ transition: none;
170
+ }
171
+ }
172
+ </style>
@@ -0,0 +1,324 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from "vue";
3
+ import { useBlockPreviewComponent } from "../composables/useBlockPreviewComponent";
4
+ import { useChatI18n } from "../composables/useChatI18n";
5
+ import { BLOCK_PREVIEW_MAIN_TAB } from "../constants/block-preview";
6
+ import type { BlockPreview } from "../types/protocol";
7
+ import {
8
+ type BlockPreviewTab,
9
+ formatTree,
10
+ previewTabs,
11
+ type ReferenceView,
12
+ } from "../utils/block-preview";
13
+ import SketchFrame from "./SketchFrame.vue";
14
+
15
+ interface Props {
16
+ preview: BlockPreview;
17
+ reference: ReferenceView | null;
18
+ }
19
+
20
+ interface TabItem {
21
+ value: string;
22
+ label: string;
23
+ }
24
+
25
+ const props = defineProps<Props>();
26
+ const open = defineModel<boolean>("open", { required: true });
27
+ const { t } = useChatI18n();
28
+ const previewComponent = useBlockPreviewComponent();
29
+
30
+ const MODAL_UI = { content: "sm:max-w-5xl" };
31
+
32
+ const activeTab = ref<string>(BLOCK_PREVIEW_MAIN_TAB);
33
+ const tabs = computed<BlockPreviewTab[]>(() => previewTabs(props.preview));
34
+ const tabItems = computed<TabItem[]>(() =>
35
+ tabs.value.map((tab) => ({
36
+ value: tab.value,
37
+ label: tab.label ?? t("dms_ai.panel.preview.proposed"),
38
+ })),
39
+ );
40
+ const activeView = computed<BlockPreviewTab>(
41
+ () =>
42
+ tabs.value.find((tab) => tab.value === activeTab.value) ?? tabs.value[0],
43
+ );
44
+ const paneCount = computed<number>(
45
+ () =>
46
+ [props.reference, activeView.value.tree, activeView.value.sketch].filter(
47
+ (pane) => pane !== null && pane !== undefined,
48
+ ).length,
49
+ );
50
+ const sketchTitle = computed<string>(() =>
51
+ t("dms_ai.panel.preview.sketch_frame", {
52
+ caption: activeView.value.sketch?.caption ?? props.preview.title,
53
+ }),
54
+ );
55
+ const mockupLabel = computed<string>(
56
+ () => props.preview.reference?.caption ?? t("dms_ai.panel.preview.mockup"),
57
+ );
58
+
59
+ watch(open, (isOpen) => {
60
+ if (isOpen) activeTab.value = BLOCK_PREVIEW_MAIN_TAB;
61
+ });
62
+ </script>
63
+
64
+ <template>
65
+ <UModal
66
+ v-model:open="open"
67
+ :title="preview.title"
68
+ :description="preview.description"
69
+ :ui="MODAL_UI"
70
+ >
71
+ <template #body>
72
+ <div class="dms-ai-chat-tokens bp-modal">
73
+ <UTabs
74
+ v-if="tabItems.length > 1"
75
+ v-model="activeTab"
76
+ :items="tabItems"
77
+ :content="false"
78
+ variant="link"
79
+ size="sm"
80
+ :aria-label="t('dms_ai.panel.preview.variants_aria')"
81
+ />
82
+ <div class="bp-modal__panes" :class="{ 'is-split': paneCount > 1 }">
83
+ <figure v-if="reference" class="bp-pane">
84
+ <figcaption class="bp-pane__label">{{ mockupLabel }}</figcaption>
85
+ <div class="bp-pane__paper">
86
+ <img
87
+ v-if="reference.kind === 'image'"
88
+ :src="reference.src"
89
+ :alt="mockupLabel"
90
+ class="bp-pane__image"
91
+ />
92
+ <div v-else-if="reference.kind === 'link'" class="bp-pane__note">
93
+ <UIcon name="i-ph-image-broken" class="bp-pane__note-icon" />
94
+ <p>{{ t("dms_ai.panel.preview.mockup_external") }}</p>
95
+ <a
96
+ :href="reference.href"
97
+ target="_blank"
98
+ rel="noopener noreferrer"
99
+ class="bp-pane__link"
100
+ >
101
+ {{ t("dms_ai.panel.preview.mockup_open") }}
102
+ <UIcon name="i-ph-arrow-square-out" />
103
+ </a>
104
+ </div>
105
+ <div v-else class="bp-pane__note">
106
+ <UIcon name="i-ph-image-broken" class="bp-pane__note-icon" />
107
+ <p>
108
+ {{
109
+ t("dms_ai.panel.preview.mockup_missing", {
110
+ name: reference.name,
111
+ })
112
+ }}
113
+ </p>
114
+ </div>
115
+ </div>
116
+ </figure>
117
+ <section
118
+ v-if="activeView.tree"
119
+ class="bp-pane"
120
+ :aria-label="t('dms_ai.panel.preview.dms_blocks')"
121
+ >
122
+ <span class="bp-pane__label">
123
+ {{ t("dms_ai.panel.preview.dms_blocks") }}
124
+ </span>
125
+ <div class="bp-pane__stage">
126
+ <component
127
+ :is="previewComponent"
128
+ v-if="previewComponent"
129
+ :key="activeTab"
130
+ :tree="activeView.tree"
131
+ />
132
+ <div v-else class="bp-fallback">
133
+ <p class="bp-fallback__title">
134
+ <UIcon name="i-ph-info" />
135
+ {{ t("dms_ai.panel.preview.fallback_title") }}
136
+ </p>
137
+ <p class="bp-fallback__body">
138
+ {{ t("dms_ai.panel.preview.fallback_body") }}
139
+ </p>
140
+ <pre
141
+ class="bp-fallback__json"
142
+ ><code>{{ formatTree(activeView.tree) }}</code></pre>
143
+ </div>
144
+ </div>
145
+ </section>
146
+ <section
147
+ v-if="activeView.sketch"
148
+ class="bp-pane bp-pane--sketch"
149
+ :aria-label="t('dms_ai.panel.preview.custom_sketch')"
150
+ >
151
+ <span class="bp-pane__label bp-pane__label--sketch">
152
+ <UIcon name="i-ph-pencil-simple-line" />
153
+ {{ t("dms_ai.panel.preview.custom_sketch") }}
154
+ </span>
155
+ <div class="bp-pane__sketch">
156
+ <SketchFrame
157
+ :key="activeTab"
158
+ :sketch="activeView.sketch"
159
+ :title="sketchTitle"
160
+ />
161
+ <p v-if="activeView.sketch.caption" class="bp-pane__caption">
162
+ {{ activeView.sketch.caption }}
163
+ </p>
164
+ </div>
165
+ </section>
166
+ </div>
167
+ </div>
168
+ </template>
169
+ </UModal>
170
+ </template>
171
+
172
+ <style scoped>
173
+ .bp-modal {
174
+ display: grid;
175
+ gap: 14px;
176
+ }
177
+
178
+ .bp-modal__panes {
179
+ display: grid;
180
+ gap: 14px;
181
+ }
182
+
183
+ .bp-modal__panes.is-split {
184
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
185
+ align-items: start;
186
+ }
187
+
188
+ .bp-pane {
189
+ display: grid;
190
+ grid-template-rows: auto 1fr;
191
+ gap: 6px;
192
+ min-width: 0;
193
+ margin: 0;
194
+ }
195
+
196
+ .bp-pane__label {
197
+ font: 600 10px var(--ai-font-mono);
198
+ letter-spacing: 0.08em;
199
+ text-transform: uppercase;
200
+ color: var(--ui-text-muted);
201
+ }
202
+
203
+ .bp-pane__paper,
204
+ .bp-pane__stage {
205
+ min-height: 220px;
206
+ max-height: 64vh;
207
+ overflow: auto;
208
+ border: 1px solid var(--ui-border);
209
+ border-radius: var(--ai-radius-md);
210
+ }
211
+
212
+ .bp-pane__paper {
213
+ display: grid;
214
+ place-items: center;
215
+ padding: 12px;
216
+ background: var(--ui-bg-muted);
217
+ }
218
+
219
+ .bp-pane__stage {
220
+ padding: 16px;
221
+ background-color: var(--ui-bg);
222
+ background-image: radial-gradient(var(--ui-border) 1px, transparent 1px);
223
+ background-size: 14px 14px;
224
+ }
225
+
226
+ .bp-pane__label--sketch {
227
+ display: inline-flex;
228
+ align-items: center;
229
+ gap: 5px;
230
+ color: var(--ai);
231
+ }
232
+
233
+ .bp-pane__sketch {
234
+ min-height: 220px;
235
+ max-height: 64vh;
236
+ overflow: auto;
237
+ border: 1px dashed var(--ai-line);
238
+ border-radius: var(--ai-radius-md);
239
+ background: var(--ai-tint);
240
+ }
241
+
242
+ .bp-pane__sketch :deep(.sketch-frame__doc) {
243
+ border-radius: calc(var(--ai-radius-md) - 1px) calc(var(--ai-radius-md) - 1px)
244
+ 0 0;
245
+ }
246
+
247
+ .bp-pane__caption {
248
+ margin: 0;
249
+ padding: 8px 12px;
250
+ border-top: 1px dashed var(--ai-line);
251
+ font-size: 12px;
252
+ line-height: 1.45;
253
+ color: var(--ui-text-muted);
254
+ }
255
+
256
+ .bp-pane__image {
257
+ max-width: 100%;
258
+ height: auto;
259
+ border-radius: var(--ai-radius-sm);
260
+ }
261
+
262
+ .bp-pane__note {
263
+ display: grid;
264
+ justify-items: center;
265
+ gap: 8px;
266
+ max-width: 280px;
267
+ font-size: 12.5px;
268
+ line-height: 1.5;
269
+ text-align: center;
270
+ color: var(--ui-text-muted);
271
+ }
272
+
273
+ .bp-pane__note p {
274
+ margin: 0;
275
+ }
276
+
277
+ .bp-pane__note-icon {
278
+ width: 22px;
279
+ height: 22px;
280
+ color: var(--ui-text-dimmed);
281
+ }
282
+
283
+ .bp-pane__link {
284
+ display: inline-flex;
285
+ align-items: center;
286
+ gap: 4px;
287
+ font-weight: 600;
288
+ color: var(--ai);
289
+ }
290
+
291
+ .bp-fallback {
292
+ display: grid;
293
+ gap: 8px;
294
+ }
295
+
296
+ .bp-fallback p {
297
+ margin: 0;
298
+ }
299
+
300
+ .bp-fallback__title {
301
+ display: flex;
302
+ align-items: center;
303
+ gap: 6px;
304
+ font-size: 13px;
305
+ font-weight: 600;
306
+ color: var(--ui-text-highlighted);
307
+ }
308
+
309
+ .bp-fallback__body {
310
+ font-size: 12.5px;
311
+ color: var(--ui-text-muted);
312
+ }
313
+
314
+ .bp-fallback__json {
315
+ margin: 0;
316
+ padding: 12px;
317
+ overflow: auto;
318
+ border: 1px solid var(--ui-border);
319
+ border-radius: var(--ai-radius-sm);
320
+ background: var(--ui-bg-elevated);
321
+ font: 500 11.5px/1.55 var(--ai-font-mono);
322
+ color: var(--ui-text-toned);
323
+ }
324
+ </style>
@@ -0,0 +1,141 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { useBlockPreviewComponent } from "../composables/useBlockPreviewComponent";
4
+ import { useChatI18n } from "../composables/useChatI18n";
5
+ import {
6
+ PLAN_MAX_CELLS,
7
+ PLAN_MAX_ROWS,
8
+ THUMBNAIL_CANVAS_WIDTH_PX,
9
+ THUMBNAIL_SCALE,
10
+ THUMBNAIL_SKETCH_HEIGHT_PX,
11
+ } from "../constants/block-preview";
12
+ import type { PreviewNode, PreviewView } from "../types/protocol";
13
+ import { countLeaves, rootsOf } from "../utils/block-preview";
14
+ import SketchFrame from "./SketchFrame.vue";
15
+
16
+ interface Props {
17
+ /** The view the thumbnail draws: its blocks, else its sketch. */
18
+ view: PreviewView;
19
+ /** Whether the preview proposes a custom sketch in any of its views. */
20
+ isSketched: boolean;
21
+ }
22
+
23
+ const props = defineProps<Props>();
24
+ const { t } = useChatI18n();
25
+ const previewComponent = useBlockPreviewComponent();
26
+
27
+ const canvasStyle = {
28
+ width: `${THUMBNAIL_CANVAS_WIDTH_PX}px`,
29
+ transform: `scale(${THUMBNAIL_SCALE})`,
30
+ };
31
+
32
+ const roots = computed<PreviewNode[]>(() =>
33
+ props.view.tree === undefined
34
+ ? []
35
+ : rootsOf(props.view.tree).slice(0, PLAN_MAX_ROWS),
36
+ );
37
+
38
+ function cellCount(node: PreviewNode): number {
39
+ return Math.min(countLeaves(node), PLAN_MAX_CELLS);
40
+ }
41
+ </script>
42
+
43
+ <template>
44
+ <span class="bp-thumb" aria-hidden="true">
45
+ <span
46
+ v-if="view.tree && previewComponent"
47
+ class="bp-thumb__canvas"
48
+ :style="canvasStyle"
49
+ inert
50
+ >
51
+ <component :is="previewComponent" :tree="view.tree" />
52
+ </span>
53
+ <span
54
+ v-else-if="!view.tree && view.sketch"
55
+ class="bp-thumb__canvas bp-thumb__canvas--sketch"
56
+ :style="canvasStyle"
57
+ inert
58
+ >
59
+ <SketchFrame
60
+ :sketch="view.sketch"
61
+ :title="t('dms_ai.panel.preview.custom_sketch')"
62
+ :height="THUMBNAIL_SKETCH_HEIGHT_PX"
63
+ />
64
+ </span>
65
+ <span v-else class="bp-thumb__plan">
66
+ <span v-for="(node, index) in roots" :key="index" class="bp-thumb__row">
67
+ <span
68
+ v-for="cellIndex in cellCount(node)"
69
+ :key="cellIndex"
70
+ class="bp-thumb__cell"
71
+ />
72
+ </span>
73
+ </span>
74
+ <span v-if="isSketched" class="bp-thumb__badge">
75
+ {{ t("dms_ai.panel.preview.sketch_badge") }}
76
+ </span>
77
+ </span>
78
+ </template>
79
+
80
+ <style scoped>
81
+ .bp-thumb {
82
+ position: relative;
83
+ flex: none;
84
+ width: 112px;
85
+ height: 72px;
86
+ overflow: hidden;
87
+ border: 1px solid var(--ai-line);
88
+ border-radius: var(--ai-radius-sm);
89
+ background-color: var(--ui-bg);
90
+ background-image: radial-gradient(var(--ui-border) 1px, transparent 1px);
91
+ background-size: 8px 8px;
92
+ }
93
+
94
+ .bp-thumb__canvas {
95
+ position: absolute;
96
+ top: 6px;
97
+ left: 0;
98
+ transform-origin: top left;
99
+ pointer-events: none;
100
+ }
101
+
102
+ .bp-thumb__canvas--sketch {
103
+ top: 0;
104
+ }
105
+
106
+ .bp-thumb__badge {
107
+ position: absolute;
108
+ right: 4px;
109
+ bottom: 4px;
110
+ padding: 1px 5px;
111
+ border: 1px dashed var(--ai-line);
112
+ border-radius: 999px;
113
+ background: var(--dms-surface-card, var(--ui-bg));
114
+ font: 600 8.5px/1.4 var(--ai-font-mono);
115
+ letter-spacing: 0.06em;
116
+ text-transform: uppercase;
117
+ color: var(--ai);
118
+ }
119
+
120
+ .bp-thumb__plan {
121
+ position: absolute;
122
+ inset: 8px;
123
+ display: flex;
124
+ flex-direction: column;
125
+ gap: 4px;
126
+ }
127
+
128
+ .bp-thumb__row {
129
+ display: flex;
130
+ flex: 1;
131
+ gap: 4px;
132
+ max-height: 18px;
133
+ }
134
+
135
+ .bp-thumb__cell {
136
+ flex: 1;
137
+ border: 1px solid var(--ai-line);
138
+ border-radius: 3px;
139
+ background: var(--ai-tint);
140
+ }
141
+ </style>
@@ -5,6 +5,7 @@ import { ACTIVITY_KINDS } from "../constants/run-status";
5
5
  import { MESSAGE_ROLES } from "../constants/conversation";
6
6
  import type {
7
7
  ConversationMessage,
8
+ BlockPreviewMessage,
8
9
  ErrorMessage,
9
10
  MessageAttachment,
10
11
  RunProgress,
@@ -14,6 +15,7 @@ import type { PermissionRequestData } from "../types/permission";
14
15
  import type { ChangeSetSummary } from "../types/protocol";
15
16
  import { lastMatching } from "../utils/arrays";
16
17
  import { formatBytes, isInlineImage } from "../utils/attachments";
18
+ import { type ReferenceView, referenceView } from "../utils/block-preview";
17
19
  import { formatTimeOfDay } from "../utils/format";
18
20
  import { buildRenderItems, type RenderItem } from "../utils/render-items";
19
21
  import {
@@ -24,6 +26,7 @@ import {
24
26
  runElapsedMs,
25
27
  } from "../utils/run-status";
26
28
  import AnswerRecord from "./AnswerRecord.vue";
29
+ import BlockPreviewCard from "./BlockPreviewCard.vue";
27
30
  import ChangeSetCard from "./ChangeSetCard.vue";
28
31
  import MarkdownContent from "./MarkdownContent.vue";
29
32
  import NoticeCard from "./NoticeCard.vue";
@@ -135,6 +138,10 @@ function attachmentsOf(message: UserMessage): MessageAttachment[] {
135
138
  return message.attachments ?? [];
136
139
  }
137
140
 
141
+ function referenceOf(message: BlockPreviewMessage): ReferenceView | null {
142
+ return referenceView(message.preview.reference, props.messages);
143
+ }
144
+
138
145
  function canRetry(error: ErrorMessage): boolean {
139
146
  return !props.isRunning && error.id === lastErrorId.value;
140
147
  }
@@ -307,6 +314,19 @@ async function copyAnswer(content: string): Promise<void> {
307
314
  >
308
315
  <AnswerRecord :answers="item.message.answers" />
309
316
  </li>
317
+
318
+ <li
319
+ v-else-if="
320
+ item.kind === 'preview' && item.message.role === 'block_preview'
321
+ "
322
+ class="message-item"
323
+ data-role="preview"
324
+ >
325
+ <BlockPreviewCard
326
+ :preview="item.message.preview"
327
+ :reference="referenceOf(item.message)"
328
+ />
329
+ </li>
310
330
  </template>
311
331
 
312
332
  <li v-if="isStalled" class="message-item" data-role="status">