@xbghc/warden 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,20 +6,25 @@ with line comments you can copy back to the agent as a prompt.
6
6
  - Runs as a single local process per repository (`127.0.0.1` only, no auth, no database).
7
7
  - Diff sources: working tree, staged, working tree vs HEAD, a branch since it forked off its base (commits and
8
8
  uncommitted work together), any commit, any two refs, and git worktrees.
9
- - Side-by-side **Unstaged** and **Staged** file lists, so staging a hunk in your editor is what marks it reviewed.
9
+ - Worktrees are made and taken down from the page, one per agent branch, with the path ready to paste.
10
+ - Side-by-side **Unstaged** and **Staged** file lists: staged means reviewed. Stage from the UI by dragging
11
+ over the lines you have read (or a hunk, or a file), and unstage the same way from the Staged view.
10
12
  - GitHub-style unified / side-by-side diff with syntax highlighting, collapsed file tree, lazy per-file loading, context expansion, virtual scrolling.
11
13
  - Line comments (single line or a dragged range), Markdown, edit / delete.
12
14
  - One click copies all comments as an agent-readable prompt to the clipboard.
13
15
  - Review state (viewed files, comments, local issues, todos, preferences) persists outside the repo and survives restarts.
14
16
  - Comments follow the code: they move with a hunk that gets staged, re-attach after the agent edits the file, and are cleaned up once the change is committed.
15
17
  - Auto-refresh — warden watches the repository and reloads itself when you edit, stage, commit or switch branches.
16
- - Local issues (title, Markdown body, open/closed) that link comments.
17
- - Branch-scoped todos, each copied to the clipboard on its own — one task at a time for the agent.
18
+ - Local issues (title, Markdown body, open/closed) that link comments, and branch-scoped todos, each
19
+ copied to the clipboard on its own — one task at a time for the agent. Both are task lists in the
20
+ manner of Google Tasks: add at the top, Enter for the next one, tick to file it under 已完成, drag
21
+ into your own order, edit in place, 撤消 after a delete.
18
22
  - Commit history browser: grouped by day, branch / tag labels, search by message, sha, author or
19
23
  path (each a `git log` on the server, not a filter over the rows already loaded), and a
20
24
  first-parent view that folds merged branches into their merge commits.
21
25
  - Click a line number to jump to that line in a running nvim instance (WSL2 friendly).
22
- - Read-only with respect to git: only whitelisted read sub-commands are ever executed.
26
+ - Git is touched through whitelisted read sub-commands plus exactly one write, `git apply --cached`, which is
27
+ what the stage / unstage controls run. The working tree and HEAD are never written.
23
28
 
24
29
  ## Install / run
25
30
 
@@ -56,7 +61,8 @@ Several instances on the same repository can run at the same time.
56
61
  | Worktree, everything since base | `worktree:<path>:base:<ref>` | same, run inside the worktree |
57
62
 
58
63
  Refs accept anything git can resolve (`main`, `v1.2`, `HEAD~3`, a sha). `@` is `HEAD`.
59
- Worktrees are discovered with `git worktree list` and share the review state of the main repository.
64
+ Worktrees are discovered with `git worktree list` (and made in the *Worktrees* tab, see below) and
65
+ share the review state of the main repository.
60
66
  One whose directory is gone (git lists it as *prunable*) is not offered, and a remembered target
61
67
  inside a removed worktree falls back to the working tree on the next load. The comments and viewed
62
68
  flags kept under that worktree's key stay in the state file and come back if a worktree is created
@@ -80,11 +86,36 @@ everything since the branch forked off `<ref>` — the commits plus whatever is
80
86
  untracked. The diff runs against the merge base, so commits that landed on `<ref>` after the fork are
81
87
  not listed as reverted (which is what a plain `git diff <ref>` would do). The *Branch vs* field in the
82
88
  Commits panel suggests the base for you — the main worktree's branch when a sibling worktree is under
83
- review, otherwise `main` or `master` — and takes any ref. Unlike the three local views, `base` keeps its own
89
+ review, otherwise `main` or `master` — and takes any ref. Beside it the panel names the fork point (the
90
+ merge base with that ref, `GET /api/fork-point`) with how many commits the branch is ahead of it and how
91
+ many landed on the base since, and the list marks that commit with a *分叉自* chip and a rule above it:
92
+ what sits above is the branch's own work. Unlike the three local views, `base` keeps its own
84
93
  pool of comments and a commit never deletes them: the round under review is not over when the agent
85
94
  commits, so a comment whose lines changed stays *orphaned*, snippet and all, until you have checked
86
95
  the fix and delete or re-attach it.
87
96
 
