Skip to content

Commit a85a206

Browse files
committed
fix(pages): expose compile diagnostics through VFS
1 parent eed54fc commit a85a206

3 files changed

Lines changed: 83 additions & 9 deletions

File tree

apps/sim/lib/copilot/vfs/workspace-vfs.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ import { listAllWorkspaceFiles } from '@/lib/workspace-files/application/list-wo
177177
import { readWorkspaceFileContent } from '@/lib/workspace-files/application/read-workspace-file-content'
178178
import { listWorkspaceFileFoldersOperation } from '@/lib/workspace-files/application/workspace-file-folders'
179179
import { parseWorkspaceFileFolderDisplayPath } from '@/lib/workspace-files/folder-display-path'
180+
import {
181+
collectSimPageDiagnostics,
182+
isSimPageSource,
183+
SIM_PAGE_CONTENT_TYPE,
184+
} from '@/lib/workspace-files/page-compile'
180185
import { getWorkspaceHostContextForViewer } from '@/lib/workspaces/host-context'
181186
import {
182187
assertActiveWorkspaceAccess,
@@ -1549,7 +1554,12 @@ export class WorkspaceVFS {
15491554
const e2bFmt = isDocSandboxEnabled ? await getE2BDocFormat(record.name) : null
15501555
const taskId = BINARY_DOC_TASKS[ext]
15511556
const isMermaidFile = ext === 'mmd' || ext === 'mermaid'
1552-
if (!e2bFmt && !taskId && !isMermaidFile) return null
1557+
// Sim pages (and legacy .html-named page source) compile-check too:
1558+
// this is the only way an agent can retrieve the "block skipped"
1559+
// diagnostics for an ALREADY-written page — without it, "find the
1560+
// malformed table" degenerates into guessing.
1561+
const maybeSimPage = record.type === SIM_PAGE_CONTENT_TYPE || ext === 'html'
1562+
if (!e2bFmt && !taskId && !isMermaidFile && !maybeSimPage) return null
15531563
const { content: buffer } = await readWorkspaceFileContent.execute({
15541564
principal: this.requireFilePrincipal(),
15551565
input: {
@@ -1565,6 +1575,37 @@ export class WorkspaceVFS {
15651575
totalLines: 1,
15661576
})
15671577
}
1578+
if (maybeSimPage && isSimPageSource(code)) {
1579+
const diagnostics = collectSimPageDiagnostics(code)
1580+
const result =
1581+
diagnostics.length === 0
1582+
? { ok: true }
1583+
: {
1584+
ok: false,
1585+
error: `${diagnostics.length} block(s) fail to compile and are omitted from the rendered page: ${diagnostics.join('; ')}`,
1586+
}
1587+
return bindWorkspaceFileResult(record, {
1588+
content: JSON.stringify(result),
1589+
totalLines: 1,
1590+
})
1591+
}
1592+
if (maybeSimPage && !e2bFmt && !taskId && !isMermaidFile) {
1593+
if (record.type === SIM_PAGE_CONTENT_TYPE) {
1594+
// A page-typed file whose bytes are not page source (e.g. a crash
1595+
// between upload registration and source restore) — report it
1596+
// rather than pretending the path does not exist.
1597+
return bindWorkspaceFileResult(record, {
1598+
content: JSON.stringify({
1599+
ok: false,
1600+
error:
1601+
'Stored content is not page source (no YAML frontmatter with a title) — the file renders as raw HTML',
1602+
}),
1603+
totalLines: 1,
1604+
})
1605+
}
1606+
// Bespoke raw HTML has no compiler to check.
1607+
return null
1608+
}
15681609
if (isMermaidFile) {
15691610
const result = await validateMermaidSource(code)
15701611
const json = JSON.stringify(result)

apps/sim/lib/workspace-files/page-compile.test.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,18 +106,43 @@ describe('compileSimPage', () => {
106106
const source = '---\ntitle: T\n---\n```sim:kv\n- { key: }\n```'
107107
expect(compileSimPage(source)).not.toContain('was skipped')
108108
expect(collectSimPageDiagnostics(source)).toEqual([
109-
'sim:kv block skipped: its payload did not match the expected shape',
109+
'sim:kv block starting "- { key: }" skipped: its payload did not match the expected shape',
110110
])
111111
})
112112

113113
it('omits retired fence kinds and reports them as diagnostics', () => {
114114
const source = '---\ntitle: T\n---\n```sim:cards\n- title: X\n```'
115115
expect(compileSimPage(source)).not.toContain('sim:cards')
116116
expect(collectSimPageDiagnostics(source)).toEqual([
117-
'sim:cards block skipped: its payload did not match the expected shape',
117+
'sim:cards block starting "- title: X" skipped: its payload did not match the expected shape',
118118
])
119119
})
120120

121+
// A page can carry several blocks of the same kind — the diagnostic must
122+
// name WHICH one failed and distinguish a YAML syntax error from a shape
123+
// mismatch, or "a table is malformed" sends the fixer hunting through all
124+
// of them.
125+
it('identifies the failing block by its first payload line and reports YAML errors', () => {
126+
const source = [
127+
'---',
128+
'title: T',
129+
'---',
130+
'```sim:table',
131+
'columns: [A, B]',
132+
'rows:',
133+
' - [a, b]',
134+
'```',
135+
'',
136+
'```sim:table',
137+
'columns: [C: D]',
138+
'rows: broken',
139+
'```',
140+
].join('\n')
141+
const diagnostics = collectSimPageDiagnostics(source)
142+
expect(diagnostics).toHaveLength(1)
143+
expect(diagnostics[0]).toContain('sim:table block starting "columns: [C: D]"')
144+
})
145+
121146
it('reports nothing for a fully valid page', () => {
122147
expect(
123148
collectSimPageDiagnostics('---\ntitle: T\n---\n```sim:kv\n- key: A\n value: B\n```')

apps/sim/lib/workspace-files/page-compile.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
2+
import { truncate } from '@sim/utils/string'
13
import { JSON_SCHEMA, load } from 'js-yaml'
24
import { marked } from 'marked'
35
import { z } from 'zod'
@@ -407,20 +409,26 @@ function compileBody(source: string, diagnostics?: string[]): string {
407409
const renderer = FENCE_RENDERERS[kind]
408410
let payload: unknown
409411
let rendered: string | null = null
412+
let parseError: string | null = null
410413
if (renderer) {
411414
try {
412415
payload = loadYaml(body)
413-
rendered = renderer(payload)
414-
} catch {
415-
rendered = null
416+
} catch (err) {
417+
parseError = getErrorMessage(err, 'invalid YAML')
416418
}
419+
if (parseError === null) rendered = renderer(payload)
417420
}
418421
if (rendered !== null) {
419422
html.push(rendered)
420423
} else {
421-
diagnostics?.push(
422-
`sim:${kind} block skipped: its payload did not match the expected shape`
423-
)
424+
// Name WHICH block failed and WHY: a page can carry several blocks
425+
// of the same kind, and a bare "a table is malformed" sends the
426+
// fixing agent hunting through all of them.
427+
const preview = truncate(body.trim().split('\n')[0] ?? '', 80)
428+
const reason = parseError
429+
? `its payload is not valid YAML/JSON — ${truncate(parseError, 160)}`
430+
: 'its payload did not match the expected shape'
431+
diagnostics?.push(`sim:${kind} block starting "${preview}" skipped: ${reason}`)
424432
}
425433
}
426434
index = bodyEnd + 1

0 commit comments

Comments
 (0)