mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fa19a9fc6f8156d13dca1f0701761c2f886a6a7d
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fa19a9fc6f |
fix(daemon): pass Grok Build prompts via files (#3898)
* fix(daemon): pass Grok Build prompts via files * fix(web): recover Grok HTML emitted before artifact tag * fix(daemon): filter Grok models and effort args * fix(web): persist standalone HTML agent responses * fix(web): persist fenced HTML agent responses * fix(web): hide Grok fallback HTML in chat * fix(web): hide recovered pre-artifact HTML Generated-By: looper 0.9.7 (runner=fixer, agent=opencode) --------- Co-authored-by: audit <a@b.c> |
||
|
|
5555b2eca9 |
fix(web): keep the streaming question-form id stable (#3582 follow-up) (#3644)
* fix(web): keep the streaming question-form id stable (no churn) `parsePartialQuestionForm` derived the form `id` from the streaming body `id` token, but that arrives char-by-char and `parsePartialJson` repairs the open string, so `form.id` churned (`"d"` → `"di"` → …). `id` keys the live, still-editable Questions panel (ProjectView's questionFormKey, QuestionForm's data-form-id / input names), so each partial-id change remounted the panel and discarded in-progress answers. Derive the preview id from the open-tag attr (complete the instant the tag streams) or the stable default instead; the final parse (tryParseForm, after the close tag) still adopts the real body id. This also keeps the earlier nested-id-masquerade guarantee (no body scan). Follow-up to #3582. Regression: the form id stays `discovery` while the body id token streams in. * fix(web): adopt the question-form body id once complete (no preview→final remount) Building on the char-churn fix: when a form omits the open-tag id attribute but carries a body id, the preview used 'discovery' and the final parse adopted the body id — a single remount at the close tag that could drop answers. Adopt the body id as soon as its string literal is terminated (depth-1, nested ids ignored); that value equals what tryParseForm assigns, so preview id == final id and the panel never remounts on the swap. The id settles early (it's a leading field), before any question is answerable. Regressions: a partial body id is not adopted (no churn); a complete body id is adopted and matches splitOnQuestionForms' final id; nested question ids are ignored. * fix(web): decouple the question-form React key from the parsed form id Root-cause fix for the identity churn three reviewers flagged. The Questions panel was keyed on `${conversation}:${message}:${form.id}`, so any change to the parsed id remounted it and dropped in-progress answers. The streaming preview shows the `discovery` fallback id until the body id arrives, and a form that emits answerable questions before its id settles flips identity while the user is mid-answer — exactly the remount they described. There is at most one (first) form per assistant message, so the id is redundant for uniqueness: `${conversation}:${message}` already identifies the occurrence, and a distinct later form lives in its own message (its own key, still replays the reveal). Extract buildQuestionFormKey (id-free) and unit-test that the key is stable across a streaming id flip, distinct across messages, and null until conversation/message/form are all present. completeTopLevelString stays — it settles the *displayed* id early — but the id no longer drives identity, so even the questions-before-id ordering can't remount the panel. |
||
|
|
6e10fa4852 |
feat: stream agent questions token-by-token (#3582)
* feat(web): stream AskUserQuestion and discovery questions token-by-token Render both agent question surfaces as they generate instead of popping in only once the whole payload finishes. - AskUserQuestion (Claude tool): builds on the live tool-input streaming landed in #3382. When a still-streaming tool's name is AskUserQuestion, render a read-only StreamingAskUserQuestionCard that grows its questions and options from the repaired JSON prefix; the interactive (persisted) card takes over once the full tool_use lands and the id leaves liveToolInput. - <question-form> discovery (any agent, incl. Codex): the streaming parser now repairs the truncated prefix so a question shows the moment its label exists and its options grow in one at a time, instead of waiting for each question object to fully close. The Questions panel UX (skip countdown, auto-continue, reveal) is unchanged. Shared repairJsonPrefix/parsePartialJson live in runtime/partial-json.ts. No daemon, contract, or provider changes — this reuses #3382's tool_input_delta / liveToolInput plumbing. * fix(web): harden streaming <question-form> parser (review follow-ups) Two merge-safe follow-ups from review on parsePartialQuestionForm: - Derive form-level id/title/description from the parsed top-level object instead of regex-scanning the whole partial body, so a nested question or option id/description can no longer masquerade as the form's own. form.id keys the live Questions panel (ProjectView), so a mid-stream identity change would remount it. Removes extractJsonStringField. - Strip a (possibly partial) trailing ``` fence so the live preview keeps streaming in the gap between the fenced JSON body closing and the </question-form> close tag arriving, instead of dropping back to empty. Both covered by regression tests in question-form.test.ts. * fix(web): preserve submitLabel + string-safe fence trim in streaming form parser Second round of review follow-ups on parsePartialQuestionForm: - Carry top-level submitLabel through the streaming preview, mirroring tryParseForm, so a custom CTA label doesn't flicker in only once the close tag arrives. - Only strip the trailing ``` fence when those backticks are the closing wrapper, not content of a JSON string value still being typed — a label like "Use ```" mid-stream kept losing its backticks. The trim now checks it isn't inside an open JSON string first. Both covered by regression tests in question-form.test.ts. * fix(web): honest status badge on WebFetch/WebSearch cards WebFetchCard / WebSearchCard hard-coded a green check badge regardless of the real tool state, so a slow or failed fetch/search rendered as false-green. Thread result / runStreaming / runSucceeded through both and reuse ResultBadge (as the other tool cards do) so the badge reflects the actual outcome. Pre-existing on main (only surfaced in this PR's changed range because the new streaming card additions shifted these lines down); folded in here since it's a one-line-per-card fix in a file this PR already touches. * fix(web): keep partial-JSON repair stable across a dangling string escape repairJsonPrefix() appended a closing quote even when the stream stopped on an incomplete escape inside an open string, leaving invalid JSON: `{"a":"x\` became `{"a":"x\"` (the quote got escaped) and `{"a":"x\u` became an invalid `\uXXXX`. parsePartialJson() then returned null, collapsing the live AskUserQuestion / question-form preview to its empty frame mid-stream whenever a prompt mentioned a Windows path, regex, or escaped quote. Now neutralize a trailing incomplete escape before closing the string: drop a partial `\uXXXX` (0-3 hex) and a lone (odd-run) trailing backslash. Completed escapes and even backslash pairs (e.g. `C:\\Users\\`) are preserved. Regression tests in ask-user-question.test.ts cover the repair cases and a path-like prompt that keeps its last visible content. * fix(web): trim unfinished scalar values in partial-JSON repair repairJsonPrefix() only trimmed dangling commas / empty `"key":`, so a delta that stopped mid-scalar left invalid JSON: `{"…","multiSelect":f` closed to `{…:f}` and failed to parse, collapsing the live preview whenever a boolean, null, or number token split across deltas. The cleanup loop now also trims a partial `true`/`false`/`null` and a number cut on `.`/`e`/sign, leaving the `"key":` / `,` / `[` boundary for the next iteration. Complete literals (`true`, `12`, `1.5`, `1e3`) and string values that merely start like a literal are not prefixes of these patterns, so they survive. Regressions added in both the AUQ and question-form suites (a boolean split across deltas keeps already-visible questions) plus repair-level unit tests. * fix(web): stable preview question ids while a form streams shapeStreamingQuestions surfaced a question as soon as its label existed, but mapRawQuestion falls back to q${index+1} until the real id streams — so a question that arrives label-first and gains its canonical id later would change id mid-stream. The Questions panel stays editable while building and keys fields / answers off q.id, so that id change remounts the field and orphans whatever the user already picked. Pin the preview id to the array index for the whole streaming phase (questions only append once their label exists, so the index is stable); the final non-preview parse still uses the real id. Regression in question-form.test.ts: label-first then id-later keeps id q1. * fix(web): preview question ids match the final parse (no orphaned answers) The previous index-pin kept ids stable *within* the preview but still differed from the final canonical id, so an answer entered while the panel was building got stranded at the preview→final swap (field re-keys from `q1` to `platform`, reads nothing). Surface a streaming question only once its canonical id is determinable instead: use the streamed `id`; or, for an already-closed (not in-flight) object with no id, the `q${index+1}` fallback that the final parse will also assign; hold the in-flight object until it gains an id. Preview id now always equals the final id. Updated/added regressions in question-form.test.ts: a label-first object is held until its id is determinable, and preview ids match splitOnQuestionForms. * fix(web): stable positional keys in the streaming AskUserQuestion card The streaming card keyed fields on q.question and options on opt.label, but those strings grow token-by-token, so every delta changed the key and remounted the subtree — replaying the reveal animation on each token instead of mounting once. Switch to positional keys (questionIndex / questionIndex:optionIndex); questions and options only append during streaming, so the index is stable and each node updates its text in place. Regression: StreamingAskUserQuestionCard.test.tsx asserts the field/option DOM nodes are reused (===) across successive grown prefixes. * fix(web): stream a closed no-id question before the close tag The previous gate held *every* last no-id question as if in-flight, but a last object whose braces have already streamed has a final fallback id (q${index+1}) and should appear immediately — holding it broke the "question shows the moment its label exists" behavior for forms that omit explicit ids. Distinguish closed vs still-open via a string-aware balanced-object count: surface a question once its object has closed (id final) or it already carries an `id` (stable); keep holding only an in-flight object with no id yet. Regressions: a single closed no-id question streams pre-close-tag (id q1), while the same content with an unclosed object stays held. * fix(web): suppress hedging fallback text while a live AskUserQuestion streams suppressAskUserQuestionFallbackText ran on the persisted blocks before the liveBlocks pass, so during streaming (no persisted tool_use yet) seenAskUser Question never flipped and any duplicate hedging markdown survived into persistedBlocks while the live card was appended after — the exact duplicate-question state the card removes, just earlier in the turn. Seed the suppressor with whether a live AskUserQuestion is present in liveToolInput so it fires during the streaming phase too. Regression in AssistantMessage.test.tsx: fallback prose coexisting with a live AskUserQuestion in liveToolInput is suppressed, while normal prose without a live AUQ is kept. * fix(web): order-aware fallback suppression for live AskUserQuestion The previous fix seeded the suppressor with "a live AskUserQuestion exists", which dropped ALL text blocks in the turn — including legitimate preamble ("I need one clarification:") that precedes the tool call, not just duplicate hedging. Make it order-aware instead: suppress text only after an AskUserQuestion in block order, and have the suppressor recognize a live (streaming) AskUserQuestion live-tool block the same way it recognizes a settled tool-group. Preamble before the card is kept; hedging after it is dropped — matching the settled-case semantics. Regression updated in AssistantMessage.test.tsx: intro preamble coexisting with a live AskUserQuestion in liveToolInput is preserved. * fix(web): place the live AskUserQuestion at its real stream position The order-aware pass still appended the live AUQ block after all persisted blocks, so hedging text the model emits *after* starting the tool call (which is already in events, hence before the appended block) was kept. Carry the event index the tool started at (`seq`, recorded in liveToolInput when the first input_json_delta arrives) and split the events around it: blocks before `seq` are preamble, the live AUQ block goes next, blocks after `seq` are hedging. Suppression then keeps the preamble and drops the trailing hedging, exactly matching the settled tool_use case. Regression in AssistantMessage.test.tsx: with the tool starting after the preamble (seq=1), preamble before the card is preserved and hedging after it is dropped. * fix(web): account for buffered preamble when pinning live-tool seq `seq` was read from `latestAssistantMsg.events.length`, but preamble text goes through `appendTextEvent` and isn't committed to `events` until the next rAF/250ms flush. When intro prose and the first `tool_input_delta` arrive in the same burst, that read undercounts by one, so the live AskUserQuestion was positioned before the preamble and suppression dropped it as hedging. The buffer now exposes `hasPendingText()`, and seq adds 1 for a still-pending preamble chunk (it commits as one text event just before the tool position). Regression in buffered-text-pending.test.tsx: appended text is reported pending and absent from events until flush, then commits as one event. * fix(web): re-render the streaming row on live tool-input deltas AssistantMessage is memoized with a prop allowlist that omitted liveToolInput, so a burst of pure tool_input_delta traffic (which only changes liveToolInput, not message) was swallowed by the comparator — the live AskUserQuestion card / code box wouldn't grow until some unrelated change, usually the final tool_use. Add liveToolInput to ASSISTANT_MESSAGE_COMPARED_PROPS, and have ChatPane pass it only to the streaming row (undefined elsewhere) so just that row re-renders per delta, not all N messages. Also widens ChatPane's liveToolInput type with the optional seq field. Regression in AssistantMessage.test.tsx: with the same message object, changing only liveToolInput re-renders and grows the card prompt. |
||
|
|
f3ec4968af |
feat(web): revamp chat UI styling + live streaming code card (#3382)
* feat(web): revamp chat UI styling + live streaming code card Restyle the chat pane (chat/tokens/code/tools/routines/composio CSS) and add a live code-component card that streams Write/Edit tool input and <artifact type="text/html"> bodies character-by-character via a new ephemeral tool_input_delta SSE event, flipping to the completed FileWriteCard/FileEditCard view on close. Includes shiki highlighting, markdown/artifact strip handling, tool-status cleanup, and i18n keys. * chore(nix): refresh pnpm deps hash for merged lock --------- Co-authored-by: qiongyu1999 <2694684348@qq.com> |
||
|
|
9641c9e11c |
fix(web): parse ask-question blocks as question-form alias (#1194) (#3053)
Accept <ask-question> as an alias for <question-form> and locate close tags with a Unicode-safe scan so Turkish dotted-I prose before the tag does not desync parser indices. |
||
|
|
fce444bcab |
Consolidate chat comments preview on main (#2906)
* feat(web): queue chat sends * feat(web): render code comment directives * feat(web): add preview comments and manual edits * fix(web): polish shared chrome controls * fix(web): align queued send loading state * feat(web): open primary project artifacts * fix(web): keep queued sends and tests aligned * fix(web): restore docked comment tools layout * fix(web): align preview comment toolbar * fix(web): place local cli beside handoff * fix(web): move agent menu beside handoff * fix(web): make project instructions a direct header action * fix(web): compact handoff and toolbar labels * fix(web): clarify handoff menu and annotation label * fix(web): restore compact cursor handoff trigger * fix(web): align agent menu trigger with handoff * fix(web): add draw toolbar close action * fix(web): move inspect editing into edit mode * fix(web): avoid reserving comment sidebar in annotation mode * fix(web): float preview comments panel * fix(web): keep edit canvas full width * fix(web): polish preview annotation tools * fix(web): highlight active preview comments * fix(web): open comments panel after annotation save * fix(web): polish comment handoff controls * fix(web): remove palette preview tool * fix(web): simplify draw annotation toolbar * fix(web): restore queued tasks into composer * fix(web): restore queued send strip styling * fix(web): hide internal comment target ids * fix(web): align manual edit panel header * test(web): cover visual interaction contracts * fix(web): address PR feedback regressions * fix(web): preserve artifact chrome state * fix(daemon): restore project raw file routes --------- Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local> Co-authored-by: mrcfps <mrc@powerformer.com> |
||
|
|
22e057cc6a |
Reject reserved project paths in HTML artifacts (#2484)
* Reject reserved paths in HTML artifacts * Handle reserved artifact path edge cases --------- Co-authored-by: Ghxst <200635707+GHX5T-SOL@users.noreply.github.com> |
||
|
|
575d5aaf2f | fix(web): strip trailing punctuation from markdown link URLs (#2568) (#2591) | ||
|
|
ac748d53fc |
Fix pointer HTML artifact previews (#2075)
Resolves short pointer-only HTML artifacts to the existing project HTML target before persisting preview artifacts, with helper and ProjectView regression coverage for #536. Validation: CI and nix-check were green on PR head
|
||
|
|
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. |