Skip to content

fix(core): recover html from fenced revision replies#19

Merged
Sun-sunshine06 merged 2 commits intomainfrom
fix/inline-comment-artifact-fallback
Apr 18, 2026
Merged

fix(core): recover html from fenced revision replies#19
Sun-sunshine06 merged 2 commits intomainfrom
fix/inline-comment-artifact-fallback

Conversation

@Sun-sunshine06
Copy link
Copy Markdown
Collaborator

Summary

  • recover HTML artifacts when a model returns fenced html instead of <artifact> tags
  • tighten revision prompting so inline comments emphasize the selected element and require artifact-tag output
  • add regression coverage for fenced revision replies in both generate and apply-comment flows

Validation

  • corepack pnpm --filter @open-codesign/core test

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Blocker] Silent artifact-format fallback violates Principle 10 — runModel() now repairs a malformed model response by extracting HTML from collected.text when no <artifact> tag was returned, but it never surfaces that recovery to the caller or throws with context. That masks an upstream contract failure and conflicts with the repo’s “no silent fallbacks” rule, evidence packages/core/src/index.ts:240.
    Suggested fix:

    if (collected.artifacts.length === 0) {
      const fallback = extractFallbackArtifact(collected.text);
      if (fallback.artifact) {
        collected.artifacts.push(fallback.artifact);
        collected.text = [
          'Warning: recovered HTML from a Markdown-fenced response because the model skipped the required <artifact> tag.',
          fallback.message,
        ].filter(Boolean).join('\n\n');
      }
    }
  • [Major] Absolute local paths are now sent to the model — replacing the repo basename and attachment filename with designSystem.rootPath / file.path leaks filesystem structure to external providers on every generate/apply-comment request, evidence packages/core/src/index.ts:138, packages/core/src/index.ts:156.
    Suggested fix:

    import { basename } from 'node:path';
    
    const lines = [
      '## Design system to follow',
      `Repository: ${basename(designSystem.rootPath)}`,
      `Summary: ${designSystem.summary}`,
    ];
    
    const lines = [`${index + 1}. ${file.name}`];

Summary

  • Review mode: initial
  • 2 issues found: the new fenced-HTML recovery is silent even though Principle 10 requires these failures to be user-visible or thrown, and the prompt context now exposes absolute filesystem paths. Regression tests were added for fenced replies, but I did not find coverage that preserves path redaction.

Testing

  • Not run (automation): corepack pnpm --filter @open-codesign/core exec vitest run packages/core/src/generate.test.ts failed in this runner because Node v20.20.2 does not satisfy the repo’s >=22 engine requirement, and vitest was not available in the filtered package.

open-codesign Bot

collect(parser.feed(result.content), collected);
collect(parser.flush(), collected);

if (collected.artifacts.length === 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocker] This silently repairs malformed model output instead of surfacing the contract failure. Principle 10 forbids silent fallbacks, so if we keep this recovery path it needs to be explicit in the returned message (or throw with context).

Suggested fix:

if (collected.artifacts.length === 0) {
  const fallback = extractFallbackArtifact(collected.text);
  if (fallback.artifact) {
    collected.artifacts.push(fallback.artifact);
    collected.text = [
      'Warning: recovered HTML from a Markdown-fenced response because the model skipped the required <artifact> tag.',
      fallback.message,
    ].filter(Boolean).join('\n\n');
  }
}