97
+ ## Worktrees
98
+
99
+ Agents do their best work each in a worktree of its own, and the *Worktrees* tab in the top bar is
100
+ where those are made and taken down without a trip to the terminal:
101
+
102
+ - *新建 worktree* takes a branch and a base. A name that is not a branch yet becomes one from the
103
+ base (`main` or `master` unless you say otherwise; any ref goes) — `git worktree add -b <branch>
104
+ <path> <base>`; an existing branch is checked out as it is, unless another worktree already has it.
105
+ The path is suggested as a sibling of the main worktree named `<repo>-<branch>` (slashes become
106
+ dashes) and can be edited, within limits: it has to sit under the main worktree's parent directory,
107
+ outside every existing worktree, and be new or an empty directory.
108
+ - Each row names the checkout, its branch, and whether it is clean or how many paths `git status`
109
+ reports. *查看* switches the review to it (the kind of target carries over, as with the selector
110
+ in the top bar), *复制路径* is for the agent's prompt.
111
+ - *删除* runs `git worktree remove`, never with `--force` on its own: a worktree with uncommitted
112
+ changes is not removed until you confirm in the row, since those changes go with it, and whatever
113
+ else git refuses without `--force` is put to you the same way. With *一并删除已合并的分支* ticked
114
+ the branch goes too, by `git branch -d`: one that is not merged is kept and the toast says why. The
115
+ comments and viewed flags kept under the worktree's key stay in the state file, as before.
116
+ - A worktree whose directory was deleted behind git's back is listed struck through; *清理* drops that
117
+ one entry (`git worktree remove` handles it; nothing is pruned wholesale).
118
+
88
119
  ## Keyboard
89
120
 
90
121
  | Key | Action |
@@ -92,8 +123,36 @@ the fix and delete or re-attach it.
92
123
  | `r` | Refresh the current target (file list + open diff, then re-anchor comments) |
93
124
  | `j` / `k` | Next / previous file, walking Unstaged then Staged and switching view at the boundary |
94
125
  | `n` / `p` | Next / previous hunk |
126
+ | `s` | Stage (Unstaged view) or unstage (Staged view) the picked lines |
95
127
  | `Ctrl+Enter` | Save the comment being edited |
