Skip to content

feat(open): add complete-file viewer and lazy directory browser - #1148

Open
benvinegar wants to merge 8 commits into
mainfrom
feat/open-browser
Open

benvinegar wants to merge 8 commits into
mainfrom
feat/open-browser

Conversation

@benvinegar

Copy link
Copy Markdown
Member

Summary

Add hunk open [path] for reading complete syntax-highlighted files and browsing directories without a repository or changeset. The default path is ..

hunk open .
hunk open README.md
hunk open ~/project

The read-only browser supports lazy tree expansion, keyboard/mouse navigation, wrapping and horizontal scrolling, line numbers, themes, remappable commands, live refresh, hidden/ignored visibility, lightweight Git status, and opening the displayed file in $EDITOR.

Approach

  • Introduce an internal DocumentSource with opaque entry identities, explicit availability results, lazy listing/reading, observation, and optional editor capabilities. Filesystem and Git I/O stay in the application tier.
  • Add a document route to the existing HunkSessionHost, sharing renderer lifecycle, theme ownership, chrome, semantic keymaps and command construction.
  • Extract complete-document language registration/highlighting and native text measurement from review-specific directories; existing review/file-view consumers use those same implementations.
  • Keep review documents, stores, broker publication and public extension contracts unchanged. Browsing creates no synthetic DiffFile or hunks and does not register a review session.
  • Add CLI/config/README/website documentation and a minor Changeset.

Why a host feature rather than an extension?

Existing extension pane navigation and file views operate on files already represented in a review changeset. They cannot express an independent lazy collection of unchanged filesystem documents without inventing review data. This introduces the internal document-source seam and exercises it with a real host consumer; a public document extension API remains a separate, capability-based follow-up.

Safety and limits

  • Hidden and Git-ignored entries are excluded together by default, with an i toggle.
  • Symlinks are displayed but not followed. Binary/non-UTF-8, missing, unreadable, special and oversized entries have deliberate placeholders.
  • Reads are bounded to 1 MiB, directories to 10,000 entries, and expansion to 128 directories; shutdown cancels obsolete demand, releases observers and settles started reads.
  • Optional Git metadata disables hooks/fsmonitor and declared clean/process filters, does not recurse into submodules, and disables lazy object fetching and transport protocols.
  • Stdin, multiple paths and file:line addressing are not included in this MVP.

Validation

Ran on Linux x64; macOS and Windows were not tested on actual machines.

  • bun run typecheck
  • bun run lint
  • bun run format:check
  • bun run deps:check
  • bun run test — both shards pass, zero failures
  • bun run test:integration — 193 pass, 1 platform skip, zero failures on two full reruns
  • bun run test:tty-smoke — 10 pass
  • bun test test/review-conformance — 118 pass
  • bun test packages/hunk/src/app/documents packages/hunk/src/ui/documents test/pty/open-integration.test.ts — 21 pass
  • bun run build:npm && bun run check:pack — packed facade/consumer checks pass, including 37 documented extension examples
  • bun run website:check and bun run check:docs
  • bun run install:bin, followed by real-PTY checks of the compiled document browser and a split diff review
  • git diff --check

The initial full integration run hit two navigation timeouts; both passed in the dedicated navigation rerun and two subsequent full integration runs. bun run knip reports the same six unused files, one dependency and two exports as clean upstream a3321c82; reproduced against a temporary archive of that revision, with no new findings.

Coverage includes opaque identities, lazy demand, stale reads, observer teardown, ignored/tracked/hidden entries, binary/invalid UTF-8/large/missing/unreadable/symlink cases, atomic replacement, metadata hook/filter safety (including nested repositories), native wrapping, resizing, explicit keybinding unbindings, and keyboard/mouse navigation.

Real terminal evidence

Captured from the compiled binary in a real PTY using tuistory/Ghostty: Linux x64, 100 columns × 20 rows, github-dark-default, directory tree and complete-document layout. The sanitized fixture is not a repository. Syntax color is additionally asserted in the PTY tests.

Command: hunk open <fixture-directory> --theme github-dark-default. Keyboard expands src and opens example.ts; the PTY suite also clicks files/menus and exercises watch refresh and deletion.

  File  View  Help                          Open · hunk-open-preview-3lG30h
