feat(viewer): Layer 1 viewer features #12–#16 (live update, TOC toggle, explorer, diff, TUI) - #43
Merged
Merged
Conversation
Layer 1 viewer enhancements built on the SourceRange parser. - Outline panel: replace the flat TOC with a hierarchical tree nested by heading level, plus scroll-spy that highlights the current section. - Fuzzy search: a filter box in the outline that subsequence-matches headings and keeps the ancestors of each hit visible. - Front matter panel: mdpeek-core gains BlockTree::frontmatter(); the server surfaces the leading YAML/+++ block (otherwise hidden by the renderer) into a collapsible panel. Escaped via escape_html_min so arbitrary front matter can't break out of its element. Backlinks (the remaining #19 bullet) are deferred: true backlinks need the multi-file / repo context from #14, so they land with that work. core: +2 frontmatter tests; server: +2 escape tests. All 104 unit + integration tests pass; clippy clean; JS syntax-checked; front matter injection and asset serving verified against a running server. Refs #19. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PuQCZQdopu8Xw9S7FzkcFQ
Replace the clear+reprint watch loop with a ratatui/crossterm full-screen viewer when `term -w` runs on a TTY. Editing the file re-renders in place without flicker, scroll position is preserved (clamped to new line count), and `q`/Ctrl-C quits with robust terminal restoration (RAII guard + panic hook). Non-TTY / piped stdout keeps the existing clear+reprint fallback. - Reuse render_term output, converted to ratatui Text via ansi-to-tui. - Add non-blocking watch_events() to mdpeek-watcher (background thread + mpsc Receiver) alongside the unchanged notify_on_change. - Scroll keys: j/k, g/G, PgUp/PgDn, arrows. - New src/tui.rs with a testable clamp_scroll() helper + unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
…r 1 #16) Replace the full-page reload on file change with a block-diff update so the browser patches only what changed and keeps its scroll position — the flow the "agent writes markdown live" use case needs. - server: `serve` now shares `AppState` (file_path/theme behind RwLock) with the watch callback; on change it re-renders via a new shared `render_markdown()` and broadcasts `{type:"update", html, frontmatter}` (serde_json) instead of the `"reload"` string. `run_server` takes the prebuilt state. - client (vanilla JS, per Layer 1 scope; full Preact is Layer 3 #25): diff the new server HTML against a clean pre-highlight snapshot (front/back trim), splice only changed top-level blocks into the live article, re-run highlight/mermaid/MathJax on the new nodes, rebuild outline + front matter panels (collapse state preserved), and flash `.mdpeek-changed` (~1.6s fade). - auto-scroll-to-first-change toggle in a new floating toolbar (localStorage `mdpeek-autoscroll`); the old `"reload"` message still triggers a full reload for backwards compatibility. - css: toolbar + `.mdpeek-changed` flash keyframes for both themes. +2 server tests (render_markdown). Build + clippy clean; JS syntax-checked; verified end-to-end against a running server: editing the file pushes a JSON `update` frame with the re-rendered HTML. Refs #16. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
Add a toolbar button to show/hide the outline regardless of viewport width, persisted in localStorage so the choice survives reloads and live updates (#16). - New `#mdpeek-toc-toggle` button in the toolbar; hidden unless an outline exists (headings >= 2), mirroring the existing outline gate. - `mdpeek-toc-visible` localStorage key: "visible"/"hidden" override the width-based default; unset keeps the responsive `@media (max-width:1280px)` behavior. Body classes `mdpeek-toc-user-visible/hidden` drive higher-specificity CSS overrides in both themes. - State re-applied after each live update (refreshTocState in applyUpdate) and on window resize; the existing list-collapse toggle is untouched. Verified in a jsdom harness: toggling stores the preference, sets the body class, and updates the button's pressed state; the button stays hidden when no outline is present. JS syntax-checked; assets served correctly. Refs #13. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
…#14) `mdpeek serve` now discovers the enclosing git repository and its linked worktrees and exposes a file-explorer sidebar, so you can browse and switch markdown across worktrees/branches without restarting. - new `mdpeek-server::explorer`: `git rev-parse --show-toplevel` + `git worktree list --porcelain` discovery (graceful CWD-scan fallback outside a repo), walkdir markdown scan (skips `.git` and nested worktree roots), path-safety `resolve_within` (canonicalize + must be a markdown file under a discovered root), and `initial_active` file selection. - server: `GET /api/tree` (grouped worktree/branch → files + active file) and `POST /api/select {path}` (validated switch of the active/watched file). `AppState` carries the canonical roots, scan root, and a re-watch channel. - watcher: new controllable `watch_channel()` / `WatchHandle` (watch / unwatch / watch_recursive) so `serve` can re-point at the selected file at runtime; the watch loop coalesces changes and broadcasts #16 updates. `serve` no longer bails when the default file is absent — it enters explorer mode. - client: sidebar built from `/api/tree` with a worktree/branch grouping toggle and a toolbar show/hide button (both persisted); clicking a file POSTs `/api/select` and the server pushes the new content over the existing WS. - css: sidebar styling for both themes. +3 explorer tests. Build + clippy clean; JS syntax-checked. Verified end-to-end against a temp repo with a linked worktree: `/api/tree` lists both worktrees with branches and files; `/api/select` switches the file and pushes an update; paths outside the roots and non-markdown files are rejected with 403. Refs #14. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
…llow-up) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
Compare two markdown files — notably the same path across worktrees/branches (#14) — with a live source-level diff. - server: `POST /api/diff {a,b}` validates both paths against the discovered roots (#14 guard) and returns a rendered diff HTML fragment. The watch target generalizes to `Single` | `Pair`: in pair mode the watcher follows both files and re-broadcasts a `{type:"diff-update", html}` on any change; selecting a file (`/api/select`) returns to single-file mode. - `render_diff` uses `similar::TextDiff::from_lines` for a phase-1 line diff, emitting an add/del/context-classed table with a header naming both files. - client: each sidebar file gets a compare (⇄) button — pick two files to open a full-screen diff overlay; it refreshes live from `diff-update` messages and restores the previous single file on close. - css: diff view, compare button, and add/del/context row styling for both themes. +1 server test (render_diff). Build + clippy clean; JS syntax-checked. Verified end-to-end: diffing the same file across two worktrees returns add/del rows; editing either file pushes a refreshed diff over the WS; a path outside the roots is rejected with 403. Refs #15. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
takeshiD
marked this pull request as ready for review
July 5, 2026 00:43
…ff; fix TOC overlap UI refinements on the Layer 1 viewer (#13/#14/#15): - TOC: move the outline panel left of the toolbar (right: 62px, top: 16px) so it no longer overlaps the stacked toolbar buttons (Toggle contents etc.). - Breadcrumb (#14): new top bar showing which worktree/branch the open file belongs to, e.g. "layer1-viewer › README.md" — worktree name in worktree grouping, branch name in branch grouping, with a matching folder/git-branch icon. Updates on file switch and grouping change; shifts right when the sidebar is open. - Grouping toggle (#14): relabel "by worktree"/"by branch" → "worktree"/"branch" with folder / git-branch icons. - Diff (#15): add a rendered-HTML diff mode (block-level diff of the rendered markdown) alongside the source line diff, and a unified/split (two-column) layout toggle — both switchable from the diff bar and preserved for live re-diffs. Server `POST /api/diff` gains `mode` (source|rendered) and `layout` (unified|split); `render_diff` dispatches to four renderers with block splitting and aligned two-column pairing. - css: breadcrumb, diff-bar controls, split-table, rendered-diff, and generic add/del backgrounds for both themes. Server render_diff test extended to cover rendered+split. Build + clippy clean; 134 tests pass; JS syntax-checked. Verified end-to-end: all four diff mode/layout combinations return the expected markup, and a jsdom harness confirms the breadcrumb and grouping toggle render the right label + icon in both worktree and branch views. Refs #13, #14, #15. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
…/search Address TUI feedback on the interactive viewer (#12): 1. Wrap long lines to the terminal width (Paragraph `Wrap { trim: false }`); scroll bookkeeping now works in wrapped-row units. 2. `term` watches (opens the TUI) by default on a TTY. `--no-watch` forces a one-shot render; `-w` still forces watching (e.g. over a pipe). Piped/non-TTY stdout stays one-shot, so `mdpeek term FILE | less` doesn't loop. 3. `?` toggles a keybindings help overlay (centered popup). 4. Ctrl-d / Ctrl-u scroll half a page. 5. `/` opens a vim-style search: matches are highlighted (spans split at match boundaries, highlight style overlaid), `n`/`N` jump to next/previous match (current match gets a distinct colour), Esc clears the search. A bottom status line shows the query + match count, or a key hint. cli: new `--no-watch`; `term_watch` defaults to `stdout().is_terminal()`. tui: reworked into an `App` state with search/help modes; pure helpers (`wrapped_height`, `wrapped_row_of`, `find_matches`, `highlight_line`) unit tested (+5 tests). Build + clippy clean; 139 tests pass. Verified the non-TTY one-shot path (plain `term` and `--no-watch`) does not hang; the interactive TTY path can't be driven headlessly. Refs #12. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
Wrap the viewer content and status line in a Block with horizontal padding (2 cols) and a top padding (1 row) so text no longer touches the terminal edges. The wrap width and scroll/viewport math use the padded inner size, and the search cursor accounts for the status-line left padding. Build + clippy clean; 139 tests pass; fmt applied. Refs #12. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
Replace the two flip-on-click diff toggles with clearer segmented controls: a pill per setting showing both options side by side with the active one highlighted (shadcn ToggleGroup look — muted track, raised active item), each option labelled with an icon (code/eye for source/rendered, rows/columns for unified/split). Controls rebuild from diffState so they stay in sync after toggles and live re-diffs. Verified in a jsdom harness: opening a diff renders two segments (Source/ Rendered, Unified/Split) with the correct defaults active, and clicking a segment switches only that setting and refreshes the active state. JS syntax-checked; assets build. Refs #15. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
In split (two-column) layout, every left cell got `mdpeek-diff-del` and every right cell `mdpeek-diff-add`, so unchanged (Equal) context rows were painted red/green — the whole diff looked changed. Track a `changed` flag per split row (false for Equal) and colour only add/delete rows; context rows are neutral. +1 regression assertion: a shared line stays context and only the one changed line is coloured. Build + clippy clean; 10 server tests pass. Refs #15. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
The README still described a one-shot "detect README.md" tool. Bring it in line with what mdpeek actually does now: - Features: in-place live updates with change highlighting, repo/worktree explorer, two-file diff (source/rendered, unified/split), interactive TUI. - Quick Start (browser): no-arg run discovers the repo's markdown and opens the explorer sidebar; describe grouping, compare/diff, outline+fuzzy search, breadcrumb, and in-place updates (not full reload). - Quick Start (terminal): `term` opens the interactive TUI and watches by default on a TTY; `--no-watch` for one-shot; added a keybindings table (scroll, half-page, g/G, / search + n/N, ? help). - Status: new "Viewer" section listing the above capabilities. Installation "Planned" entries and the GLFM section are left as-is (roadmap). Rendered via `mdpeek term` to sanity-check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
…owns (#14/#15/#16) Diff view (#15/#16): - Show which side is which: the diff bar now labels A (left/deletions) and B (right/additions) with their worktree/branch + relative path, coloured to match the diff. The server no longer draws the header (the client knows the worktree/branch of each path). - Add a ⇄ swap button to exchange the left/right files. - Live re-diff now flashes the rows/blocks that changed (front/back trim of the diff units against the previous render), like #16 does for normal updates. Explorer (#14): - Sidebar toggle moved out of the toolbar: it sits at the body's top-left when collapsed and inside the sidebar's top-right when open. - The active file is highlighted more prominently in the sidebar (accent colour + left accent border); active state is tracked by path so breadcrumb-driven selection also marks it. - The top breadcrumb is now interactive: clicking the worktree/branch segment opens a dropdown of the other worktrees/branches (switching keeps the same relative file when it exists), and clicking the file segment opens a dropdown of that group's files. Verified in a jsdom harness (breadcrumb dropdowns switch group keeping the file, file menu lists the group's files, sidebar active marked by path) and against a running server (assets served, standalone toggle). Build + clippy clean; 139 tests pass. Refs #14, #15, #16. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
…lacement - Sidebar active file: keep only the background + text color change; drop the left accent border and its padding shift. - Sidebar toggle: remove the slide animation. - Sidebar header: drop the "Files" label and put the worktree/branch grouping button in its place (top-left), with right padding so it no longer collides with the sidebar toggle floating at the top-right. JS syntax-checked; jsdom confirms the header now contains only the grouping button (no "Files"); build clean. Refs #14. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
…14) Make the sidebar a flex column: a fixed header row (grouping button + the floating toggle) with no scrollbar, above a `#mdpeek-sidebar-content` file list that scrolls on its own. Previously the whole sidebar scrolled, so its vertical scrollbar ran behind the toggle at the top-right. JS wraps the groups in the scrolling content div; CSS moves overflow from the sidebar to the content row (sidebar is now `overflow: hidden`, opens as flex). jsdom confirms header = grouping button only, content = the file groups. Build + JS clean. Refs #14. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
Split Features into "Available now" and "Planned (roadmap)" and list the upcoming layers: semantic view (document model/doctype + side panel), generated UI per document type (rules → Claude), non-dev document types with domain widgets and spoiler control, repository-aware checks, and editor/tooling integration (TUI IR renderer, Neovim, GitHub preview). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
…#14) - Collapsed sidebar toggle now blends into the page (transparent background, no border/shadow); a subtle hover background remains for affordance. - Open/close is animated: the sidebar slides in/out (translateX + 0.2s transition) and the body's content padding and the breadcrumb position ease with it. The toggle itself still jumps (no position animation, as requested). CSS only, both themes; build clean. Refs #14. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWi1CJ9UiwBfu6FMYWBr2E
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the remaining Layer 1 viewer issues #12–#16 on top of the workspace merge (fine-grained crate split from #40 now on
main).Base note
The remote
worktree-layer1-viewerbranch (PR #41) still sits on the oldmdpeek-corecrate layout. This branch is the #19 viewer work rebased onto the current fine-grained crates (mdpeek-gfm/parser/render-html/render-term/server/watcher) withorigin/mainmerged in, plus #12–#16. Opened as a new branch rather than force-pushing #41.Features
#16 — In-place live update + changed-block highlight
{type:"update", html, frontmatter}(sharedrender_markdown) instead of"reload"..mdpeek-changed. Scroll position preserved; auto-scroll-to-change toggle (localStorage). Old"reload"still honored for compat.#13 — User-controlled TOC visibility toggle
mdpeek-toc-visiblepersisted; body-class overrides beat the width media query. State re-applied after live updates and resize. List-collapse untouched.#14 — Repository + worktree explorer sidebar
mdpeek-server::explorer:git rev-parse --show-toplevel+git worktree list --porcelaindiscovery (CWD-scan fallback), walkdir markdown scan (skips.git/nested worktrees), path-safetyresolve_within,initial_active.GET /api/tree,POST /api/select {path};AppStatecarries roots + scan root + re-watch channel.watch_channel()/WatchHandlein mdpeek-watcher for runtime re-pointing;serveenters explorer mode when the default file is absent.#15 — Two-file diff view
POST /api/diff {a,b}(root-validated) returns asimilar-based line-diff fragment; watch target generalizes toSingle | Pairso both files are watched and re-diffed live ({type:"diff-update"}).#12 — Interactive live-update TUI (
mdpeek term -won a TTY)clamp_scroll),q/j/k/g/G/PgUp/PgDn. RAII terminal-restore guard + panic hook. Non-TTY/piped stdout keeps the clear+reprint fallback.Verification
cargo build --workspace,cargo clippy --workspace --all-targetsclean; 134 tests pass. (cargo fmt --all --checkreports only two pre-existing diffs in files this PR does not touch —highlight.rs,term_output.rs— present onmainas well.)updateframes on edit ([Feature] ライブ更新箇所のハイライト(Agent 執筆向け差分更新) #16), TOC toggle persistence via jsdom ([Feature] WebUI の目次(TOC)をトグルボタンで開閉可能にする #13/[Feature] ライブ更新箇所のハイライト(Agent 執筆向け差分更新) #16 diff patching),/api/treelists worktrees+branches and/api/selectswitches with 403 on traversal/non-markdown ([Feature] リポジトリ + worktree 横断の markdown エクスプローラーサイドバー #14),/api/diffadd/del rows + cross-worktree compare + live re-diff + 403 guard ([Feature] 2つの markdown の差分表示 #15). TUI verified via non-TTY fallback +clamp_scrollunit tests (interactive TTY not drivable headless).🤖 Generated with Claude Code