Skip to content

feat(viewer): Layer 1 viewer features #12–#16 (live update, TOC toggle, explorer, diff, TUI) - #43

Merged
takeshiD merged 20 commits into
mainfrom
layer1-viewer-features-12-16
Jul 5, 2026
Merged

takeshiD merged 20 commits into
mainfrom
layer1-viewer-features-12-16

Conversation

@takeshiD

@takeshiD takeshiD commented Jul 5, 2026

Copy link
Copy Markdown
Owner

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-viewer branch (PR #41) still sits on the old mdpeek-core crate layout. This branch is the #19 viewer work rebased onto the current fine-grained crates (mdpeek-gfm/parser/render-html/render-term/server/watcher) with origin/main merged in, plus #12–#16. Opened as a new branch rather than force-pushing #41.

Features

#16 — In-place live update + changed-block highlight

  • Server broadcasts {type:"update", html, frontmatter} (shared render_markdown) instead of "reload".
  • Client diffs new server HTML against a clean snapshot (front/back trim), splices only changed top-level blocks into the live article, re-runs highlight/mermaid/MathJax on new nodes, rebuilds outline + front-matter panels, and flashes .mdpeek-changed. Scroll position preserved; auto-scroll-to-change toggle (localStorage). Old "reload" still honored for compat.

#13 — User-controlled TOC visibility toggle

  • Toolbar button shows/hides the outline regardless of width; mdpeek-toc-visible persisted; 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 --porcelain discovery (CWD-scan fallback), walkdir markdown scan (skips .git/nested worktrees), path-safety resolve_within, initial_active.
  • GET /api/tree, POST /api/select {path}; AppState carries roots + scan root + re-watch channel.
  • New controllable watch_channel()/WatchHandle in mdpeek-watcher for runtime re-pointing; serve enters explorer mode when the default file is absent.
  • Client sidebar with worktree/branch grouping toggle + show/hide button (persisted); click switches file over the existing WS.

#15 — Two-file diff view

  • POST /api/diff {a,b} (root-validated) returns a similar-based line-diff fragment; watch target generalizes to Single | Pair so both files are watched and re-diffed live ({type:"diff-update"}).
  • Client compare (⇄) buttons pick two files (e.g. same path across worktrees) → full-screen diff overlay, live-refreshed, restores previous file on close.

#12 — Interactive live-update TUI (mdpeek term -w on a TTY)

  • ratatui + crossterm alternate-screen viewer; re-renders in place without flicker, preserves scroll (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

🤖 Generated with Claude Code

takeshiD and others added 9 commits July 5, 2026 08:49
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
takeshiD marked this pull request as ready for review July 5, 2026 00:43
takeshiD and others added 11 commits July 5, 2026 10:05
…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
@takeshiD
takeshiD merged commit 49ea763 into main Jul 5, 2026
4 checks passed
@takeshiD
takeshiD deleted the layer1-viewer-features-12-16 branch July 5, 2026 03:49
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