@antelopejs/dms-ai 0.3.0 → 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.
- package/README.md +64 -205
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/lifecycle/skill-sources.d.ts +1 -1
- package/dist/lifecycle/skill-sources.js +29 -13
- package/dist/lifecycle/skill-sources.js.map +1 -1
- package/frontend-vue/app/chat/components/BlockPreviewCard.vue +172 -0
- package/frontend-vue/app/chat/components/BlockPreviewModal.vue +324 -0
- package/frontend-vue/app/chat/components/BlockPreviewThumb.vue +141 -0
- package/frontend-vue/app/chat/components/MessageList.vue +20 -0
- package/frontend-vue/app/chat/components/SketchFrame.vue +59 -0
- package/frontend-vue/app/chat/composables/conversation-events.ts +23 -1
- package/frontend-vue/app/chat/composables/useBlockPreviewComponent.ts +15 -0
- package/frontend-vue/app/chat/composables/useDashboardScheme.ts +42 -0
- package/frontend-vue/app/chat/constants/block-preview.ts +27 -0
- package/frontend-vue/app/chat/constants/conversation.ts +1 -0
- package/frontend-vue/app/chat/constants/protocol.ts +1 -0
- package/frontend-vue/app/chat/constants/sketch.ts +105 -0
- package/frontend-vue/app/chat/types/conversation.ts +11 -1
- package/frontend-vue/app/chat/types/protocol.ts +56 -0
- package/frontend-vue/app/chat/utils/block-preview.ts +145 -0
- package/frontend-vue/app/chat/utils/conversation-messages.ts +23 -1
- package/frontend-vue/app/chat/utils/render-items.ts +13 -2
- package/frontend-vue/app/chat/utils/sketch.ts +56 -0
- package/frontend-vue/app/chat/utils/tool-lexicon.ts +7 -0
- package/frontend-vue/i18n/locales/dms-ai-panel-en-GB.json +23 -1
- package/frontend-vue/i18n/locales/dms-ai-panel-fr-FR.json +23 -1
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/chat/block-preview.test.ts +448 -0
- package/package.json +20 -6
- package/sidecar/dist/constants/mcp.js +2 -0
- package/sidecar/dist/constants/mcp.js.map +1 -1
- package/sidecar/dist/constants/preview.d.ts +39 -0
- package/sidecar/dist/constants/preview.js +55 -0
- package/sidecar/dist/constants/preview.js.map +1 -0
- package/sidecar/dist/constants/tool-kinds.js +2 -0
- package/sidecar/dist/constants/tool-kinds.js.map +1 -1
- package/sidecar/dist/mcp/tool-definitions.js +5 -0
- package/sidecar/dist/mcp/tool-definitions.js.map +1 -1
- package/sidecar/dist/mcp/tools/preview-blocks.d.ts +65 -0
- package/sidecar/dist/mcp/tools/preview-blocks.js +176 -0
- package/sidecar/dist/mcp/tools/preview-blocks.js.map +1 -0
- package/sidecar/dist/mcp/types.d.ts +3 -0
- package/sidecar/dist/prompts/agent.d.ts +1 -1
- package/sidecar/dist/prompts/agent.js +3 -2
- package/sidecar/dist/prompts/agent.js.map +1 -1
- package/sidecar/dist/protocol/events.d.ts +313 -0
- package/sidecar/dist/protocol/events.js +9 -0
- package/sidecar/dist/protocol/events.js.map +1 -1
- package/sidecar/dist/protocol/preview.d.ts +130 -0
- package/sidecar/dist/protocol/preview.js +91 -0
- package/sidecar/dist/protocol/preview.js.map +1 -0
- package/sidecar/dist/server/chat-events.d.ts +3 -0
- package/sidecar/dist/server/chat-events.js +21 -0
- package/sidecar/dist/server/chat-events.js.map +1 -1
- package/sidecar/dist/server/ws.js +3 -1
- package/sidecar/dist/server/ws.js.map +1 -1
- package/sidecar/dist/state/types.d.ts +3 -1
- package/sidecar/dist/state/types.js +1 -0
- package/sidecar/dist/state/types.js.map +1 -1
- package/sidecar/package.json +1 -1
- package/skills/dms-ai/SKILL.md +30 -0
- package/skills/dms-ai-code-mode/SKILL.md +51 -0
- package/skills/dms-builder-safe/SKILL.md +9 -2
- package/skills/dms-builder-page/SKILL.md +0 -80
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import { useDashboardScheme } from "../composables/useDashboardScheme";
|
|
4
|
+
import { SKETCH_DEFAULT_HEIGHT_PX } from "../constants/sketch";
|
|
5
|
+
import type { PreviewSketch } from "../types/protocol";
|
|
6
|
+
import {
|
|
7
|
+
buildSketchDocument,
|
|
8
|
+
computedStyleReader,
|
|
9
|
+
sketchTokensCss,
|
|
10
|
+
} from "../utils/sketch";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
sketch: PreviewSketch;
|
|
14
|
+
title: string;
|
|
15
|
+
/** Overrides the sketch's own height, e.g. for a thumbnail. */
|
|
16
|
+
height?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = defineProps<Props>();
|
|
20
|
+
const host = ref<HTMLElement | null>(null);
|
|
21
|
+
const { scheme, revision } = useDashboardScheme();
|
|
22
|
+
|
|
23
|
+
const frameHeight = computed<string>(
|
|
24
|
+
() => `${props.height ?? props.sketch.height ?? SKETCH_DEFAULT_HEIGHT_PX}px`,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const srcdoc = computed<string>(() => {
|
|
28
|
+
if (revision.value === 0 || host.value === null) return "";
|
|
29
|
+
const tokens = sketchTokensCss(scheme.value, computedStyleReader(host.value));
|
|
30
|
+
return buildSketchDocument(props.sketch, tokens);
|
|
31
|
+
});
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<div ref="host" class="sketch-frame">
|
|
36
|
+
<iframe
|
|
37
|
+
v-if="srcdoc"
|
|
38
|
+
class="sketch-frame__doc"
|
|
39
|
+
sandbox=""
|
|
40
|
+
referrerpolicy="no-referrer"
|
|
41
|
+
:title="title"
|
|
42
|
+
:srcdoc="srcdoc"
|
|
43
|
+
:style="{ height: frameHeight }"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<style scoped>
|
|
49
|
+
.sketch-frame {
|
|
50
|
+
display: block;
|
|
51
|
+
width: 100%;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.sketch-frame__doc {
|
|
55
|
+
display: block;
|
|
56
|
+
width: 100%;
|
|
57
|
+
border: 0;
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import type {
|
|
8
8
|
AllowedBy,
|
|
9
9
|
AttachmentMeta,
|
|
10
|
+
BlockPreview,
|
|
10
11
|
ChangeSetSummary,
|
|
11
12
|
ConversationModeState,
|
|
12
13
|
FullAutoState,
|
|
@@ -19,6 +20,7 @@ import type {
|
|
|
19
20
|
import type { PendingAttachment } from "../utils/attachments";
|
|
20
21
|
import {
|
|
21
22
|
buildAssistantMessage,
|
|
23
|
+
buildBlockPreviewMessage,
|
|
22
24
|
buildErrorMessage,
|
|
23
25
|
buildUserMessage,
|
|
24
26
|
closedStatus,
|
|
@@ -91,6 +93,10 @@ interface ChangeSetEvent {
|
|
|
91
93
|
changeSet?: ChangeSetSummary;
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
interface BlockPreviewEvent {
|
|
97
|
+
preview?: BlockPreview;
|
|
98
|
+
}
|
|
99
|
+
|
|
94
100
|
interface UsageEvent {
|
|
95
101
|
totalTokens?: number;
|
|
96
102
|
}
|
|
@@ -197,7 +203,8 @@ function isTurnStart(msg: ConversationMessage): boolean {
|
|
|
197
203
|
function isOutOfTurn(msg: ConversationMessage): boolean {
|
|
198
204
|
return (
|
|
199
205
|
msg.role === MESSAGE_ROLES.NOTICE ||
|
|
200
|
-
msg.role === MESSAGE_ROLES.QUESTION_ANSWER
|
|
206
|
+
msg.role === MESSAGE_ROLES.QUESTION_ANSWER ||
|
|
207
|
+
msg.role === MESSAGE_ROLES.BLOCK_PREVIEW
|
|
201
208
|
);
|
|
202
209
|
}
|
|
203
210
|
|
|
@@ -357,6 +364,20 @@ function applyChangeSet(state: ConversationState, event: ChangeSetEvent): void {
|
|
|
357
364
|
});
|
|
358
365
|
}
|
|
359
366
|
|
|
367
|
+
function appendBlockPreview(
|
|
368
|
+
state: ConversationState,
|
|
369
|
+
event: BlockPreviewEvent,
|
|
370
|
+
): void {
|
|
371
|
+
const preview = event.preview;
|
|
372
|
+
if (preview === undefined) return;
|
|
373
|
+
const isKnown = state.messages.value.some(
|
|
374
|
+
(msg) =>
|
|
375
|
+
msg.role === MESSAGE_ROLES.BLOCK_PREVIEW && msg.preview.id === preview.id,
|
|
376
|
+
);
|
|
377
|
+
if (isKnown) return;
|
|
378
|
+
appendMessage(state, buildBlockPreviewMessage(preview));
|
|
379
|
+
}
|
|
380
|
+
|
|
360
381
|
function applyUsage(state: ConversationState, event: UsageEvent): void {
|
|
361
382
|
if (typeof event.totalTokens !== "number") return;
|
|
362
383
|
state.totalTokens.value = event.totalTokens;
|
|
@@ -377,6 +398,7 @@ const EVENT_HANDLERS: Record<string, EventHandler> = {
|
|
|
377
398
|
[SERVER_EVENT_TYPES.NOTICE]: appendNotice,
|
|
378
399
|
[SERVER_EVENT_TYPES.CHANGE_SET]: applyChangeSet,
|
|
379
400
|
[SERVER_EVENT_TYPES.USAGE]: applyUsage,
|
|
401
|
+
[SERVER_EVENT_TYPES.BLOCK_PREVIEW]: appendBlockPreview,
|
|
380
402
|
};
|
|
381
403
|
|
|
382
404
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Component, getCurrentInstance } from "vue";
|
|
2
|
+
import { BLOCK_PREVIEW_COMPONENT_NAMES } from "../constants/block-preview";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The DMS's `DmsBlockPreview`, looked up among the app's components rather
|
|
6
|
+
* than imported: the chat must keep working on a DMS that predates it, where
|
|
7
|
+
* the preview falls back to the tree as JSON.
|
|
8
|
+
*/
|
|
9
|
+
export function useBlockPreviewComponent(): Component | null {
|
|
10
|
+
const components = getCurrentInstance()?.appContext.components ?? {};
|
|
11
|
+
const name = BLOCK_PREVIEW_COMPONENT_NAMES.find(
|
|
12
|
+
(candidate) => components[candidate] !== undefined,
|
|
13
|
+
);
|
|
14
|
+
return name === undefined ? null : components[name];
|
|
15
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { onBeforeUnmount, onMounted, type Ref, ref } from "vue";
|
|
2
|
+
import { DARK_THEME_CLASS, type SketchScheme } from "../constants/sketch";
|
|
3
|
+
|
|
4
|
+
const OBSERVED_ATTRIBUTES = ["class", "style", "data-scale"];
|
|
5
|
+
|
|
6
|
+
function currentScheme(): SketchScheme {
|
|
7
|
+
return document.documentElement.classList.contains(DARK_THEME_CLASS)
|
|
8
|
+
? "dark"
|
|
9
|
+
: "light";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DashboardScheme {
|
|
13
|
+
scheme: Ref<SketchScheme>;
|
|
14
|
+
revision: Ref<number>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The dashboard's light or dark theme, followed live. `revision` also moves
|
|
19
|
+
* when the root's styles change, so readers of its CSS variables re-read them.
|
|
20
|
+
*/
|
|
21
|
+
export function useDashboardScheme(): DashboardScheme {
|
|
22
|
+
const scheme = ref<SketchScheme>("light");
|
|
23
|
+
const revision = ref(0);
|
|
24
|
+
let observer: MutationObserver | null = null;
|
|
25
|
+
|
|
26
|
+
const sync = () => {
|
|
27
|
+
scheme.value = currentScheme();
|
|
28
|
+
revision.value += 1;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
onMounted(() => {
|
|
32
|
+
sync();
|
|
33
|
+
observer = new MutationObserver(sync);
|
|
34
|
+
observer.observe(document.documentElement, {
|
|
35
|
+
attributes: true,
|
|
36
|
+
attributeFilter: OBSERVED_ATTRIBUTES,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
onBeforeUnmount(() => observer?.disconnect());
|
|
40
|
+
|
|
41
|
+
return { scheme, revision };
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Names the DMS may register its block preview component under. */
|
|
2
|
+
export const BLOCK_PREVIEW_COMPONENT_NAMES = [
|
|
3
|
+
"DmsBlockPreview",
|
|
4
|
+
"dms-block-preview",
|
|
5
|
+
] as const;
|
|
6
|
+
|
|
7
|
+
export const BLOCK_PREVIEW_MAIN_TAB = "proposed";
|
|
8
|
+
export const BLOCK_PREVIEW_VARIANT_TAB_PREFIX = "variant-";
|
|
9
|
+
|
|
10
|
+
export const JSON_INDENT = 2;
|
|
11
|
+
|
|
12
|
+
/** Mirror of the sidecar's reference URL rules (sidecar/src/constants/preview.ts). */
|
|
13
|
+
export const SAME_ORIGIN_PATH = /^\/(?![/\\])/;
|
|
14
|
+
export const REMOTE_URL = /^https?:\/\//i;
|
|
15
|
+
|
|
16
|
+
/** Width the card's live thumbnail is laid out at before it is scaled down. */
|
|
17
|
+
export const THUMBNAIL_CANVAS_WIDTH_PX = 560;
|
|
18
|
+
export const THUMBNAIL_SCALE = 0.2;
|
|
19
|
+
/** Height a sketch is drawn at in the thumbnail, before the scale. */
|
|
20
|
+
export const THUMBNAIL_SKETCH_HEIGHT_PX = 360;
|
|
21
|
+
|
|
22
|
+
/** Most block types the card names before it counts the rest. */
|
|
23
|
+
export const CARD_TYPE_LIMIT = 2;
|
|
24
|
+
|
|
25
|
+
/** The schematic thumbnail drawn when the DMS cannot render the blocks. */
|
|
26
|
+
export const PLAN_MAX_ROWS = 3;
|
|
27
|
+
export const PLAN_MAX_CELLS = 4;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an agent's sketch is drawn. The frame is the security boundary: an empty
|
|
3
|
+
* `sandbox` (no scripts, opaque origin, no forms, popups or top navigation)
|
|
4
|
+
* and a CSP that loads nothing but inline styles and `data:` images and fonts.
|
|
5
|
+
*/
|
|
6
|
+
export const SKETCH_CSP =
|
|
7
|
+
"default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:";
|
|
8
|
+
|
|
9
|
+
/** Mirror of the sidecar's default (sidecar/src/constants/preview.ts). */
|
|
10
|
+
export const SKETCH_DEFAULT_HEIGHT_PX = 320;
|
|
11
|
+
|
|
12
|
+
export type SketchScheme = "light" | "dark";
|
|
13
|
+
|
|
14
|
+
/** Class the dashboard puts on `<html>` in its dark theme. */
|
|
15
|
+
export const DARK_THEME_CLASS = "dark";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The `--sketch-*` tokens a sketch's CSS may use, read from the dashboard's
|
|
19
|
+
* own variables (a sandboxed document inherits none of them), with the
|
|
20
|
+
* dashboard's default values when a variable is unset.
|
|
21
|
+
*/
|
|
22
|
+
export interface SketchToken {
|
|
23
|
+
name: string;
|
|
24
|
+
source: string;
|
|
25
|
+
light: string;
|
|
26
|
+
dark: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SANS_STACK =
|
|
30
|
+
'"Geist", "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif';
|
|
31
|
+
const MONO_STACK = '"Geist Mono", ui-monospace, "SF Mono", Menlo, monospace';
|
|
32
|
+
|
|
33
|
+
export const SKETCH_TOKENS: readonly SketchToken[] = [
|
|
34
|
+
{
|
|
35
|
+
name: "canvas",
|
|
36
|
+
source: "--ui-bg-muted",
|
|
37
|
+
light: "#f4f6f7",
|
|
38
|
+
dark: "#07090b",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "surface",
|
|
42
|
+
source: "--dms-surface-card",
|
|
43
|
+
light: "#ffffff",
|
|
44
|
+
dark: "#10151a",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "surface-muted",
|
|
48
|
+
source: "--dms-bg-muted",
|
|
49
|
+
light: "#f7f9fa",
|
|
50
|
+
dark: "#0d1114",
|
|
51
|
+
},
|
|
52
|
+
{ name: "border", source: "--ui-border", light: "#e2e7ea", dark: "#262e36" },
|
|
53
|
+
{ name: "text", source: "--ui-text", light: "#2a333b", dark: "#c7d0d8" },
|
|
54
|
+
{
|
|
55
|
+
name: "text-strong",
|
|
56
|
+
source: "--ui-text-highlighted",
|
|
57
|
+
light: "#0b1114",
|
|
58
|
+
dark: "#eef3f6",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "text-muted",
|
|
62
|
+
source: "--ui-text-muted",
|
|
63
|
+
light: "#49525b",
|
|
64
|
+
dark: "#9ea7b0",
|
|
65
|
+
},
|
|
66
|
+
{ name: "accent", source: "--ui-primary", light: "#167f8a", dark: "#4dd9e6" },
|
|
67
|
+
{
|
|
68
|
+
name: "accent-fill",
|
|
69
|
+
source: "--dms-accent-fill",
|
|
70
|
+
light: "#4dd9e6",
|
|
71
|
+
dark: "#4dd9e6",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "success",
|
|
75
|
+
source: "--ui-success",
|
|
76
|
+
light: "#15803d",
|
|
77
|
+
dark: "#4ade80",
|
|
78
|
+
},
|
|
79
|
+
{ name: "error", source: "--ui-error", light: "#b91c1c", dark: "#f87171" },
|
|
80
|
+
{
|
|
81
|
+
name: "radius",
|
|
82
|
+
source: "--dms-radius-card",
|
|
83
|
+
light: "0.875rem",
|
|
84
|
+
dark: "0.875rem",
|
|
85
|
+
},
|
|
86
|
+
{ name: "font", source: "--font-sans", light: SANS_STACK, dark: SANS_STACK },
|
|
87
|
+
{
|
|
88
|
+
name: "font-mono",
|
|
89
|
+
source: "--font-mono",
|
|
90
|
+
light: MONO_STACK,
|
|
91
|
+
dark: MONO_STACK,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/** A dashboard value that could end the style element it is written into. */
|
|
96
|
+
export const UNSAFE_TOKEN_VALUE = /[<>{};]/;
|
|
97
|
+
|
|
98
|
+
/** Neutral base under the agent's CSS, so a bare sketch already looks native. */
|
|
99
|
+
export const SKETCH_BASE_CSS = `*,*::before,*::after{box-sizing:border-box}
|
|
100
|
+
html{color-scheme:var(--sketch-scheme);background:var(--sketch-canvas)}
|
|
101
|
+
body{margin:0;padding:16px;font:14px/1.5 var(--sketch-font);color:var(--sketch-text);-webkit-font-smoothing:antialiased}
|
|
102
|
+
h1,h2,h3,h4{margin:0 0 .5em;color:var(--sketch-text-strong);line-height:1.25}
|
|
103
|
+
p{margin:0 0 .5em}
|
|
104
|
+
img,svg{max-width:100%}
|
|
105
|
+
code,kbd,pre{font-family:var(--sketch-font-mono)}`;
|
|
@@ -2,6 +2,7 @@ import type { ACTIVITY_KINDS } from "../constants/run-status";
|
|
|
2
2
|
import type { PendingAttachment } from "../utils/attachments";
|
|
3
3
|
import type {
|
|
4
4
|
AllowedBy,
|
|
5
|
+
BlockPreview,
|
|
5
6
|
Notice,
|
|
6
7
|
ProviderName,
|
|
7
8
|
QuestionAnswerRecord,
|
|
@@ -91,6 +92,14 @@ export interface QuestionAnswerMessage {
|
|
|
91
92
|
timestampMs: number;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/** A block preview the agent shared, drawn as an attachment card. */
|
|
96
|
+
export interface BlockPreviewMessage {
|
|
97
|
+
id: string;
|
|
98
|
+
role: "block_preview";
|
|
99
|
+
preview: BlockPreview;
|
|
100
|
+
timestampMs: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
94
103
|
/**
|
|
95
104
|
* What the running turn is doing, as last reported by the sidecar.
|
|
96
105
|
* `receivedAtMs` is the local time of that report, from which the elapsed and
|
|
@@ -126,7 +135,8 @@ export type ConversationMessage =
|
|
|
126
135
|
| ErrorMessage
|
|
127
136
|
| NoticeMessage
|
|
128
137
|
| ChangeSetMessage
|
|
129
|
-
| QuestionAnswerMessage
|
|
138
|
+
| QuestionAnswerMessage
|
|
139
|
+
| BlockPreviewMessage;
|
|
130
140
|
|
|
131
141
|
export interface QueuedMessage {
|
|
132
142
|
id: string;
|
|
@@ -203,6 +203,61 @@ export interface AttachmentMeta {
|
|
|
203
203
|
size: number;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/** One block of a preview tree, as the DMS `DmsBlockPreview` takes it. */
|
|
207
|
+
export interface PreviewNode {
|
|
208
|
+
type?: string;
|
|
209
|
+
componentName?: string;
|
|
210
|
+
options?: Record<string, unknown>;
|
|
211
|
+
id?: string;
|
|
212
|
+
slot?: string;
|
|
213
|
+
colSpan?: number;
|
|
214
|
+
children?: PreviewNode[];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export type PreviewTree = PreviewNode | PreviewNode[];
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A custom component the agent proposes as static HTML and CSS, drawn in a
|
|
221
|
+
* sandboxed frame. Never a DMS block.
|
|
222
|
+
*/
|
|
223
|
+
export interface PreviewSketch {
|
|
224
|
+
html: string;
|
|
225
|
+
css?: string;
|
|
226
|
+
caption?: string;
|
|
227
|
+
height?: number;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** What one tab of a preview draws: DMS blocks, a sketch, or both. */
|
|
231
|
+
export interface PreviewView {
|
|
232
|
+
tree?: PreviewTree;
|
|
233
|
+
sketch?: PreviewSketch;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface BlockPreviewVariant extends PreviewView {
|
|
237
|
+
label: string;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The user's mockup a preview is compared with: an image they attached in the
|
|
242
|
+
* conversation (by name) or a URL.
|
|
243
|
+
*/
|
|
244
|
+
export interface BlockPreviewReference {
|
|
245
|
+
attachmentName?: string;
|
|
246
|
+
url?: string;
|
|
247
|
+
caption?: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** A block preview the agent shared with its PreviewBlocks tool. */
|
|
251
|
+
export interface BlockPreview extends PreviewView {
|
|
252
|
+
id: string;
|
|
253
|
+
title: string;
|
|
254
|
+
description?: string;
|
|
255
|
+
label?: string;
|
|
256
|
+
reference?: BlockPreviewReference;
|
|
257
|
+
variants?: BlockPreviewVariant[];
|
|
258
|
+
timestampMs: number;
|
|
259
|
+
}
|
|
260
|
+
|
|
206
261
|
export interface SnapshotMessage {
|
|
207
262
|
role: string;
|
|
208
263
|
content: string;
|
|
@@ -217,6 +272,7 @@ export interface SnapshotMessage {
|
|
|
217
272
|
changeSetId?: string;
|
|
218
273
|
notice?: Notice;
|
|
219
274
|
answers?: QuestionAnswerRecord[];
|
|
275
|
+
preview?: BlockPreview;
|
|
220
276
|
}
|
|
221
277
|
|
|
222
278
|
export interface QueuedItemWire {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { MESSAGE_ROLES } from "../constants/conversation";
|
|
2
|
+
import { lastMatching } from "./arrays";
|
|
3
|
+
import {
|
|
4
|
+
BLOCK_PREVIEW_MAIN_TAB,
|
|
5
|
+
BLOCK_PREVIEW_VARIANT_TAB_PREFIX,
|
|
6
|
+
JSON_INDENT,
|
|
7
|
+
REMOTE_URL,
|
|
8
|
+
SAME_ORIGIN_PATH,
|
|
9
|
+
} from "../constants/block-preview";
|
|
10
|
+
import type { ConversationMessage, UserMessage } from "../types/conversation";
|
|
11
|
+
import type {
|
|
12
|
+
BlockPreview,
|
|
13
|
+
BlockPreviewReference,
|
|
14
|
+
PreviewNode,
|
|
15
|
+
PreviewTree,
|
|
16
|
+
PreviewView,
|
|
17
|
+
} from "../types/protocol";
|
|
18
|
+
|
|
19
|
+
export interface BlockPreviewTab extends PreviewView {
|
|
20
|
+
value: string;
|
|
21
|
+
/** The view's own label; null for an unlabelled main view, named by the chat. */
|
|
22
|
+
label: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How the mockup is drawn: an image the page may load (an attachment of this
|
|
27
|
+
* session, or a same-origin path), a link for an external URL — loading it
|
|
28
|
+
* could send what the agent read to its address — or a note when the
|
|
29
|
+
* attachment is gone (only its name survives a reload).
|
|
30
|
+
*/
|
|
31
|
+
export type ReferenceView = ImageReference | LinkReference | MissingReference;
|
|
32
|
+
|
|
33
|
+
interface ImageReference {
|
|
34
|
+
kind: "image";
|
|
35
|
+
src: string;
|
|
36
|
+
name: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface LinkReference {
|
|
40
|
+
kind: "link";
|
|
41
|
+
href: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface MissingReference {
|
|
45
|
+
kind: "missing";
|
|
46
|
+
name: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function rootsOf(tree: PreviewTree): PreviewNode[] {
|
|
50
|
+
return Array.isArray(tree) ? tree : [tree];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function countIn(node: PreviewNode): number {
|
|
54
|
+
return 1 + (node.children ?? []).reduce((sum, c) => sum + countIn(c), 0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The blocks with no children under the node: what a row actually shows. */
|
|
58
|
+
export function countLeaves(node: PreviewNode): number {
|
|
59
|
+
const children = node.children ?? [];
|
|
60
|
+
if (children.length === 0) return 1;
|
|
61
|
+
return children.reduce((sum, child) => sum + countLeaves(child), 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Every block of the tree, nested ones included; none without a tree. */
|
|
65
|
+
export function countBlocks(tree: PreviewTree | undefined): number {
|
|
66
|
+
if (tree === undefined) return 0;
|
|
67
|
+
return rootsOf(tree).reduce((sum, node) => sum + countIn(node), 0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The block types at the top of the tree, for the card's subtitle. */
|
|
71
|
+
export function topBlockTypes(tree: PreviewTree | undefined): string[] {
|
|
72
|
+
if (tree === undefined) return [];
|
|
73
|
+
const names = rootsOf(tree).map(
|
|
74
|
+
(node) => node.type ?? node.componentName ?? "",
|
|
75
|
+
);
|
|
76
|
+
return [...new Set(names.filter((name) => name !== ""))];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function previewTabs(preview: BlockPreview): BlockPreviewTab[] {
|
|
80
|
+
const variants = (preview.variants ?? []).map((variant, index) => ({
|
|
81
|
+
value: `${BLOCK_PREVIEW_VARIANT_TAB_PREFIX}${index}`,
|
|
82
|
+
label: variant.label,
|
|
83
|
+
tree: variant.tree,
|
|
84
|
+
sketch: variant.sketch,
|
|
85
|
+
}));
|
|
86
|
+
const main = {
|
|
87
|
+
value: BLOCK_PREVIEW_MAIN_TAB,
|
|
88
|
+
label: preview.label ?? null,
|
|
89
|
+
tree: preview.tree,
|
|
90
|
+
sketch: preview.sketch,
|
|
91
|
+
};
|
|
92
|
+
return [main, ...variants];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether any view of the preview proposes a custom sketch. */
|
|
96
|
+
export function hasSketch(preview: BlockPreview): boolean {
|
|
97
|
+
return [preview, ...(preview.variants ?? [])].some(
|
|
98
|
+
(view) => view.sketch !== undefined,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatTree(tree: PreviewTree): string {
|
|
103
|
+
return JSON.stringify(tree, null, JSON_INDENT);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isUser(message: ConversationMessage): message is UserMessage {
|
|
107
|
+
return message.role === MESSAGE_ROLES.USER;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The latest image of that name the user attached in this session. */
|
|
111
|
+
function findAttachedImage(
|
|
112
|
+
name: string,
|
|
113
|
+
messages: readonly ConversationMessage[],
|
|
114
|
+
): string | null {
|
|
115
|
+
const attachments = messages
|
|
116
|
+
.filter(isUser)
|
|
117
|
+
.flatMap((message) => message.attachments ?? []);
|
|
118
|
+
const match = lastMatching(
|
|
119
|
+
attachments,
|
|
120
|
+
(attachment) =>
|
|
121
|
+
attachment.name === name && attachment.dataUrl !== undefined,
|
|
122
|
+
);
|
|
123
|
+
return match?.dataUrl ?? null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function viewOfUrl(url: string): ReferenceView | null {
|
|
127
|
+
if (SAME_ORIGIN_PATH.test(url)) return { kind: "image", src: url, name: url };
|
|
128
|
+
if (REMOTE_URL.test(url)) return { kind: "link", href: url };
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function referenceView(
|
|
133
|
+
reference: BlockPreviewReference | undefined,
|
|
134
|
+
messages: readonly ConversationMessage[],
|
|
135
|
+
): ReferenceView | null {
|
|
136
|
+
if (reference === undefined) return null;
|
|
137
|
+
const name = reference.attachmentName;
|
|
138
|
+
if (name !== undefined) {
|
|
139
|
+
const src = findAttachedImage(name, messages);
|
|
140
|
+
return src === null
|
|
141
|
+
? { kind: "missing", name }
|
|
142
|
+
: { kind: "image", src, name };
|
|
143
|
+
}
|
|
144
|
+
return reference.url === undefined ? null : viewOfUrl(reference.url);
|
|
145
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "../constants/conversation";
|
|
8
8
|
import type {
|
|
9
9
|
AssistantMessage,
|
|
10
|
+
BlockPreviewMessage,
|
|
10
11
|
ConversationMessage,
|
|
11
12
|
ErrorMessage,
|
|
12
13
|
MessageAttachment,
|
|
@@ -14,7 +15,11 @@ import type {
|
|
|
14
15
|
ToolStatus,
|
|
15
16
|
UserMessage,
|
|
16
17
|
} from "../types/conversation";
|
|
17
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
BlockPreview,
|
|
20
|
+
SnapshotMessage,
|
|
21
|
+
ToolOutcome,
|
|
22
|
+
} from "../types/protocol";
|
|
18
23
|
import { newUuid } from "./ids";
|
|
19
24
|
|
|
20
25
|
const STOPPED_OUTCOME: ToolOutcome = "stopped";
|
|
@@ -81,6 +86,17 @@ export function buildLocalError(
|
|
|
81
86
|
};
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
export function buildBlockPreviewMessage(
|
|
90
|
+
preview: BlockPreview,
|
|
91
|
+
): BlockPreviewMessage {
|
|
92
|
+
return {
|
|
93
|
+
id: newUuid(),
|
|
94
|
+
role: MESSAGE_ROLES.BLOCK_PREVIEW,
|
|
95
|
+
preview,
|
|
96
|
+
timestampMs: preview.timestampMs,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
function normalizeToolStatus(raw: string | undefined): ToolStatus {
|
|
85
101
|
if (raw === TOOL_STATUS.SUCCESS) return TOOL_STATUS.SUCCESS;
|
|
86
102
|
if (raw === TOOL_STATUS.ERROR) return TOOL_STATUS.ERROR;
|
|
@@ -223,6 +239,11 @@ function addQuestionAnswer(build: SnapshotBuild, snap: SnapshotMessage): void {
|
|
|
223
239
|
});
|
|
224
240
|
}
|
|
225
241
|
|
|
242
|
+
function addBlockPreview(build: SnapshotBuild, snap: SnapshotMessage): void {
|
|
243
|
+
if (snap.preview === undefined) return;
|
|
244
|
+
build.out.push(buildBlockPreviewMessage(snap.preview));
|
|
245
|
+
}
|
|
246
|
+
|
|
226
247
|
const SNAPSHOT_BUILDERS: Record<
|
|
227
248
|
string,
|
|
228
249
|
(build: SnapshotBuild, snap: SnapshotMessage) => void
|
|
@@ -235,6 +256,7 @@ const SNAPSHOT_BUILDERS: Record<
|
|
|
235
256
|
[MESSAGE_ROLES.NOTICE]: addNotice,
|
|
236
257
|
[MESSAGE_ROLES.CHANGE_SET]: addChangeSet,
|
|
237
258
|
[MESSAGE_ROLES.QUESTION_ANSWER]: addQuestionAnswer,
|
|
259
|
+
[MESSAGE_ROLES.BLOCK_PREVIEW]: addBlockPreview,
|
|
238
260
|
};
|
|
239
261
|
|
|
240
262
|
/**
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from "../constants/conversation";
|
|
5
5
|
import type {
|
|
6
6
|
AssistantMessage,
|
|
7
|
+
BlockPreviewMessage,
|
|
7
8
|
ChangeSetMessage,
|
|
8
9
|
ConversationMessage,
|
|
9
10
|
ErrorMessage,
|
|
@@ -29,7 +30,14 @@ interface WhoItem {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
interface MessageItem {
|
|
32
|
-
kind:
|
|
33
|
+
kind:
|
|
34
|
+
| "user"
|
|
35
|
+
| "assistant"
|
|
36
|
+
| "error"
|
|
37
|
+
| "notice"
|
|
38
|
+
| "change"
|
|
39
|
+
| "answers"
|
|
40
|
+
| "preview";
|
|
33
41
|
key: string;
|
|
34
42
|
message:
|
|
35
43
|
| UserMessage
|
|
@@ -37,7 +45,8 @@ interface MessageItem {
|
|
|
37
45
|
| ErrorMessage
|
|
38
46
|
| NoticeMessage
|
|
39
47
|
| ChangeSetMessage
|
|
40
|
-
| QuestionAnswerMessage
|
|
48
|
+
| QuestionAnswerMessage
|
|
49
|
+
| BlockPreviewMessage;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
interface ToolsItem {
|
|
@@ -66,6 +75,7 @@ const KIND_BY_ROLE: Record<string, MessageItem["kind"]> = {
|
|
|
66
75
|
[MESSAGE_ROLES.NOTICE]: "notice",
|
|
67
76
|
[MESSAGE_ROLES.CHANGE_SET]: "change",
|
|
68
77
|
[MESSAGE_ROLES.QUESTION_ANSWER]: "answers",
|
|
78
|
+
[MESSAGE_ROLES.BLOCK_PREVIEW]: "preview",
|
|
69
79
|
};
|
|
70
80
|
|
|
71
81
|
/** Roles drawn on the agent's side, under one "✦ Assistant" line per turn. */
|
|
@@ -73,6 +83,7 @@ const AGENT_ROLES: ReadonlySet<string> = new Set([
|
|
|
73
83
|
MESSAGE_ROLES.ASSISTANT,
|
|
74
84
|
MESSAGE_ROLES.TOOL,
|
|
75
85
|
MESSAGE_ROLES.CHANGE_SET,
|
|
86
|
+
MESSAGE_ROLES.BLOCK_PREVIEW,
|
|
76
87
|
]);
|
|
77
88
|
|
|
78
89
|
function startOfDay(ms: number): number {
|