Review-first terminal diff viewer for agentic coders
# hunk agent notes
## purpose
- Terminal-first diff viewer for understanding coding-agent changesets.
- Product target is "modern desktop diff tool in a terminal", not a pager-style TUI.
## major dependencies
- [Bun](https://bun.sh) runtime and package manager
- [OpenTUI](https://github.com/anomalyco/opentui) React terminal UI framework
- [Pierre](https://www.npmjs.com/package/@pierre/diffs) diff engine and terminal renderer
## architecture
```text
CLI input
-> parse runtime + config-backed view options
-> normalize into one Changeset / DiffFile model
-> App shell coordinates state, layout, and review navigation
-> pane components render review UI
-> Pierre-backed terminal renderer draws diff rows
```
### shared review seam
Review core serves multiple surfaces: TUI today; web, API, and agent/runtime consumers later. Do
not recreate semantic review behavior in a surface:
```text
DiffFile[] -> projectReviewDocument -> ReviewDocumentV1 -> ReviewStore
ReviewIntent + caller facts -> planReviewIntent -> ReviewAction[] -> reducer -> surface projection
```
- **Model:** `src/core/review/{types,document,identity}.ts` owns the ordered, JSON-safe document.
File order is review/sidebar order; use `key` (referenced as `fileKey` elsewhere),
`contentIdentity`, and `sourceIdentity` (cached source text additionally requires
`sourceAttested`) β not runtime IDs or indexes β across reloads/surfaces.
- **Shared derivations:** `geometry.ts`, `expansion.ts`, `anchors.ts`, `stml.ts`, and
`contentManifest.ts` own ranges, gaps, source splitting, note targets/ownership, tag roles, and
parity manifests. Consume them; never re-derive those facts in a renderer.
- **State:** `state.ts` is semantic state; `actions.ts` transitions; `reducer.ts` pure/no-I/O;
`selectors.ts` shared policies; `store.ts` synchronous observable storage. New cross-surface
operations start as intents. Callers supply mutable-note IDs/timestamps; core derives identities.
- **Surfaces/publishers:** `useTerminalReview.ts` is the TUI adapter and
`reviewNoteMapping.ts` is terminal-only. Rows, measurement, scrolling, layout, themes, DOM
mechanics, and source I/O stay local. `useHunkSessionBridge.ts` publishes the current terminal
session export; `registration.ts` builds its metadata/initial snapshot and `bridge.ts` receives
agent commands. This broker export is not a full `ReviewState` mirror.
- **Future consumers:** Web/API consumers reuse the model, derivations, state, intents, and then
the producer/protocol tier (see Phases 2β3 in `docs/browser-review-rebuild.md` for modules and
status). Never build a parallel protocol. Keep presentation/client-local state local;
host/extension commands need explicit remote capabilities.
- **Conformance:** `test/review-conformance/` has hand-authored semantic fixtures and currently
covers core plus terminal render planning. Every new semantic consumer registers its real
projection and runs the whole corpus. `scripts/source-boundaries.test.ts` keeps the seam
renderer/platform-free; its Node-debt list is shrink-only and tombstone lists append-only. A
repaid seam finding deletes copies, adds a file or banned-symbol tombstone and adversarial
fixture, registers consumers, and updates `docs/browser-review-seam-audit.md`.
- CLI entrypoints: `diff`, `show`, `stash show`, `patch`, `pager`, `difftool`.
- All input sources normalize into one internal changeset model.
- Bundled VCS implementations live under `src/extensions/default/vcs/<provider>/` and consume the
public extension contract; `src/app` composes their registrations into the provider-neutral
core VCS catalog. Do not add provider commands, spawning, or source readers under `src/core`.
- Pager mode has two paths: full diff UI for patch-like stdin, plain-text fallback for non-diff pager content.
- View defaults are layered through built-ins, user config, repo `.hunk/config.toml`, command sections, pager sections, and CLI flags.
- `hunk daemon serve` runs one loopback daemon that brokers agent commands to many live Hunk sessions. Normal Hunk sessions should auto-start and register with that daemon when session brokering is enabled. Keep it local-only and session-brokered rather than opening per-TUI ports. The daemon also mirrors each session's current review publication (generation plus resource catalog) and reads bulky content β patch text, canonical files, source β back as bounded, digest-verified resource chunks instead of holding it in the registration. Order publications with `classifyReviewPublication` and assemble chunks with `ReviewChunkAssembler`; do not add a second acceptance rule or a second assembly loop. The same daemon serves each session's review over HTTP (`src/session/broker/browserReviewServer.ts`): loopback and same-origin only, no CORS, and every route authorized by a per-session capability the session mints and publishes only the digest of. That surface is transport and authorization β its routes, capability grammar, SSE event contract, and error messages are the browser-safe modules `src/session/review{HttpProtocol,EventProtocol,ErrorCatalog}.ts`, and its semantic answers come from the producer through the existing intent path.
- Extensions come in two tiers β user TypeScript extensions and the bundled tier in `src/extensions/default/` β running through one per-extension API object and registry (`src/extensions/runExtension.ts`, resolved via `src/extensions/apply.ts`). Every shipped VCS backend and the built-in sidebar are bundled extensions registering through the public API; that dogfooding keeps `hunkdiff/extension` honest. Hard rules: `src/extension-api/types.ts` stays import-free (declaration emission publishes whatever it reaches; `scripts/check-pack.ts` gates it); `src/extensions/default/vcs/` loads from VCS adapter resolution and must stay renderer-free (the sidebar loads separately via `getBundledSidebarView`); repo-local `.hunk/extensions/` never executes without the trust prompt; bundled extensions stay loaded under `--no-extensions`. The full architecture β host-served runtime modules, sidebar pane model, command dispatch, VCS detection ordering, conversion boundaries β is mapped in `docs/extension-architecture.md` and documented in depth by the module headers it names; the authoring guide is `docs/extensions.md`, and `skills/hunk-extensions/SKILL.md` is the agent-facing map of those touchpoints.
- Agent rationale is optional sidecar JSON matched onto files/hunks.
- The order of `files` in the sidecar is intentional. Hunk uses that order for the sidebar and main review stream.
- Prefer one source of truth for each user-visible behavior. When rendering, navigation, scrolling, or note placement share the same model, derive them from the same planning layer rather than maintaining parallel implementations.
- When UI behavior depends on derived structure or metrics, make that structure explicit in helper modules and reuse it across rendering and interaction code instead of re-deriving it ad hoc in multiple places.
- If a new implementation makes an older path obsolete, remove the dead path instead of keeping two overlapping systems around.
## architectural rules
- Keep the app review-first: the main pane is a single top-to-bottom stream of all visible file diffs.
- The sidebar is for navigation. Selecting a file jumps to that file in the main review stream; it should not collapse the main pane to one file.
- Keep Pierre as the diff engine and renderer foundation. Do not switch the main renderer back to OpenTUI's built-in `<diff>` widget.
- Keep split and stack views terminal-native and driven from the same normalized diff model.
- Preserve mouse + keyboard parity for primary actions.
- Keep the chrome restrained: top menu bar, minimal borders, no redundant metadata headers.
## component guidance
- `App` should remain the orchestration shell for app state, navigation, layout mode, theme, filtering, and pane coordination.
- Pane rendering should live in dedicated components.
- Confirmation prompts with a small set of choices should reuse `ConfirmDialog` (body rows plus a clickable key-legend action row) instead of composing `ModalFrame` with a hand-rolled footer; keyboard handling for its actions stays in `useAppKeyboardShortcuts`.
- New UI work should extend existing components or add new ones, not grow `App` back into a monolith.
- Shared formatting, ids, and small derivations belong in helper modules, not repeated inline.
- Prefer one implementation path per feature instead of separate "old" and "new" codepaths that duplicate behavior.
- When refactoring logic that spans helpers and UI components, add tests at the level where the user-visible behavior actually lives, not only at the lowest helper layer.
## theme guidance
- Built-in themes live in `src/ui/themes/<theme-id>.ts`; register them in `src/ui/themes.ts` `THEMES` to control menu/cycle order.
- When adding or renaming a built-in theme, update config validation, OpenTUI theme exports, docs/README examples, changelog, and tests that assert theme order.
- Keep official palette tokens separate from Hunk's semantic `AppTheme` mapping, and cover non-trivial derived colors with tests.
## testing
- Colocate unit tests with the code they cover (`src/core/foo.ts` + `src/core/foo.test.ts`, `src/ui/AppHost.*.test.tsx`, `src/ui/lib/*.test.ts`).
- Put shared unit-test helpers in `test/helpers/`.
- Name test helpers so they explicitly include `Test` and are clearly test-only (`createTestDiffFile`).
- Use repo-level `test/` directories by intent:
- `test/cli/` for black-box CLI contract coverage.
- `test/session/` for daemon/session integration and end-to-end flows.
- `test/pty/` for PTY-backed live UI integration tests.
- `test/review-conformance/` for the shared review model's golden fixtures and per-consumer conformance suites.
- `test/smoke/` for opt-in terminal transcript smoke coverage.
## code comments
- Add short JSDoc-style comments to functions and helpers.
- Write header comments in active voice: the first sentence says what the module or function
does ("Applies one action to the review state and returns the next state."), followed by its
invariants. Avoid passive or self-important framing ("The one place whereβ¦", "the single
source of truth forβ¦") β name the behavior, not the architecture's opinion of itself.
- Add inline comments for intent, invariants, or tricky behavior that would not be obvious to a fresh reader.
- Skip comments that only narrate what the code already says.
## naming
- Prefer names that match the role the code plays in the product and architecture.
- Use `layout` for structural placement or arrangement data.
- Use `geometry` for aggregate spatial data used by rendering, scrolling, or interaction.
- Use `bounds` for one concrete visible extent within a larger structure.
## review behavior
- Default behavior is a multi-file review stream in sidebar order.
- Layout modes: `auto`, `split`, `stack`.
- `auto` should choose split on wide terminals and stack on narrow ones.
- Explicit `split` and `stack` choices override responsive `auto` layout selection.
- `[` and `]` navigate hunks across the full review stream. Do not reintroduce `j`/`k` hunk navigation unless the user asks.
- Agent context belongs beside the code, not hidden in a separate mode or workflow.
- Agent notes are hunk-specific: show notes for the selected hunk, render them in the diff flow near the annotated row, and keep a clear spatial relationship to the code they explain.
- Keep note behavior explicit. If the UI intentionally prioritizes one note, one selection, or one active target, encode that as a named policy rather than scattering array-index assumptions through the codebase.
- STML markup notes (experimental) live in `src/ui/lib/stml/`. The layout engine is deliberately a deterministic line layout, not OpenTUI flexbox: the row-windowed review stream needs exact note heights before mount, so `(markup, width)` must always produce the same lines. Colors stay symbolic until render time so measurement never needs a theme. Do not "simplify" this into flexbox renderables, and keep note-card geometry in `agentNoteGeometry` as the single source for rendering, measurement, and agent-facing width reporting.
- If you choose to use a local sidecar for temporary review context, keep it concise and review-oriented: one changeset summary, file summaries in narrative order, and a few hunk-level annotations with real rationale.
- If a local sidecar is present, its file order is intentional, but the visible note UI should stay hunk-note driven rather than showing generic file or changeset explainer cards.
- `hunk diff` working-tree reviews include untracked files by default. Use `--exclude-untracked` if you explicitly want tracked changes only.
- Agents review via `skills/hunk-review/SKILL.md` using `hunk session *` commands; do not run interactive TUI commands directly.
- `skills/hunk-review/SKILL.md` is generated. Edit `src/hunk-review/skillDocument.ts`, `src/session/agent/surface.ts`, or `src/session/agent/errors.ts`, then run `bun run generate:skill`; never hand-edit the skill file.
## commands
- install deps: `bun install`
- run from source: `bun run src/main.tsx -- diff`
- review a commit from source: `bun run src/main.tsx -- show HEAD~1`
- fast smoke test: `bun run src/main.tsx -- diff /tmp/before.ts /tmp/after.ts`
- typecheck: `bun run typecheck`
- tests: `bun test`
- PTY integration tests: `bun run test:integration`
- TTY smoke test: `bun run test:tty-smoke`
- format: `bun run format`
- lint: `bun run lint`
- build binary: `bun run build:bin`
- install binary: `bun run install:bin`
## binary notes
- Installed `hunk` is a compiled snapshot, not linked to source.
- After source changes, rebuild/reinstall with `bun run install:bin`.
- For rendering verification, prefer a real TTY smoke run over redirected stdout capture.
## verification
- For rendering changes: run `bun run typecheck`, `bun test`, `bun run test:integration`, `bun run test:tty-smoke`, and do one real TTY smoke run on an actual diff.
- For interaction, layout, scrolling, navigation, windowing, or other terminal-native behavior: add or update PTY integration coverage in `test/pty/*-integration.test.ts` and run it with `bun run test:integration`.
- For CLI, config, or pager work: make sure the relevant source invocation still works (`diff`, `show`, `patch`, or `pager`).
- Preserve current interaction model unless the user asks to change it explicitly.
## cross-platform support
- Hunk should work on macOS, Linux, and Windows. Keep tests and CI portable unless a case is explicitly Unix-only (PTY/TTY smoke coverage is Unix-only).
- In tests, avoid hard-coded POSIX paths, separators, shell syntax, and filenames invalid on Windows; use Node path helpers for real filesystem paths while preserving user-provided/protocol paths when pass-through is intentional.
- If Windows-only Bun behavior appears around timers, sockets, or line endings, prefer a small compatibility fix or a narrowly scoped skip with a comment over broadening Unix assumptions.
## releases
- Use Changesets for user-visible release notes. Add a `.changeset/*.md` entry with `bun run changeset` instead of editing `CHANGELOG.md` directly.
- Target the public `hunkdiff` package in changesets. Use `patch` for fixes and small behavior changes, `minor` for new user-facing features, and `major` for breaking changes.
- For maintenance-only PRs that should not appear in release notes, add an empty changeset with `bun run changeset -- --empty`.
- Keep the top-level `CHANGELOG.md` as the released changelog artifact. It is updated during release prep with `bun run release:version`, not by normal feature/fix PRs.
- Use the released changelog section as the starting point for the GitHub release body.
- GitHub releases should follow this format:
```md
## What's Changed
- <change title> by @<author> in <PR URL>
- ...
**Full Changelog**: https://github.com/modem-dev/hunk/compare/<previous-tag>...<new-tag>
```
- Do not rely blindly on autogenerated GitHub release notes. After publishing, verify the release body and edit it if needed.
- Prefer `gh release create/edit --notes-file` for multi-line release notes so the exact body is reviewed before posting.
- After publishing, verify npm packages and GitHub release assets point at the new version. For Homebrew, Hunk is distributed through `Homebrew/homebrew-core`; do not open manual simple version-bump PRs yourself. Let Homebrew Autobump create the `hunk <version>` PR, then verify it merges and `brew install hunk` resolves to the new version. Only use `brew bump-formula-pr hunk --version <version>` if Homebrew maintainers request a manual bump or Autobump stalls unexpectedly.
- For mise, verify with `MISE_AQUA_BAKED_REGISTRY=false mise latest hunk`. mise resolves Hunk through the community `aqua:modem-dev/hunk` entry, bakes an aqua registry snapshot into each of its own releases (`aqua.baked_registry`, default true), and caches downloaded registry sources for a week (`aqua.registry_cache_ttl`), so a default check can report stale registry data unrelated to our release; `mise cache clear` forces a refresh.
- For patch releases and backports, list only changes actually present between the previous tag and the new tag on that release branch.
- Prefer concise, user-visible entries over internal refactors unless the refactor changes user-visible behavior.
- Keep each changeset summary to one concise user-facing sentence; put implementation detail in the PR or supporting docs.
## repo notes
- Local review artifacts are ignored on purpose. Leave them alone unless the user explicitly wants them updated, and do not commit them.
- Keep this doc short and architectural. Fresh-context agents can discover file paths themselves.
## commits
Commit titles should follow Conventional Commits. Format: `<type>[scope]: <description>`. Common types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `ci`, `build`. Use `!` or `BREAKING CHANGE:` footer for breaking changes. Description should explain the "why", not just the "what".