Skip to content
Merged
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
a72002d
feat(open): add complete-file viewer and lazy directory browser
benvinegar Oct 6, 2026
251ad80
fix(open): bind document operations to checked file identities
benvinegar Oct 6, 2026
b6656bc
test(open): canonicalize temporary collection paths on Windows
benvinegar Oct 6, 2026
49b0825
fix(open): preserve cancellation when an ignore query is interrupted
benvinegar Oct 6, 2026
dad40e3
test(open): cover Windows handle pinning during editor conflicts
benvinegar Oct 6, 2026
4e24909
refactor(open): split document workflows and bound function complexity
benvinegar Oct 6, 2026
c50c8d9
style(open): separate document functions and workflow phases
benvinegar Oct 6, 2026
82190d1
style(open): separate document test declarations
benvinegar Oct 6, 2026
0b400f3
fix(open): address initial document review regressions
benvinegar Oct 6, 2026
3d73ef1
fix(open): coordinate saves across document source instances
benvinegar Oct 6, 2026
85e8c22
fix(documents): retain all editor failure notices through shutdown
benvinegar Oct 6, 2026
8ea518c
fix(documents): reuse unchanged watch subscriptions
benvinegar Oct 6, 2026
46774b1
fix(ui): keep document keyboard burst state live
benvinegar Oct 6, 2026
530cd24
fix(ui): dispatch document menu accelerators
benvinegar Oct 6, 2026
0c27ac1
fix(ui): preserve logical document scroll anchors after layout
benvinegar Oct 6, 2026
a00f17e
fix(open): fill initial document and tree viewports
benvinegar Oct 7, 2026
dad0a92
fix(open): dismiss menus when focusing document content
benvinegar Oct 7, 2026
618c16b
fix(open): capture editor targets before asynchronous preparation
benvinegar Oct 7, 2026
e32740f
fix(documents): preserve pending changes across observation handover
benvinegar Oct 7, 2026
09ebed6
fix(documents): repair watches after same-path directory replacement
benvinegar Oct 7, 2026
094c3fd
fix(open): keep save coordination out of project markers
benvinegar Oct 11, 2026
5c575d3
fix(open): accommodate Windows account lookup cold starts
benvinegar Oct 11, 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
5 changes: 5 additions & 0 deletions .changeset/account-save-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Store document save coordination in the account's Hunk state directory without creating a `.hunk` project marker in the user home. This preserves project discovery and reload bounds for directories beneath the home, including Windows temporary files.
5 changes: 5 additions & 0 deletions .changeset/captured-document-editor-targets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Capture the document editor's file and visible line when editing is requested, so later scrolling or selection during copy preparation cannot retarget the launch.
5 changes: 5 additions & 0 deletions .changeset/checked-documents-stay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Harden document browsing and editor saves against path replacement, reject incomplete Git ignore results, and fix Windows path checks.
5 changes: 5 additions & 0 deletions .changeset/clear-document-menu-accelerators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Allow document menu shortcuts to refresh, edit, and quit with configured keybindings while keeping help and theme dialogs isolated.
5 changes: 5 additions & 0 deletions .changeset/coordinated-document-saves.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Coordinate document saves across Hunk windows in the same OS account to prevent overlapping writeback and preserve losing editors' recovery copies.
5 changes: 5 additions & 0 deletions .changeset/dismissed-document-menus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Dismiss document-browser menus when clicking the document or tree so subsequent keys follow the newly focused content.
5 changes: 5 additions & 0 deletions .changeset/filled-document-viewports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Fill initial document and tree viewports without requiring scrolling when native layout temporarily reports a negative scroll position.
2 changes: 2 additions & 0 deletions .changeset/focused-document-workflows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
5 changes: 5 additions & 0 deletions .changeset/fresh-documents-scroll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Preserve the visible source line in complete documents when wrapping, resizing, toggling line numbers, or refreshing shortened files.
5 changes: 5 additions & 0 deletions .changeset/lazy-documents-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": minor
---