const body = attachments
.map((file, index) => {
const lines = [`${index + 1}. ${file.name}`];
const lines = [`${index + 1}. ${file.name} (${file.path})`];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] This now includes absolute local paths in prompt context. That leaks filesystem structure to the provider even though the basename/filename was enough before.

Suggested fix:

import { basename } from 'node:path';

const lines = [
  '## Design system to follow',
  `Repository: ${basename(designSystem.rootPath)}`,
  `Summary: ${designSystem.summary}`,
];

const lines = [`${index + 1}. ${file.name}`];

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Blocker] Silent recovery still hides an invalid model response from the caller — runModel() reparses Markdown-fenced/raw HTML when no <artifact> tag was emitted, but it returns that repaired artifact without any warning or thrown error. That violates Principles §10 and makes malformed provider replies indistinguishable from compliant ones, evidence packages/core/src/index.ts:240.
    Suggested fix:

    if (collected.artifacts.length === 0) {
      const fallback = extractFallbackArtifact(collected.text);
      if (fallback.artifact) {
        collected.artifacts.push(fallback.artifact);
        collected.text = [
          'Recovered HTML from a fenced response because the model skipped the required <artifact> tag.',
          fallback.message,
        ].filter(Boolean).join('\n\n');
      }
    }
  • [Major] Absolute local paths are still sent to the model prompt — the new Root path: ${designSystem.rootPath} and (${file.path}) strings expose local filesystem structure to the provider, whereas the previous prompt only shared repo/file names. That is a privacy regression directly introduced by this diff, evidence packages/core/src/index.ts:138, packages/core/src/index.ts:156.
    Suggested fix:

    import { basename } from 'node:path';
    
    const lines = [
      '## Design system to follow',
      `Repository: ${basename(designSystem.rootPath)}`,
      `Summary: ${designSystem.summary}`,
    ];
    
    const lines = [`${index + 1}. ${file.name}`];

Summary

  • Review mode: follow-up after new commits
  • 2 unresolved issues remain at the current head: the fenced-HTML recovery is still silent, and prompt construction still leaks absolute local paths. Test coverage was added for the recovery path, but I did not find coverage that makes the warning user-visible or preserves path redaction.

Testing

  • Not run (static review only; this review flow avoids executing PR code)

open-codesign Bot

collect(parser.feed(result.content), collected);
collect(parser.flush(), collected);

if (collected.artifacts.length === 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Blocker] This still silently repairs malformed model output instead of surfacing the contract failure. Principles §10 requires the caller/UI to see that recovery happened, otherwise a bad provider response is indistinguishable from a valid <artifact> reply.

Suggested fix:

if (collected.artifacts.length === 0) {
  const fallback = extractFallbackArtifact(collected.text);
  if (fallback.artifact) {
    collected.artifacts.push(fallback.artifact);
    collected.text = [
      'Recovered HTML from a fenced response because the model skipped the required <artifact> tag.',
      fallback.message,
    ].filter(Boolean).join('\n\n');
  }
}

const lines = [
'## Design system to follow',
`Repository: ${repoLabel}`,
`Root path: ${designSystem.rootPath}`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] This now sends the absolute design-system root path to the provider, and the same leak happens again below with file.path. That is a privacy regression: the prompt only needs repo/file names here, not the user’s local filesystem layout.

Suggested fix:

import { basename } from 'node:path';

const lines = [
  '## Design system to follow',
  `Repository: ${basename(designSystem.rootPath)}`,
  `Summary: ${designSystem.summary}`,
];

const lines = [`${index + 1}. ${file.name}`];

@Sun-sunshine06 Sun-sunshine06 merged commit 9120dec into main Apr 18, 2026
7 checks passed
hqhq1025 added a commit that referenced this pull request Apr 18, 2026
…in features

Rebase onto origin/main exposed conflicts where PRs #18, #19, #20 had
advanced store, preload, shared and main/index.ts beyond the branch
point. Resolve by taking origin/main as the base and layering our
cancellation additions on top:

- shared/index.ts: add LocalInputFile, SelectedElement, ElementSelectionRect,
  ApplyCommentPayload, StoredDesignSystem exports; add generationId to
  GeneratePayload; keep CancelGenerationPayloadV1
- preload/index.ts: add locale API, pickInputFiles, pickDesignSystemDirectory,
  clearDesignSystem, applyComment; add generationId to generate payload;
  keep cancelGeneration with schemaVersion: 1
