@xbghc/warden 0.15.0 → 0.16.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
@@ -1,43 +1,11 @@
1
1
  # warden
2
2
 
3
- Local web UI for reviewing git diffs — built for reviewing changes that a coding agent just made,
4
- with line comments you can copy back to the agent as a prompt.
3
+ A local web page for reviewing what a coding agent just changed in a git repository, and for handing
4
+ your review back to it — line comments the agent fetches and answers itself.
5
5
 
6
- - Runs as a single local process per repository (`127.0.0.1` only, no auth, no database).
7
- - Diff sources: working tree, staged, working tree vs HEAD, a branch since it forked off its base (commits and
8
- uncommitted work together), any commit, any two refs, git worktrees, and checkpoints.
9
- - Checkpoints: note the working tree as it is — untracked files included — and later see only what
10
- changed since, staged, committed or neither. Taking one writes nothing to the repository (see
11
- [Checkpoints](#checkpoints)).
12
- - Worktrees are made and taken down from the page, one per agent branch, with the path ready to paste.
13
- Their directories are numbered slots beside the repository that get reused rather than remade:
14
- releasing one keeps the directory, installed dependencies included, for the next branch.
15
- - Side-by-side **Unstaged** and **Staged** file lists: staged means reviewed. Stage from the UI by dragging
16
- over the lines you have read (or a hunk, or a file), and unstage the same way from the Staged view.
17
- - Debug code marked in a comment is folded out of the review and left out of whole-file staging, and
18
- the Staged block warns while any of it is in the index (see [Debug code](#debug-code)).
19
- - Code blocks fold by indentation, as an editor does for a language it has no grammar for: the
20
- chevron beside a line that heads one shuts its body, and the head then says how many lines and
21
- changes it hides. A dragged pick across a shut block takes none of them; stage the hunk to take them.
22
- - GitHub-style unified / side-by-side diff with syntax highlighting, collapsed file tree, lazy per-file loading, context expansion, virtual scrolling.
23
- - Line comments (single line or a dragged range), Markdown, edit / delete.
24
- - One click copies all comments as an agent-readable prompt to the clipboard.
25
- - Review state (viewed files, comments, local issues, todos, preferences) persists outside the repo and survives restarts.
26
- - 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.
27
- - Auto-refresh — warden watches the repository and reloads itself when you edit, stage, commit or switch branches.
28
- - Local issues (title, Markdown body, open/closed) that link comments, and branch-scoped todos, each
29
- copied to the clipboard on its own — one task at a time for the agent. Both are task lists in the
30
- manner of Google Tasks: add at the top, Enter for the next one, tick to file it under 已完成, drag
31
- into your own order, edit in place, 撤消 after a delete.
32
- - Commit history browser: grouped by day, branch / tag labels, search by message, sha, author or
33
- path (each a `git log` on the server, not a filter over the rows already loaded), and a
34
- first-parent view that folds merged branches into their merge commits.
35
- - Click a line number to jump to that line in a running nvim instance (WSL2 friendly).
36
- - Git is touched through whitelisted read sub-commands plus a few writes the server composes itself:
37
- `git apply --cached`, which is what the stage / unstage controls run, and the worktree slot
38
- operations (`worktree add` / `remove`, `branch`, `switch`). Reviewing and staging never write the
39
- working tree or HEAD; only checking a branch out into a free slot, or releasing one, does.
40
- Checkpoints write into warden's own index copy and object store, never the repository.
6
+ One process per repository, on `127.0.0.1` only, with no account and no database. It stages from the
7
+ page and manages worktree slots, but otherwise leaves the repository alone; see
8
+ [State and security](docs/internals.md).
41
9
 
42
10
  ## Install / run
43
11
 
@@ -53,6 +21,8 @@ or install globally:
53
21
  ```sh
54
22
  npm i -g @xbghc/warden
55
23
  warden [repoPath] [--port <n>] [--no-open] [--no-update-check]
24
+ warden feedback [--peek] # for the agent, see Handing comments to the agent
25
+ warden reply <id> <message>
56
26
  ```
57
27
 
58
28
  The server picks the first free port from 4100 (or `--port`), prints the URL and tries to open a
@@ -66,113 +36,34 @@ for an npx run, whose cache otherwise keeps serving the old copy, `npm i -g @xbg
66
36
  for a global install. The check never delays startup and fails silently. `--no-update-check`,
67
37
  `WARDEN_NO_UPDATE_CHECK=1`, `NO_UPDATE_NOTIFIER=1` or `CI` turn it off.
68
38
 
69
- ## Targets (what is being reviewed)
39
+ ## The loop
40
+
41
+ A round with an agent, in the order it happens:
42
+
43
+ 1. **Point the agent at a worktree.** In *Worktree*, *检出分支* puts a branch into a numbered slot
44
+ beside the repository (`<repo>-1`, …) that keeps its installed dependencies between branches;
45
+ *复制路径* is for the agent's prompt. Or let it work in the main checkout.
46
+ 2. **Review what it did.** *工作区* shows 未暂存 and 已暂存 side by side. Read a change, then stage it —
47
+ drag over the lines, or take a hunk or a file: staged means reviewed, and the figure at the top
48
+ counts it. Commits, branches (*审阅整条分支*) and ranges are reviewed with an *已读* box instead.
49
+ 3. **Comment.** The `+` beside a line, or a drag over several, opens a comment in the rail on the
50
+ right. A task that is bigger than one place goes in *待办*, and can carry comments with it.
51
+ 4. **Hand it back.** The agent runs `warden feedback` in its worktree and gets every comment it has
52
+ not had yet; or copy them with *复制评论* and paste. Either way a checkpoint is taken, so
53
+ *对比检查点* afterwards shows only what the agent did about this round.
54
+ 5. **Close the round.** The agent answers each comment with `warden reply <id> "…"`; the answer
55
+ appears under the comment, marked *待确认*. *解决* accepts it, *回复* asks again and sends the
56
+ comment back out with the thread. The *Worktree* view counts, per worktree, what is still
57
+ waiting on either side.
58
+
59
+ For step 4 to happen without being asked, put this in the agent's instructions (`CLAUDE.md`,
60
+ `AGENTS.md`, …):
70
61
 
71
- | Target | Key | Git equivalent |
72
- |---|---|---|
73
- | Working tree, uncommitted (incl. untracked) | `working` | `git diff` + `git diff --no-index /dev/null <file>` |
74
- | Staged | `staged` | `git diff --cached` |
75
- | Working tree, everything vs HEAD | `all` | `git diff HEAD` (+ untracked) |
76
- | Branch, everything since it forked off a base (committed or not) | `base:<ref>` | `git diff $(git merge-base <ref> HEAD)` (+ untracked) |
77
- | One commit | `commit:<sha>` | `git diff <sha>^ <sha>` |
78
- | Two refs | `range:<base>..<head>` | `git diff <base>...<head>` |
79
- | Worktree, working tree | `worktree:<path>:working` | same, run inside the worktree |
80
- | Worktree, branch vs base | `worktree:<path>:range:<base>..<head>` | same, run inside the worktree |
81
- | Worktree, everything since base | `worktree:<path>:base:<ref>` | same, run inside the worktree |
82
- | Working tree since a checkpoint (incl. untracked) | `checkpoint:<n>` | `git diff <checkpoint tree>`, see [Checkpoints](#checkpoints) |
83
- | Worktree, since one of its checkpoints | `worktree:<path>:checkpoint:<n>` | same, run inside the worktree |
84
-
85
- Refs accept anything git can resolve (`main`, `v1.2`, `HEAD~3`, a sha). `@` is `HEAD`.
86
- Worktrees are discovered with `git worktree list` (and made in the *Worktree* view, see below) and
87
- share the review state of the main repository.
88
- One whose directory is gone (git lists it as *prunable*) is not offered, and a remembered target
89
- inside a removed worktree falls back to the working tree on the next load. The comments and viewed
90
- flags kept under a worktree's key stay in the state file until a branch is checked out into that
91
- slot again, which drops them: they were about another branch.
92
-
93
- The sidebar is the navigation, and the only navigation: the picker at its top — *工作区*, *提交历史*,
94
- *Worktree* — fills the left column with the controls for what the middle is showing, and the top bar
95
- carries none of it. There is no separate target switcher: *工作区*'s two blocks *are* the working
96
- tree, a commit is picked from the *提交历史* list, and the *审阅整条分支* and *对比两个 ref* forms below
97
- its filters open the other two. While a commit, range or branch is under review the picker gains an
98
- entry naming it — *提交 89d8ba2*, *main..feature*, *分支 vs main* — and reads that while its files are
99
- in front, so the control at the top of the column says what the files below belong to; *工作区* is
100
- then the way back.
101
-
102
- `working`, `staged` and `all` are the three **local views** of one worktree. While any of them is
103
- selected the sidebar shows two blocks — 未暂存 (`working`) and 已暂存 (`staged`) — instead of a
104
- single tree, and clicking a file switches to the view it belongs to. A file that is only partly
105
- staged appears in both. Above them sits the one figure in the app: how many of the files under review
106
- are fully staged, which is what "staged means reviewed" amounts to. Switching between the three keeps
107
- your comments, the draft you are typing and the current selection; only the diff is reloaded. `all`
108
- has no control of its own (a saved `lastTarget` can still restore it; *审阅整条分支* against `@` shows
109
- the same diff). Commit, range and `base` targets keep the single tree, and their figure counts 已读
110
- instead, since nothing can be staged there.
111
-
112
- So there is one mark of "done" per kind of target, never two. In the local views it is staging and
113
- nothing else: a row has no 已读 box, and which block a file sits in is the whole of its review state.
114
- Everywhere else it is the [已读 mark](#viewed-files).
115
-
116
- `base:<ref>` is for a branch a coding agent has been working on, committing as it goes: one tree with
117
- everything since the branch forked off `<ref>` — the commits plus whatever is still uncommitted or
118
- untracked. The diff runs against the merge base, so commits that landed on `<ref>` after the fork are
119
- not listed as reverted (which is what a plain `git diff <ref>` would do). The *相对于* field under
120
- *审阅整条分支* suggests the base for you — the main worktree's branch when a sibling worktree is under
121
- review, otherwise `main` or `master` — and takes any ref. Under it the form names the fork point (the
122
- merge base with that ref, `GET /api/fork-point`) with how many commits the branch is ahead of it and how
123
- many landed on the base since, and the list marks that commit with a *分叉自* chip and a rule above it:
124
- what sits above is the branch's own work. Unlike the three local views, `base` keeps its own
125
- pool of comments and a commit never deletes them: the round under review is not over when the agent
126
- commits, so a comment whose lines changed stays *orphaned*, snippet and all, until you have checked
127
- the fix and delete or re-attach it.
128
-
129
- ## Worktrees
130
-
131
- Agents do their best work each in a worktree of its own, and the sidebar's *Worktree* view is
132
- where those are made and taken down without a trip to the terminal. The directories are numbered
133
- **slots** beside the main worktree — `<repo>-1`, `<repo>-2`, … (工位 in the UI) — rather than one
134
- directory per branch: a directory is where the dependencies get installed, and a fresh one for every
135
- branch meant installing them for every branch. A slot outlives its branch. Releasing it leaves the
136
- directory behind, `node_modules` and all, and the next branch is checked out into it.
137
-
138
- - *检出分支*, the form in the sidebar, takes a branch, a base and a slot. An existing branch is
139
- checked out as it is, unless another worktree already has it. A branch only a remote has is
140
- checked out tracking the remote one (what `git worktree add` itself guesses) rather than started
141
- afresh; when several remotes have it, the base says which. The branch list offers local branches
142
- only, since a remote can carry thousands: the name typed is looked up on its own
143
- (`GET /api/worktrees/remotes`), so the form says it is a remote branch before you submit. Any
144
- other name becomes a branch from the base (`main` or `master` unless you say otherwise; any ref
145
- goes). The server looks the name up when the request arrives, so a branch fetched or made after the page
146
- loaded its list is still found, and a base typed for a name that already exists is refused.
147
- - The slot picker lists the free slots, lowest first, then *新目录*: the next number, which is made
148
- with `git worktree add [--track] [-b <branch>] <repo>-<n> [<base>]`. Into a free slot the checkout
149
- is `git branch [--track] <branch> <base>` in the main worktree — so a base such as `HEAD` means what
150
- it would to `worktree add` — followed by `git switch <branch>` in the slot, which rewrites the
151
- tracked files and leaves the ignored ones alone; the toast says which of the two happened. Nothing
152
- in the request names a directory: the server composes the path from the number, so a checkout
153
- cannot land anywhere but beside the main worktree. Whatever review state was kept under the slot's
154
- key (comments, viewed flags, its `base` pool included) is dropped: it was about another branch.
155
- - Each row names the branch, its directory, and whether it is clean or how many paths `git status`
156
- reports. *查看* switches the review to it (the kind of target carries over), *复制路径* is for the
157
- agent's prompt. A free slot is a row of its own, marked *空闲*, and its *检出到这里* points the form
158
- at it.
159
- - *释放* is what a slot's row offers, and it always asks first, in the row: `git switch --detach` in
160
- the slot leaves the last commit checked out with no branch on it and touches no file, and the slot
161
- is free for the next checkout. A slot with uncommitted changes is not released until you confirm
162
- again, since those changes go: only that second request discards them — `git reset --hard`, then
163
- `git clean -fd` for the untracked files, never `-x`, so the ignored ones, the installed
164
- dependencies above all, stay. *一并删除分支* is offered in the same confirmation,
165
- ticked by default, and takes the branch by `git branch -d`: one that is not merged is kept and the
166
- toast says why. *连目录一起删除* turns the release into a removal.
167
- - *删除* is for a worktree that is not a slot (one made by hand, wherever it is) and for a free slot
168
- whose directory you no longer want; a slot on a branch gets it through *连目录一起删除*. It runs
169
- `git worktree remove` without `--force`: uncommitted changes are put to you first, as with a
170
- release, and so is whatever else git refuses without `--force`.
171
- - A worktree whose directory was deleted behind git's back is listed struck through; *清理* drops that
172
- one entry (`git worktree remove` handles it; nothing is pruned wholesale). A slot in that state
173
- cannot be released or checked out into, and its number is skipped until the entry is gone.
174
-
175
- `git switch` needs git 2.23 or newer.
62
+ ```markdown
63
+ When you finish a task, run `warden feedback` (or `npx @xbghc/warden feedback`) and address every
64
+ comment it prints. After dealing with each one, run `warden reply <id> "<what you changed, or why
65
+ you did not>"`.
66
+ ```
176
67
 
177
68
  ## Keyboard
178
69
 
@@ -185,327 +76,17 @@ directory behind, `node_modules` and all, and the next branch is checked out int
185
76
  | `Ctrl+Enter` | Save the comment being edited |
186
77
  | `Esc` | Drop the picked lines / cancel editing / cancel re-attach mode / shut the rail / back to the files under review |
187
78
 
188
- ## Staging
189
-
190
- The Staged block is the "reviewed" pile, and warden can move lines into it without a trip to the
191
- terminal. In the Unstaged view every changed line has a small box at the left edge; press it and drag
192
- to pick a range inside one hunk (in split view a replacement is one row, so its old and new line go
193
- together). The picked rows are outlined in ink, a bar at the bottom of the diff says how many lines
194
- are in, and `s` or the bar's button stages them. Each hunk header has *暂存此 hunk*, the file header
195
- and the sidebar rows have *暂存文件* / *暂存*. The Staged view has the same controls the other way
196
- round: *取消暂存* takes lines back out of the index.
197
-
198
- A partial pick is turned into the patch `git add -p`'s edit mode would want — the unpicked deletions
199
- stay as context and the unpicked additions are left out (the mirror image when unstaging) — and applied
200
- with `git apply --cached`. It never touches the working tree, so what you did not pick is still
201
- there to stage next. Comments on the lines you staged follow them into the Staged view (see
202
- [Re-anchoring](#re-anchoring)). Every page open on the worktree reloads at once.
203
-
204
- Limits, each reported as a plain error rather than a half-applied patch:
205
-
206
- - Only the Unstaged and Staged views stage; `all`, `base`, commits and ranges are read-only.
207
- - Binary files and mode-only changes go whole or not at all; a mode change rides along only with *暂存文件*.
208
- - A staged deletion can be unstaged whole, not by line: the index has nothing left to put lines back into.
209
- - A pick that would split a file's missing trailing newline from its neighbours is refused; include them.
210
- - The request carries the hash of the diff the pick was made on. If the file or the index moved since
211
- (the agent kept editing), the server answers 409 and the page reloads instead of staging the wrong lines.
212
- - If another git process holds the index lock the server retries briefly, then gives up with 409.
213
-
214
- ## Debug code
215
-
216
- Code written to poke at a problem and not meant to be committed can be marked in a comment. There is no
217
- single convention for this, so warden takes the ones in use, in any comment syntax (`//`, `/*`, `#`,
218
- `--`, `<!--`, `;`, `%`) and any case, as long as the marker opens the comment:
219
-
220
- - a block between `debug:start` and `debug:end`, or `develblock:start` and `develblock:end` (the
221
- webpack-strip-block / gulp-strip-block spelling); markers included, blocks nest, and a block left
222
- open runs to the end of the file;
223
- - a single line tagged `nocommit`, `no-commit` or `do not commit` (optionally `@nocommit` / `!nocommit`),
224
- the tag the usual pre-commit hooks look for.
225
-
226
- With *忽略调试代码* ticked in the sidebar (the default):
227
-
228
- - runs of debug lines in a hunk fold into one striped row that opens with a click; jumping to a comment
229
- inside one opens it;
230
- - *暂存文件* and *暂存此 hunk* leave debug lines out. Lines picked one by one go in regardless — that
231
- is how debug code is staged on purpose. A file whose changes are all debug code is refused with a
232
- note to pick the lines instead;
233
- - a file whose unstaged changes are all debug code does not count as still to review.
234
-
235
- Whether it is ticked or not, the 已暂存 heading carries a *调试代码 N* mark while N added lines in the
236
- index are debug code, with the files in its tooltip.
237
-
238
- A hunk rarely shows the marker that opened the block its lines sit in, so warden reads both sides of a
239
- file whole — only for the files `git grep` finds a marker in. Marker detection is textual: a marker
240
- inside a string literal that follows a comment leader counts too.
241
-
242
- ## Comments and export
243
-
244
- Press the `+` that appears next to a line (drag to cover several lines) and write Markdown. What you
245
- write lands in the right-hand rail, which is shut until it has something to hold: it costs 360px of
246
- the code column, which is most of it once the diff is side by side on a laptop. Writing a comment,
247
- focusing one or re-attaching one opens it; so does the *评论* switch at the right of the top bar,
248
- which carries the count and turns violet while any comment is still waiting to go back to the agent.
249
- `Esc` shuts it again.
250
-
251
- Each comment belongs to the `old` or `new` side of the diff and has a status:
252
-
253
- - `active` (待导出) — not yet exported
254
- - `exported` (已导出) — copied at least once (excluded from "copy all" unless *含已导出* is checked)
255
- - `orphaned` (已失联) — the code it referred to no longer exists in the current diff
256
-
257
- "复制评论" copies every active comment of the current target; each comment also has a "复制此条" button.
258
- The clipboard format is fixed so an agent can read it directly:
259
-
260
- ````markdown
261
- # Review comments
262
- Target: working
263
- Repo: /home/user/project
264
- Count: 2
265
-
266
- ## src/features/order/OrderList.tsx:120-124 (new)
267
- ```tsx
268
- 120 | const total = items.reduce((s, i) => s + i.price, 0);
269
- 121 | // ...
270
- ```
271
- > 这里没有考虑 discount 字段,参考 utils/price.ts 里的 calcTotal。
272
-
273
- ## src/features/order/hooks/useOrder.ts:42 (new)
274
- ```ts
275
- 42 | useEffect(() => { fetchOrder(id) }, []);
276
- ```
277
- > 依赖数组缺少 id。
278
- ````
279
-
280
- Issues can be copied in the same format (with the issue title, status and body on top).
281
-
282
- ## Re-anchoring
283
-
284
- Every comment stores a hash of its hunk, of each covered line and of three context lines above and
285
- below. On refresh the comment is re-attached in this order: same hunk still present → same relative
286
- position; otherwise search the file for the same line sequence (context lines disambiguate duplicates).
287
-
288
- The three local views of a worktree share one pool of comments, and re-anchoring searches all of
289
- them — the comment's current view first, then `working`, `staged`, `all`. So:
290
-
291
- - Comment on an unstaged hunk, then `git add` it → the comment turns up in the Staged view,
292
- unchanged. The card shows which view it moved to; clicking it jumps there.
293
- - The agent edits the commented line → nothing matches, and the comment becomes *orphaned*: listed
294
- at the top of the rail with its original snippet, ready to be deleted or re-attached to a new selection.
295
- - `git commit` → re-anchoring notices HEAD moved. Comments that no longer have a home anywhere are
296
- **deleted** (and unlinked from any issue), because the code they were about is now history.
297
- Anything still visible in Unstaged or Staged survives, including as a context line.
298
-
299
- Commit, range, `base` and checkpoint targets each keep their own pool and only ever search
300
- themselves, and none of them deletes on a moved HEAD.
301
-
302
- Comment markers in the diff belong to one view; the rail's *全部* tab lists the whole pool and
303
- *此文件* lists every comment on the open file regardless of which view it currently sits in.
304
-
305
- ## Checkpoints
306
-
307
- An agent works in rounds, and after the first one the question is no longer "what is uncommitted"
308
- but "what did it do since I last looked". The working tree answers that only while the agent
309
- neither stages nor commits, and a `base` target shows the whole branch every time. A checkpoint is
310
- the working tree noted at the moment you choose, and `checkpoint:<n>` is one diff from it to the
311
- working tree now — whatever was staged or committed in between does not show, only what changed.
312
-
313
- - *新建检查点*, under the progress figure in 工作区, takes one: tracked and untracked files as they are
314
- on disk, ignored ones left out. *对比检查点 #n* beside it opens the newest. A checkpoint of a working
315
- tree that has not changed since the newest one is not taken twice; that one is handed back.
316
- - In a checkpoint's view the picker reads *检查点 #n*, and the row under the figure switches to another
317
- checkpoint, takes a new one — the round is over, so the view moves on to it, empty — or deletes
318
- the one in front after asking.
319
- - Files there carry the [已读 mark](#viewed-files), which the agent's next edit to a file drops, and
320
- comments on a checkpoint are a pool of their own. Deleting the checkpoint deletes both.
321
- - Checkpoints are numbered per worktree. A worktree keeps the newest 20; taking one more drops the
322
- oldest, with its comments. A slot checked out to another branch drops its own.
323
-
324
- Nothing of this is written to the repository. The snapshot is `git add --all` plus
325
- `git write-tree` run against a copy of the index kept in warden's data directory, with
326
- `GIT_OBJECT_DIRECTORY` pointing there too, so the tree and every blob git hashes for it land beside
327
- the state file; files the index already has right are not hashed again, so taking one costs about
328
- what changed. A diff runs `git diff <tree>` under another throwaway copy of the index with the
329
- untracked files added as intent-to-add (which hashes nothing), reading the repository's objects
330
- through `GIT_ALTERNATE_OBJECT_DIRECTORIES`. The alternates are only ever set for reads: git refreshes
331
- the mtime of an object it finds in one while writing, and that would be a write to `.git`. Clean
332
- filters configured for the repository run as they do for any `git add` (Git LFS keeps its cache in
333
- `.git/lfs`).
334
-
335
- ## Viewed files
79
+ ## Documentation
336
80
 
337
- Commit, range and `base` targets cannot be staged from, so a file there is ticked *已读* instead — the
338
- box beside it in the sidebar, or the one in the file header. A directory whose files are all read
339
- recedes with a check. The mark is bound to a hash of the file's diff: when the diff changes (a `base`
340
- target reads the working tree, so the agent's next edit does that) the mark is dropped and the file
341
- is flagged *已变化*. The local views keep no such mark — see [Targets](#targets-what-is-being-reviewed).
342
-
343
- ## Auto-refresh
344
-
345
- warden polls the worktree every 1.5 s while the page is open (`git status --porcelain
346
- --untracked-files=all`, HEAD, the branch name, plus mtime and size of the paths status reports — enough to notice a second edit to an
347
- already-modified file) and pushes a change event over `GET /api/events` (SSE). The page then re-runs
348
- the normal refresh: re-anchor, reload both file lists, reload the open diff. Your draft comment,
349
- selection, current view and current file are left alone, and the diff is scrolled back to where you
350
- were reading. Turn it off with the *自动* toggle next to the refresh button; `r` still
351
- refreshes by hand.
352
-
353
- ## Todos
354
-
355
- Notes attached to a branch rather than to a line of code, for the things you notice while reviewing
356
- that do not belong in a comment. They are not deleted when you commit. They sit in the right-hand
357
- rail next to the comments, under the *待办* tab, as a task list that works the way Google Tasks does:
358
-
359
- - *添加待办* opens an empty row at the top; type the title and press Enter, and the next row opens
360
- right under it. Esc or Backspace on an empty row drops it.
361
- - Titles and details are edited where they are: click into the text. Enter at the end of a title
362
- starts the next todo under it; Backspace on an emptied title deletes the todo. The details field
363
- (Markdown) appears while the row is open, and is rendered under the title otherwise.
364
- - The circle strikes the todo through and files it under *已完成 (N)* at the bottom, collapsed;
365
- open it to tick one back or *全部删除*.
366
- - Rows are dragged into your own order by the handle at their left edge. That order is what the state
367
- file keeps, and new todos go on top.
368
- - Delete asks nothing; the toast offers *撤消* for a few seconds.
369
- - The picker at the top is the list selector: the current branch, any other branch that has todos,
370
- or all of them with the branch on each row. A new todo goes to the branch being shown.
371
-
372
- Issues (the rail's third tab) are the same list: closing an issue is ticking it, a
373
- *已关闭* section holds the closed ones, and the comments linked to an issue sit under its open row
374
- like subtasks, each with *跳转* and *解除关联*. Tick comments in the rail and the *创建 Issue* button
375
- switches to that tab with the add row ready and the comments attached on creation; for an existing
376
- issue, open its row and use *关联选中的评论*.
377
-
378
- Each row has its own *复制* button, and there is deliberately no "copy all": a todo is one task to
379
- hand to an agent, and the agent it goes to is already working in that branch. So the clipboard gets
380
- the title and the body — no branch, repo, count or numbering:
381
-
382
- ```markdown
383
- 补充 OrderList 的空状态
384
-
385
- 描述正文(Markdown)…
386
- ```
387
-
388
- ## nvim integration
389
-
390
- Requirements:
391
-
392
- - `nvim` on `PATH` of the machine running warden (WSL2 in the typical setup).
393
- - nvim started normally so it creates its default server socket (`$XDG_RUNTIME_DIR/nvim.<pid>.0`
394
- or `/tmp/nvim.<user>/…/nvim.<pid>.0`), or with `--listen` into one of those directories.
395
- - nvim's working directory is the repository (or worktree) root or somewhere below it.
396
-
397
- warden scans those socket directories, asks each instance for `getcwd()` (500 ms timeout) and keeps the
398
- instances whose cwd is inside the current target's root. One match is used automatically; with several
399
- matches a selector appears in the top bar and the choice is remembered per repository root. Results
400
- are cached for 10 s; use ⟳ to rescan.
401
-
402
- Clicking a line number runs, roughly, `:edit +<line> <absolute path>` in that instance. Deleted lines
403
- jump to the nearest new-side line; for commit targets the working-tree file is opened.
404
-
405
- ## State
406
-
407
- `~/.local/share/warden/<sha1(repoRoot)[:12]>/state.json` (respects `XDG_DATA_HOME`). Plain JSON with a
408
- `schemaVersion`, written atomically (temp file + rename) under a small lock file so multiple instances
409
- can share it. Delete the directory to reset. Checkpoint objects live beside it in `checkpoints/objects`;
410
- deleting a checkpoint leaves them there, since another may share them.
411
-
412
- `targets` is keyed by target key, plus one *comment scope* per worktree — `local`, or
413
- `worktree:<path>:local` — holding the comments the three local views share and the HEAD sha the last
414
- re-anchor saw. `viewed` sits on the key of the commit, range, `base` or checkpoint target it was
415
- ticked in; the local view keys hold nothing. Issues, todos and `checkpoints` (the tree sha, HEAD and
416
- time of each, per worktree) are top level. State written by an older version is
417
- migrated on load: comments filed under `working` / `staged` / `all` move into the matching scope the
418
- first time the file is read, and the 已读 marks those views used to keep are dropped.
419
-
420
- ## WSL2 notes
421
-
422
- - Access the UI from the Windows browser at the printed `http://127.0.0.1:<port>/` URL; WSL2 forwards
423
- localhost automatically.
424
- - Install [wslu](https://github.com/wslutilities/wslu) for `wslview` if `explorer.exe` doesn't open the
425
- URL for you, or run with `--no-open`.
426
- - Keep the repository on the Linux filesystem for reasonable git performance.
427
- - Clipboard access uses `navigator.clipboard`, which works on `localhost`; if you expose the port via
428
- another hostname the copy button falls back to `document.execCommand('copy')`.
429
-
430
- ## Security model
431
-
432
- Single user, local only. The server binds to `127.0.0.1` and executes git only through
433
- `execFile('git', [...])`. Reads go through an argument whitelist (`rev-parse`, `diff`, `show`, `log`,
434
- `worktree list`, `ls-files`, `status`, `merge-base`, `rev-list`, `grep` without `-O` /
435
- `--open-files-in-pager`) that refuses option-looking refs and any write-capable
436
- flag; requests that would need anything else get HTTP 400. The writes are few and each composes its
437
- own arguments. `POST /api/targets/:key/stage` runs `git apply --cached` (with `--reverse` for the
438
- Staged view) on a patch the server itself builds from the diff it just produced — the patch is never
439
- taken from the request, only the line indices are, and they are checked against the diff's hash
440
- first. `POST /api/worktrees`, `POST /api/worktrees/release` and `POST /api/worktrees/remove` run
441
- `git worktree add`, `git branch`, `git switch`, `git worktree remove` and `git branch -d` with a
442
- branch name git has validated, a ref that resolves, and a slot path the server composes from a
443
- number — `<repo>-<n>` beside the main worktree; no request names a directory. The working tree and
444
- HEAD of an existing checkout are written only inside such a slot: `git switch <branch>` into a free
445
- one (detached, clean) when a branch is checked out there, `git switch --detach` when it is released,
446
- and `git reset --hard` plus `git clean -fd` only on the forced release the reviewer confirmed after
447
- a 409. Refs change only when a branch is made for a checkout (plus its upstream setting when it comes
448
- from a remote) — undone with `branch -D` should the switch into the slot then fail — or deleted on
449
- request after a release or removal (`-d`, so only a merged one), and the review state lives outside
450
- the repository. A checkpoint writes only there: `git add --all [--intent-to-add]` and
451
- `git write-tree --missing-ok`, with `GIT_INDEX_FILE` and `GIT_OBJECT_DIRECTORY` set to files in
452
- warden's data directory and no alternates (see [Checkpoints](#checkpoints)). A mutating request the
453
- browser labels as coming from another site (`Sec-Fetch-Site: cross-site`) is refused with 403, so a
454
- page from elsewhere cannot drive the server through the browser it is open in.
455
-
456
- The only request warden makes off the machine is the update check: a `GET` of
457
- `https://registry.npmjs.org/@xbghc%2fwarden/latest`, no more than once a day, carrying nothing about
458
- the repository or the review. `--no-update-check` (or the environment switches under *Install / run*)
459
- removes it.
460
-
461
- ## Development
462
-
463
- ```sh
464
- pnpm install
465
- pnpm dev # API server on :4100 (tsx watch) + Vite dev server on :5173 with /api proxied
466
- pnpm test # vitest: diff parser, staging patches, anchoring, comment scopes, watcher, todos, worktrees, export, state, HTTP API
467
- pnpm typecheck
468
- pnpm build # dist/web (Vite) + dist/cli.js (tsup, zero runtime dependencies)
469
- node dist/cli.js path/to/repo
470
- ```
471
-
472
- Layout:
473
-
474
- ```
475
- bin/cli.ts argument parsing, start server, open browser
476
- packages/shared types + target key / comment scope helpers (bundled into both sides)
477
- packages/server Hono API, git wrapper, diff parser, staging patches, targets, worktrees, anchoring, watcher, state, export, nvim
478
- packages/web React + Vite UI (shiki highlighting, @tanstack/react-virtual, react-markdown)
479
- test/ API integration tests on a generated git repository
480
- ```
481
-
482
- Only `dist/`, `README.md`, `LICENSE` and `package.json` are published.
483
-
484
- Releasing: bump `version` in `package.json`, commit, then push a matching tag —
485
- `git tag v0.2.0 && git push origin v0.2.0`. The `Publish to npm` workflow checks the tag against the
486
- version, runs build + tests through `prepublishOnly`, and publishes over OIDC trusted publishing — no
487
- `NPM_TOKEN` secret to store or rotate. It relies on a trusted publisher configured on the package's
488
- npmjs.com settings page (user `xbghc`, repo `warden`, workflow `publish.yml`), and passes
489
- `--provenance` explicitly, because npm's automatic attestation does not fire through `pnpm publish`
490
- (0.2.0 shipped without one). CI runs typecheck, tests, build and a `publish --dry-run` on every push
491
- to `main` and every pull request.
81
+ | | |
82
+ |---|---|
83
+ | [Reviewing](docs/reviewing.md) | Targets and the sidebar, staging, debug code, 已读, checkpoints, auto-refresh |
84
+ | [Comments and the agent](docs/agent.md) | Comments, the export format, `warden feedback` / `reply`, re-anchoring, todos |
85
+ | [Worktrees](docs/worktrees.md) | Slots, checkout and release, commit counts, tmux |
86
+ | [Editor and platform](docs/environment.md) | Jumping to nvim, WSL2 |
87
+ | [State and security](docs/internals.md) | The state file, what git commands run and why |
88
+ | [Development](docs/development.md) | Building, testing, releasing, Storybook |
492
89
 
493
90
  ## License
494
91
 
495
92
  MIT
496
-
497
- ## Storybook
498
-
499
- [Browse the hosted Storybook](https://xbghc.github.io/warden/). Every push to `main` builds and deploys it through `.github/workflows/storybook-pages.yml`; the workflow can also be run manually. Repository Settings → Pages must use **GitHub Actions** as the publishing source.
500
-
501
- Run `pnpm storybook` for isolated review and layout components at http://127.0.0.1:6006. Stories use in-memory fixtures and include interaction checks. Run `pnpm build:storybook` to build the static preview.
502
-
503
- ### Worktree details and tmux
504
-
505
- Each checkout reports how many commits it is ahead of and behind a base, named beside the counts. A branch with an upstream, the main checkout's included, is counted against it (`origin/topic`): what is not pushed and what is not pulled yet, as `git status` counts them. Any other checkout, or one whose upstream is gone (`[origin/topic: gone]`, which the row notes), is counted against the branch currently checked out in the main repository; “已合并” there means its HEAD is reachable from the main checkout HEAD, which squash merges and cherry-picks do not imply. Either way the base is the ref `git branch -d` checks, so a branch with nothing ahead is one *一并删除分支* will take. Expand a row to browse its paginated commit history (including shared commits) or view that branch’s todos.
506
-
507
- The optional tmux integration discovers sessions whose `session_path` resolves to the main repository directory. Click tmux to create a window immediately when exactly one session matches. With multiple matches, choose a session first. The new window starts in the worktree directory. It uses `tmux new-window -d -c` without sending a shell command; existing windows stay selected. No session is created automatically. Run warden alongside tmux in Linux, macOS, or WSL, using the same user/server environment. See the [tmux manual](https://man.openbsd.org/tmux.1).
508
-
509
- `GET /api/tmux/sessions` lists matching sessions. `POST /api/tmux/windows` accepts a registered worktree `path` and a matching `sessionId`; the server revalidates both before creating a window. This optional action creates a terminal window but does not modify repository files.
510
-
511
- The sidebar uses a view selector for Changes, Commits, and Worktrees. The `Layout/Sidebar` stories demonstrate the selector together with each view’s real sidebar controls and content.