▼ hunk-open-preview-3lG30h         src/example.ts
  ▼ src                            1  // Complete source, not just changed lines.
      example.ts                   2
    README.md                      3  interface Document {
                                   4    title: string;
                                   5    lines: readonly string[];
                                   6  }
                                   7
                                   8  export function openDocument(title: string): Document {
                                   9    return {
                                  10      title,
                                  11      lines: [
                                  12        "Browse a directory lazily",
                                  13        "Read the complete file",
                                  14        "Use the same Hunk theme",
                                  15      ],
                                  16    };
                                  17  }
 Document · read-only · hidden/ignored hidden

This is a text dump of the captured terminal frame, not redirected CLI stdout or a mockup. A PNG was also captured locally; native GitHub media attachment remains to be uploaded manually.

This PR description was generated by Pi using gpt-6.1-sol

@vercel

vercel Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hunk-web Ignored Ignored Preview Oct 6, 2026 3:10pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[Medium risk] Adds a new file browser command with document viewing.

The PR should not merge until incomplete ignore queries are handled and the explicit environment-access requirement is satisfied.

Findings

  1. P1 Incomplete Ignore Results Expose Entries ▶
  2. P2 Security Path Checks Can Be Bypassed ▶
  3. P2 Direct Environment Access Violates Requirement ▶
Fix with agent prompt
### Issue 1
packages/hunk/src/app/documents/filesystemSource.ts:249
If `git check-ignore` takes longer than 1.5 seconds, the timeout kills it, but this code treats its partial output as complete. Ignored entries missing from that output are marked as not ignored and appear in the browser despite the default exclusion policy. Check that the query completed before using its results.

### Issue 2
packages/hunk/src/app/documents/filesystemSource.ts:204
If another process can replace entries in the browsed directory, it can swap a checked directory for a symlink before `opendir` uses the path. The browser can then list names outside the chosen collection. The same check-then-use gap affects editor authorization: replacing a checked regular file before `$EDITOR` opens its path can redirect editing outside the collection. These operations need to preserve the identity of the entry they checked.

**How this was verified:** The listing opens a path after its symlink check, and the editor receives a path string after a separate regular-file check.

### Issue 3
packages/hunk/src/app/documents/filesystemSource.ts:19
`gitQueryEnvironment` spreads `process.env` directly, and the new document bootstrap also defaults its environment parameter to `process.env`. The repository guideline says never to access `process.env` directly and to pass environment values from the application boundary. This requirement needs to be satisfied before merging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds hunk open as a complete-file viewer and lazy directory browser, with a filesystem document source, shared session chrome and highlighting, Git metadata, and documentation. The review found that a timed-out ignore query can expose entries meant to be hidden, filesystem checks can race with path use, and new environment access conflicts with a repository requirement.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  CLI["hunk open [path]"] --> Bootstrap["Document bootstrap"]
  Bootstrap --> Source["Filesystem document source"]
  Source --> Git["Optional Git metadata"]
  Bootstrap --> Host["HunkSessionHost"]
  Host --> Controller["Document browser controller"]
  Controller --> Tree["Lazy directory tree"]
  Controller --> Pane["Complete-document pane"]
  Pane --> Syntax["Shared syntax and text measurement"]
Loading

Reviews (1) · Last reviewed commit: "feat(open): add complete-file viewer and..."