- main/index.ts: add applyComment, scanDesignSystem, preparePromptContext,
  registerLocaleIpc; wire AbortController into generate handler; keep
  codesign:v1:cancel-generation handler
- store.ts: merge all origin/main state (selectedElement, inputFiles,
  referenceUrl, i18n tr(), applyInlineComment, etc.) with PR's
  activeGenerationId / cancelGeneration / finishIfCurrent guards
- store.test.ts: update sendPrompt calls to object form; add designSystem:
  null to OnboardingState fixture; relax toast title assertion for i18n

Signed-off-by: hqhq1025 <1506751656@qq.com>
hqhq1025 added a commit that referenced this pull request Apr 18, 2026
…in features

Rebase onto origin/main exposed conflicts where PRs #18, #19, #20 had
advanced store, preload, shared and main/index.ts beyond the branch
point. Resolve by taking origin/main as the base and layering our
cancellation additions on top:

- shared/index.ts: add LocalInputFile, SelectedElement, ElementSelectionRect,
  ApplyCommentPayload, StoredDesignSystem exports; add generationId to
  GeneratePayload; keep CancelGenerationPayloadV1
- preload/index.ts: add locale API, pickInputFiles, pickDesignSystemDirectory,
  clearDesignSystem, applyComment; add generationId to generate payload;
  keep cancelGeneration with schemaVersion: 1
- main/index.ts: add applyComment, scanDesignSystem, preparePromptContext,
  registerLocaleIpc; wire AbortController into generate handler; keep
  codesign:v1:cancel-generation handler
- store.ts: merge all origin/main state (selectedElement, inputFiles,
  referenceUrl, i18n tr(), applyInlineComment, etc.) with PR's
  activeGenerationId / cancelGeneration / finishIfCurrent guards
- store.test.ts: update sendPrompt calls to object form; add designSystem:
  null to OnboardingState fixture; relax toast title assertion for i18n

Signed-off-by: hqhq1025 <1506751656@qq.com>
hqhq1025 added a commit that referenced this pull request Apr 18, 2026
* feat(desktop): redesign sidebar input + add cancellation IPC

- Replace single-line input with autosize textarea (1–6 rows, max-h 144 px)
- Move send button into bottom-right corner of textarea container, icon-only 28 px
- During generation, send button becomes a stop button (Square icon) wired to cancelGeneration
- Remove redundant keyboard-hint bar; fold hint into placeholder text
- Enter sends, Shift+Enter inserts newline, ⌘↵ global send preserved
- main: extract runGenerate helper; maintain Map<id, AbortController> for in-flight
  requests; new codesign:cancel-generation IPC handler; logger scope 'ipc'
- preload: expose cancelGeneration(id) on window.codesign
- shared: GeneratePayload accepts optional generationId
- store: add activeGenerationId state + cancelGeneration action; suppress abort
  errors silently without pushing an error toast

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix: harden generation cancellation flows

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix: version cancellation ipc and tokenise sidebar

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix: preserve current-generation failures

Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): guard cancel race + version IPC payload + add tests

- applyGenerateSuccess/applyGenerateError already guarded via
  finishIfCurrent and the early-return id check; confirm with tests
- Add noop test: unknown generationId leaves all in-flight intact
- Add schema rejection test: empty generationId and missing
  schemaVersion both throw via CancelGenerationPayloadV1.parse
- Fix biome formatting in store.test.ts (pre-existing)

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): properly merge rebase onto main — restore all origin/main features

Rebase onto origin/main exposed conflicts where PRs #18, #19, #20 had
advanced store, preload, shared and main/index.ts beyond the branch
point. Resolve by taking origin/main as the base and layering our
cancellation additions on top:

- shared/index.ts: add LocalInputFile, SelectedElement, ElementSelectionRect,
  ApplyCommentPayload, StoredDesignSystem exports; add generationId to
  GeneratePayload; keep CancelGenerationPayloadV1
