bc70ce36e0037495aa236fec7055ca0f41c0f884
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bc70ce36e0 |
feat(chat): render markdown tables in answers, viewer, and thinking
GFM pipe tables in the shared renderer (TODO.md L6): a table-protection
pass in frontend/assets/markdown.js (fences -> tables -> escape order)
pulls each header+separator+body block out as a placeholder, renders
cells escape-first with the same inline transforms, and reinserts a
semantic <table class="md-table"> inside a horizontal-overflow
.md-table-wrap — so a pipe table in a chat answer, the document
viewer/modal, and the thinking block all render the same semantic
table. Fences win over tables; lone pipes stay text.
- styles.css: .md-table palette rules (PLAN §7.2 tokens, no motion);
min-width: max-content so a WIDE table keeps its natural width and
the wrapper is the real scroller (width:100% alone wrapped the wide
table's cells — proven by the new E2E).
- mock_llm.py: TABLE_TRIGGER ("show me a table") -> byte-stable
TABLE_ANSWER (3-column table, <img onerror> XSS probe line, wide
5-column table), checked before DEFLECT_MODE like SUMMARY_MODE.
- tests/fixtures/docs/homelab/tables.md: 3x3 pipe table + pipe-heavy
fenced block (viewer/fence subject); the shared fixture set grows
8 -> 9 docs, so every suite pinning the count (added/formats/
stat-docs/EXPECTED_ROWS) is updated accordingly.
- tests/e2e/test_markdown_tables.py (new, story suite): chat table
shape + non-deflection, wide-table wrapper scroll (no page
overflow), XSS probe inert, viewer modal table, fence-not-a-table,
lone pipe stays text.
- tests/e2e/test_agent_document_tools.py: fix a pre-existing flake —
the "Calling tool…" label window is ~0.4 s at the mock's 0.1 s
tool-frame pacing, and a polling expect could stride over it
(failed 3 of 5 runs on the committed baseline). The pre-submit
MutationObserver record is the deterministic source of truth; the
racy to_have_text gate is gone.
uv run pytest: 738 passed, app/ coverage 99% (TOTAL unchanged);
ruff + pyright clean; story E2E 6/6 in isolation; regression E2E
suites (chat_rag, document_viewer, document_summaries, smoke) green.
|
||
|
|
27b7cb96d5 |
feat(chat): thinking window scrolls again, follows the tail only while pinned
TODO.md L7: "Add scrolling back to the thinking block, but have it autoscroll while thinking content is generating." Owner direction 2026-08-27 (roadmap A2) reverses the phase-21 no-scroll choice (2026-08-24): details.thinking .thinking-text is user-scrollable again (overflow-y: auto — the 320px clip stays, owner-confirmed), and the phase-17 per-chunk bottom-pin is GATED: the window follows the live tail only while the user is pinned near its bottom (THINKING_NEAR_ BOTTOM_PX = 32); scrolling up pauses the follow, returning to the bottom re-arms it on the next chunk (the gate re-runs on every frame). - frontend/assets/styles.css: .thinking-text overflow-y: hidden -> auto; the phase-21 owner-choice comment is replaced with the 2026-08-27 direction; max-height: 320px and every other declaration in the rule byte-identical. - frontend/assets/app.js: export const THINKING_NEAR_BOTTOM_PX = 32 + isThinkingNearBottom(textEl) (scrollHeight - scrollTop - clientHeight <= band); the thinking-handler pin becomes `if (block.open && isThinkingNearBottom(textEl))` — a scrolled-up reader is never re-pinned and a closed (restored) block is never pinned; everything else in the handler (and phase 42's no page scroll) untouched. - tests/unit/test_thinking_scroll.py (new, replaces the deleted tests/unit/test_thinking_no_scroll.py): pins the CSS contract (auto + 320px + owner-direction comment, no hidden/scroll left), the exported 32px band, the gate math, the gated pin (no unconditional `if (block.open)` remains), and the surviving collapsed-restore pin. - tests/e2e/test_thinking_scroll.py (new, mock-only, replaces the deleted tests/e2e/test_thinking_no_scroll.py — its pins asserted the reversed phase-21 behavior, so both phase-21 files are deleted in this commit): user scroll restored on the frozen 4s-hesitation tail (wheel is 1:1; click+Home keyboard — the plain div is not keyboard-focusable by design, tabindex is test scaffolding; the literal drag holds the user's position — headless Chromium's overlay scrollbars are not grabbable by synthetic mouse events, documented in the suite), follow-while-pinned at the 2nd-to-last and last chunk (±1px) with the last chunk's text inside the visible rectangle, no re-pin over ≥5 mid-stream chunks after a half-window scroll-up, re-pin on the next chunk after returning to the bottom, the CSS contract, plus the phase-11 (long answer: page scrolls, bubble overflow untouched) and phase-17 (restored collapsed block with full text) regressions. - tests/unit/test_chat_persistence.py: the CSS pin flips with the contract (auto in, hidden out — owner direction 2026-08-27). - tests/unit/test_frontend_scroll.py: the "page-level band constant is gone" pin now excludes the phase-43 window-level THINKING_NEAR_BOTTOM_PX (a different band — the window's, not the page's). E2E (isolation): test_thinking_scroll 7/7 (twice); regressions test_thinking_display 5/5, test_chat_persistence 4/4, test_no_reply_autoscroll 5/5, test_smoke 3/3; unit+integration 725 passed, app/ coverage 99% (unchanged — frontend-only phase); ruff + pyright clean. |