mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
agent-runtime
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8e6250f2c1 |
feat(web): render GFM tables in markdown artifact and chat renderers (#1496)
* feat(web): render GFM pipe tables in markdown artifact and chat renderers Both hand-rolled markdown parsers (artifact preview and assistant chat) ignored pipe-table syntax, so GFM tables collapsed into a single paragraph. Add a table block to each parser that: - detects header+alignment+body rows; - honors :--- / :---: / ---: column alignment; - preserves inline formatting (code, bold, italic, links) inside cells; - restores escaped \| as a literal pipe; - breaks a preceding paragraph at a table start without a blank line. Closes #1495 * fix(web): use scan-based table cell splitter, drop NUL placeholder Address review on #1496: - lefarcen (P1): the placeholder approach in both splitters was unsafe. artifacts/markdown.ts embedded literal NUL bytes around 'ODPIPE', which flipped the whole file to binary in GitHub diffs and hid the patch from review. runtime/markdown.tsx used a printable ' ODPIPE ' sentinel that could collide with real cell content. - codex (P2): both splitters ran before inline parsing, so a literal '|' inside a backtick code span (e.g. a TypeScript union cell like \`\"ready\" | \"done\"\`) was treated as a column boundary and shredded the row. Both issues collapse to one root cause — splitting by regex on '|' without knowing what's a code span and what's an escape. Replace the placeholder-then-split routine in both files with a single character- level state-machine scanner that: - treats '|' inside backticks as cell content (tracks inCode); - resolves '\\|' to a literal '|' as it accumulates each cell; - strips a single optional leading '|' and a single unescaped trailing '|' as row terminators rather than empty cells. No placeholders, no NUL bytes, no collision surface. Add a test in each test file covering the GFM-style pipe-inside-code-span cell, and the runtime test additionally asserts the row produces exactly two <td> cells (i.e. no phantom column from the embedded pipe). * style(web): make markdown table apply in artifact viewer + add header bg, borders The prior `.md-table` rules were scoped to `.prose-block`, which only wraps the chat assistant path (AssistantMessage). The artifact preview in FileViewer renders into `.markdown-rendered`, so tables in long-form documents (the most common case) inherited no styling and showed as borderless, structureless rows. Lift the rule out of `.prose-block` so both paths share it. Visual treatment matches the existing prose-block ecosystem (blockquote, markdown-status): - `var(--border)` for cell + outer borders, 6px outer radius. - `var(--bg-panel)` on `<th>` for the header band — same token already used by blockquote and code-copy chip. - Even-row zebra via `color-mix(in oklab, var(--bg-panel) 40%, transparent)` for a very faint stripe that holds up in both light and dark themes (no hardcoded color). - Padding bumped from 4×8 to 8×12; cells were touching before. - `display: block; overflow-x: auto` kept so wide tables scroll inside narrow columns instead of pushing layout. * fix(web): wrap md-table in scroll container so columns fill width Previously `.md-table` itself was `display: block; overflow-x: auto` to let wide tables scroll horizontally. The side effect: a block-mode table no longer behaves like `<table>` for layout, so columns collapse to natural content width and leave empty space on the right when the content is narrower than the container. Split the two concerns: the new `.md-table-wrap` <div> owns the scroll viewport (border, radius, overflow-x), and the inner `<table>` keeps its native `display: table` with `width: 100%` so column widths distribute across the available space again. Wide tables still scroll; narrow tables now fill the container. Both render paths (chat runtime + artifact HTML) emit the wrapper, and the corresponding unit tests assert the nested shape. |
||
|
|
5ff578dc8d |
fix: support object-style question-form options (#1293)
* fix: support object-style question-form options * fix: preserve stable option values in form submissions |
||
|
|
12ce5ad38b |
fix(web): ignore <artifact> tags inside markdown code spans and fences (#1132)
* test(web): add failing parser cases for <artifact> recitation in markdown code Cover the three real-world prose contexts where the model legitimately quotes the artifact tag without intending to emit one: - inside an inline backtick span - inside a fenced code block - spread across streaming chunks crossing the fence boundary Establishes the RED baseline before parser code-fence awareness lands. * fix(web): ignore <artifact> tags inside markdown code spans and fences The streaming artifact parser scanned the buffer with a raw indexOf, guarded only by 'next char must be whitespace'. That meant any literal <artifact ...> the model recited while documenting the protocol — even inside backticks or a ```html fence — flipped the parser into artifact mode, swallowed the rest of the reply from the chat UI, and (when a matching </artifact> appeared in the recitation) silently wrote a spurious file to disk via persistArtifact. Replace findOpenTag with a linear scan that tracks fenced code blocks (```) and inline code spans (`), skipping any <artifact prefix found inside either. If the buffer ends mid-fence, return a partial match anchored at the fence start so the next streaming chunk can resolve the boundary without losing fence context. Closes #1130. * fix(web): match renderer fence/inline-code rules in artifact parser Codex review on PR #1132 caught that the previous fix toggled inFence on any triple-backtick run anywhere in the buffer, including mid-line, while the chat renderer (apps/web/src/runtime/markdown.tsx) only treats ``` as a fence when it occupies a whole line matching /^[ ]{0,3}```(\w[\w+-]*)?\s*$/. That asymmetry would suppress a real <artifact> tag emitted after a prose sentence like "the opening marker is ```html and the response then writes:". Rework findOpenTag in three passes that mirror the renderer: 1. Walk \n-terminated lines; only a line that matches FENCE_LINE_RE toggles fence state. Open fences without a close (or with an unterminated tail line) return partial so the next chunk can resolve. 2. Collect inline code spans with /`[^`]+`/g — the same regex used by renderInline — so what the parser skips matches what the user sees as code. Unmatched trailing backticks after the last \n hold back. 3. Find the first <artifact …> outside any skip range; preserve the existing partial-prefix tail handling. Adds a regression test covering the exact case Codex reported. * test(web): pin parser behavior on double-backtick and in-fence string literal recitation Two cases raised in PR #1132 review: - a real artifact tag wrapped in '``<artifact …>``' (double-backtick inline code span) should not be treated as a real artifact - a fenced JS example whose body contains a string literal like 'const fence = "```";' should not pop fence state early and let a later literal <artifact> be parsed as real Both already pass on 96e88ca because the line-anchored fence regex and the renderer-aligned inline regex handle them correctly. Pinning the behavior so future regressions surface as test failures. * fix(web): make stripArtifact markdown-aware to stop truncating literal recitations The streaming artifact parser was hardened in 96e88ca to skip <artifact> recitations inside backticks and fences, but the post-stream stripper at AssistantMessage.tsx still ran a naive 'content.indexOf("<artifact")' over the same text events. As reported by lefarcen on PR #1132, that meant chat replies with literal protocol recitations could still get silently truncated mid-explanation — even though the parser preserved them in the text stream and the file panel was no longer polluted with ghost files. Extract the renderer-aligned classification (FENCE_LINE_RE, INLINE_CODE_RE, computeSkipRanges, rangeContains) into a single source of truth at apps/web/src/artifacts/markdown-context.ts so the parser and the stripper agree on what counts as code. Add apps/web/src/artifacts/strip.ts with a markdown-aware stripArtifact that: - ignores any <artifact open inside a fenced block or inline code span - looks for </artifact> with the same skip-range filter, so a real open paired with a literal close inside backticks does not strip a literal body that is meant to render - returns content unchanged when an open exists with no matching real close (the previous implementation sliced to end-of-string, which would nuke trailing prose on a malformed or still-streaming tag) Refactor parser.ts to import the shared helpers; behavior preserved (all seven existing parser tests still pass). New strip.test.ts covers six cases including the empirically-verified inline-backtick regression. * fix(web): align artifact stripper/parser fence rules with renderer exactly Two gaps surfaced in review at a0bf05f: - markdown-context.ts used a single FENCE_LINE_RE that allowed 0-3 leading spaces and reused the same pattern for opening and closing fences. The chat renderer (runtime/markdown.tsx:44 and :49) is asymmetric — opens with /^```(\w[\w+-]*)?\s*$/, closes with /^```\s*$/, and rejects any leading indentation on either side. Indented " ```html" was being treated as a code fence even though the renderer keeps it as a paragraph, and a literal "```html" line inside an open fenced example was closing the skip range early — both could expose a real or literal <artifact …> to the wrong handler. - stripArtifact discarded computeSkipRanges' unclosedFenceStart, so a fenced literal that ends at EOF without a trailing newline (very common for chat output) leaked the inner <artifact …> recitation to the stripper, reproducing the original #1130 truncation symptom on a narrower input shape. Split FENCE_LINE_RE into FENCE_OPEN_RE / FENCE_CLOSE_RE with no leading indentation, gate the fence state machine on the right side of the toggle, and have stripArtifact extend skip ranges to end-of-content when a fence is left open. Also tightened the parser's tail-line hold-back regex to match the renderer's no-leading-space rule. Added regression tests for the EOF-unclosed-fence case, the indented pseudo-fence (renderer treats as paragraph, stripper must strip the real artifact), and a "```html" line inside an open fence. Refs nexu-io/open-design#1130 * refactor(web): align streaming tail-line fence guard with FENCE_OPEN_RE The streaming parser's tail-line hold-back used a stricter local regex (/^```\w*$/) than the renderer's FENCE_OPEN_RE (/^```(\w[\w+-]*)?\s*$/), missing valid opener tails like ```c++, ```ts-, or ``` (trailing space). In practice these tails are still held back by the unmatched-backtick parity scan that runs immediately after — three backticks in a tail line are odd, so firstUnmatched stays set and the parser holds from that position. So this wasn't a runtime correctness bug, just a regex divergence that future readers could trip on. Drop the local regex and reuse FENCE_OPEN_RE so the tail check matches the same shape the rest of the pipeline already uses. Pinned the behavior with three new parser tests (`+`/`-` info-string suffix and trailing-space tails arriving as the first chunk) — they pass at HEAD, proving the parity scan was already covering these cases. Refs nexu-io/open-design#1132 (lefarcen polish P2) * fix(web): scope inline-code skip ranges per block and reject <artifact prefix-shared opens INLINE_CODE_RE previously ran over the whole buffer, so an unmatched backtick in one paragraph could pair with a backtick in a later paragraph and create a phantom inline span that swallowed any real <artifact …> between them. Mirror runtime/markdown.tsx by splitting the buffer on fence / blank / heading / list / hr boundaries and running INLINE_CODE_RE per block region instead. stripArtifact accepted any unskipped `<artifact` substring as a real open, while the streaming parser already required a following whitespace character — so prose like `<artifactual>demo</artifact>` was being truncated to `prefix suffix`. Extract the parser's real-open guard into isRealArtifactOpenAt and reuse it from both sides. While reordering findOpenTag for the shared guard, also fix the related hold-back ordering issue tracked at #1141: a stray tail-line backtick or fence-opener prefix used to suppress an artifact already complete earlier in the buffer. Scan for the earliest complete real open first, then pick the earliest hold-back position only when no complete tag was found. Regressions pinned in parser.test.ts and strip.test.ts for both new finding shapes. * fix(web): keep HR-shaped lines inside paragraph regions for inline-code scanning The previous walker closed inline-scan regions on lines matching the HR regex, but `parseBlocks()` in runtime/markdown.tsx does not break a paragraph on HR — its inner accumulation loop only breaks on blank / fence / heading / ul / ol (runtime/markdown.tsx:95-104). HR is only an HR block in the outer loop's first-look, never mid-paragraph. So inputs like `intro \`\n---\n<artifact …>…</artifact>\n---\nclosing \`` are one paragraph in the renderer, whose two stray backticks pair to cover the literal artifact recitation — but the walker was splitting on the `---` lines, leaving the recitation outside skip ranges, and the parser/stripper would treat it as a real tag. Drop HR from the paragraph-break list (HR-shaped lines carry no backticks of their own, so keeping them inside the surrounding region is benign either way) and document the renderer-mirror rationale. Regressions pinned on both sides. |
||
|
|
7c1db80893 |
fix(web): 写盘前拦截 prose-as-HTML artifact (#50) (#1144)
* fix(web): reject prose-as-HTML artifacts at write time (#50) AI 偶尔会在仅做 in-place 编辑(无新 canonical HTML 产出)时仍按 system prompt 的非协商性收尾规则发出 `<artifact type="text/html">` 块,但块内只装一句中文总结。`persistArtifact` 之前不做内容校验, 此类 prose 会作为合法 HTML 落盘到 `.od/projects/<id>/<id>.html`, 并附带 `kind: html` manifest,污染项目文件面板(截图见 #50 评论)。 新增 `validateHtmlArtifact` 纯函数:要求非空 + 长度 ≥64 + 含 `<!doctype html>` 或 `<html>` 标签(大小写不敏感、容忍 BOM)。 `persistArtifact` 在 `ext === '.html'` 分支调用 gate,失败时 通过 `setError` 报错且不写文件。 scope 限于 `<artifact>`-tag 持久化路径——FileViewer/FileWorkspace 里用户手动保存草稿 HTML 走的是不同入口,不受影响。 prompt 层根因(缺少免发条款)已拆出 #1143 单独跟进,本 PR 是 持久化层的兜底防御。 * fix(web): anchor HTML structural check at content start (#1144 review) mrcfps 在 #1144 review 指出原实现的 false negative: HTML_OPENING_TAG_RE / DOCTYPE_RE 用 .test() 在整个字符串里搜, 所以 AI 描述改动时 inline 引一个 tag 名("Updated the <html lang> attribute...")就能蒙混过关——长度过 64、含 `<html `——同样 落地为幽灵 HTML 文件。 修复:合并两个 regex 成 STARTS_WITH_DOCUMENT_RE,加 ^ anchor, 要求 trimmed 内容的首个非空白 token 必须是 `<!doctype html>` 或 `<html`。Mid-string 出现的 tag 名不再算数。 同时按 lefarcen 的非阻塞文档建议把 docblock 改写得更精确: - "structural sniff" 替代 "validation",明确不是 HTML 校验器 - 列出 not-a-linter / .jsx-tsx-skipped / 用户手动保存路径不受 影响 三条 scope 边界 - 64 字符阈值会拒收 49 字符的最小空 doc(如 `<!doctype html><html><body></body></html>`),明确这是有意 trade-off:AI 产出预期是 non-trivial deliverable 新增 3 例测试覆盖 mrcfps 描述的 false negative: - 长 prose 中 inline 引 `<html lang>` 应拒收 - 长 prose 中 inline 引 `<!doctype html>` 应拒收 - 首个 token 是 `<p>` 等非文档标签的 fragment 应拒收 |
||
|
|
bbdd4e84b5 |
chore: enforce test directory conventions (#496)
* chore: enforce test directory conventions Move package, app, and tool tests out of src and add guard enforcement so source directories stay source-only. * ci: use guard and package-scoped tests Run the new repository guard in CI and keep test execution aligned with package-scoped commands after removing root aliases. * ci: align stable release guard check Use the new repository guard in stable release verification after replacing the residual-JS-only script. * chore: tighten test layout enforcement Enforce sibling tests directories, typecheck moved test suites with dedicated configs, and refresh remaining guidance that pointed at src-based tests. * chore: clarify no-emit test tsconfigs Explicitly disable declaration-only emit in test tsconfigs so review tooling sees they are no-emit typecheck configs. |
||
|
|
3d306cb450 |
Add React artifact output support (#121)
* Add React artifact output support Generated-By: looper 0.2.7 (runner=worker, agent=codex) * Fix React component artifact preview repairs Generated-By: looper 0.2.7 (runner=fixer, agent=codex) * fix: address React preview review feedback Generated-By: looper 0.2.7 (runner=fixer, agent=codex) |
||
|
|
87280c379f |
feat: add first-class SVG artifact viewer (#177)
* feat: add first-class SVG artifact viewer * fix: improve SVG source fetch diagnostics |
||
|
|
f430a68766 |
feat: add markdown and svg artifact renderers (#73)
* feat: add markdown and svg artifact renderers * fix: harden markdown preview rendering * fix: address markdown renderer review follow-ups |
||
|
|
cfebff9653 |
Align app directories and isolate e2e tests (#102)
* chore: align app directories * test: consolidate external suites under e2e |