Add `hunk open` to view complete syntax-highlighted files and browse directories with lazy loading, live refresh, and keyboard/mouse navigation.
5 changes: 5 additions & 0 deletions .changeset/patient-windows-account-lookup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Allow bounded cold PowerShell startup time when locating the Windows account's document save namespace. Account lookup failures still refuse writeback and preserve recovery copies.
5 changes: 5 additions & 0 deletions .changeset/quiet-watch-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Preserve pending filesystem refreshes when selecting a file or changing expanded directories in the document browser.
2 changes: 2 additions & 0 deletions .changeset/readable-document-spacing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
5 changes: 5 additions & 0 deletions .changeset/retained-document-editor-notices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Preserve every unique document editor failure notice and print its full recovery location after terminal teardown, even after later successful edits.
5 changes: 5 additions & 0 deletions .changeset/settled-document-reviews.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Prevent overlapping document edits and lost recovery notices on quit, preserve wrapped text beside scrollbars, release vanished directory expansions, and restore keyboard scrolling in controls help.
2 changes: 2 additions & 0 deletions .changeset/spaced-document-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
5 changes: 5 additions & 0 deletions .changeset/stable-document-watch-demand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Keep document browser watch subscriptions alive across unchanged refreshes so files created during directory metadata queries appear automatically.
5 changes: 5 additions & 0 deletions .changeset/steady-directory-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Keep document browsing updates live after watched directories are replaced or deleted and recreated at the same path.
5 changes: 5 additions & 0 deletions .changeset/tidy-document-keyboard-bursts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Keep document focus, help, themes, and menus consistent when keyboard or mouse actions arrive before the next render.
14 changes: 13 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
{
"rules": {
"no-control-regex": "off"
}
},
"overrides": [
{
"files": [
"packages/hunk/src/app/documents/*.ts",
"packages/hunk/src/ui/documents/*.ts",
"packages/hunk/src/ui/documents/*.tsx"
],
"rules": {
"complexity": ["error", { "max": 20 }]
}
}
]
}
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ ReviewIntent + caller facts -> planReviewIntent -> ReviewAction[] -> reducer ->
conformance under `test/session-broker-node/`. Run the dedicated command documented in
`test/README.md` when changing those areas.

## source formatting

- Separate functions, class methods, nested helpers, and test declarations with a blank line; keep JSDoc and test-specific comments attached to their declaration.
- Use blank lines between logical phases inside functions. Keep related statements grouped rather than packing an entire workflow into one dense block.

## code comments

- Add short JSDoc-style comments to functions and helpers.
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ private repositories and higher API limits. GitHub Enterprise is not currently s