const timeout = setTimeout(abort, 1500);
let ignored: Set<string>;
try {
ignored = new Set((await new Response(process.stdout).text()).split("\0"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Incomplete Ignore Results Expose Entries

If git check-ignore takes longer than 1.5 seconds, the timeout kills it, but this code treats its partial output as complete. Ignored entries missing from that output are marked as not ignored and appear in the browser despite the default exclusion policy. Check that the query completed before using its results.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/hunk/src/app/documents/filesystemSource.ts
Line: 249

Comment:
**Incomplete Ignore Results Expose Entries**

If `git check-ignore` takes longer than 1.5 seconds, the timeout kills it, but this code treats its partial output as complete. Ignored entries missing from that output are marked as not ignored and appear in the browser despite the default exclusion policy. Check that the query completed before using its results.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 251ad80: ignore results are used only after the process exits normally with status 0 or 1. Timeout, cancellation, and error exits make the directory unavailable rather than exposing entries based on partial output. Added regression coverage for timeout with partial output and an error exit, and normalized Git output paths for Windows.

This comment was generated by Pi using GPT-6.1

detail: "This directory is unavailable; symlinks are not followed.",
};
const entries: DocumentEntry[] = [];
for await (const child of await opendir(key)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security Path Checks Can Be Bypassed

If another process can replace entries in the browsed directory, it can swap a checked directory for a symlink before opendir uses the path. The browser can then list names outside the chosen collection. The same check-then-use gap affects editor authorization: replacing a checked regular file before $EDITOR opens its path can redirect editing outside the collection. These operations need to preserve the identity of the entry they checked.

How this was verified: The listing opens a path after its symlink check, and the editor receives a path string after a separate regular-file check.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/hunk/src/app/documents/filesystemSource.ts
Line: 204

Comment:
**Path Checks Can Be Bypassed**

If another process can replace entries in the browsed directory, it can swap a checked directory for a symlink before `opendir` uses the path. The browser can then list names outside the chosen collection. The same check-then-use gap affects editor authorization: replacing a checked regular file before `$EDITOR` opens its path can redirect editing outside the collection. These operations need to preserve the identity of the entry they checked.

**How this was verified:** The listing opens a path after its symlink check, and the editor receives a path string after a separate regular-file check.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 251ad80. Linux reads/enumeration now walk no-follow ancestors through retained directory handles and enumerate the checked descriptor via /proc/self/fd. Editors never receive the collection path: they edit a private copy, and writeback uses the retained original handle after identity/content conflict checks. Conflicts retain the edited copy for recovery. Regression tests cover directory swap-and-restore, file replacement, symlink redirection, and the real PTY editor workflow.

Non-Linux directory enumeration still uses portable Node path APIs with ancestry/identity revalidation; this is hardening, not a sandbox against an adversary repeatedly swapping and restoring ancestors. That limitation is explicit in docs/open.md rather than claiming the portable checks are race-proof.

This comment was generated by Pi using GPT-6.1


/** Prevent metadata inspection from fetching missing objects or launching transport helpers. */
function gitQueryEnvironment() {
return { ...process.env, GIT_NO_LAZY_FETCH: "1", GIT_ALLOW_PROTOCOL: "" };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Direct Environment Access Violates Requirement

gitQueryEnvironment spreads process.env directly, and the new document bootstrap also defaults its environment parameter to process.env. The repository guideline says never to access process.env directly and to pass environment values from the application boundary. This requirement needs to be satisfied before merging.

Context Used: guidelines.mdc Cursor rule (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/hunk/src/app/documents/filesystemSource.ts
Line: 19

Comment:
**Direct Environment Access Violates Requirement**

`gitQueryEnvironment` spreads `process.env` directly, and the new document bootstrap also defaults its environment parameter to `process.env`. The repository guideline says never to access `process.env` directly and to pass environment values from the application boundary. This requirement needs to be satisfied before merging.

**Context Used:** guidelines.mdc Cursor rule ([source](https://github.com/modem-dev/modem/blob/main/.cursor/rules/guidelines.mdc))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applying this suggestion: the cited rule belongs to modem-dev/modem, not this repository. Hunk has no blanket ban on process.env; its existing filesystem/process and editor boundaries use it. Please scope guideline findings to Hunk’s own AGENTS.md and CONTRIBUTING.md.

This comment was generated by Pi using GPT-6.1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Hunk's AGENTS.md and CONTRIBUTING.md do not prohibit direct process.env access, and the repository already uses it at application/process boundaries. My comment incorrectly carried over a guideline from another repository, so it is not an actionable finding for this PR. I'll scope future guideline findings to Hunk's own checked-in instructions.

@benvinegar

benvinegar commented Oct 6, 2026 •

Copy link
Copy Markdown
Member Author

Audited PR #1148 with Oxlint’s eslint/complexity rule (cyclomatic complexity), comparing against origin/main, and pushed the focused refactor in 4e24909.

New document hotspot Before After
DocumentApp 33 15
Command dispatcher 38 1; navigation helpers ≤7
Directory listing 43 7; checked enumeration 15
Editor workflow 35 19

Git metadata, bounded byte I/O, private-copy editing, navigation, keyboard routing, and content/status rendering now have focused modules. DocumentApp.tsx is 215 lines (previously 353); filesystemSource.ts is 290 (previously 547). .oxlintrc.json now enforces complexity ≤20 across both document directories.

The older high-complexity startup/review functions remain outside this refactor; the audit did not treat them as new document code. Added coverage for physical-row navigation, live tree selection, menu parity, overlay priority, status parsing, and bounded/partial byte I/O. Typecheck, lint, formatting, dependency boundaries, full unit tests, TTY smoke, and all 194 PTY tests pass. GitHub CI also passes, including Windows compatibility, Linux validation/packaging, PTY integration, and macOS/Windows compiled portability.

This comment was generated by Pi using GPT-6.1

This branch has not been deployed

No deployments
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