- preload/index.ts: add locale API, pickInputFiles, pickDesignSystemDirectory,
  clearDesignSystem, applyComment; add generationId to generate payload;
  keep cancelGeneration with schemaVersion: 1
- main/index.ts: add applyComment, scanDesignSystem, preparePromptContext,
  registerLocaleIpc; wire AbortController into generate handler; keep
  codesign:v1:cancel-generation handler
- store.ts: merge all origin/main state (selectedElement, inputFiles,
  referenceUrl, i18n tr(), applyInlineComment, etc.) with PR's
  activeGenerationId / cancelGeneration / finishIfCurrent guards
- store.test.ts: update sendPrompt calls to object form; add designSystem:
  null to OnboardingState fixture; relax toast title assertion for i18n

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): wait for cancel IPC ack + version generate payload

Major #1: cancelGeneration now waits for the IPC round-trip before
clearing isGenerating/activeGenerationId. If main rejects the cancel
(bad id, already aborted) the error becomes visible via toast instead
of being silently swallowed. Updated store test to reflect async cancel
and drain microtasks between cancel and the follow-up sendPrompt.

Major #2: add GeneratePayloadV1 (schemaVersion: 1, generationId required)
to @open-codesign/shared and rename the IPC channel to codesign:v1:generate,
matching the pattern established by codesign:v1:cancel-generation. Preload
builds the v1 envelope with satisfies check; main validates with
GeneratePayloadV1.parse(). Add 5 schema tests for missing/wrong schemaVersion
and empty generationId. Add notifications.cancellationFailed i18n key.

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): restore legacy generate IPC shim + use ui tokens in sidebar

- Add codesign:generate legacy handler that accepts old GeneratePayload
  (no schemaVersion) and promotes it to V1 by injecting schemaVersion:1
  and falling back generationId to gen-${Date.now()} when absent.
  Emits a warn log to track removal in the next minor release.
- Add --color-on-accent token to packages/ui tokens.css (light + dark)
  so accent-button text never hard-codes a color value.
- Replace text-white in Sidebar.tsx with text-[var(--color-on-accent)]
  on both the stop and send IconButton variants.
- Add 3 tests for GeneratePayload legacy schema and the V1 promotion
  path that the shim relies on.

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): surface error when renderer bridge missing during cancel

Split the combined `!id || !window.codesign` guard in `cancelGeneration`
into two separate checks. A missing `activeGenerationId` is a legitimate
no-op, but a missing IPC bridge is a real error that must be surfaced via
`errorMessage`/`lastError` and a toast. Adds a Vitest test to cover the
new error path (with `beforeAll` i18n init so `tr()` keys resolve).

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): restore attachments/refURL/designSystem entries + i18n sidebar strings

Restores the local context panel (attachments, reference URL, design
system) that was accidentally dropped during rebase when the cancel
button was added.  All hardcoded English strings are now routed through
useT(); chat.send and chat.stop keys were added to en.json / zh-CN.json.

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): fix import order and trailing blank line after rebase

Biome import sort and formatter fixes introduced during rebase conflict
resolution.

Signed-off-by: hqhq1025 <1506751656@qq.com>

* fix(desktop): remove hardcoded textarea fallback, throw on missing tokens

Removes FALLBACK_FONT_SIZE and FALLBACK_LINE_HEIGHT_MULTIPLIER constants
from getTextareaLineHeight. When computed lineHeight is not a finite
positive number and either fontSize or --leading-body token is absent or
invalid, the function now throws instead of silently using magic numbers.
The --leading-body token is already defined in packages/ui/src/tokens.css.

Updates Sidebar.test.ts: replaces the "falls back" test with two tests —
one verifying valid-token multiplication and one verifying the throw path.

Signed-off-by: hqhq1025 <1506751656@qq.com>

---------

Signed-off-by: hqhq1025 <1506751656@qq.com>
Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Co-authored-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant