Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
aa0f22e
docs(plan): native docs explorer
cameronsjo Aug 29, 2026
b60d52d
feat(docs): add native terminal reader
cameronsjo Aug 29, 2026
bb82b9d
docs: explain native terminal reader
cameronsjo Aug 29, 2026
a5798b8
fix(docs): preserve Kitty image payloads
cameronsjo Aug 29, 2026
696e47b
fix(docs): transmit Kitty image data
cameronsjo Aug 29, 2026
aecf08c
refactor(docs): reuse Kitty diacritics
cameronsjo Aug 29, 2026
81f5015
docs(plan): pivot docs reader to embedded preview
cameronsjo Aug 30, 2026
f350ca5
feat(docs): open preview inside cmux
cameronsjo Aug 30, 2026
44ccb4e
feat(docs): add reading appearance controls
cameronsjo Aug 30, 2026
57d380f
feat(docs): render referenced local images
cameronsjo Aug 30, 2026
8ee667b
docs: explain embedded docs preview
cameronsjo Aug 30, 2026
089cb15
fix(docs): preserve preview boundaries
cameronsjo Aug 30, 2026
9113693
fix(docs): satisfy preview safety checks
cameronsjo Aug 30, 2026
96a5214
docs(plan): record embedded preview verification
cameronsjo Aug 30, 2026
0889019
docs(plan): record PR update
cameronsjo Aug 30, 2026
b987028
docs(plan): refine docs preview for reading
cameronsjo Aug 30, 2026
4b26215
feat(docs): make embedded preview reading-first
cameronsjo Aug 30, 2026
0f2d659
fix(docs): keep preview chrome inline
cameronsjo Aug 30, 2026
bbf7e9c
docs(plan): record preview stopping point
cameronsjo Aug 30, 2026
0d87050
docs(plan): record draft PR handoff
cameronsjo Aug 30, 2026
c3a46ec
Merge origin/main into feat/docs-tui-reader (v0.15.0: docs reader v2)
cameronsjo Sep 1, 2026
5e33537
style(docs): gofmt assets.go after the merge union
cameronsjo Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ deep-dive get a link here.
| `branch` | Prune stale/orphaned git branches (alias: `br`) | Usage below |
| `clean` | Reclaim dep/build directories under a project root (alias: `cln`) | Usage below |
| `docker` | Build/run/shell images tagged from git repo/branch/sha | Usage below |
| `docs` | Local markdown reader: render + serve an indexed doc set over loopback HTTP | [docs](docs/commands/docs.md) |
| `docs` | Native terminal docs explorer, with the loopback web reader retained as a fallback | [docs](docs/commands/docs.md) |
| `net` | Check cached reachability of the configured probe endpoint | Usage below |
| `proxy` | Apply config-defined profiles to the current shell through an explicit wrapper | [proxy](docs/commands/proxy.md) |
| `k8s` | Safely stream ordinary kubectl logs, plus bounded namespace/exec/inspect helpers | [k8s](docs/commands/k8s.md) |
Expand Down Expand Up @@ -201,9 +201,10 @@ forgectl docker build [context] -- --platform linux/arm64 # args after -- pass
forgectl docker run [-- args...] # run the built (or --tag) image
forgectl docker shell # open a shell in the built (or --tag) image

# docs — local markdown reader: render + serve an indexed doc set over loopback HTTP
# docs — rich HTML reader embedded in the current cmux workspace
forgectl docs [dir|file ...] # serve + open a right-hand cmux browser pane (cwd by default)
forgectl docs serve [dir|file ...] # render + serve, loopback-only (DNS-rebinding-safe)
forgectl docs serve --open # also open the system browser
forgectl docs serve --open # serve + open a separate system-browser tab
forgectl docs open [path] # point the browser at a doc on the already-running reader
forgectl docs list [dir|file ...] # list the indexed docs, no server (--json for scripting)

Expand Down Expand Up @@ -301,6 +302,24 @@ forgectl y last 5 # print the 5 most recent zsh commands,
# acknowledgement only: forgectl does not scan or redact the history
```

Inside cmux, the ordinary `docs` command creates a browser pane on the right of
the invoking terminal without moving keyboard focus. The terminal owns the
foreground loopback server, so leave it running while you read and press Ctrl-C
there to close the server. Outside cmux, the same command opens the system
browser instead. Use `docs serve` when another process should own presentation,
or when you want remote or phone access through the existing address/token
options.

The reader opens directly on the document, with a compact preview toolbar and a
document navigator that stays out of the reading path until opened. It renders
sanitized Markdown, syntax highlighting, tables, Mermaid,
inline SVG, and relative local PNG, JPEG, GIF, WebP, AVIF, and SVG images. The
`Aa` control independently changes body, heading, and code fonts, text size,
line height, and line length; those choices persist in that browser. Relative
images are served only when an indexed document references them and still pass
the configured-root containment checks. Remote images remain blocked so opening
a local document does not notify a third party.

The cask doesn't stage an `fx` command — it's a shell alias you add yourself:

```sh
Expand Down
69 changes: 69 additions & 0 deletions docs/plans/2026-08-29-embedded-docs-preview.md
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.
60 changes: 60 additions & 0 deletions docs/plans/2026-08-29-native-docs-explorer.md
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.
68 changes: 68 additions & 0 deletions docs/plans/2026-08-29-reading-first-docs-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Reading-first docs preview

## Goal

Make the embedded HTML reader feel like a native editor preview rather than a
regular web application. The document must dominate the pane immediately,
especially at the narrow widths created by a right-hand cmux split.

## Chosen approach

- Replace the persistent/stacking sidebar with an off-canvas document drawer
that is closed by default at every viewport width. A compact toolbar button
opens it; Escape, the scrim, or choosing a document closes it.
- Reduce the app bar to editor-preview chrome: navigator button, current
document title, reading settings, and theme. Remove the oversized brand-first
presentation from the reading path.
- Keep the article centered on a configurable reading measure with compact,
responsive gutters. The document begins immediately below the toolbar and
never waits below a stacked navigator.
- Preserve the existing server, local-image authorization, Mermaid, live
reload, typography persistence, filtering, and explicit CLI contracts.
- Add a small same-origin behavior asset for drawer state, focus return, Escape,
and scrim dismissal; keep the no-inline-script CSP invariant.

## Alternatives declined

- A permanently visible VS Code-style activity rail still consumes meaningful
width in the common half-screen cmux pane without helping the reading task.
- A desktop-only persistent sidebar would reintroduce the dashboard feel and
make behavior jump as the split crosses one breakpoint.
- Removing navigation entirely would make one-file previews pleasant but turn
indexed doc sets into a dead end.

## Checklist

- [x] Persist and commit the approved reading-first refinement.
- [x] Replace the stacking sidebar with an accessible off-canvas drawer.
- [x] Reduce the header and make the current document the primary label.
- [x] Tune article spacing and responsive behavior for a half-screen cmux pane.
- [x] Update tests and user-facing documentation.
- [x] Run fresh formatting, JavaScript syntax, lint, vet, and full tests.
- [ ] Complete live cmux visual acceptance. The content-first shell was
captured successfully; drawer open/filter/Escape/scrim behavior remains to
be exercised after the session was stopped.
- [x] Push and document the stopping point in draft pull request #427.

## Acceptance

- Opening a document at the current cmux split width shows article content at
the top of the pane; the full navigator is not stacked above it.
- The navigator opens as a drawer, focuses its filter, closes with Escape and
scrim click, and returns focus to its toggle.
- The compact toolbar identifies the current document and keeps `Aa` and theme
controls available without visually competing with the article.
- Existing document rendering, local images, Mermaid, appearance persistence,
and server security tests remain green.

## Stopping point

The reading-first shell is implemented and the automated verification is
green. Live inspection confirmed that the article now starts directly beneath
the compact toolbar with no stacked navigator. The remaining acceptance step
is to exercise the navigator interactions in cmux and decide whether this
visual direction is sufficiently native-feeling before taking the PR out of
draft.

Next action: rebuild the branch, run `forgectl docs .` inside cmux, then verify
the navigator toggle, filter focus, Escape, scrim dismissal, and focus return.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/yuin/goldmark v1.8.4
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.abhg.dev/goldmark/frontmatter v0.3.0
golang.org/x/net v0.55.0
golang.org/x/sys v0.46.0
golang.org/x/term v0.44.0
gopkg.in/yaml.v3 v3.0.1
Expand Down Expand Up @@ -64,7 +65,6 @@ require (
github.com/rogpeppe/go-internal v1.16.0 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.37.0 // indirect
)
33 changes: 20 additions & 13 deletions internal/cli/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import (
"github.com/cameronsjo/forgectl/internal/module"
)

// docsModule declares the local markdown reader extension (ADR-0005): owns
// the [docs] config section. See forgectl#93 for the full design; this is
// PR1's slice (render + index, no live reload).
// docsModule declares the local markdown reader extension (ADR-0005) and owns
// the [docs] config section. See forgectl#93 for the full design.
var docsModule = module.Manifest{
Name: "docs",
Tier: module.TierExtension,
Expand All @@ -21,25 +20,33 @@ var docsModule = module.Manifest{
// shape.
func newDocsCmd(deps module.Deps) *cobra.Command {
cmd := &cobra.Command{
Use: "docs",
Short: "Local markdown reader — render + serve an indexed doc set over loopback HTTP",
Use: "docs [dir|file ...]",
Short: "Read an indexed Markdown doc set in an embedded HTML preview",
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if handled, err := docsHelpForNonTTY(cmd, args); handled {
return err
}
return runDocsPreview(cmd, deps, args)
},
Long: `docs is forgectl's local markdown reader (forgectl#93): pure-Go
server-side rendering (goldmark+GFM, class-based chroma highlighting,
bluemonday sanitization), Artificer-themed, served over loopback HTTP so it
behaves the same whether you're at the machine or SSH'd in from the headless
workbench — no terminal-specific rendering, no popping between windows.
rendering with an Artificer-themed HTML preview as the ordinary path. Inside
cmux, the preview opens as a right-hand browser pane in the caller's workspace
without taking focus. Elsewhere it opens in the system browser. The invoking
terminal owns the foreground loopback server; press Ctrl-C there to stop it.

forgectl docs [dir|file ...] serve + open the reading preview
forgectl docs serve [dir|file ...] render + serve an indexed doc set
forgectl docs serve --open also open the system browser
forgectl docs open [path] point the browser at a doc on the
already-running reader
forgectl docs list [dir|file ...] list the indexed docs, no server
forgectl docs list --json machine-readable output for scripts

Diagrams render in the page: a fenced code block tagged mermaid becomes a live
diagram themed from the same Artificer tokens as the rest of the reader, and
both those and inline SVG pan and zoom (drag to pan, modifier-scroll or
click-then-scroll to zoom, double-click or 0 to reset).
Mermaid.js renders fenced mermaid blocks, and Mermaid and inline SVG can pan and
zoom (drag to pan, modifier-scroll or click-then-scroll to zoom, double-click or
0 to reset). Reading settings in the app bar control body, heading, and code
fonts plus text size, line height, and content width.

With no arguments, both verbs index cwd, ./docs (if present), and
$CADENCE_FIELD_REPORTS_DIR (if set), plus any extra roots configured in the
Expand Down
41 changes: 41 additions & 0 deletions internal/cli/docs_preview.go
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")
}
Loading
Loading