Hunk auto-detects Jujutsu and Sapling checkouts, so `hunk diff [revset]` and `hunk show [revset]` use native revsets inside jj or Sapling workspaces. `hunk log --vcs jj` also reads JJ history directly, including in a non-colocated workspace. To override VCS detection, set `vcs = "git"` or `vcs = "jj"` or `vcs = "sl"` in [config](#config).

### Viewing complete files and browsing directories

```bash
hunk open # browse the current directory
hunk open README.md # view a complete, syntax-highlighted document
hunk open path/to/project # browse another directory without requiring a repository
```

The read-only browser loads directories and documents lazily and refreshes watched content.
Use arrows and Enter to navigate the tree, Tab to switch focus, `i` to show hidden/Git-ignored
entries, and `e` to open the displayed file in `$EDITOR`. Themes, menus, line numbers, and wrapping
use Hunk's normal controls. Symlinks are listed but not followed; binary, unreadable, missing,
and oversized files (over 1 MiB) show placeholders. See [the open workflow](docs/open.md) for details.

### Working with raw files and patches

```bash
Expand Down
10 changes: 9 additions & 1 deletion docs/module-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Other workspaces are bounded units rather than one tier in that stack:
adapters. They do not import Hunk source internals.
- `packages/term-video` owns terminal capture tooling and does not import Hunk source internals.

Current `core/` modules are `changeset/`, `history/`, `install/`, `patch/`, `process/`, `review/`,
Current `core/` modules are `changeset/`, `documents/`, `history/`, `install/`, `patch/`, `process/`, `review/`,
`run/`, `theme/`, `vcs/`, and `watch/`. Its root contains `bootstrap.ts`, `liveComments.ts`,
`reviewDescriptor.ts`, and `reviewDigest.ts` plus tests.

Expand All @@ -51,6 +51,14 @@ Intentional exceptions, allowed by the rules:
surface by design.
- Tests are excluded: they are colocated and free to reach across boundaries.

Complete-document browsing uses `core/documents/source.ts` rather than `DiffFile` or review
state. `app/documents` owns filesystem I/O and optional Git metadata; `ui/documents` consumes
only document capabilities and finalized preferences. `HunkSessionHost` routes documents,
history, and review in one renderer lifetime. Language selectors live in `core/documents`;
complete-document syntax paint lives in `ui/syntax`, and native word-wrap measurement lives in
`ui/text`. Review/file-view consumers use those same services. The existing public extension
review contracts and broker protocol remain unchanged.

## Module interiors

Tier rules say which trees may reach each other; interior rules say which _files_ in a tree
Expand Down
105 changes: 105 additions & 0 deletions docs/open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Complete-document browsing

`hunk open [path]` views a file or browses a directory. The default path is `.`. It requires an
interactive terminal, but not a repository or a diff.

```bash
hunk open README.md
hunk open ~/project
hunk open hypr/ --theme nord
hunk open file.ts --wrap --tab-width 4
```

## Controls

- Arrow keys or `j`/`k`: move through the focused tree or document.
- Enter/Space: open a file or expand/collapse a directory.
- Left/Right: collapse/expand the selected directory, or scroll the focused document horizontally.
- Tab: switch tree/document focus. `s`: toggle the tree.
- PageUp/PageDown, `g`/`G`, Home/End: scroll the focused surface.
- `i`: toggle hidden and Git-ignored entries together. Hidden entries are names starting with `.`.
- `e`: edit a private copy of the displayed regular file in `$EDITOR`, using the visible line as
the editor target; save back when the editor exits.
- `t`, `l`, `w`, `?`, `r`, `q`: themes, line numbers, wrapping, help, refresh, quit.
- File/View/Help menus provide the same actions with mouse support. F10 opens the menu by keyboard.

Common app/view commands keep their existing command ids. Document actions use `hunk.documents.*`:
`activate`, `edit`, `stepDown`, `stepUp`, `pageDown`, `pageUp`, `jumpToTop`, `jumpToBottom`,
`scrollLeft`, `scrollRight`, and `toggleExcluded`. User `[keybindings]` remaps apply to menus and help.

```toml
[open]
theme = "nord"
wrap_lines = true

[keybindings]
"hunk.documents.stepDown" = "ctrl+n"
"hunk.documents.stepUp" = "ctrl+p"
```

## Filesystem policy

Only immediate children of expanded directories are read. The document viewer mounts a bounded
row window and reads only the selected document. Refresh preserves the displayed document and
scroll position; selecting a directory does not replace the document already displayed.

In a Git worktree, Git supplies ignore rules and lightweight status markers. Metadata queries are
optional, bounded, and asynchronous; repository fsmonitor hooks and clean/process filters are
disabled. Status queries do not recurse into submodules or fetch missing objects. Ordinary
directory browsing works without Git. An interrupted or failed ignore query makes that directory
unavailable rather than treating partial ignore output as complete. Hidden and
ignored entries are initially excluded, but an explicitly named hidden file still opens normally.

Symlinks are listed but not followed, including explicitly opened links and symlinked descendants.
Devices and other non-regular entries are not opened. Binary/non-UTF-8 files, files over 1 MiB,
and missing or unreadable entries show a placeholder. Each directory is limited to 10,000 entries
and at most 128 directories may be expanded. Source reads remain bounded when a file grows while
it is being read. Terminal control sequences are stripped before painting.

On Linux, directory enumeration uses the checked directory's retained handle. Other platforms
recheck ancestry and directory identity before publishing entries, but portable Node APIs do not
provide handle-relative directory enumeration. Browsing is not a sandbox against a hostile process
that can repeatedly replace and restore ancestors; use it on trusted local directories.

Editors receive a private temporary copy, not a checked collection path. Hunk retains the original
file handle and checks that the file identity and content still match before writeback. Detected
replacement or modification cancels writeback and retains the edited copy at the path shown in the
notice. Editor errors also retain that copy. Known GUI editors receive `--wait`; custom editor
wrappers must wait until editing finishes. Each document browser permits one active editor action;
repeated editor actions in that browser are rejected while it is active. Quitting waits for the editor transaction to settle, and late
failures/recovery paths are printed after the terminal is restored. `$EDITOR` itself is trusted
code, not sandboxed.

Updated Hunk sessions in the same OS account and process namespace coordinate validation and writeback with
private, inode-keyed save claims under the OS account home's `.config/hunk/document-save-claims/`
directory. This state directory does not create a `.hunk` project marker in the account home.
This location is independent of per-session `HOME`, XDG and temporary-directory overrides. Account
lookup uses OS records, including built-in PowerShell without profiles on Windows; lookup or private
directory failures refuse writeback and retain the edited copy. Competing
saves are rejected with their editor copies retained; claims left by definitely dead processes are
removed. An unrelated process reusing an owner's PID can conservatively delay saving until it exits.
Save claims do not coordinate different hosts/accounts or container process namespaces, older Hunk
versions, or non-Hunk writers.
Those writers can still race the final conflict check, and in-place writeback is not crash-atomic.

Expanded directories and the displayed file's parent are observed for changes, covering atomic
file replacements and deletions. Watch notifications are debounced; manual and watch refreshes
serialize. A failed watcher can be worked around with `r`. Shutdown cancels pending source work,
releases observers, and waits for started reads to settle before tearing down the terminal.

## Architecture and current boundaries

`core/documents` defines opaque entry identities, complete text and availability results, and lazy
source capabilities. Filesystem operations and Git metadata live in `app/documents`; the browser
controller and geometry live in `ui/documents`. The existing session host routes this surface
alongside history and review, reusing terminal lifecycle, theme ownership, menu components,
keymaps, editor launching, syntax paint, and exact native text measurement.

Browsing never constructs `DiffFile`, hunks, or `ReviewDocument`, and it does not publish a review
session to the broker. User extensions remain review-specific and are not loaded by `open`.
The source contract is currently internal; a future public document extension API should expose
explicit capabilities without weakening review navigation or workspace-write policies.

Stdin, multiple path arguments, and `file:line` addressing are deferred. The browser writes only
when saving a private copy from an explicitly launched external editor; it does not reopen the
collection path for writeback.
14 changes: 14 additions & 0 deletions packages/hunk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ private repositories and higher API limits. GitHub Enterprise is not currently s

Hunk auto-detects Jujutsu and Sapling checkouts, so `hunk diff [revset]` and `hunk show [revset]` use native revsets inside jj or Sapling workspaces. `hunk log --vcs jj` also reads JJ history directly, including in a non-colocated workspace. To override VCS detection, set `vcs = "git"` or `vcs = "jj"` or `vcs = "sl"` in [config](#config).

### Viewing complete files and browsing directories

```bash
hunk open # browse the current directory
hunk open README.md # view a complete, syntax-highlighted document
hunk open path/to/project # browse another directory without requiring a repository
```

The read-only browser loads directories and documents lazily and refreshes watched content.
Use arrows and Enter to navigate the tree, Tab to switch focus, `i` to show hidden/Git-ignored
entries, and `e` to open the displayed file in `$EDITOR`. Themes, menus, line numbers, and wrapping
use Hunk's normal controls. Symlinks are listed but not followed; binary, unreadable, missing,
and oversized files (over 1 MiB) show placeholders. See [the open workflow](docs/open.md) for details.

### Working with raw files and patches

```bash
Expand Down
39 changes: 39 additions & 0 deletions packages/hunk/src/app/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,25 @@ const DIFF_OPTIONS = [

/** Non-session command tree consumed by the parser and generated reference. */
export const CLI_REFERENCE_COMMANDS = {
open: {
path: "open",
summary: "view a complete file or browse a directory",
synopsis: ["hunk open [path]"],
options: [
{ flag: "--theme <theme>", description: "named theme override" },
{ flag: "--line-numbers", description: "show line numbers" },
{ flag: "--no-line-numbers", description: "hide line numbers" },
{ flag: "--sidebar", description: "show the directory tree" },
{ flag: "--no-sidebar", description: "hide the directory tree" },
{ flag: "--wrap", description: "wrap long document lines" },
{ flag: "--no-wrap", description: "keep long document lines on one row" },
{ flag: "-x, --tab-width <columns>", description: "tab stop width: 1-16", parse: "tabWidth" },
],
details: [
"Defaults to the current directory. Reads are lazy and the browser refreshes automatically.",
"Press i to show hidden and Git-ignored entries. Symlinks are displayed but not followed.",
],
},
diff: {
path: "diff",
summary: "review diffs or compare two concrete files",
Expand Down Expand Up @@ -639,6 +658,7 @@ function renderCliHelp() {
"Desktop-inspired terminal diff viewer for agent-authored changesets.",
"",
"Commands:",
" hunk open [path] view a file or browse a directory",
" hunk diff [target] [-- <pathspec...>] review working tree changes or compare against a target",
" hunk diff <from> <to> compare two revisions",
" hunk diff --staged [-- <pathspec...>] review staged changes",
Expand Down Expand Up @@ -1260,6 +1280,7 @@ function requireReloadableCliInput(input: ParsedCliInput): CliInput {
input.kind === "extension-manage" ||
input.kind === "extension-cli" ||
input.kind === "history" ||
input.kind === "open" ||
input.kind === "update"
) {
throw new Error(
Expand Down Expand Up @@ -2514,6 +2535,24 @@ export async function parseCli(argv: string[]): Promise<ParsedCliInput> {
// Host bootstrap options must stay before a review command's `--` pathspec separator.
const reviewRest = [...extensionFlagTokens, ...rest];
switch (commandName) {
case "open": {
const command = createCliReferenceCommand("open").argument(
"[path]",
"file or directory",
".",
);
if (rest.includes("--help") || rest.includes("-h")) {
return { kind: "help", text: `${command.helpInformation().trimEnd()}\n` };
}
let path = ".";
let options: Record<string, unknown> = {};
command.action((value: string, flags: Record<string, unknown>) => {
path = value;
options = flags;
});
await parseStandaloneCommand(command, rest);
return { kind: "open", path, options: buildCommonOptions(options, argv) };
}
case "diff":
return parseDiffCommand(reviewRest, argv);
case "show":
Expand Down
Loading
Loading