Review feedback found that pre-escaping the entire markdown payload caused double-encoding in inline/fenced code, making HTML/JSON/XML examples unreadable. Switch to raw markdown parsing with renderer-level HTML token escaping so code formatting stays readable while raw HTML remains inert.
Constraint: Maintain issue #221 security boundary without broad sanitizer rewrite
Rejected: Keep full-string pre-escape | breaks code snippet readability
Confidence: high
Scope-risk: narrow
Directive: Escape only raw HTML tokens, not the whole markdown source
Tested: npm run build (openless-all/app)
Not-tested: Manual QA panel rendering across all markdown edge cases
Code review flagged a UX regression after sanitizer refactor: assistant single newlines stopped rendering as hard breaks. Re-enable marked breaks mode in the shared QA renderer to preserve previous panel readability for streamed and lightly formatted answers.
Constraint: Keep sanitizer fix while matching prior QA markdown display behavior
Rejected: Add newline post-processing in QaPanel | duplicates renderer concern and increases drift risk
Confidence: high
Scope-risk: narrow
Directive: QA markdown display flags must be centralized in qaMarkdown.ts to keep streaming/final parity
Tested: npm run build (openless-all/app)
Not-tested: Manual visual QA panel check on desktop runtime
Two parallel reviews flagged a high-risk fallback path and a link-encoding regression in the new QA sanitizer. This patch keeps the minimal #221 approach while ensuring failure fallback stays inert and safe links preserve query strings correctly.
Constraint: Preserve minimal change scope for issue #221
Rejected: Expand into CSP/capability hardening in same patch | out of issue scope and higher blast radius
Confidence: high
Scope-risk: narrow
Directive: Never return raw model output into dangerouslySetInnerHTML fallback paths
Tested: npm run build (openless-all/app)
Not-tested: Manual desktop exploit reproduction on Windows/macOS
Issue #221 requires closing the QA markdown XSS surface without broad CSP or architecture changes. This switches assistant and streaming markdown to one shared safe renderer that escapes raw HTML and rejects non-http(s)/mailto/tel links.
Constraint: Keep markdown readability while preventing executable HTML in QA WebView
Rejected: Add DOMPurify dependency | avoid new dependency for minimal fix
Confidence: medium
Scope-risk: narrow
Directive: Keep streaming and final assistant rendering on the same sanitizer path
Tested: npm run build (openless-all/app)
Not-tested: Manual malicious payload interaction in packaged desktop app
Issue #239 required the close button to be clickable, but a plain window.show() regression can steal focus and break selection capture assumptions in QA flow. Keep an explicit no-activate show on Windows while simplifying the native call path to only SW_SHOWNOACTIVATE.
Constraint: QA panel must not steal foreground focus when opening
Rejected: Keep plain window.show() | violates non-activation contract and risks wrong selection source
Confidence: medium
Scope-risk: narrow
Directive: If Windows QA show logic changes again, verify both close-button clickability and front-app selection capture
Tested: cargo check -q (openless-all/app/src-tauri)
Not-tested: Manual Windows interaction on physical device
Address review feedback by removing fragile runtime import of the autostart
plugin package and invoking plugin commands via Tauri core bridge, so production
bundles no longer depend on unresolved bare imports.
Also update preset batch apply to re-enable existing disabled terms and continue
processing remaining terms/presets when one add operation fails.
Constraint: Minimal changes limited to existing Settings/Vocab flows
Rejected: New backend batch endpoint for preset apply | broader cross-layer change
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Preset apply should be idempotent and restore disabled matching terms
Tested: npm run build
Not-tested: Manual autostart toggle check on packaged desktop build
Preset edits were applied to local UI state before persistence finished, so
storage failures could show a misleading saved state and then lose changes on
reload. Also, batch apply aborted on first add_vocab error and skipped later
selected presets.
Make preset save await persistence before mutating UI/closing editor, and make
batch apply continue across per-term failures while reporting aggregated errors.
Constraint: Minimal behavior fix within existing Vocab page flow
Rejected: Add transactional backend endpoint for batch apply | larger cross-layer change
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep UI success states aligned with durable persistence completion
Tested: npm run build
Not-tested: Manual readonly-disk simulation for persist failure
Saving presets previously wrote the merged list (including built-ins) into user
storage, which froze old builtin copies and shadowed future app updates with the
same preset ids. Switch preset persistence to a delta store: custom presets,
overrides for edited built-ins, and disabled builtin ids.
Constraint: Keep current preset UI and command names unchanged
Rejected: Force builtin presets to always overwrite user file | would discard intentional user edits
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Never persist untouched builtin presets into user store
Tested: cargo test vocab_presets_roundtrip_json_file -q; npm run build
Not-tested: Manual end-to-end UX for builtin delete/restore (no delete UI yet)
Loading presets from app-data JSON previously replaced bundled defaults entirely,
so adding any custom preset made shipped scenarios disappear on the next load.
Merge user presets onto bundled defaults by id so built-ins always remain
available while still allowing user overrides.
Constraint: Minimal change in preset loading path without altering storage schema
Rejected: Persist defaults into user file on first run | unnecessary write churn and migration complexity
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Preserve default+user merge behavior when adding future preset sources
Tested: npm run build
Not-tested: Manual Tauri UI check for merged list ordering
New preset previously persisted an empty entry immediately on click, so cancel
or navigation left permanent junk rows without any delete path. Keep new preset
as in-memory draft and persist only after explicit save.
Also add a regression test for vocab preset JSON persistence roundtrip to cover
the new preset storage workflow with automated verification.
Constraint: Minimal UI change without introducing preset delete UX in this patch
Rejected: Auto-delete blank presets on load | implicit cleanup can remove user-intended drafts
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep create flow non-persistent until user confirms save
Tested: cargo test vocab_presets_roundtrip_json_file -- --nocapture; npm run build
Not-tested: Manual UX walkthrough for preset draft cancel across app restart
The frontend build failed because the autostart plugin import could not be
resolved in the browser-targeted Vite bundle, and strict TS also flagged
implicit any in the same flow. Switch the autostart integration to runtime
lazy-loading behind the Tauri guard and type the callback parameters explicitly.
Constraint: Keep autostart UX behavior unchanged on Tauri runtime
Rejected: Add Vite external alias for plugin package | hides integration mismatch in app code
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Tauri-only plugin modules should stay runtime-gated to keep web build healthy
Tested: npm run build (openless-all/app) passed
Not-tested: End-to-end autostart toggle interaction on all desktop platforms
Issue #224 requires scenario presets with multi-select apply and editable custom
presets, while preparing for import/export. This revision moves built-in presets
into a repository JSON file and stores user presets in the app data directory as
JSON via Tauri commands, so preset persistence uses one JSON shape both in repo
and user space.
Constraint: Keep vocabulary entry backend and UI interaction model unchanged
Rejected: Keep presets in localStorage/TS constants | does not satisfy unified JSON storage requirement
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Preserve vocab-presets JSON schema compatibility for future import/export
Tested: cargo check (src-tauri) passed
Not-tested: npm build currently fails on pre-existing Settings.tsx typing issues unrelated to this change
Intel and Apple Silicon macOS jobs produced the same updater bundle filename,
which let one matrix job overwrite the other release asset and could point
both architecture manifests to a single binary. Rename macOS updater bundles
per architecture before release upload and prefer arch-specific artifact lookup
when generating updater manifests.
Constraint: Keep existing updater manifest format and release action usage
Rejected: Split macOS uploads into separate release jobs | larger workflow churn
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Any new macOS updater lane must keep unique asset names to preserve manifest correctness
Tested: release-tauri.yml YAML parse; diff audit for rename+manifest path alignment
Not-tested: End-to-end tagged GitHub Actions release run
The release workflow only produced Apple Silicon macOS packages, so Intel
users had no official GitHub Release artifact. Extend the matrix with an
x86_64 macOS lane and align artifact naming and updater-manifest paths
with per-arch outputs.
Constraint: Keep existing release pipeline shape and signing flow unchanged
Rejected: Split Intel build into separate workflow file | higher maintenance and duplicated logic
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep macOS conditions architecture-agnostic so new mac runners are not skipped accidentally
Tested: YAML syntax parse via python yaml.safe_load; workflow diff reviewed for mac gating/artifact paths
Not-tested: Full GitHub Actions matrix run on tag push