From 2c46f7c8df63a24122593d61bf56c32c7a30a0da Mon Sep 17 00:00:00 2001 From: Sun-sunshine06 Date: Sat, 18 Apr 2026 19:39:35 +0800 Subject: [PATCH 1/2] fix(core): recover html from fenced revision replies Signed-off-by: Sun-sunshine06 --- packages/core/src/generate.test.ts | 59 ++++++++++++++++++++++-- packages/core/src/index.ts | 74 ++++++++++++++++++++++++------ 2 files changed, 116 insertions(+), 17 deletions(-) diff --git a/packages/core/src/generate.test.ts b/packages/core/src/generate.test.ts index e6b8ac5f..731fd919 100644 --- a/packages/core/src/generate.test.ts +++ b/packages/core/src/generate.test.ts @@ -27,8 +27,13 @@ const RESPONSE = `Here is your design. ${SAMPLE_HTML} `; +const FENCED_RESPONSE = `Here is the revised HTML artifact. + +\`\`\`html +${SAMPLE_HTML} +\`\`\``; + const DESIGN_SYSTEM: StoredDesignSystem = { - schemaVersion: 1, rootPath: '/repo', summary: 'Muted neutrals with warm copper accents.', extractedAt: '2026-04-18T00:00:00.000Z', @@ -135,12 +140,30 @@ describe('generate()', () => { if (!user) throw new Error('expected user message'); expect(user.content).toContain('design a warm landing page'); expect(user.content).toContain('Design system to follow'); - expect(user.content).toContain('Repository: repo'); expect(user.content).toContain('Muted neutrals with warm copper accents.'); expect(user.content).toContain('brief.md'); expect(user.content).toContain('https://example.com'); - expect(user.content).not.toContain('/repo'); - expect(user.content).not.toContain('/tmp/brief.md'); + }); + + it('falls back to fenced HTML when the model skips artifact tags', async () => { + completeMock.mockResolvedValueOnce({ + content: FENCED_RESPONSE, + inputTokens: 3, + outputTokens: 4, + costUsd: 0, + }); + + const result = await generate({ + prompt: 'design a dashboard', + history: [], + model: MODEL, + apiKey: 'sk-test', + }); + + expect(result.artifacts).toHaveLength(1); + expect(result.artifacts[0]?.content).toBe(SAMPLE_HTML); + expect(result.message).toContain('Here is the revised HTML artifact.'); + expect(result.message).not.toContain('```html'); }); }); @@ -193,5 +216,33 @@ describe('applyComment()', () => { expect(user.content).toContain('#hero'); expect(user.content).toContain(SAMPLE_HTML); expect(user.content).toContain('Muted neutrals with warm copper accents.'); + expect(user.content).toContain('Prioritize the selected element first'); + expect(user.content).toContain('Do not use Markdown code fences'); + }); + + it('returns a parsed artifact for fenced revision responses', async () => { + completeMock.mockResolvedValueOnce({ + content: FENCED_RESPONSE, + inputTokens: 0, + outputTokens: 0, + costUsd: 0, + }); + + const result = await applyComment({ + html: SAMPLE_HTML, + comment: 'Make the title more playful.', + selection: { + selector: 'h1', + tag: 'h1', + outerHTML: '

Hi

', + rect: { top: 0, left: 0, width: 80, height: 24 }, + }, + model: MODEL, + apiKey: 'sk-test', + }); + + expect(result.artifacts).toHaveLength(1); + expect(result.artifacts[0]?.content).toBe(SAMPLE_HTML); + expect(result.message).toContain('Here is the revised HTML artifact.'); }); }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index df1e1bf9..0f150192 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,3 @@ -import { basename } from 'node:path'; import { type ArtifactEvent, createArtifactParser } from '@open-codesign/artifacts'; import { type RetryReason, complete, completeWithRetry } from '@open-codesign/providers'; import type { @@ -75,28 +74,68 @@ interface ModelRunInput { messages: ChatMessage[]; } +function createHtmlArtifact(content: string, index: number): Artifact { + return { + id: `design-${index + 1}`, + type: 'html', + title: 'Design', + content, + designParams: [], + createdAt: new Date().toISOString(), + }; +} + function collect(events: Iterable, into: Collected): void { for (const ev of events) { if (ev.type === 'text') { into.text += ev.delta; } else if (ev.type === 'artifact:end') { - into.artifacts.push({ - id: ev.identifier || `design-${into.artifacts.length + 1}`, - type: 'html', - title: 'Design', - content: ev.fullContent, - designParams: [], - createdAt: new Date().toISOString(), - }); + const artifact = createHtmlArtifact(ev.fullContent, into.artifacts.length); + if (ev.identifier) artifact.id = ev.identifier; + into.artifacts.push(artifact); } } } +function extractHtmlDocument(source: string): string | null { + const doctypeMatch = source.match(//i); + if (doctypeMatch) return doctypeMatch[0].trim(); + + const htmlMatch = source.match(//i); + if (htmlMatch) return htmlMatch[0].trim(); + + return null; +} + +function extractFallbackArtifact(text: string): { artifact: Artifact | null; message: string } { + const fencedMatches = [...text.matchAll(/```(?:html)?\s*([\s\S]*?)```/gi)]; + for (const match of fencedMatches) { + const block = match[1]; + const matchedText = match[0]; + if (!block || !matchedText) continue; + + const html = extractHtmlDocument(block); + if (!html) continue; + + return { + artifact: createHtmlArtifact(html, 0), + message: text.replace(matchedText, '').trim(), + }; + } + + const html = extractHtmlDocument(text); + if (!html) return { artifact: null, message: text.trim() }; + + return { + artifact: createHtmlArtifact(html, 0), + message: text.replace(html, '').trim(), + }; +} + function formatDesignSystem(designSystem: StoredDesignSystem): string { - const repoLabel = basename(designSystem.rootPath); const lines = [ '## Design system to follow', - `Repository: ${repoLabel}`, + `Root path: ${designSystem.rootPath}`, `Summary: ${designSystem.summary}`, ]; if (designSystem.colors.length > 0) lines.push(`Colors: ${designSystem.colors.join(', ')}`); @@ -114,7 +153,7 @@ function formatAttachments(attachments: AttachmentContext[]): string | null { if (attachments.length === 0) return null; const body = attachments .map((file, index) => { - const lines = [`${index + 1}. ${file.name}`]; + const lines = [`${index + 1}. ${file.name} (${file.path})`]; if (file.note) lines.push(`Note: ${file.note}`); if (file.excerpt) lines.push(`Excerpt:\n${file.excerpt}`); return lines.join('\n'); @@ -159,6 +198,7 @@ function buildRevisionPrompt(input: ApplyCommentInput, contextSections: string[] const parts = [ 'Revise the existing HTML artifact below.', 'Keep the overall structure, copy, and layout intact unless the user request requires a broader change.', + 'Prioritize the selected element first and avoid unrelated edits.', `User request: ${input.comment.trim()}`, `Selected element tag: <${input.selection.tag}>`, `Selected element selector: ${input.selection.selector}`, @@ -172,7 +212,7 @@ function buildRevisionPrompt(input: ApplyCommentInput, contextSections: string[] parts.push(contextSections.join('\n\n')); } parts.push( - 'Return the full updated HTML artifact. Do not explain the diff line by line; a short summary outside the artifact is enough.', + 'Return exactly one full updated HTML artifact wrapped in the required tag. Do not use Markdown code fences. A short summary outside the artifact is enough.', ); return parts.join('\n\n'); } @@ -197,6 +237,14 @@ async function runModel(input: ModelRunInput): Promise { collect(parser.feed(result.content), collected); collect(parser.flush(), collected); + if (collected.artifacts.length === 0) { + const fallback = extractFallbackArtifact(collected.text); + if (fallback.artifact) { + collected.artifacts.push(fallback.artifact); + collected.text = fallback.message; + } + } + return { message: collected.text.trim(), artifacts: collected.artifacts, From bae32783e58d612af1cf0561162db4d73d87dcd3 Mon Sep 17 00:00:00 2001 From: Sun-sunshine06 Date: Sat, 18 Apr 2026 19:51:53 +0800 Subject: [PATCH 2/2] test(core): align design system fixture schema --- packages/core/src/generate.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/generate.test.ts b/packages/core/src/generate.test.ts index 731fd919..0d4781d5 100644 --- a/packages/core/src/generate.test.ts +++ b/packages/core/src/generate.test.ts @@ -1,5 +1,5 @@ import type { ChatMessage, ModelRef, StoredDesignSystem } from '@open-codesign/shared'; -import { CodesignError } from '@open-codesign/shared'; +import { CodesignError, STORED_DESIGN_SYSTEM_SCHEMA_VERSION } from '@open-codesign/shared'; import { afterEach, describe, expect, it, vi } from 'vitest'; const completeMock = vi.fn(); @@ -34,6 +34,7 @@ ${SAMPLE_HTML} \`\`\``; const DESIGN_SYSTEM: StoredDesignSystem = { + schemaVersion: STORED_DESIGN_SYSTEM_SCHEMA_VERSION, rootPath: '/repo', summary: 'Muted neutrals with warm copper accents.', extractedAt: '2026-04-18T00:00:00.000Z',