-
Notifications
You must be signed in to change notification settings - Fork 0
feat(docs): embed HTML preview in cmux #427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
cameronsjo
wants to merge
22
commits into
main
Choose a base branch
from
feat/docs-tui-reader
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
aa0f22e
docs(plan): native docs explorer
cameronsjo b60d52d
feat(docs): add native terminal reader
cameronsjo bb82b9d
docs: explain native terminal reader
cameronsjo a5798b8
fix(docs): preserve Kitty image payloads
cameronsjo 696e47b
fix(docs): transmit Kitty image data
cameronsjo aecf08c
refactor(docs): reuse Kitty diacritics
cameronsjo 81f5015
docs(plan): pivot docs reader to embedded preview
cameronsjo f350ca5
feat(docs): open preview inside cmux
cameronsjo 44ccb4e
feat(docs): add reading appearance controls
cameronsjo 57d380f
feat(docs): render referenced local images
cameronsjo 8ee667b
docs: explain embedded docs preview
cameronsjo 089cb15
fix(docs): preserve preview boundaries
cameronsjo 9113693
fix(docs): satisfy preview safety checks
cameronsjo 96a5214
docs(plan): record embedded preview verification
cameronsjo 0889019
docs(plan): record PR update
cameronsjo b987028
docs(plan): refine docs preview for reading
cameronsjo 4b26215
feat(docs): make embedded preview reading-first
cameronsjo 0f2d659
fix(docs): keep preview chrome inline
cameronsjo bbf7e9c
docs(plan): record preview stopping point
cameronsjo 0d87050
docs(plan): record draft PR handoff
cameronsjo c3a46ec
Merge origin/main into feat/docs-tui-reader (v0.15.0: docs reader v2)
cameronsjo 5e33537
style(docs): gofmt assets.go after the merge union
cameronsjo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Embedded docs preview | ||
|
|
||
| ## Goal | ||
|
|
||
| Explore whether `forgectl docs` becomes a genuinely pleasant reading tool when | ||
| its existing HTML reader is embedded in the caller's cmux workspace. Optimize | ||
| this slice for learning: remove the Kitty/TUI prototype, make the HTML path the | ||
| ordinary path, and address the reading gaps exposed by the live cmux proof. | ||
|
|
||
| ## Evidence and chosen approach | ||
|
|
||
| - A loopback docs server was opened with `cmux new-pane --type browser` in the | ||
| caller's workspace. It rendered the existing sidebar, sanitized Markdown, | ||
| syntax highlighting, live reload, and Mermaid without taking focus. | ||
| - `forgectl docs [dir|file ...]` will start the existing foreground loopback | ||
| server, open its URL in a right-hand cmux browser pane when | ||
| `CMUX_WORKSPACE_ID` is present, and otherwise fall back to the system browser. | ||
| The terminal remains the server owner, so Ctrl-C stops the preview. | ||
| - `docs serve`, `docs open`, and `docs list` keep their explicit contracts. | ||
| `docs serve --open` remains the spelling for a separate system-browser tab. | ||
| - Add an unobtrusive reading-settings control for body, heading, and code font | ||
| families plus text size, line height, and measure. Settings are browser-local | ||
| and persist with local storage, making this exploratory without expanding the | ||
| config schema. | ||
| - Rewrite relative Markdown image URLs to a same-origin resource endpoint. The | ||
| endpoint will resolve files through the indexed root's existing containment | ||
| boundary, reject excluded/hidden paths and unsupported media types, and keep | ||
| remote images blocked by the current content-security policy. | ||
| - Remove the terminal explorer, Kitty graphics, Glamour, and pure-Go Mermaid | ||
| code and dependencies. The completed native-reader plan remains in history as | ||
| the record of the explored approach and this plan records the deliberate | ||
| pivot. | ||
|
|
||
| ## Alternatives deferred | ||
|
|
||
| - A background daemon would return the invoking terminal immediately, but it | ||
| introduces lifecycle and stale-process questions before the reading model is | ||
| proven. | ||
| - Bundling proprietary or large font binaries would make typography identical | ||
| across machines, but system/local font stacks are sufficient to evaluate the | ||
| interaction first. | ||
| - Opening a generic external browser inside forgectl would couple the command | ||
| to browser automation. This experiment uses cmux's supported CLI when it is | ||
| present and preserves the portable system-browser fallback. | ||
|
|
||
| ## Checklist | ||
|
|
||
| - [x] Prove a loopback reader can open in the caller's cmux workspace without | ||
| stealing focus. | ||
| - [x] Persist and commit the approved pivot before implementation. | ||
| - [x] Replace the bare docs/TUI entry point with embedded-cmux preview startup. | ||
| - [x] Add persisted reading typography and measure controls. | ||
| - [x] Serve contained local Markdown images through the loopback reader. | ||
| - [x] Remove terminal-reader code and dependencies. | ||
| - [x] Update help and README; leave generated changelog prose to Release Please. | ||
| - [x] Run fresh build, vet, tests, formatting, lint, and live cmux acceptance. | ||
| - [x] Update and push the existing pull request, then monitor its checks. | ||
|
|
||
| ## Acceptance | ||
|
|
||
| - From a cmux terminal, `forgectl docs [dir|file ...]` creates a readable | ||
| right-hand browser pane in that same workspace and leaves keyboard focus in | ||
| the invoking terminal; Ctrl-C stops its foreground server. | ||
| - Markdown, syntax highlighting, tables, Mermaid, inline SVG, and contained | ||
| relative raster/SVG images render without a network dependency. | ||
| - The reader offers visibly different body, heading, and code font choices and | ||
| persists the chosen typography, size, line height, and content width. | ||
| - Outside cmux, the same command opens the system browser and explains the | ||
| server lifecycle; explicit `serve`, `open`, and `list` behavior remains green. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # Native docs explorer | ||
|
|
||
| > Superseded by [Embedded docs preview](2026-08-29-embedded-docs-preview.md) | ||
| > after live reading acceptance showed the rasterized terminal surface was not | ||
| > competitive with the existing HTML renderer. Retained as the exploration | ||
| > record, not the current implementation plan. | ||
|
|
||
| ## Goal | ||
|
|
||
| Make `forgectl docs` a terminal-native, Artificer-styled document explorer while | ||
| retaining the existing HTTP reader for remote, phone, and exact Mermaid.js | ||
| fallback use. Local images, SVG, and Mermaid diagrams render in compatible | ||
| terminals through the Kitty graphics protocol and degrade to readable text when | ||
| graphics are unavailable. | ||
|
|
||
| ## Chosen approach | ||
|
|
||
| - `forgectl docs [dir|file ...]` launches the TUI on an interactive terminal; | ||
| `forgectl docs browse` is the explicit equivalent. Existing `serve`, `open`, | ||
| and `list` behavior remains compatible. | ||
| - Reuse the docs index, root resolution, watcher, and browser opener. Add an | ||
| adaptive Bubble Tea explorer with a filterable tree and scrollable Markdown | ||
| pane, rendered with Glamour and an Artificer stylesheet. | ||
| - Use Charm's already-pinned Kitty encoder with Unicode virtual placements. | ||
| Add capability detection, stable IDs, resize retransmission, and cleanup. | ||
| - Render local PNG, JPEG, static GIF, and SVG references only. Resolve every | ||
| path relative to its Markdown document and keep it inside the indexed root. | ||
| - Render Mermaid with a pinned pure-Go renderer and rasterizer. Unsupported | ||
| syntax remains visible as source and points to the retained web reader. | ||
| - External links require confirmation before opening the system browser; | ||
| relative Markdown links and anchors navigate inside the TUI. | ||
|
|
||
| ## Alternatives declined | ||
|
|
||
| - Full HTTP-reader replacement: loses remote and phone access and removes the | ||
| exact Mermaid.js fallback. | ||
| - Headless Chrome: matches Mermaid.js more closely but makes a browser a hidden | ||
| runtime dependency, contrary to the native-reader goal. | ||
| - Text-only first release: does not deliver the requested graphics experience. | ||
|
|
||
| ## Checklist | ||
|
|
||
| - [x] Create an isolated worktree and feature branch from fresh `origin/main`. | ||
| - [x] Persist and commit the approved plan before implementation. | ||
| - [x] Add terminal Markdown, resource, diagram, and Kitty graphics primitives. | ||
| - [x] Add the adaptive docs TUI and wire the native-first CLI entry points. | ||
| - [x] Cover fallback, containment, navigation, resize, reload, and cleanup. | ||
| - [x] Update help and README; leave generated changelog prose to Release Please. | ||
| - [x] Run fresh build, vet, tests, formatting, lint, and Ghostty acceptance. | ||
|
|
||
| ## Acceptance | ||
|
|
||
| - Markdown, local raster images, SVG, and supported Mermaid diagrams render in | ||
| cmux/Ghostty; scrolling and resizing keep images attached to document rows. | ||
| - Unsupported terminals, remote images, invalid diagrams, and decode failures | ||
| show deliberate readable fallbacks without raw graphics control sequences. | ||
| - Live reload, internal links, history, filtering, and external-link | ||
| confirmation work without opening a separate browser for ordinary reading. | ||
| - Existing `docs serve`, `docs open`, and `docs list` contracts and tests remain | ||
| green. |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package cli | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "golang.org/x/term" | ||
|
|
||
| docspkg "github.com/cameronsjo/forgectl/internal/docs" | ||
| "github.com/cameronsjo/forgectl/internal/module" | ||
| ) | ||
|
|
||
| var docsStreamIsTerminal = func(stream any) bool { | ||
| fd, ok := stream.(interface{ Fd() uintptr }) | ||
| return ok && term.IsTerminal(int(fd.Fd())) | ||
| } | ||
|
|
||
| func runDocsPreview(cmd *cobra.Command, deps module.Deps, args []string) error { | ||
| if !docsStreamIsTerminal(cmd.InOrStdin()) || !docsStreamIsTerminal(cmd.OutOrStdout()) { | ||
| return fmt.Errorf("docs preview requires an interactive terminal; use `forgectl docs list` for text output or `forgectl docs serve` for a server-only process") | ||
| } | ||
| roots, err := resolveDocsRoots(args, deps.Cfg.Docs) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| idx, err := docspkg.NewIndex(roots) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return runDocsPreviewServer(cmd, deps, idx) | ||
| } | ||
|
|
||
| func docsHelpForNonTTY(cmd *cobra.Command, args []string) (bool, error) { | ||
| if docsStreamIsTerminal(cmd.InOrStdin()) && docsStreamIsTerminal(cmd.OutOrStdout()) { | ||
| return false, nil | ||
| } | ||
| if len(args) == 0 { | ||
| return true, cmd.Help() | ||
| } | ||
| return true, fmt.Errorf("docs preview requires an interactive terminal; use `forgectl docs list` for text output or `forgectl docs serve` for a server-only process") | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| package cli | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/cameronsjo/forgectl/internal/config" | ||
| forgexec "github.com/cameronsjo/forgectl/internal/exec" | ||
| "github.com/cameronsjo/forgectl/internal/module" | ||
| ) | ||
|
|
||
| func TestDocsCommand_NonTTYBareInvocationKeepsHelpBehavior(t *testing.T) { | ||
| previous := docsStreamIsTerminal | ||
| docsStreamIsTerminal = func(any) bool { return false } | ||
| t.Cleanup(func() { docsStreamIsTerminal = previous }) | ||
|
|
||
| cmd := newDocsCmd(module.Deps{Cfg: config.Config{}, Runner: &forgexec.FakeRunner{}}) | ||
| var out bytes.Buffer | ||
| cmd.SetOut(&out) | ||
| cmd.SetErr(&out) | ||
| cmd.SetIn(strings.NewReader("")) | ||
| cmd.SetArgs(nil) | ||
| if err := cmd.ExecuteContext(context.Background()); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if !strings.Contains(out.String(), "serve + open the reading preview") || !strings.Contains(out.String(), "docs serve") { | ||
| t.Fatalf("help = %q", out.String()) | ||
| } | ||
| } | ||
|
|
||
| func TestDocsPreview_RejectsNonTTY(t *testing.T) { | ||
| previous := docsStreamIsTerminal | ||
| t.Cleanup(func() { docsStreamIsTerminal = previous }) | ||
| deps := module.Deps{Cfg: config.Config{}, Runner: &forgexec.FakeRunner{}} | ||
| cmd := newDocsCmd(deps) | ||
| cmd.SetIn(strings.NewReader("")) | ||
| cmd.SetOut(new(bytes.Buffer)) | ||
| docsStreamIsTerminal = func(any) bool { return false } | ||
| if err := runDocsPreview(cmd, deps, nil); err == nil || !strings.Contains(err.Error(), "interactive terminal") { | ||
| t.Fatalf("non-TTY error = %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestDocsCommand_RegistersPreviewAndServerEntrypoints(t *testing.T) { | ||
| cmd := newDocsCmd(module.Deps{Cfg: config.Config{}, Runner: &forgexec.FakeRunner{}}) | ||
| for _, name := range []string{"serve", "open", "list"} { | ||
| if found, _, err := cmd.Find([]string{name}); err != nil || found.Name() != name { | ||
| t.Fatalf("Find(%q) = %v, %v", name, found, err) | ||
| } | ||
| } | ||
| if found, _, err := cmd.Find([]string{"browse"}); err == nil && found.Name() == "browse" { | ||
| t.Fatal("obsolete terminal browse command is still registered") | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.