Skip to content

feat(layer3): Generative UI foundation (IR + generator + cache + web) - #39

Open
takeshiD wants to merge 20 commits into
mainfrom
worktree-layer3-generative-ui
Open

takeshiD wants to merge 20 commits into
mainfrom
worktree-layer3-generative-ui

Conversation

@takeshiD

@takeshiD takeshiD commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Layer 3 — Generated UI

Implements the Layer 3 core from AGENTS.md §10, as additive modules on the existing single-binary crate. To avoid interfering with the in-flight Layer 1 (parser) and Layer 2 (analyzer/model) worktrees, this PR does not restructure into a workspace (論点 B) and does not touch those areas.

Rust core

  • src/ir/ — UiNode (12 core + 6 domain kinds), SourceRange + LineIndex, registry allowlist, and validate (schema + allowlist + sourceRange bounds + low-confidence flagging). This is the security boundary (§3.5/§8): an LLM cannot emit a component outside the registry or a fabricated range.
  • src/generator/ — Generator trait + lightweight GenInput (stand-in for Layer 2's DocumentModel); deterministic offline RulesGenerator (task lists→Checklist, tables→DataTable, mermaid→Diagram, json/yaml/toml/env→ConfigViewer, GFM alerts→Callout); ClaudeGenerator scaffold behind feature = "llm" with offline fallback.
  • src/cache/ — content-hash key (markdown + generator + schema version) + .cache/mdpeek/*.gui.json store.
  • src/gui.rs — generate→validate→cache facade. New CLI: mdpeek gen <file> (emits validated IR JSON; --no-cache).

Web (Preact)

  • 2-layer component registry + Render dispatcher, components for all 18 node kinds, 3-pane layout (Outline / Content / Generated UI) with SourceRangeLink jump, hand-maintained ir.ts mirror.

Verification

  • cargo test — 18 Layer-3 unit tests + tests/gen_output.rs integration, all pass.
  • cargo clippy clean on default and --features llm.
  • cd web && npm run build (tsc --noEmit + vite) builds clean (~28 KB JS).
  • mdpeek gen SAMPLE.md produces Checklist / DataTable / Callout×5 / Diagram with valid sourceRanges.

Deferred (documented in docs/layer3.md)

Integration points that touch shared files or depend on other worktrees: server /api/gui route + island mount + web/dist embed (論点 A/C), Layer 2 DocumentModel/planner adapter, ts-rs auto-gen, and #16 live diff.

🤖 Generated with Claude Code

Implements the Layer 3 core from AGENTS.md §10 as additive modules on the
existing crate, without workspace restructuring or touching parser/analyzer/
model (owned by the in-flight Layer 1/2 worktrees).

Rust core:
- ir/: UiNode (12 core + 6 domain), SourceRange+LineIndex, registry allowlist,
  validate (schema + allowlist + sourceRange bounds + low-confidence flag)
- generator/: Generator trait + GenInput, deterministic RulesGenerator
  (task lists→Checklist, tables→DataTable, mermaid→Diagram, config→ConfigViewer,
  GFM alerts→Callout), ClaudeGenerator scaffold behind feature="llm"
  (offline fallback to rules)
- cache/: content-hash key (markdown + generator + schema version) + store
- gui.rs: generate→validate→cache facade; `mdpeek gen <file>` CLI subcommand

Web (Preact):
- 2-layer component registry + Render dispatcher, all 18 node kinds,
  3-pane layout (Outline/Content/Generated UI) with SourceRangeLink jump,
  hand-maintained ir.ts mirror. tsc + vite build clean.

Tests: 18 unit + tests/gen_output.rs integration; clippy clean (default + llm).
Deferred integration points (server /api/gui, ts-rs, #16 diff) documented in
docs/layer3.md to avoid worktree interference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
@takeshiD
takeshiD marked this pull request as ready for review July 5, 2026 00:07
takeshiD and others added 12 commits July 5, 2026 14:02
…ative-ui

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/main.rs
… model/effort

Adds selectable LLM backends to the Layer 3 generator, configurable via
[llm] in config.toml or `mdpeek gen` CLI flags:

- provider: "claude_code" (shells `claude` CLI) and "codex" (shells `codex`
  CLI) work in the default build (std::process, no extra crates); "anthropic_api"
  (direct HTTP) stays behind feature="llm". Default provider is anthropic_api.
- model: backend-specific model id (--model / [llm] model).
- effort: low|medium|high — codex -> model_reasoning_effort; claude_code ->
  thinking keyword (think/ultrathink); anthropic advisory.

Wiring:
- src/generator/llm/: LlmProvider/Effort/LlmBackendConfig + build() factory,
  claude_code.rs, codex.rs, anthropic.rs (renamed from claude.rs); prompt gains
  extract_json_array for noisy CLI stdout. llm module no longer feature-gated.
- config.rs: [llm] gains provider/model/effort; llm_backend_config()/llm_enabled().
- cli.rs: `gen` gains --llm/--provider/--model/--effort (override config).
- gui.rs: generate_with_llm() with rules fallback on any backend failure.
- config.example.toml + docs/layer3.md updated.

Tests: config parsing for provider/model/effort, prompt extraction, and a CLI
integration test that --llm falls back to rules when the backend is unavailable.
cargo test + clippy (default and --features llm) + web build all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
The TOC sidebar (and the whole page) inherited the body font stack, which had
no Japanese font — so CJK text fell back to whatever the OS/browser substituted.
Add Hiragino Sans / Yu Gothic / Meiryo / Noto Sans CJK JP before the generic
`sans-serif` in both server themes (static/css/github-{light,dark}.css) and the
Layer 3 web island (web/src/styles.css). Latin still prefers the system UI font;
only CJK glyphs pick the new fonts.

Verified: rebuilt server serves the updated embedded CSS for both themes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
The font stack lives on `.markdown-body` (the content <article>), but the TOC,
file sidebar, toolbar and front-matter panel are siblings outside it, so they
fell back to the browser default (serif) — English and Japanese both looked
different from the content. Set the same stack on `body` (inherited by all the
chrome) and add `input,button,select,textarea { font-family: inherit }` so the
TOC search box matches too. Placed in the index.html inline style (theme-
independent), kept identical to `.markdown-body`.

Verified: served page exposes the body font rule for both themes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
…and)

Completes Layer 3's "動く Generative UI" deliverable: opening `mdpeek serve` and
clicking the ✨ toolbar button reveals a Generated UI pane rendered from the IR,
co-existing with the existing SSR content (design 論点 A).

- Extract Layer 3 core into `crates/mdpeek-gui` (ir + generator + cache +
  pipeline; design §2 mdpeek-core) so the CLI and server share one impl. Root
  binary now depends on it; `llm` feature forwards to mdpeek-gui/-server.
- Server: `GET /api/gui` generates validated IR for the active file (rules by
  default, or the configured LLM backend, on a blocking thread) and returns
  { nodes, markdown }. `serve()` gains an optional LlmBackendConfig, resolved in
  main.rs from [llm] config (enabled → backend).
- Web: new `panel.tsx` island (Generated UI pane only, not the full 3-pane
  harness) fetches /api/gui and renders via the shared registry; scoped
  `panel.css` (no global resets). Built as a stable-named IIFE and committed to
  web/dist, embedded by the server via include_bytes! (論点 C).
- Template: toolbar ✨ toggle + panel container + /static/gui asset refs.

Verified end-to-end: page ships the toggle/panel/assets, /static/gui/mdpeek-gui.
{js,css} serve 200, /api/gui returns IR. cargo test --workspace + clippy
(default and --features llm) + web build all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
The panel overlapped the ✨ toolbar toggle when open, so there was no way to
dismiss it. Add a sticky panel header with a ✕ close button that clears the
`mdpeek-gui-open` state (and resets the toggle's aria-pressed), plus an Escape
key handler. Panel padding moved into the scrollable body so the header stays
flush at the top.

Verified: rebuilt server serves the updated island JS/CSS with the close button.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
Pressing the Generated-UI toggle hits /api/gui but the terminal stayed silent,
so it was unclear what was happening. Log the start (file + backend: rules vs
LLM provider/model/effort) and the result (node count, elapsed ms, and a
kind×count breakdown), plus clearer error/panic logs.

Example:
  INFO gui: generating UI for 'SAMPLE.md' [rules (offline)]
  INFO gui: generated 8 node(s) for 'SAMPLE.md' in 0 ms [Checklist×1, DataTable×1, Callout×5, Diagram×1]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
The panel only showed static "Generating UI…" text. Add an indeterminate
spinner + an animated progress bar under the panel header while /api/gui is in
flight (single fetch = no % progress, so indeterminate). Respects
prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
Connect the Layer 3 pipeline to Layer 2's rules analyser and add a planner that
emits semantic, document-type-aware UI nodes alongside the structural ones — so
rules generation now produces kinds it previously couldn't (RiskPanel, Timeline,
review Checklists).

- mdpeek-gui depends on mdpeek-analyzer; new `planner` module maps
  `Analysis` (DocumentModel + SemanticPanel) → IR:
  - `## Risk` sections → RiskPanel
  - open questions → an unchecked Checklist
  - DesignDoc/Readme → missing-section review Checklist (§9.1)
  - Adr/Changelog/Minutes → Timeline from the outline
- pipeline: `generate`/`generate_with_llm`/`generate_json` now take
  `filename` (sharpens doctype inference); structural rules nodes + planner
  nodes are concatenated then validated; cache entry records the real doctype.
- cache key now includes the filename basename (it can change the output).
- callers (mdpeek gen, server /api/gui) pass the active file name.

Verified: `mdpeek gen docs/sample-design-doc.md` and `/api/gui` now include a
RiskPanel + review/open-question checklists; README gets a section checklist.
cargo test --workspace + clippy (default and --features llm) all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
-54)

Reframe generated UI as reading lenses (design doc §8) instead of re-listing
body content. Addresses the "not actually a reading aid" critique.

IR (mdpeek-gui/ir):
- Add Confidence enum (low/medium/high, §14) + per-item confidence.
- New lens node kinds: SemanticOutline, SummaryCards, DecisionLog, ActionItems,
  OpenQuestions; extend RiskPanel (assumptions/likelihood/mitigation) and
  Glossary (aliases/inferredDefinition). Registered in the allowlist.
- Cache SCHEMA_VERSION → 2 (invalidates old body-node entries).

Pipeline:
- #45: drop body-reprint nodes (DataTable/ConfigViewer/Diagram/Callout) from
  the lens pane — those stay in the Markdown Body (§7.2).
- #46: LLM-first — LLM backend is primary, deterministic planner is the
  fallback; every node re-validated (schema + allowlist + sourceRange). No
  ANTHROPIC_API_KEY now errors (→ falls back to planner) instead of emitting
  body nodes. Prompt rewritten to steer toward reading lenses.
- planner rewritten to emit lenses from Layer 2 analysis: SemanticOutline
  (BlockClass groups), DecisionLog (markers), ActionItems (todos), OpenQuestions,
  RiskPanel, Glossary (acronyms). #48/#50/#51/#52/#53/#54.

Web: ir.ts + components + registry for all new lenses; confidence badges;
panel.css styling; dev fixture regenerated.

Verified end-to-end: `/api/gui` on the sample design doc returns
[SemanticOutline, ActionItems, OpenQuestions, RiskPanel, Glossary] with no body
reprints. cargo test --workspace + clippy (default and --features llm) + web
build all clean.

Refs #44 #45 #46 #48 #50 #51 #52 #53 #54

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
Add planner::recommended_order / order_lenses (design §13 / §9.3): the Generated
UI pane now leads with the lenses that matter for the doctype (design doc →
SemanticOutline/DecisionLog/RiskPanel…; ADR → DecisionLog first; meeting notes →
Decisions/ActionItems/Timeline). Unlisted lenses keep their order after the
recommended ones. Applied to both the rules planner and LLM output.

Refs #47

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
- config.example.toml: default [llm] strategy → "llm_first" with clarified note
  (LLM is primary for lenses; the rules planner is the fallback).
- docs/layer3.md: describe the reading-lens model, the lens set, body/lens
  separation, Lens Selector, and per-item Confidence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
takeshiD and others added 7 commits July 5, 2026 19:13
The `[llm] effort` config was ignored by the Anthropic-API backend (advisory
only). Map it to `output_config.effort` (GA, no beta header) on the Messages
request, so low/medium/high now control reasoning depth for anthropic_api the
same way they do for codex. Effort also participates in the cache key
(anthropic-<model>-<effort>) since it changes output. A model without effort
support (e.g. Haiku 4.5) returns 400 → handled by the rules fallback.

cargo build (default + --features llm) + clippy + tests all clean.

Refs #46

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
…ut of the box)

The anthropic_api backend needed `--features llm`, which was a friction point.
Make `llm` a default feature so `cargo build` / `cargo install` include it and
`--provider anthropic_api` works with no flag. For a lean fully-offline build
(no reqwest/tokio/TLS), use `--no-default-features` — rules + claude_code/codex
still work there.

Verified: `cargo build` (default, includes llm) and `cargo build
--no-default-features` (lean) both succeed; tests + clippy clean; `mdpeek gen
--llm --provider anthropic_api` with no key falls back to rules instead of
erroring on the missing feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXGWTTALewHoUjGfVkDwEV
…an docs)

A new web reading mode ("Guided Reading") for design/planning documents that
reduces cognitive load via segmenting + attention cueing, kept reader-paced
(never auto-driven) so critical review stays active.

- `/api/scrolly`: one LLM call → whole-doc overview + per-section commentary
  (additive, non-restating), aligned to rendered heading anchors. Offline-safe
  rules fallback; disk cache keyed by content+model.
- `mdpeek-gui::complete_text_blocking`: raw completion across the existing
  anthropic/claude_code/codex backends (no IR contract).
- `mdpeek-render-html::heading_anchor`: expose the emitter's slug so server
  section anchors match SSR `<h*>` ids.
- Preact/DOM mode: toolbar toggle, grey-out + spotlight the reached section,
  side panel with overview + typewriter-revealed commentary, prev/next nav,
  reduced-motion aware.

Deferred to later slices: in-section Q&A, resumable/saved sessions, true
server-side token streaming, inline anchored commentary.

Verified: cargo build + tests (scrolly section-split, fallback, json-extract),
web typecheck+build, and end-to-end `/api/scrolly` (origin=llm via ClaudeCode,
anchors resolve to SSR ids, cache hits on re-request).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EdRbWDkNYQFPVGYeWA6BQF
…fallback

- In-section Q&A: `POST /api/scrolly/ask` answers a reader's question grounded
  in the current section's text (+ whole-doc context) and prior turns; panel
  gets a chat log + input with a "thinking" spinner and typewriter reveal.
- Language control: auto / 日本語 / English selector (persisted in
  localStorage), threaded through `/api/scrolly?lang=` prompts + cache key.
- Loading spinner while the guide generates (replaces the plain text state).
- LLM-only: removed the rules/offline fallback per feedback — `generate` now
  returns an error (surfaced in the panel) when no backend or on failure.

Verified end-to-end (ClaudeCode backend): lang=en yields English overview,
Q&A on the 'architecture' section answers in Japanese and correctly notes the
section doesn't cover double-charging while pointing to the Risks section.
cargo build + scrolly tests + web typecheck/build all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EdRbWDkNYQFPVGYeWA6BQF
…ailures)

The single-JSON contract broke on larger docs (e.g. README, 22 sections):
free-text commentary with literal newlines/quotes invalidates the JSON, and a
truncated response loses everything — which, since the offline fallback was
removed, surfaced as a hard failure.

Switch the model contract to a newline-safe marker format:

    @@overview@@
    <text>
    @@section 0@@
    <commentary>
    ...

`parse_guide_output` is tolerant of code fences, stray prose, and a cut-off
tail (every section before the cut is kept; missing sections degrade to empty
rather than failing the whole guide).

Verified: README now guides reliably — 22 sections, 0 empty commentaries,
Japanese overview. New unit tests cover multiline commentary, truncated tails,
code fences, and marker parsing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EdRbWDkNYQFPVGYeWA6BQF
The chat could only see the original document text, so readers couldn't ask
about the guide's own explanation. Q&A is now scoped to BOTH the guide (its
overview + current-section commentary) and the original source text.

- `AskRequest` gains optional `guide_overview` + `guide_commentary`; the answer
  prompt presents them alongside the section text and whole-doc context, and the
  system prompt tells the model either is fair game.
- Client sends the loaded guide's overview and the active section's commentary
  with each question (stores overview in runtime).

Verified: a question about the guide's "foreshadowing" remark is answered from
the commentary and correctly tied back to the document's Risks section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EdRbWDkNYQFPVGYeWA6BQF
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