Introduce _safeSmdRenderer — a renderer wrapper that hooks smd's set_attr
to validate URL schemes inline as each DOM node is created, eliminating
the per-token _sanitizeSmdLinks(assistantBody) full-DOM scan that caused
progressive freezing on answers of 30,000+ chars (5,000+ DOM nodes).
Also add parse-result caching to _scheduleRender to avoid redundant
_extractInlineThinkingFromContent scans between the token event handler
and the rAF render callback.
Changes:
- New _safeSmdRenderer(el): URL scheme safety inline via set_attr hook
(identical algorithm to proven _streamFadeRenderer.set_attr, no animation)
- _smdNewParser: non-fade path uses _safeSmdRenderer instead of bare
default_renderer
- _smdWrite: removed _sanitizeSmdLinks(assistantBody) call — safety now
handled by renderer's set_attr at node-creation time
- _scheduleRender(parsed): accepts optional cached parse result from
token handler to avoid duplicate O(n) text scans
- Token handler: passes _parseStreamState() result to _scheduleRender
_sanitizeSmdLinks is preserved at _smdEndParser (stream-end safety net)
and in fallback paths where smd is not available.
All 166 existing tests pass.
test_smooth_text_fade.py::test_preferences_ui_exposes_and_saves_fade_text_effect
extracts loadSettingsPanel() via a brace-matcher that does NOT skip comments. The
new structured-code wiring added a comment containing 'isn't' + ''auto' mode' —
the apostrophes were mis-read as string delimiters, scrambling the brace depth so
the matcher closed the function early (before the fade-checkbox wiring), failing
the test. Reworded to 'is not' / 'auto mode' (zero behavior change; node -c clean).
The fade feature + the structured-code feature are both unchanged. Co-authored-by: metaember
#4748 added a _recordSessionProfileCount() call inside _applySessionListPayload();
the pre-existing #4766 test isolates that function in node and must stub the new
helper (same pattern as the other stubbed helpers) or it ReferenceErrors. Not a
weakened assertion — the test still verifies old+new payload tab-count coverage.
The JSON/YAML tree viewer (#484) hardcoded its default: valid json/yaml
fenced blocks opened in Tree view at 10+ lines (`const showTree=lineCount>=10;`).
Make that default user-configurable while preserving current behavior.
- New settings (persisted via /api/settings -> settings.json):
- structured_code_default_view: auto | on | off (default auto)
- structured_code_auto_tree_lines: int 1..1000 (default 10)
- ui.js: replace the hardcode with a sanitized, unit-testable decision
helper _structuredCodeShowTree(mode, threshold, lineCount); Tree/Raw
toggle, Raw <pre><code>, parse-failure fallback, and YAML lazy-load
are unchanged.
- Appearance settings: 'JSON/YAML code blocks' select + 'Auto threshold
lines' input (disabled outside auto mode); wired through the existing
appearance-autosave and explicit-save paths; live re-render on change.
- boot.js seeds window globals; i18n labels added to all 13 locales.
- Default (auto + 10) reproduces the original >=10 behavior exactly.
#4748 added lines inside _applySessionListPayload() that pushed the
'_sessionListSkeletonActive = false;' assertion past the test's fixed 4000-char
window. Anchor the slice on the next function def (_mergeRenderSessionListOptions)
instead — robust to future additions. Assertion unchanged.
#4741 added redact_enabled= to _sidebar_session_response_item's call site
(routes.py:2199); the pre-existing #4766 test mocked it with a bare 'lambda row: row'
that rejects the kwarg -> full-suite TypeError. Mock now accepts the keyword-only
arg. Not a weakened assertion — the test still asserts bucket-count omission.
- env-change-with-unchanged-mtime must re-expand (proves the cache stores raw
un-expanded YAML, not the expanded result -> no stale cross-profile expansion)
- no-cross-serve between two profile config paths (proves the parse cache is
path-keyed). Both pass on the current impl; locks down the highest-risk
cross-profile credential-bleed invariant per Opus's non-blocking note.
Opus caught a narrow timing race: if a reader re-pins between the scroll-rebuild
guard engaging and its deferred rAF firing, the stale re-restore would yank them
back off the bottom. Gate the re-restore on _messageUserUnpinned at rAF time; the
minHeight release still always runs. Normal single-render unpinned case unaffected.
Codex regression gate caught: the guard predicate keyed on a raw messagesEl.scrollTop>0,
so a pinned reader transiently at bottomDistance>250 mid-render (assistant body grows
before the anchor scene re-renders) got flipped to _messageUserUnpinned=true/_scrollPinned=false
and lost auto-follow. Require an explicit non-follow signal (_messageUserUnpinned || _scrollPinned===false)
instead. Updated the existing guard test to a genuine unpinned reader + added a pinned-large-growth
regression. Co-authored-by: franksong2702