Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .changeset/quiet-status-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
5 changes: 5 additions & 0 deletions .changeset/tidy-status-workspaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": minor
---

Add a live read-only workspace status view with sibling inspection, shared-theme navigation into Log and full diff reviews, indented tracked/untracked groups with readable theme-colored staging facts and independent ten-file expansion, clearly separated sibling worktrees, and static or versioned JSON output.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,14 @@ hunk show # review the latest commit
hunk show HEAD~1 # review an earlier commit
hunk log # browse history on a terminal; print when redirected
hunk log --static # force static output, paging when needed
hunk status # live workspace compass and sibling worktrees (Git)
hunk status --static # print one read-only workspace snapshot
hunk status --json # emit one versioned workspace snapshot
```

`hunk status` uses provider-owned read-only workspace facts; no fetch or repository mutation is performed.
See [workspace status](docs/status.md) for navigation, live refresh, JSON and fetch-age provenance.

`hunk log` is one auto-responsive, read-only history surface, not a repository manager. On a
terminal it opens the desktop history browser; pipes and redirects receive shell-native static
records automatically, and `--static` forces static output that pages only when needed. The selected VCS adapter
Expand Down
48 changes: 46 additions & 2 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,9 @@ and retires the replaced instance at that explicit ownership boundary.

### `hunk.apiVersion`

The API generation this Hunk speaks (currently `25`). Branch on it if you want
one file to support several Hunk versions. Version 25 adds Promise-returning watch signatures and
The API generation this Hunk speaks (currently `26`). Branch on it if you want
one file to support several Hunk versions. Version 26 adds optional read-only workspace status,
sibling inspection and status review planning; version 25 adds Promise-returning watch signatures and
watch cancellation; version 24 adds review metadata to VCS patch results and
short display revisions to commit descriptors; version 23 adds canonical unified-layout fields
while preserving the previous event vocabulary; version 22 adds frame-derived pane preferred sizing,
Expand Down Expand Up @@ -498,6 +499,49 @@ those two reserved extensions are skipped with a notice.
This API selects a language already available to Pierre/Shiki. It does not load a new syntax
grammar; an unknown language remains plain text.

### Workspace status capability

API version 26 adds optional `ExtensionVcsAdapter.status`, independent of `history` and
review operations. Providers without it report unsupported for `hunk status`; detection never
falls back from a native JJ/Sapling workspace to Git status semantics. The bundled Git provider
implements the capability in `packages/hunk-git/src/status.ts`.

- `read({ targetPath? }, context)` returns an `ExtensionVcsStatusSnapshot` with `schemaVersion: 1`.
`context.cwd` is the launch repository authority; a target may inspect only that repository's
worktrees. A missing repository/worktree rejects. No process-wide cwd change, extension loading,
fetch, checkout, staging or other mutation belongs in this capability.
- `readSiblings(snapshot, context)` returns bounded same-repository summaries. Return an
unavailable/error row, not zero changes, when a sibling cannot be read. The initial snapshot
can publish `siblings: { state: "loading" }` before this slower scan completes.
- `planReview(snapshot, actionId, context)` revalidates source/target identity and the opaque
snapshot token, then returns `{ cwd, input }` for the existing full working-tree comparison.
The UI passes only a provider-offered action id; it does not construct revision expressions or
narrow the comparison to a selected file. A live working tree can still change after planning;
the token is not content attestation.
- Optional async `watchPlan(snapshot, context)` uses the existing watch-plan vocabulary for
worktree and shared/per-worktree metadata. Hosts keep periodic polling for missed events,
serialize/coalesce refreshes, cancel superseded reads and reject late generations.

Every asynchronous operation receives `context.signal` and must honor cancellation, bound its
I/O, and reap subprocesses. Status results allow at most 20,000 unique changed paths, 100 sibling
rows and 32 review actions. Oversized current status fails explicitly; bounded sibling lists set
`truncated`. Fields marked unknown/error must not become clean, aligned, or zero-count results.
The extension boundary validates and copies snapshots before publishing them. Paths retain
literal Unicode, tabs and newlines for navigation/JSON; renderers escape control characters.
Providers without an index omit `paths[].index` rather than inventing Git staging states.

`changedPathCount` counts unique destination paths, including rename destinations and untracked
files. A partially staged path appears once with independent index/worktree states. Submodule
commit/tracked/untracked changes and conflicts stay explicit. No summed numstat is presented as
a net change; detailed comparisons belong in the existing diff view.

Fetch timestamps are optional facts with provenance, not observation timestamps. Git retains the
inspected worktree's local `FETCH_HEAD` mtime with `local-fetch-head-mtime` provenance in structured
facts: it is not authoritative for the named upstream and may describe a different remote or a
manually modified file. Human output therefore omits age; `(last fetched 18m ago)` may appear only
when provenance supports that claim. Hunk never fetches to populate it. Missing/unreadable metadata
remains unknown/error. See [workspace status](status.md) for command and scan policy.

### `hunk.registerVcsAdapter(adapter)`

Contribute an additional VCS backend. This is the same call Hunk's own bundled
Expand Down
108 changes: 108 additions & 0 deletions docs/status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Workspace status

`hunk status --static` prints a compact current-worktree snapshot followed by sibling worktrees.
`hunk status --json` prints the same normalized facts as a JSON object with `schemaVersion: 1`,
without color or paging. Redirects automatically select static output; `--static` pages only
when the snapshot exceeds terminal height, using the ordinary Hunk pager policy.

In a terminal, `hunk status` opens one live workspace compass. Select rows with ↑/↓ (or J/K),
press Enter to open a path in its full comparison or inspect a sibling, U for working-tree changes,
S for staged changes, and L for the existing Log. F10 opens the normal menus, T selects a theme,
R refreshes, and W expands/collapses the bounded worktree list. Menu actions and clickable review
labels provide the same actions with a mouse; double-click a path or sibling to open it.

Q returns from diff to its caller, from Log to status, or from an inspected sibling to its origin;
at the root it quits. Escape/Backspace also return from sibling inspection. Pending preparation
can be cancelled with Q; failures leave the caller usable. Cleanup failures while returning from
Log are reported on status without requiring a second Q or blocking shutdown. Ctrl-C and OS shutdown signals end the
whole session. These journeys share one renderer, the ordinary Pierre-backed multi-file review
stream and the existing Log, not nested terminal applications. Selection and scrolling survive
returns and refresh. Narrow terminals stack path facts rather than adding a file inspector;
arrow/page keys and the mouse wheel scroll within a row taller than the viewport before moving
to another row, keeping all wrapped facts reachable.

Status uses normal config, theme/custom-theme, extension enablement and trust resolution. Its
bootstrap retains one ExtensionSession, launch options, keybindings, session initialization and
view-preference baseline. Inspecting another worktree changes only the provider's explicit target;
it does not load or auto-trust that sibling's extensions or change the shell cwd. Custom palettes,
keybindings and experimental launch opt-ins remain owned by the launch session, including during
review reloads. The resolved transparent-background preference also spans status, Log and diff
through the shared surface-theme derivation. Theme and review preference changes seed later reviews opened directly or through
Log, and the root status surface owns the normal save-preferences prompt. Only a mounted review
registers with the session broker; its source and cwd refer to the inspected target.

## Facts and failures

Git is the initial provider. Native JJ/Sapling detection remains authoritative: unsupported
providers exit nonzero, rather than silently using Git. Explicit `--vcs git` selects Git when
that is the intended comparison model. Outside-repository and bare/no-worktree invocations exit
nonzero with a diagnostic on stderr (including with `--json`). Partial sibling failures remain
in the snapshot and do not fail a readable current worktree.

Paths appear in two counted groups: **Tracked changes** and **Untracked files**. Indented rows
initially follow provider order; live updates retain surviving paths in place within each group and
append new paths. Each group independently shows up to ten files (not wrapped lines). When more
exist, select **and N more files** and press Enter or Space, or click it, to expand that group;
**Show fewer** collapses it again. Exactly ten files need no toggle. Expansion stays local to the
status presentation and survives refresh, Log/diff visits, and sibling inspection/back. Expanding
does not open a file or move focus. Collapsing a group with a now-hidden selected file moves focus
to the group's toggle and brings it into view. Static output keeps the groups and indentation but
prints every file; JSON remains the complete, unchanged fact snapshot.

Tracked paths use readable Git-familiar facts such as `modified (staged)`, `new file (staged)`,
`deleted (unstaged)`, and `renamed (staged)`. Mixed staging displays both sides explicitly, counting
the path only once. There are no XY markers, leading unchanged dots, or marker legends; ordinary
untracked rows need only their filename under **Untracked files**. Staged facts use the active theme's
positive (green-family) sign color; unstaged/untracked facts use its negative (red-family) sign color,
regardless of change type. Custom sign-color overrides are honored. Single-state filenames share
that color; mixed filenames stay neutral with both facts colored independently. Text stays meaningful
without color. Conflicts retain an explicit label and attention color. Rename origins, type changes,
submodule facts and unavailable/error states remain visible, with hanging indentation on wrapped file
rows. A spaced, ruled Other worktrees heading separates the sections in the scrollable stream;
sibling branch identity is emphasized over muted location and facts. No selected-file inspector or
line-count aggregate duplicates the existing diff view. The primary action strip offers the existing
review actions and, only while inspecting a sibling, Back. Log and Quit remain available through
their existing keys and menus rather than occupying that strip.
Snapshot tokens revalidate identity/status when planning reviews, not immutable file contents;
opening a live diff reads the actual working tree through the existing review loader. An explicit
untracked-row open includes untracked files in that full comparison even when launch config sets
`exclude_untracked = true`; its refreshes keep that effective input. Aggregate review actions
and later independent opens still honor launch config. Ignore rules and source safety limits
remain unchanged.

Upstream text omits zero counters: `3 ahead · 1 behind origin/main`. Aligned, absent, deleted
upstream, detached HEAD and unborn branches have distinct facts. Human output omits fetch age:
the current Git provenance records only the inspected worktree's local `FETCH_HEAD` mtime,
which may refer to another remote or have been manually changed. Structured facts retain that
timestamp and `local-fetch-head-mtime` provenance, or explicit unknown/error states when metadata
is missing/unreadable. The approved suffix `(last fetched 18m ago)` requires provenance that
supports that claim; local file mtime does not. Observation time is never used as fetch time,
and no network fetch runs.

## Bounds and refresh seam

Current status loads first; sibling enumeration is a separate cancellable call. Git queries are
shell-free with optional index locking disabled, a 5-second timeout, and an 8 MiB combined output
limit per subprocess. Current paths are capped at 20,000 (exceeding the cap is an error, not a
truncated clean snapshot); sibling scans return at most 100 rows with four concurrent reads and
an explicit truncation flag. Invalid/truncated porcelain and non-UTF-8 paths fail explicitly.
All counts represent unique destination paths, not overlapping staged/unstaged totals.

Linked worktree metadata is resolved separately from the common Git directory. Locked but
accessible worktrees remain inspectable read-only; bare, prunable, missing and inaccessible
worktrees remain distinguishable. Failure to read operation metadata is not reported as idle.

`StatusBootstrap.load`, `loadSiblings`, `planReview` and `watchPlan` accept route cancellation.
`close()` cancels and drains active provider reads; the session host separately shuts down its
ExtensionSession exactly once. Watch plans cover the current tree plus shared/per-worktree Git
metadata and prune object storage and Git-ignored directories. The controller coalesces event bursts,
serializes current reads, and polls every five seconds while watching (more frequently if watching fails).
Current facts appear before sibling summaries. Current refreshes do not wait for a secondary
scan, and same-target safety polls let that scan finish even when it spans multiple poll intervals.
Secondary results update only sibling facts, never newer current observations; the next refresh
after completion starts another scan. Suspending or changing targets cancels and drains the scan. Refreshing/stale/error states stay explicit; prior
sibling rows remain visible during refresh rather than vanishing under the cursor. Target changes
cancel and drain reads/watchers. Observations suspend during Log/diff and resume on return without
resetting navigation. Static CLI never starts a status watcher.

The renderer-free public API is documented in [extensions](extensions.md#workspace-status-capability).
2 changes: 2 additions & 0 deletions packages/hunk-git/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
openGitHistory,
planGitHistoryRangeReview,
} from "./history";
import { createGitStatusCapability } from "./status";
import { gitEndpointSourceSpec, readGitFileSource } from "./source";
import { commitReviewInfo, comparisonReviewInfo } from "@hunk/vcs/review-info";
import {
Expand Down Expand Up @@ -352,6 +353,7 @@ export function createGitVcsAdapter({
name: "Git",
detect: detectGitRepo,
detectionPriority: HUNK_VCS_DETECTION_BASELINE_PRIORITY,
status: createGitStatusCapability(gitExecutable),
history: {
open(input, { cwd }) {
return openGitHistory(input, { cwd, gitExecutable });
Expand Down
Loading
Loading