96
- | `Esc` | Cancel editing / close a panel / cancel re-attach mode |
128
+ | `Esc` | Drop the picked lines / cancel editing / close a panel / cancel re-attach mode |
129
+
130
+ ## Staging
131
+
132
+ The Staged block is the "reviewed" pile, and warden can move lines into it without a trip to the
133
+ terminal. In the Unstaged view every changed line has a small box at the left edge; press it and drag
134
+ to pick a range inside one hunk (in split view a replacement is one row, so its old and new line go
135
+ together). The picked rows are outlined in ink, a bar at the bottom of the diff says how many lines
136
+ are in, and `s` or the bar's button stages them. Each hunk header has *暂存此 hunk*, the file header
137
+ and the sidebar rows have *暂存文件* / *暂存*. The Staged view has the same controls the other way
138
+ round: *取消暂存* takes lines back out of the index.
139
+
140
+ A partial pick is turned into the patch `git add -p`'s edit mode would want — the unpicked deletions
141
+ stay as context and the unpicked additions are left out (the mirror image when unstaging) — and applied
142
+ with `git apply --cached`. It never touches the working tree, so what you did not pick is still
143
+ there to stage next. Comments on the lines you staged follow them into the Staged view (see
144
+ [Re-anchoring](#re-anchoring)); the file's *viewed* flag is dropped because its diff changed. Every
145
+ page open on the worktree reloads at once.
146
+
147
+ Limits, each reported as a plain error rather than a half-applied patch:
148
+
149
+ - Only the Unstaged and Staged views stage; `all`, `base`, commits and ranges are read-only.
150
+ - Binary files and mode-only changes go whole or not at all; a mode change rides along only with *暂存文件*.
151
+ - A staged deletion can be unstaged whole, not by line: the index has nothing left to put lines back into.
152
+ - A pick that would split a file's missing trailing newline from its neighbours is refused; include them.
153
+ - The request carries the hash of the diff the pick was made on. If the file or the index moved since
154
+ (the agent kept editing), the server answers 409 and the page reloads instead of staging the wrong lines.
155
+ - If another git process holds the index lock the server retries briefly, then gives up with 409.
97
156
 
98
157
  ## Comments and export
99
158
 
@@ -170,11 +229,28 @@ refreshes by hand.
170
229
 
171
230
  Notes attached to a branch rather than to a line of code, for the things you notice while reviewing
172
231
  that do not belong in a comment. They are not deleted when you commit. They sit in the right-hand
173
- rail next to the comments, under the *Todo* tab: type a title and press Enter to add one, expand a
174
- card to read or edit its Markdown body, filter by status, and switch between the current branch
175
- and all branches.
176
-
177
- Each card has its own *复制* button, and there is deliberately no "copy all": a todo is one task to
232
+ rail next to the comments, under the *Todo* tab, as a task list that works the way Google Tasks does:
233
+
234
+ - *添加 Todo* opens an empty row at the top; type the title and press Enter, and the next row opens
235
+ right under it. Esc or Backspace on an empty row drops it.
236
+ - Titles and details are edited where they are: click into the text. Enter at the end of a title
237
+ starts the next todo under it; Backspace on an emptied title deletes the todo. The details field
238
+ (Markdown) appears while the row is open, and is rendered under the title otherwise.
239
+ - The circle strikes the todo through and files it under *已完成 (N)* at the bottom, collapsed;
240
+ open it to tick one back or *全部删除*.
241
+ - Rows are dragged into your own order by the handle at their left edge. That order is what the state
242
+ file keeps, and new todos go on top.
243
+ - Delete asks nothing; the toast offers *撤消* for a few seconds.
244
+ - The picker at the top is the list selector: the current branch, any other branch that has todos,
245
+ or all of them with the branch on each row. A new todo goes to the branch being shown.
246
+
247
+ Issues (the *Issues* tab in the top bar) are the same list: closing an issue is ticking it, a
248
+ *已关闭* section holds the closed ones, and the comments linked to an issue sit under its open row
249
+ like subtasks, each with *跳转* and *解除关联*. Tick comments in the rail and the *创建 Issue* button
250
+ opens the drawer with the add row ready and the comments attached on creation; for an existing
251
+ issue, open its row and use *关联选中的评论*.
252
+
253
+ Each row has its own *复制* button, and there is deliberately no "copy all": a todo is one task to
178
254
  hand to an agent, and the agent it goes to is already working in that branch. So the clipboard gets
179
255
  the title and the body — no branch, repo, count or numbering:
180
256
 
@@ -225,17 +301,27 @@ move into the matching scope the first time the file is read.
225
301
 
226
302
  ## Security model
227
303
 
228
- Single user, local only. The server binds to `127.0.0.1`, executes git only through
229
- `execFile('git', [...])` with an argument whitelist (`rev-parse`, `diff`, `show`, `log`, `worktree list`,
230
- `ls-files`, `status`, `merge-base`), refuses option-looking refs and any write-capable flag, and never writes into the
231
- repository. Requests that would need anything else get HTTP 400.
304
+ Single user, local only. The server binds to `127.0.0.1` and executes git only through
305
+ `execFile('git', [...])`. Reads go through an argument whitelist (`rev-parse`, `diff`, `show`, `log`,
306
+ `worktree list`, `ls-files`, `status`, `merge-base`, `rev-list`) that refuses option-looking refs and any write-capable
307
+ flag; requests that would need anything else get HTTP 400. The writes are few and each composes its
308
+ own arguments. `POST /api/targets/:key/stage` runs `git apply --cached` (with `--reverse` for the
309
+ Staged view) on a patch the server itself builds from the diff it just produced — the patch is never
310
+ taken from the request, only the line indices are, and they are checked against the diff's hash
311
+ first. `POST /api/worktrees` and `POST /api/worktrees/remove` run `git worktree add`, `git worktree
312
+ remove` and `git branch -d` with a branch name git has validated, a ref that resolves, and a
313
+ path confined to the main worktree's parent directory. Nothing writes to the working tree or HEAD of
314
+ an existing checkout; refs change only when a worktree is made (its new branch) or removed (its merged
315
+ branch, on request), and the review state lives outside the repository. A mutating request the
316
+ browser labels as coming from another site (`Sec-Fetch-Site: cross-site`) is refused with 403, so a
317
+ page from elsewhere cannot drive the server through the browser it is open in.
232
318
 
233
319
  ## Development
234
320
 
235
321
  ```sh
236
322
  pnpm install
237
323
  pnpm dev # API server on :4100 (tsx watch) + Vite dev server on :5173 with /api proxied
238
- pnpm test # vitest: diff parser, anchoring, comment scopes, watcher, todos, export, state, HTTP API
324
+ pnpm test # vitest: diff parser, staging patches, anchoring, comment scopes, watcher, todos, worktrees, export, state, HTTP API
239
325
  pnpm typecheck
240
326
  pnpm build # dist/web (Vite) + dist/cli.js (tsup, zero runtime dependencies)
241
327
  node dist/cli.js path/to/repo
@@ -246,7 +332,7 @@ Layout:
246
332
  ```
247
333
  bin/cli.ts argument parsing, start server, open browser
248
334
  packages/shared types + target key / comment scope helpers (bundled into both sides)
249
- packages/server Hono API, git wrapper, diff parser, targets, anchoring, watcher, state, export, nvim
335
+ packages/server Hono API, git wrapper, diff parser, staging patches, targets, worktrees, anchoring, watcher, state, export, nvim
250
336
  packages/web React + Vite UI (shiki highlighting, @tanstack/react-virtual, react-markdown)
251
337
  test/ API integration tests on a generated git repository
252
338
  ```