@xbghc/warden 0.8.0 → 0.10.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 +53 -28
- package/dist/cli.js +110 -40
- package/dist/web/assets/index-CQYijMEg.css +1 -0
- package/dist/web/assets/index-DaXYev4-.js +198 -0
- package/dist/web/index.html +2 -2
- package/package.json +7 -3
- package/dist/web/assets/index-C8TtGAMN.css +0 -1
- package/dist/web/assets/index-CwQ9JHmk.js +0 -198
package/README.md
CHANGED
|
@@ -68,25 +68,29 @@ inside a removed worktree falls back to the working tree on the next load. The c
|
|
|
68
68
|
flags kept under that worktree's key stay in the state file and come back if a worktree is created
|
|
69
69
|
at the same path again.
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
The sidebar is the navigation, and the only navigation: its three tabs — *变更*, *提交*, *Worktree* —
|
|
72
|
+
each fill the left column with the controls for what the middle is showing, and the top bar carries
|
|
73
|
+
none of it. There is no target switcher: the *变更* tab's two blocks *are* the working tree, a commit
|
|
74
|
+
is picked from the *提交* list, and the *审阅整条分支* and *对比两个 ref* forms below that tab's filters
|
|
75
|
+
open the other two. The sidebar names whatever is under review in its block header and puts a
|
|
74
76
|
*返回工作区* link above it.
|
|
75
77
|
|
|
76
78
|
`working`, `staged` and `all` are the three **local views** of one worktree. While any of them is
|
|
77
|
-
selected the sidebar shows two blocks —
|
|
79
|
+
selected the sidebar shows two blocks — 未暂存 (`working`) and 已暂存 (`staged`) — instead of a
|
|
78
80
|
single tree, and clicking a file switches to the view it belongs to. A file that is only partly
|
|
79
|
-
staged appears in both.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
staged appears in both. Above them sits the one figure in the app: how many of the files under review
|
|
82
|
+
are fully staged, which is what "staged means reviewed" amounts to. Switching between the three keeps
|
|
83
|
+
your comments, the draft you are typing and the current selection; only the diff is reloaded. `all`
|
|
84
|
+
has no control of its own (a saved `lastTarget` can still restore it; *审阅整条分支* against `@` shows
|
|
85
|
+
the same diff). Commit, range and `base` targets keep the single tree, and their figure counts 已读
|
|
86
|
+
instead, since nothing can be staged there.
|
|
83
87
|
|
|
84
88
|
`base:<ref>` is for a branch a coding agent has been working on, committing as it goes: one tree with
|
|
85
89
|
everything since the branch forked off `<ref>` — the commits plus whatever is still uncommitted or
|
|
86
90
|
untracked. The diff runs against the merge base, so commits that landed on `<ref>` after the fork are
|
|
87
|
-
not listed as reverted (which is what a plain `git diff <ref>` would do). The
|
|
88
|
-
|
|
89
|
-
review, otherwise `main` or `master` — and takes any ref.
|
|
91
|
+
not listed as reverted (which is what a plain `git diff <ref>` would do). The *相对于* field under
|
|
92
|
+
*审阅整条分支* suggests the base for you — the main worktree's branch when a sibling worktree is under
|
|
93
|
+
review, otherwise `main` or `master` — and takes any ref. Under it the form names the fork point (the
|
|
90
94
|
merge base with that ref, `GET /api/fork-point`) with how many commits the branch is ahead of it and how
|
|
91
95
|
many landed on the base since, and the list marks that commit with a *分叉自* chip and a rule above it:
|
|
92
96
|
what sits above is the branch's own work. Unlike the three local views, `base` keeps its own
|
|
@@ -96,10 +100,10 @@ the fix and delete or re-attach it.
|
|
|
96
100
|
|
|
97
101
|
## Worktrees
|
|
98
102
|
|
|
99
|
-
Agents do their best work each in a worktree of its own, and the *
|
|
103
|
+
Agents do their best work each in a worktree of its own, and the sidebar's *Worktree* tab is
|
|
100
104
|
where those are made and taken down without a trip to the terminal:
|
|
101
105
|
|
|
102
|
-
- *新建 worktree
|
|
106
|
+
- *新建 worktree*, the form in the sidebar, takes a branch and a base. A name that is not a branch yet becomes one from the
|
|
103
107
|
base (`main` or `master` unless you say otherwise; any ref goes) — `git worktree add -b <branch>
|
|
104
108
|
<path> <base>`; an existing branch is checked out as it is, unless another worktree already has it.
|
|
105
109
|
The path is suggested as a sibling of the main worktree named `<repo>-<branch>` (slashes become
|
|
@@ -108,11 +112,12 @@ where those are made and taken down without a trip to the terminal:
|
|
|
108
112
|
- Each row names the checkout, its branch, and whether it is clean or how many paths `git status`
|
|
109
113
|
reports. *查看* switches the review to it (the kind of target carries over, as with the selector
|
|
110
114
|
in the top bar), *复制路径* is for the agent's prompt.
|
|
111
|
-
- *删除*
|
|
112
|
-
changes is not removed until you confirm
|
|
113
|
-
else git refuses without `--force` is put to you the same way.
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
- *删除* always asks first, in the row it would remove, and runs `git worktree remove` without
|
|
116
|
+
`--force`: a worktree with uncommitted changes is not removed until you confirm again, since those
|
|
117
|
+
changes go with it, and whatever else git refuses without `--force` is put to you the same way.
|
|
118
|
+
*一并删除分支* is offered in that same confirmation, ticked by default, and takes the branch by
|
|
119
|
+
`git branch -d`: one that is not merged is kept and the toast says why. The comments and viewed
|
|
120
|
+
flags kept under the worktree's key stay in the state file, as before.
|
|
116
121
|
- A worktree whose directory was deleted behind git's back is listed struck through; *清理* drops that
|
|
117
122
|
one entry (`git worktree remove` handles it; nothing is pruned wholesale).
|
|
118
123
|
|
|
@@ -125,7 +130,7 @@ where those are made and taken down without a trip to the terminal:
|
|
|
125
130
|
| `n` / `p` | Next / previous hunk |
|
|
126
131
|
| `s` | Stage (Unstaged view) or unstage (Staged view) the picked lines |
|
|
127
132
|
| `Ctrl+Enter` | Save the comment being edited |
|
|
128
|
-
| `Esc` | Drop the picked lines / cancel editing /
|
|
133
|
+
| `Esc` | Drop the picked lines / cancel editing / cancel re-attach mode / shut the rail / back to 变更 |
|
|
129
134
|
|
|
130
135
|
## Staging
|
|
131
136
|
|
|
@@ -156,12 +161,18 @@ Limits, each reported as a plain error rather than a half-applied patch:
|
|
|
156
161
|
|
|
157
162
|
## Comments and export
|
|
158
163
|
|
|
159
|
-
Press the `+` that appears next to a line (drag to cover several lines) and write Markdown.
|
|
164
|
+
Press the `+` that appears next to a line (drag to cover several lines) and write Markdown. What you
|
|
165
|
+
write lands in the right-hand rail, which is shut until it has something to hold: it costs 360px of
|
|
166
|
+
the code column, which is most of it once the diff is side by side on a laptop. Writing a comment,
|
|
167
|
+
focusing one or re-attaching one opens it; so does the *评论* switch at the right of the top bar,
|
|
168
|
+
which carries the count and turns violet while any comment is still waiting to go back to the agent.
|
|
169
|
+
`Esc` shuts it again.
|
|
170
|
+
|
|
160
171
|
Each comment belongs to the `old` or `new` side of the diff and has a status:
|
|
161
172
|
|
|
162
|
-
- `active` — not yet exported
|
|
163
|
-
- `exported` — copied at least once (excluded from "copy all" unless *含已导出* is checked)
|
|
164
|
-
- `orphaned` — the code it referred to no longer exists in the current diff
|
|
173
|
+
- `active` (待导出) — not yet exported
|
|
174
|
+
- `exported` (已导出) — copied at least once (excluded from "copy all" unless *含已导出* is checked)
|
|
175
|
+
- `orphaned` (已失联) — the code it referred to no longer exists in the current diff
|
|
165
176
|
|
|
166
177
|
"复制评论" copies every active comment of the current target; each comment also has a "复制此条" button.
|
|
167
178
|
The clipboard format is fixed so an agent can read it directly:
|
|
@@ -211,7 +222,7 @@ them deletes on a moved HEAD.
|
|
|
211
222
|
Comment markers in the diff belong to one view; the rail's *全部* tab lists the whole pool and
|
|
212
223
|
*此文件* lists every comment on the open file regardless of which view it currently sits in.
|
|
213
224
|
|
|
214
|
-
|
|
225
|
+
*已读* is per view, so a half-staged file can be marked read on one side and not the other. It is
|
|
215
226
|
bound to a hash of the file's diff; when the diff changes the flag is dropped and the file is marked
|
|
216
227
|
*已变化*.
|
|
217
228
|
|
|
@@ -229,9 +240,9 @@ refreshes by hand.
|
|
|
229
240
|
|
|
230
241
|
Notes attached to a branch rather than to a line of code, for the things you notice while reviewing
|
|
231
242
|
that do not belong in a comment. They are not deleted when you commit. They sit in the right-hand
|
|
232
|
-
rail next to the comments, under the
|
|
243
|
+
rail next to the comments, under the *待办* tab, as a task list that works the way Google Tasks does:
|
|
233
244
|
|
|
234
|
-
-
|
|
245
|
+
- *添加待办* opens an empty row at the top; type the title and press Enter, and the next row opens
|
|
235
246
|
right under it. Esc or Backspace on an empty row drops it.
|
|
236
247
|
- Titles and details are edited where they are: click into the text. Enter at the end of a title
|
|
237
248
|
starts the next todo under it; Backspace on an emptied title deletes the todo. The details field
|
|
@@ -244,10 +255,10 @@ rail next to the comments, under the *Todo* tab, as a task list that works the w
|
|
|
244
255
|
- The picker at the top is the list selector: the current branch, any other branch that has todos,
|
|
245
256
|
or all of them with the branch on each row. A new todo goes to the branch being shown.
|
|
246
257
|
|
|
247
|
-
Issues (the
|
|
258
|
+
Issues (the rail's third tab) are the same list: closing an issue is ticking it, a
|
|
248
259
|
*已关闭* section holds the closed ones, and the comments linked to an issue sit under its open row
|
|
249
260
|
like subtasks, each with *跳转* and *解除关联*. Tick comments in the rail and the *创建 Issue* button
|
|
250
|
-
|
|
261
|
+
switches to that tab with the add row ready and the comments attached on creation; for an existing
|
|
251
262
|
issue, open its row and use *关联选中的评论*.
|
|
252
263
|
|
|
253
264
|
Each row has its own *复制* button, and there is deliberately no "copy all": a todo is one task to
|
|
@@ -351,3 +362,17 @@ to `main` and every pull request.
|
|
|
351
362
|
## License
|
|
352
363
|
|
|
353
364
|
MIT
|
|
365
|
+
|
|
366
|
+
## Storybook
|
|
367
|
+
|
|
368
|
+
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.
|
|
369
|
+
|
|
370
|
+
### Worktree details and tmux
|
|
371
|
+
|
|
372
|
+
Each checkout reports commits ahead/behind the branch currently checked out in the main repository. “Merged” means its HEAD is reachable from the main checkout HEAD; squash merges and cherry-picks do not imply this. Expand a row to browse its paginated commit history (including shared commits) or view that branch’s todos.
|
|
373
|
+
|
|
374
|
+
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).
|
|
375
|
+
|
|
376
|
+
`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.
|
|
377
|
+
|
|
378
|
+
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.
|
package/dist/cli.js
CHANGED
|
@@ -2918,7 +2918,7 @@ function parseTargetKey(key) {
|
|
|
2918
2918
|
if (key.startsWith("worktree:")) {
|
|
2919
2919
|
const rest = key.slice("worktree:".length);
|
|
2920
2920
|
const m = /^(.+):(working|staged|all|commit:.+|range:.+|base:.+)$/.exec(rest);
|
|
2921
|
-
if (!m
|
|
2921
|
+
if (!m?.[1] || !m[2]) throw new TargetKeyError(`invalid worktree target key: ${key}`);
|
|
2922
2922
|
const worktree = m[1];
|
|
2923
2923
|
if (!worktree.startsWith("/")) throw new TargetKeyError(`worktree path must be absolute: ${worktree}`);
|
|
2924
2924
|
return { ...parseSuffix(m[2]), worktree };
|
|
@@ -2968,11 +2968,7 @@ function stageModeFor(t) {
|
|
|
2968
2968
|
function localViewKeys(key) {
|
|
2969
2969
|
const t = tryParseTargetKey(key);
|
|
2970
2970
|
const wt = t?.worktree ? { worktree: t.worktree } : {};
|
|
2971
|
-
return [
|
|
2972
|
-
formatTargetKey({ kind: "working", ...wt }),
|
|
2973
|
-
formatTargetKey({ kind: "staged", ...wt }),
|
|
2974
|
-
formatTargetKey({ kind: "all", ...wt })
|
|
2975
|
-
];
|
|
2971
|
+
return [formatTargetKey({ kind: "working", ...wt }), formatTargetKey({ kind: "staged", ...wt }), formatTargetKey({ kind: "all", ...wt })];
|
|
2976
2972
|
}
|
|
2977
2973
|
|
|
2978
2974
|
// packages/shared/src/order.ts
|
|
@@ -3049,7 +3045,19 @@ function parseCommitLog(stdout) {
|
|
|
3049
3045
|
// packages/server/src/git.ts
|
|
3050
3046
|
import { execFile } from "child_process";
|
|
3051
3047
|
import { existsSync } from "fs";
|
|
3052
|
-
var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
3048
|
+
var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
3049
|
+
"rev-parse",
|
|
3050
|
+
"diff",
|
|
3051
|
+
"show",
|
|
3052
|
+
"log",
|
|
3053
|
+
"worktree",
|
|
3054
|
+
"ls-files",
|
|
3055
|
+
"status",
|
|
3056
|
+
"merge-base",
|
|
3057
|
+
"rev-list",
|
|
3058
|
+
"for-each-ref",
|
|
3059
|
+
"check-ref-format"
|
|
3060
|
+
]);
|
|
3053
3061
|
var FORBIDDEN_OPTION_PREFIXES = ["--output", "--ext-diff", "--textconv", "-c", "--config-env", "--exec-path", "--git-dir", "--work-tree"];
|
|
3054
3062
|
var DEFAULT_GIT_TIMEOUT_MS = 3e4;
|
|
3055
3063
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
@@ -3269,7 +3277,7 @@ async function resolveRepo(dir) {
|
|
|
3269
3277
|
try {
|
|
3270
3278
|
const r = await runGit(["rev-parse", "--show-toplevel"], { cwd: dir });
|
|
3271
3279
|
top = r.stdout.trim();
|
|
3272
|
-
} catch
|
|
3280
|
+
} catch {
|
|
3273
3281
|
throw new HttpError(400, `${dir} is not inside a git repository`, "not_a_repo");
|
|
3274
3282
|
}
|
|
3275
3283
|
if (!top) throw new HttpError(400, `${dir} is not inside a git worktree (bare repository?)`, "not_a_repo");
|
|
@@ -3284,7 +3292,7 @@ async function listWorktreesAll(ctx) {
|
|
|
3284
3292
|
const out = [];
|
|
3285
3293
|
let cur = null;
|
|
3286
3294
|
const flush = () => {
|
|
3287
|
-
if (cur
|
|
3295
|
+
if (cur?.path) {
|
|
3288
3296
|
out.push({
|
|
3289
3297
|
path: cur.path,
|
|
3290
3298
|
head: cur.head ?? "",
|
|
@@ -3306,7 +3314,6 @@ async function listWorktreesAll(ctx) {
|
|
|
3306
3314
|
flush();
|
|
3307
3315
|
cur = { path: line.slice("worktree ".length) };
|
|
3308
3316
|
} else if (!cur) {
|
|
3309
|
-
continue;
|
|
3310
3317
|
} else if (line.startsWith("HEAD ")) cur.head = line.slice(5);
|
|
3311
3318
|
else if (line.startsWith("branch ")) cur.branch = line.slice(7).replace(/^refs\/heads\//, "");
|
|
3312
3319
|
else if (line === "detached") cur.detached = true;
|
|
@@ -3366,7 +3373,9 @@ function lineHash(content) {
|
|
|
3366
3373
|
return sha1(content.trimEnd());
|
|
3367
3374
|
}
|
|
3368
3375
|
function hunkHash(lineContents) {
|
|
3369
|
-
return sha1(
|
|
3376
|
+
return sha1(
|
|
3377
|
+
lineContents.map((l) => l.trimEnd()).join("\n").trim()
|
|
3378
|
+
);
|
|
3370
3379
|
}
|
|
3371
3380
|
|
|
3372
3381
|
// packages/server/src/diffparse.ts
|
|
@@ -3407,7 +3416,7 @@ function stripPrefix(p, prefix) {
|
|
|
3407
3416
|
function splitGitHeader(rest) {
|
|
3408
3417
|
if (rest.startsWith('"')) {
|
|
3409
3418
|
const m = /^("(?:[^"\\]|\\.)*")\s+("(?:[^"\\]|\\.)*")$/.exec(rest);
|
|
3410
|
-
if (m
|
|
3419
|
+
if (m?.[1] && m[2]) return { a: stripPrefix(m[1], "a/"), b: stripPrefix(m[2], "b/") };
|
|
3411
3420
|
}
|
|
3412
3421
|
for (let i = 0; i < rest.length; i++) {
|
|
3413
3422
|
if (rest[i] !== " ") continue;
|
|
@@ -3564,7 +3573,6 @@ function parseUnifiedDiff(text) {
|
|
|
3564
3573
|
if (line.startsWith("Binary files ") || line === "GIT binary patch") {
|
|
3565
3574
|
c.binary = true;
|
|
3566
3575
|
c.raw.push(line);
|
|
3567
|
-
continue;
|
|
3568
3576
|
}
|
|
3569
3577
|
}
|
|
3570
3578
|
flush();
|
|
@@ -3888,12 +3896,13 @@ function pickedLines(diff, selection) {
|
|
|
3888
3896
|
const picked = /* @__PURE__ */ new Map();
|
|
3889
3897
|
const changed = (h) => h.lines.map((l, i) => l.type === "context" ? -1 : i).filter((i) => i >= 0);
|
|
3890
3898
|
if (selection === void 0) {
|
|
3891
|
-
diff.hunks.
|
|
3899
|
+
for (const [i, h] of diff.hunks.entries()) picked.set(i, new Set(changed(h)));
|
|
3892
3900
|
return picked;
|
|
3893
3901
|
}
|
|
3894
3902
|
if (!Array.isArray(selection)) throw badRequest("hunks must be an array", "bad_selection");
|
|
3895
3903
|
for (const sel of selection) {
|
|
3896
|
-
if (!sel || !Number.isInteger(sel.index) || sel.index < 0 || sel.index >= diff.hunks.length)
|
|
3904
|
+
if (!sel || !Number.isInteger(sel.index) || sel.index < 0 || sel.index >= diff.hunks.length)
|
|
3905
|
+
throw badRequest(`no such hunk: ${sel?.index}`, "bad_selection");
|
|
3897
3906
|
const hunk = diff.hunks[sel.index];
|
|
3898
3907
|
const set = picked.get(sel.index) ?? /* @__PURE__ */ new Set();
|
|
3899
3908
|
picked.set(sel.index, set);
|
|
@@ -3986,7 +3995,22 @@ async function dirtyCount(cwd) {
|
|
|
3986
3995
|
}
|
|
3987
3996
|
async function listWorktreesDetailed(ctx) {
|
|
3988
3997
|
const all = await listWorktreesAll(ctx);
|
|
3989
|
-
|
|
3998
|
+
const main2 = all.find((w) => w.isMain);
|
|
3999
|
+
return Promise.all(
|
|
4000
|
+
all.map(async (w) => {
|
|
4001
|
+
const detail = { ...w, dirty: w.prunable || w.bare ? 0 : await dirtyCount(w.path) };
|
|
4002
|
+
if (w.isMain) return detail;
|
|
4003
|
+
if (!main2?.head || !w.head || /^0+$/.test(main2.head) || /^0+$/.test(w.head)) return { ...detail, comparisonError: "\u5206\u652F\u5C1A\u65E0\u63D0\u4EA4" };
|
|
4004
|
+
try {
|
|
4005
|
+
const result = await runGit(["rev-list", "--left-right", "--count", `${main2.head}...${w.head}`, "--"], { cwd: ctx.commonRoot });
|
|
4006
|
+
const [behind, ahead] = result.stdout.trim().split(/\s+/).map(Number);
|
|
4007
|
+
detail.comparison = { base: main2.branch ?? `HEAD ${main2.head.slice(0, 7)}`, ahead, behind, merged: ahead === 0 };
|
|
4008
|
+
} catch {
|
|
4009
|
+
detail.comparisonError = "\u65E0\u6CD5\u6BD4\u8F83\u63D0\u4EA4\u5386\u53F2";
|
|
4010
|
+
}
|
|
4011
|
+
return detail;
|
|
4012
|
+
})
|
|
4013
|
+
);
|
|
3990
4014
|
}
|
|
3991
4015
|
async function listBranches(ctx, worktrees) {
|
|
3992
4016
|
const r = await runGit(["for-each-ref", "--format=%(refname:short)%09%(objectname:short)", "refs/heads"], { cwd: ctx.commonRoot });
|
|
@@ -4014,13 +4038,11 @@ async function assertNewWorktreePath(ctx, worktrees, p) {
|
|
|
4014
4038
|
for (const w of worktrees) {
|
|
4015
4039
|
if (target === w.path || target.startsWith(w.path + path3.sep)) throw badRequest(`${target} is inside the worktree at ${w.path}`, "invalid_path");
|
|
4016
4040
|
}
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
st = await stat(target);
|
|
4020
|
-
} catch (e) {
|
|
4021
|
-
if (e.code === "ENOENT") return target;
|
|
4041
|
+
const st = await stat(target).catch((e) => {
|
|
4042
|
+
if (e.code === "ENOENT") return void 0;
|
|
4022
4043
|
throw e;
|
|
4023
|
-
}
|
|
4044
|
+
});
|
|
4045
|
+
if (!st) return target;
|
|
4024
4046
|
if (!st.isDirectory() || (await readdir(target)).length > 0) throw badRequest(`${target} already exists`, "path_exists");
|
|
4025
4047
|
return target;
|
|
4026
4048
|
}
|
|
@@ -4093,7 +4115,7 @@ import path4 from "path";
|
|
|
4093
4115
|
import { mkdir, open, readFile as readFile2, rename, stat as stat2, unlink, writeFile } from "fs/promises";
|
|
4094
4116
|
function dataDir() {
|
|
4095
4117
|
const xdg = process.env.XDG_DATA_HOME;
|
|
4096
|
-
const base = xdg
|
|
4118
|
+
const base = xdg?.trim() ? xdg : path4.join(os.homedir(), ".local", "share");
|
|
4097
4119
|
return path4.join(base, "warden");
|
|
4098
4120
|
}
|
|
4099
4121
|
function repoHash(repoRoot) {
|
|
@@ -4103,7 +4125,7 @@ function stateFilePath(repoRoot, baseDir = dataDir()) {
|
|
|
4103
4125
|
return path4.join(baseDir, repoHash(repoRoot), "state.json");
|
|
4104
4126
|
}
|
|
4105
4127
|
function defaultPrefs() {
|
|
4106
|
-
return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true };
|
|
4128
|
+
return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false };
|
|
4107
4129
|
}
|
|
4108
4130
|
function defaultState(repoRoot) {
|
|
4109
4131
|
return { schemaVersion: 1, repoRoot, targets: {}, issues: [], todos: [], prefs: defaultPrefs() };
|
|
@@ -4143,7 +4165,8 @@ function normalise(raw2, repoRoot) {
|
|
|
4143
4165
|
...defaultPrefs(),
|
|
4144
4166
|
...r.prefs ?? {},
|
|
4145
4167
|
nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {},
|
|
4146
|
-
autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true
|
|
4168
|
+
autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true,
|
|
4169
|
+
railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false
|
|
4147
4170
|
}
|
|
4148
4171
|
};
|
|
4149
4172
|
}
|
|
@@ -4294,7 +4317,7 @@ var LANG_BY_EXT = {
|
|
|
4294
4317
|
};
|
|
4295
4318
|
function langForPath(filePath) {
|
|
4296
4319
|
const m = /\.([A-Za-z0-9]+)$/.exec(filePath);
|
|
4297
|
-
if (!m
|
|
4320
|
+
if (!m?.[1]) return "";
|
|
4298
4321
|
return LANG_BY_EXT[m[1].toLowerCase()] ?? m[1].toLowerCase();
|
|
4299
4322
|
}
|
|
4300
4323
|
function formatCommentSection(c) {
|
|
@@ -4315,13 +4338,7 @@ function formatCommentsExport({ repoRoot, comments }) {
|
|
|
4315
4338
|
}
|
|
4316
4339
|
function formatIssueExport({ repoRoot, issue, comments }) {
|
|
4317
4340
|
const targets = [...new Set(comments.map((c) => c.targetKey))];
|
|
4318
|
-
const head = [
|
|
4319
|
-
`# Issue: ${issue.title}`,
|
|
4320
|
-
`Status: ${issue.status}`,
|
|
4321
|
-
`Target: ${targets.join(", ") || "-"}`,
|
|
4322
|
-
`Repo: ${repoRoot}`,
|
|
4323
|
-
`Count: ${comments.length}`
|
|
4324
|
-
];
|
|
4341
|
+
const head = [`# Issue: ${issue.title}`, `Status: ${issue.status}`, `Target: ${targets.join(", ") || "-"}`, `Repo: ${repoRoot}`, `Count: ${comments.length}`];
|
|
4325
4342
|
const parts = [head.join("\n")];
|
|
4326
4343
|
if (issue.body.trim()) parts.push(issue.body.trim());
|
|
4327
4344
|
parts.push(...comments.map(formatCommentSection));
|
|
@@ -4447,6 +4464,44 @@ var NvimService = class {
|
|
|
4447
4464
|
}
|
|
4448
4465
|
};
|
|
4449
4466
|
|
|
4467
|
+
// packages/server/src/tmux.ts
|
|
4468
|
+
import { execFile as execFile3 } from "child_process";
|
|
4469
|
+
import { realpath as realpath3 } from "fs/promises";
|
|
4470
|
+
var runTmux = (args) => new Promise((resolve, reject) => {
|
|
4471
|
+
execFile3("tmux", args, { encoding: "utf8", timeout: 5e3, maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
|
|
4472
|
+
if (!error) return resolve(stdout);
|
|
4473
|
+
const missing = error.code === "ENOENT";
|
|
4474
|
+
reject(
|
|
4475
|
+
new HttpError(503, missing ? "tmux \u4E0D\u53EF\u7528\uFF0C\u8BF7\u5728\u5B89\u88C5\u4E86 tmux \u7684 Linux/macOS \u6216 WSL \u4E2D\u8FD0\u884C warden" : stderr.trim() || "\u65E0\u6CD5\u8FDE\u63A5 tmux", "tmux_unavailable")
|
|
4476
|
+
);
|
|
4477
|
+
});
|
|
4478
|
+
});
|
|
4479
|
+
var TmuxService = class {
|
|
4480
|
+
constructor(run2 = runTmux) {
|
|
4481
|
+
this.run = run2;
|
|
4482
|
+
}
|
|
4483
|
+
run;
|
|
4484
|
+
async sessions(mainRoot) {
|
|
4485
|
+
const root = await realpath3(mainRoot);
|
|
4486
|
+
const output = await this.run(["list-sessions", "-F", "#{session_id} #{session_name} #{session_path}"]);
|
|
4487
|
+
const sessions = [];
|
|
4488
|
+
for (const line of output.trimEnd().split("\n")) {
|
|
4489
|
+
const [id, name, ...parts] = line.split(" ");
|
|
4490
|
+
const path10 = parts.join(" ");
|
|
4491
|
+
if (!id || !/^\$\d+$/.test(id) || !name || !path10) continue;
|
|
4492
|
+
if (await realpath3(path10).catch(() => null) === root) sessions.push({ id, name, path: path10 });
|
|
4493
|
+
}
|
|
4494
|
+
return sessions;
|
|
4495
|
+
}
|
|
4496
|
+
async open(mainRoot, worktreePath, sessionId) {
|
|
4497
|
+
const session = (await this.sessions(mainRoot)).find((s) => s.id === sessionId);
|
|
4498
|
+
if (!session) throw new HttpError(409, "\u4E3B\u4ED3\u5E93\u5BF9\u5E94\u7684 tmux session \u5DF2\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5", "tmux_session_missing");
|
|
4499
|
+
const directory = await realpath3(worktreePath);
|
|
4500
|
+
const window = (await this.run(["new-window", "-d", "-P", "-F", "#{window_id}", "-t", `${session.id}:`, "-c", directory.replaceAll("#", "##")])).trim();
|
|
4501
|
+
return { session: session.name, window };
|
|
4502
|
+
}
|
|
4503
|
+
};
|
|
4504
|
+
|
|
4450
4505
|
// packages/server/src/watcher.ts
|
|
4451
4506
|
import path6 from "path";
|
|
4452
4507
|
import { stat as stat4 } from "fs/promises";
|
|
@@ -4682,7 +4737,11 @@ function createApp(opts) {
|
|
|
4682
4737
|
const target = last ? tryParseTargetKey(last) : void 0;
|
|
4683
4738
|
const usable2 = !!target && (!target.worktree || info.worktrees.some((w) => w.path === target.worktree));
|
|
4684
4739
|
if (last && usable2) info.defaultTarget = last;
|
|
4685
|
-
else if (last)
|
|
4740
|
+
else if (last) {
|
|
4741
|
+
await store.update((s) => {
|
|
4742
|
+
s.prefs.lastTarget = "working";
|
|
4743
|
+
});
|
|
4744
|
+
}
|
|
4686
4745
|
return c.json(info);
|
|
4687
4746
|
});
|
|
4688
4747
|
api.get("/state", async (c) => c.json(await store.load()));
|
|
@@ -4692,6 +4751,7 @@ function createApp(opts) {
|
|
|
4692
4751
|
if (body.viewMode === "unified" || body.viewMode === "split") s.prefs.viewMode = body.viewMode;
|
|
4693
4752
|
if (typeof body.lastTarget === "string") s.prefs.lastTarget = body.lastTarget;
|
|
4694
4753
|
if (typeof body.autoRefresh === "boolean") s.prefs.autoRefresh = body.autoRefresh;
|
|
4754
|
+
if (typeof body.railOpen === "boolean") s.prefs.railOpen = body.railOpen;
|
|
4695
4755
|
if (body.nvimSocketByRoot && typeof body.nvimSocketByRoot === "object") {
|
|
4696
4756
|
s.prefs.nvimSocketByRoot = { ...s.prefs.nvimSocketByRoot, ...body.nvimSocketByRoot };
|
|
4697
4757
|
}
|
|
@@ -4781,7 +4841,8 @@ function createApp(opts) {
|
|
|
4781
4841
|
if (body.hunks !== void 0 && !Array.isArray(body.hunks)) throw badRequest("hunks must be an array", "bad_selection");
|
|
4782
4842
|
const diff = await fileDiffWithHints(ctx, body.path);
|
|
4783
4843
|
if (!diff) throw badRequest(`file ${body.path} is not part of ${key}`, "no_diff");
|
|
4784
|
-
if (diff.contentHash !== body.contentHash)
|
|
4844
|
+
if (diff.contentHash !== body.contentHash)
|
|
4845
|
+
throw new HttpError(409, "the diff changed since it was loaded; refresh and pick the lines again", "diff_changed");
|
|
4785
4846
|
const { patch, lines } = buildStagePatch(diff, mode, body.hunks);
|
|
4786
4847
|
await applyToIndex(ctx.cwd, patch, { reverse: mode === "unstage" });
|
|
4787
4848
|
void watchers.get(ctx.cwd)?.poll();
|
|
@@ -5004,7 +5065,7 @@ function createApp(opts) {
|
|
|
5004
5065
|
});
|
|
5005
5066
|
api.post("/issues", async (c) => {
|
|
5006
5067
|
const body = await c.req.json();
|
|
5007
|
-
if (!body.title
|
|
5068
|
+
if (!body.title?.trim()) throw badRequest("title is required");
|
|
5008
5069
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5009
5070
|
const issue = {
|
|
5010
5071
|
id: randomUUID(),
|
|
@@ -5097,7 +5158,7 @@ function createApp(opts) {
|
|
|
5097
5158
|
});
|
|
5098
5159
|
api.post("/todos", async (c) => {
|
|
5099
5160
|
const body = await c.req.json();
|
|
5100
|
-
if (!body.title
|
|
5161
|
+
if (!body.title?.trim()) throw badRequest("title is required");
|
|
5101
5162
|
const branch = body.branch?.trim() || await currentBranch(await knownRoot(body.root));
|
|
5102
5163
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5103
5164
|
const todo = {
|
|
@@ -5209,6 +5270,15 @@ function createApp(opts) {
|
|
|
5209
5270
|
const res = { base, sha, ahead: Number(counts[1] ?? 0), behind: Number(counts[0] ?? 0) };
|
|
5210
5271
|
return c.json(res);
|
|
5211
5272
|
});
|
|
5273
|
+
const tmux = opts.tmux ?? new TmuxService();
|
|
5274
|
+
api.get("/tmux/sessions", async (c) => c.json({ sessions: await tmux.sessions(repo.commonRoot) }));
|
|
5275
|
+
api.post("/tmux/windows", async (c) => {
|
|
5276
|
+
const body = await c.req.json();
|
|
5277
|
+
if (!body || typeof body.path !== "string" || typeof body.sessionId !== "string") throw badRequest("path and sessionId are required");
|
|
5278
|
+
const wt = (await worktrees(true)).find((w) => w.path === body.path && !w.bare);
|
|
5279
|
+
if (!wt) throw badRequest("unknown worktree", "unknown_worktree");
|
|
5280
|
+
return c.json(await tmux.open(repo.commonRoot, wt.path, body.sessionId), 201);
|
|
5281
|
+
});
|
|
5212
5282
|
api.get("/worktrees", async (c) => {
|
|
5213
5283
|
const list = await listWorktreesDetailed(repo);
|
|
5214
5284
|
const res = { worktrees: list, branches: await listBranches(repo, list), pathPrefix: worktreePathPrefix(repo) };
|
|
@@ -5309,7 +5379,7 @@ function createApp(opts) {
|
|
|
5309
5379
|
}
|
|
5310
5380
|
|
|
5311
5381
|
// packages/server/src/wsl.ts
|
|
5312
|
-
import { execFile as
|
|
5382
|
+
import { execFile as execFile4 } from "child_process";
|
|
5313
5383
|
import { readFile as readFile4 } from "fs/promises";
|
|
5314
5384
|
var PROBE_TIMEOUT_MS = 3e3;
|
|
5315
5385
|
async function isWsl(env = process.env, platform = process.platform) {
|
|
@@ -5323,7 +5393,7 @@ async function isWsl(env = process.env, platform = process.platform) {
|
|
|
5323
5393
|
}
|
|
5324
5394
|
function reachableFromWindows(port, token, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
5325
5395
|
return new Promise((resolve) => {
|
|
5326
|
-
|
|
5396
|
+
execFile4(
|
|
5327
5397
|
"curl.exe",
|
|
5328
5398
|
["--silent", "--max-time", String(Math.ceil(timeoutMs / 1e3)), `http://127.0.0.1:${port}/api/ping`],
|
|
5329
5399
|
{ timeout: timeoutMs + 1e3, encoding: "utf8", windowsHide: true },
|
|
@@ -5406,7 +5476,7 @@ async function startServer(opts) {
|
|
|
5406
5476
|
}
|
|
5407
5477
|
|
|
5408
5478
|
// bin/cli.ts
|
|
5409
|
-
var VERSION = true ? "0.
|
|
5479
|
+
var VERSION = true ? "0.10.0" : "dev";
|
|
5410
5480
|
function parseArgs(argv) {
|
|
5411
5481
|
const args = { repoPath: process.cwd(), open: true, help: false, version: false };
|
|
5412
5482
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff) format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff) format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:600;src:url(/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff) format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2) format("woff2"),url(/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff) format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2) format("woff2"),url(/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff) format("woff")}:root{--ground: #eef0f3;--surface: #ffffff;--surface-2: #f5f6f8;--surface-3: #e4e7ec;--line: #d9dde3;--line-strong: #b7bdc7;--ink: #16181d;--ink-2: #454b58;--muted: #626977;--accent: #5b47d9;--accent-strong: #4a38bf;--accent-soft: #ebe8fb;--accent-line: #c4bbf2;--add-bg: #e3f5e7;--add-bg-strong: #bfe9c9;--add-fg: #1e7a3a;--del-bg: #fce8e8;--del-bg-strong: #f5c2c2;--del-fg: #c42b2b;--warn-bg: #f6e7b8;--warn-soft: #fdf8e6;--warn-fg: #8a6100;--warn-line: #d9b64a;--r-ctl: 3px;--r-card: 6px;--ctl-h: 26px;--t-meta: 11px;--t-ui: 13px;--t-lead: 15px;--t-figure: 26px;--lift: 0 4px 16px rgba(20, 24, 33, .13);--sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "Noto Sans Mono CJK SC", monospace}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{font:var(--t-ui) / 1.5 var(--sans);color:var(--ink);background:var(--ground);-webkit-font-smoothing:antialiased}button,input,select,textarea{font:inherit;color:inherit}button{height:var(--ctl-h);display:inline-flex;align-items:center;gap:6px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-ctl);padding:0 10px;cursor:pointer;white-space:nowrap}button:hover:not(:disabled){border-color:var(--line-strong);background:var(--surface-2)}button:disabled{opacity:.45;cursor:default}button.primary{background:var(--ink);border-color:var(--ink);color:#fff}button.primary:hover:not(:disabled){background:#2b2f38;border-color:#2b2f38}button.accent{background:var(--accent);border-color:var(--accent);color:#fff}button.accent:hover:not(:disabled){background:var(--accent-strong);border-color:var(--accent-strong)}button.link{height:auto;background:none;border:none;color:var(--ink-2);padding:0 4px}button.link:hover:not(:disabled){background:none;color:var(--ink);text-decoration:underline;text-underline-offset:2px}button.link.danger{color:var(--del-fg)}button.link.active{color:var(--accent);font-weight:500}button.icon{padding:0 7px}button.quiet{background:none;border-color:transparent;color:var(--ink-2)}button.quiet:hover:not(:disabled){background:var(--surface-3);border-color:transparent;color:var(--ink)}body.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}button.toggle:before{content:"";width:6px;height:6px;border-radius:50%;border:1.5px solid currentColor}button.toggle[aria-pressed=true]{border-color:var(--ink)}button.toggle[aria-pressed=true]:before{background:currentColor}input,select,textarea{height:var(--ctl-h);border:1px solid var(--line);border-radius:var(--r-ctl);padding:0 8px;background:var(--surface)}select{padding-right:4px}textarea{height:auto;width:100%;resize:vertical;font-family:var(--mono);font-size:12px;line-height:1.6;padding:6px 8px}input[type=checkbox]{height:auto;margin:0;accent-color:var(--ink)}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}button:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--accent);outline-offset:1px}kbd{font-family:var(--mono);font-size:11px;background:var(--surface);border:1px solid var(--line);border-bottom-width:2px;border-radius:3px;padding:0 5px;color:var(--ink-2)}.mono{font-family:var(--mono);font-size:12px}.muted{color:var(--muted)}.small{font-size:11px}.spacer{flex:1}.add{color:var(--add-fg)}.del{color:var(--del-fg)}.check{display:inline-flex;align-items:center;gap:5px;white-space:nowrap;cursor:pointer}.badge{display:inline-flex;align-items:center;height:17px;padding:0 6px;border-radius:var(--r-ctl);font-family:var(--mono);font-size:11px;background:var(--surface-3);color:var(--ink-2);white-space:nowrap}.badge-active{background:var(--accent-soft);color:var(--accent)}.badge-exported{background:var(--surface-3);color:var(--muted)}.badge-orphaned{background:var(--warn-bg);color:var(--warn-fg)}.badge-open{background:var(--ink);color:#fff}.badge-closed{background:var(--surface-3);color:var(--muted)}.seg{display:inline-flex;gap:1px;padding:2px;background:var(--surface-3);border-radius:4px}.seg button{height:22px;padding:0 9px;background:transparent;border:1px solid transparent;border-radius:var(--r-ctl);color:var(--ink-2)}.seg button:hover:not(:disabled){background:transparent;border-color:transparent;color:var(--ink)}.seg button.active{background:var(--surface);border-color:var(--line);color:var(--ink);font-weight:500}.seg.small button{height:20px;padding:0 7px;font-size:12px}.seg button.active .tab-count{color:var(--ink)}.tabs{display:inline-flex;align-self:stretch;align-items:stretch;gap:2px}.tab{height:auto;background:none;border:none;border-radius:0;padding:0 6px;color:var(--ink-2);position:relative}.tab:hover:not(:disabled){background:none;color:var(--ink)}.tab.active{color:var(--ink);font-weight:500}.tab.active:after{content:"";position:absolute;left:6px;right:6px;bottom:-1px;height:2px;background:var(--ink)}.tab-count{font-family:var(--mono);font-size:11px;color:var(--muted)}.tab.active .tab-count{color:var(--ink)}.tabs.small .tab{padding:0 5px;font-size:12px}.error-box{color:var(--del-fg);background:var(--del-bg);padding:8px 12px;border-radius:var(--r-card);margin:8px}.empty{padding:20px 12px;text-align:center;color:var(--muted);font-size:12px;line-height:1.6}.app{display:flex;flex-direction:column;height:100%}.body{flex:1;display:flex;min-height:0}.main{flex:1;min-width:0;display:flex;flex-direction:column;background:var(--surface)}.loading-screen,.fatal{padding:48px;text-align:center;color:var(--muted)}.fatal h1{font-family:var(--mono);font-weight:500;font-size:20px;letter-spacing:-.02em;color:var(--ink)}.placeholder{padding:48px 24px;text-align:center;color:var(--muted)}.placeholder p{margin:6px 0}.topbar{display:flex;align-items:center;gap:14px;padding:0 10px 0 12px;min-height:40px;background:var(--ground);flex-wrap:wrap}.brand{display:flex;align-items:center;gap:7px}.mark{width:16px;height:16px;flex:none}.wordmark{font-family:var(--mono);font-weight:500;font-size:15px;letter-spacing:-.02em;color:var(--ink)}.scope{display:flex;align-items:center;gap:8px}.repo-name{font-weight:500}.branch{font-family:var(--mono);font-size:11px;color:var(--ink-2);background:var(--surface-3);border-radius:var(--r-ctl);padding:1px 6px}.inline-form{display:inline-flex;align-items:center;gap:4px}.inline-form input{width:130px;font-family:var(--mono);font-size:12px}.topbar-right{margin-left:auto;display:flex;align-items:center;gap:6px}.nvim{display:inline-flex;align-items:center;gap:4px;max-width:300px}.nvim select{max-width:240px}.nvim-one,.nvim-idle{font-family:var(--mono);font-size:12px;color:var(--muted)}.rail-switch{margin-left:8px;background:none;border-color:transparent;color:var(--ink-2)}.rail-switch:hover:not(:disabled){background:var(--surface-3);border-color:transparent;color:var(--ink)}.rail-switch.active{background:var(--surface);border-color:var(--line);color:var(--ink)}.rail-switch-n{font-family:var(--mono);font-size:var(--t-meta);color:var(--muted)}.rail-switch.has-unexported .rail-switch-n{background:var(--accent-soft);color:var(--accent);border-radius:var(--r-ctl);padding:0 5px}.sidebar{flex:none;display:flex;flex-direction:column;background:var(--ground);overflow:hidden;position:relative}.side-nav{padding:8px 12px;flex-shrink:0}.side-view-select{width:100%;min-height:30px;font-weight:500}.side-slot,.side-form{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;align-items:stretch;gap:10px;padding:12px 12px 16px}.side-form{overflow:visible;padding:0}.side-slot>.side-form{flex:none}.field{display:flex;flex-direction:column;gap:3px;font-size:var(--t-meta);color:var(--muted)}.field input{width:100%;font-size:var(--t-ui)}.field-row{display:flex;align-items:center;gap:6px}.field-row input{min-width:0;flex:1}.side-group{display:flex;flex-direction:column;gap:8px;padding-top:12px;border-top:1px solid var(--line)}.side-group-title{font-weight:600;color:var(--ink-2)}.side-form button{justify-content:center;background:var(--surface-2);border-color:var(--line-strong);font-weight:500}.side-form button:hover:not(:disabled){background:var(--surface-3);border-color:var(--line-strong)}.side-form button.toggle{justify-content:flex-start;font-weight:400}.side-form button.toggle[aria-pressed=true]{background:var(--ink);border-color:var(--ink);color:#fff}.side-form button.link{justify-content:flex-start;background:none;border:none;font-weight:400}.side-form-status{display:flex;align-items:center;gap:8px;font-variant-numeric:tabular-nums}.side-grip{position:absolute;top:0;right:0;bottom:0;width:5px;cursor:col-resize;background:var(--line);background-clip:content-box;border-left:4px solid transparent}.side-grip:hover{background:var(--line-strong)}.tree-blocks{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column}.tree-block{display:flex;flex-direction:column;flex:none;min-width:0}.tree-block+.tree-block{margin-top:6px;border-top:1px solid var(--line)}.tree-block.empty .block-head{padding:6px 12px}.progress{flex:none;padding:10px 12px 12px}.progress-figure{display:flex;align-items:baseline;gap:6px}.progress-done{font-family:var(--mono);font-size:var(--t-figure);font-weight:500;line-height:1.1;letter-spacing:-.03em;color:var(--ink);font-variant-numeric:tabular-nums}.progress-done.complete{color:var(--add-fg)}.progress-of{font-family:var(--mono);font-size:var(--t-lead);color:var(--muted);font-variant-numeric:tabular-nums}.progress-label{font-size:var(--t-meta);color:var(--muted)}.progress-track{margin-top:7px;height:3px;background:var(--surface-3);border-radius:2px;overflow:hidden}.progress-track>span{display:block;height:100%;background:var(--ink);transition:width .25s}.sidebar-target{flex:none;display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--line);white-space:nowrap}.sidebar-target-name{font-family:var(--mono);font-size:12px;color:var(--ink-2);min-width:0;overflow:hidden;text-overflow:ellipsis}.sidebar-target .link{font-size:12px}.block-head{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:8px;padding:8px 12px 4px;background:var(--ground);font-size:12px;white-space:nowrap}.block-title{font-weight:600}.block-n{font-family:var(--mono);font-size:var(--t-meta);color:var(--muted)}.block-head .counts{font-family:var(--mono);font-size:var(--t-meta);display:inline-flex;gap:4px}.tree-tools{display:inline-flex;gap:2px;opacity:0}.tree-block:hover .tree-tools,.tree-tools:focus-within{opacity:1}.tree-tools .link{font-size:var(--t-meta);color:var(--muted)}.tree{padding:0 0 4px}.tree-row{display:flex;align-items:center;gap:6px;padding:0 10px;height:24px;cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none}.tree-row:hover{background:var(--surface-3)}.tree-row.active{background:var(--surface);box-shadow:inset 3px 0 var(--ink);font-weight:500}.tree-row.active .name{color:var(--ink)}.tree-row.viewed .name{color:var(--muted)}.tree-row .name{overflow:hidden;text-overflow:ellipsis}.tree-row.dir .name{font-weight:500;color:var(--ink-2)}.chev{display:inline-block;width:12px;font-size:10px;color:var(--muted);transition:transform .1s}.chev.open{transform:rotate(90deg)}.tree-row .counts{margin-left:auto;font-family:var(--mono);font-size:11px;display:inline-flex;gap:4px}.tree-row .count{font-family:var(--mono);font-size:11px}.viewed-box{margin:0}.status{font-family:var(--mono);font-size:11px;font-weight:500;width:14px;text-align:center}.status-added{color:var(--add-fg)}.status-modified{color:var(--warn-fg)}.status-deleted{color:var(--del-fg)}.status-renamed{color:var(--ink-2)}.changed{font-size:11px;background:var(--warn-bg);color:var(--warn-fg);border-radius:var(--r-ctl);padding:0 5px}.cc{font-family:var(--mono);font-size:11px;background:var(--accent-soft);color:var(--accent);border-radius:var(--r-ctl);padding:0 5px}.diff-panel{flex:1;display:flex;flex-direction:column;min-height:0;position:relative}.file-head{display:flex;align-items:center;gap:10px;padding:0 14px;min-height:40px;border-bottom:1px solid var(--line);background:var(--surface);flex-wrap:wrap}.file-head .path{font-family:var(--mono);font-size:var(--t-lead);letter-spacing:-.01em}.file-head .path .dir{color:var(--muted)}.file-head .path .base{font-weight:500}.file-head .counts{font-family:var(--mono);font-size:11px;display:inline-flex;gap:4px}.file-head .check{font-size:12px}.diff-scroll{flex:1;overflow:auto;font-family:var(--mono);font-size:12px;line-height:20px;tab-size:4;background:var(--surface)}.diff-scroll.selecting,.diff-scroll.picking{-webkit-user-select:none;user-select:none;cursor:row-resize}.diff-scroll.reattaching .add-btn{background:var(--warn-fg)}.vrow{will-change:transform}.row{display:flex;min-height:20px}.row.line,.cell{position:relative}.pin{position:absolute;right:0;top:0;bottom:0;width:3px;background:var(--accent)}.pin.muted{background:var(--line-strong)}.pill{position:absolute;right:10px;top:2px;height:16px;min-width:16px;padding:0 5px;justify-content:center;border:none;border-radius:8px;background:var(--accent);color:#fff;font-family:var(--sans);font-size:11px;line-height:16px}.pill:hover:not(:disabled){background:var(--accent-strong);border-color:transparent}.pill.muted{background:var(--line-strong)}.row.hunk-head{align-items:center;background:var(--surface-2);color:var(--muted);font-size:11.5px;padding:3px 12px 3px 128px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);white-space:pre-wrap}.hunk-text{flex:1;min-width:0}.hunk-stage{font-family:var(--sans);font-size:11px;color:var(--muted);white-space:nowrap}.stagec{flex:0 0 18px;position:relative;-webkit-user-select:none;user-select:none}.stage-box{position:absolute;left:4px;top:4px;width:12px;height:12px;padding:0;border:1.5px solid var(--ink-2);border-radius:2px;background:var(--surface);opacity:0;cursor:pointer;z-index:1}.stage-box:hover:not(:disabled){background:var(--surface-3);border-color:var(--ink)}.row:hover .stage-box,.cell:hover .stage-box,.stage-box.on{opacity:1}.stage-box.on,.stage-box.on:hover:not(:disabled){background:var(--ink);border-color:var(--ink)}.stage-box.on:after{content:"";position:absolute;left:3px;top:0;width:3px;height:6px;border:solid #fff;border-width:0 1.5px 1.5px 0;transform:rotate(45deg)}.row.line.picked,.cell.picked{box-shadow:inset 3px 0 var(--ink)}.row.line.add.picked,.cell.add.picked{background:var(--add-bg-strong)}.row.line.del.picked,.cell.del.picked{background:var(--del-bg-strong)}.stage-bar{position:absolute;left:50%;bottom:16px;transform:translate(-50%);display:flex;align-items:center;gap:8px;padding:6px 8px 6px 12px;background:var(--surface);border:1px solid var(--line-strong);border-radius:var(--r-card);box-shadow:0 6px 20px #16181d24;font-size:12px;white-space:nowrap;z-index:3}.stage-bar-count{margin-right:4px}.stage-bar kbd{margin-left:2px}.stage-bar .primary kbd{background:#ffffff29;border-color:#ffffff4d;color:#fff}.tree-row .stage-file{display:none;font-size:11px;padding:0 2px}.tree-row:hover .stage-file{display:inline-flex}.row.gap-row{background:var(--surface-2);justify-content:center;gap:16px;padding:2px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line);font-family:var(--sans);font-size:12px}.gap-row .link{color:var(--muted)}.row.attach{background:var(--surface-2);border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:6px 12px 6px 60px;font-family:var(--sans);font-size:13px;display:block}.gut{flex:0 0 48px;text-align:right;padding-right:6px;font-size:11px;color:var(--muted);cursor:pointer;-webkit-user-select:none;user-select:none}.gut:hover{color:var(--ink)}.addc{flex:0 0 18px;position:relative}.add-btn{position:absolute;left:0;top:2px;width:16px;height:16px;padding:0;justify-content:center;line-height:16px;border-radius:var(--r-ctl);background:var(--accent);color:#fff;border:none;font-weight:600;font-size:13px;opacity:0;cursor:pointer;z-index:1}.add-btn:hover:not(:disabled){background:var(--accent-strong);border-color:transparent}.row:hover .add-btn,.cell:hover .add-btn{opacity:1}.add-btn.static{position:static;display:inline-flex;opacity:1;width:16px;height:16px;font-size:11px;vertical-align:-3px}.marker{flex:0 0 14px;text-align:center;-webkit-user-select:none;user-select:none;color:var(--muted)}.code{flex:1;min-width:0;white-space:pre-wrap;word-break:break-all;padding-right:40px}.nonl{color:var(--del-fg);margin-left:6px}.row.line.add,.cell.add{background:var(--add-bg)}.row.line.del,.cell.del{background:var(--del-bg)}.row.line.add .marker{color:var(--add-fg)}.row.line.del .marker{color:var(--del-fg)}.row.expanded,.row.pair.expanded .cell{background:var(--surface);color:var(--muted)}.row.line.selected,.cell.selected{background:var(--accent-soft)!important}.row.line.focused,.cell.focused{background:var(--accent-soft)!important;box-shadow:inset 3px 0 var(--accent)}.row.line.flash,.cell.flash{animation:flash 1.6s ease-out}@keyframes flash{0%{background:var(--accent-line)}to{background:transparent}}.row.pair{display:grid;grid-template-columns:1fr 1fr}.cell{display:flex;min-width:0;border-left:1px solid var(--line)}.cell:first-child{border-left:none}.cell.empty{background:var(--surface-2)}.diff-list.split .row.hunk-head{padding-left:78px}.comment-card{position:relative;background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:var(--r-card);padding:7px 10px 8px}.comment-card.status-exported{border-left-color:var(--line-strong)}.comment-card.status-orphaned{border-color:var(--warn-line);border-left-style:dashed;background:var(--warn-soft)}.comment-card.selected{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.comment-head{display:flex;align-items:center;gap:8px;min-height:20px;font-size:12px;color:var(--ink-2);flex-wrap:wrap}.comment-head .mono{font-size:11px}.comment-head .time{font-size:11px;color:var(--muted)}.card-actions{position:absolute;right:6px;top:5px;display:inline-flex;align-items:center;gap:2px;padding-left:8px;border-radius:var(--r-ctl);background:inherit;opacity:0;transition:opacity .1s}.card-actions .link{font-size:12px}.comment-card:hover .card-actions,.comment-card:focus-within .card-actions,.comment-card.reattaching .card-actions{opacity:1}.comment-editor{background:var(--surface);border:1px solid var(--accent);border-left-width:3px;border-radius:var(--r-card);padding:8px 10px;max-width:900px}.comment-editor-head{font-family:var(--mono);font-size:11px;color:var(--muted);margin-bottom:6px}.comment-editor textarea{border-color:var(--line)}.comment-editor-actions{display:flex;gap:6px;margin-top:8px}.comment-editor .primary{background:var(--accent);border-color:var(--accent)}.comment-editor .primary:hover:not(:disabled){background:var(--accent-strong);border-color:var(--accent-strong)}.snippet{font-family:var(--mono);font-size:11.5px;line-height:18px;background:var(--surface-2);border-radius:var(--r-ctl);padding:4px 8px;margin:6px 0;overflow-x:auto;white-space:pre}.snippet .ln{display:inline-block;width:40px;color:var(--muted);text-align:right;margin-right:10px;-webkit-user-select:none;user-select:none}.md{font-size:13px}.md p{margin:4px 0}.md pre{background:var(--surface-2);padding:6px 8px;border-radius:var(--r-ctl);overflow-x:auto}.md code{font-family:var(--mono);font-size:12px;background:var(--surface-2);padding:0 3px;border-radius:3px}.md pre code{background:none;padding:0}.md ul,.md ol{padding-left:20px;margin:4px 0}.md blockquote{margin:4px 0;padding-left:10px;border-left:3px solid var(--line);color:var(--muted)}.commits-panel{flex:1;display:flex;flex-direction:column;min-height:0}.commits-panel[hidden]{display:none}.commits-list{flex:1;overflow:auto;background:var(--surface)}.commit-day{position:sticky;top:0;z-index:1;display:flex;align-items:center;height:24px;padding:0 12px;background:var(--ground);border-bottom:1px solid var(--line);color:var(--ink-2);font-size:12px;font-weight:500}.commit-row{display:flex;align-items:center;gap:10px;height:30px;padding:0 12px;border-bottom:1px solid var(--surface-3);cursor:pointer;white-space:nowrap}.commit-row:hover{background:var(--surface-2)}.commit-row.active{background:var(--surface-2);box-shadow:inset 2px 0 var(--ink)}.commit-row .sha{flex:0 0 auto;min-width:7ch;color:var(--ink-2)}.commit-row .subject{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.commit-row .subject .pr{margin-left:6px;color:var(--muted);font-family:var(--mono);font-size:12px}.commit-row .refs{display:inline-flex;gap:4px;flex:0 1 auto;min-width:0;overflow:hidden}.ref{display:inline-block;max-width:180px;height:17px;padding:0 7px;border:1px solid var(--line-strong);border-radius:9px;color:var(--ink-2);font-family:var(--mono);font-size:11px;line-height:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ref-tag{background:var(--surface-3);border-color:var(--surface-3)}.ref-head{background:var(--ink);border-color:var(--ink);color:#fff}.ref-more{border-style:dashed;color:var(--muted)}.ref-fork{border-style:dashed;border-color:var(--ink-2);color:var(--ink)}.commit-row.fork{border-top:1px dashed var(--line-strong)}.fork-note{font-size:var(--t-meta);line-height:1.6}.fork-note .link{padding:0;font-size:var(--t-meta)}.commit-row .author{flex:0 0 110px;overflow:hidden;text-overflow:ellipsis;color:var(--ink-2);font-size:12px}.commit-row .time{flex:0 0 40px;text-align:right;color:var(--muted);font-family:var(--mono);font-size:11px}.commits-foot{display:flex;justify-content:center;align-items:center;min-height:44px;font-size:12px}.worktrees-panel{flex:1;display:flex;flex-direction:column;min-height:0;background:var(--surface)}.wt-list{flex:1;overflow:auto;max-width:740px;padding:16px 20px 24px;display:flex;flex-direction:column;gap:8px}.wt-row{display:flex;align-items:flex-start;gap:8px;padding:8px 10px;border:1px solid var(--line);border-radius:var(--r-card);background:var(--surface)}.wt-row.active{border-color:var(--line-strong);box-shadow:inset 3px 0 var(--ink)}.wt-row.gone .wt-name{color:var(--muted);text-decoration:line-through}.wt-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}.wt-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.wt-name{font-weight:500;overflow-wrap:anywhere}.wt-path{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wt-actions{display:flex;align-items:center;gap:2px;flex:0 0 auto}.wt-comparison{display:flex;gap:10px;flex-wrap:wrap;font-size:12px}.wt-commit-count{display:inline-flex;align-items:center;gap:3px;font-variant-numeric:tabular-nums}.wt-extras{display:flex;align-items:center;flex-wrap:wrap;gap:4px 10px}.wt-extras>.row-actions{margin:0;gap:2px}.wt-expanded{flex-basis:100%;min-width:0;margin-top:4px;padding:12px;background:var(--surface-2);border-radius:var(--r-card);overflow-wrap:anywhere}.wt-commit{display:flex;align-items:baseline;gap:10px;width:100%;padding:8px 0;border:0;border-bottom:1px solid var(--line);background:transparent;text-align:left;white-space:normal}.wt-commit>span{flex:1;min-width:0}.wt-commit small{color:var(--muted)}.wt-todo{padding:8px 0;border-bottom:1px solid var(--line)}.wt-todo summary{display:flex;align-items:center;gap:8px;cursor:pointer}.wt-todo.done summary>span:last-child{text-decoration:line-through;color:var(--muted)}.wt-tmux>*+*{margin-top:8px}@media(max-width:650px){.wt-commit{flex-wrap:wrap}}.wt-confirm{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:6px;padding:7px 9px;border-radius:var(--r-ctl);background:var(--warn-soft);border:1px solid var(--warn-line);color:var(--warn-fg);font-size:12px}.wt-confirm .link{color:var(--warn-fg)}.wt-confirm .link.danger{color:var(--del-fg)}.wt-empty{padding:20px 2px}.rail-list.tasks{padding:4px 0 12px;gap:0}.task-add{height:34px;margin:0 4px 2px;padding:0 10px 0 24px;justify-content:flex-start;gap:10px;border:none;border-radius:var(--r-card);background:none;color:var(--ink-2);font-weight:500}.task-add:hover:not(:disabled){background:var(--surface-3);border-color:transparent;color:var(--ink)}.task-add-plus{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background:var(--ink);color:#fff;font-size:15px;line-height:1}.task-list{display:flex;flex-direction:column}.task-row{position:relative;display:flex;align-items:flex-start;gap:6px;margin:0 4px;padding:5px 10px 5px 4px;border-radius:var(--r-card)}.task-row:hover,.task-row.open{background:var(--surface)}.task-row.open{box-shadow:0 0 0 1px var(--line)}.task-grip{flex:0 0 14px;height:20px;display:inline-flex;align-items:center;justify-content:center;color:var(--muted);cursor:grab;visibility:hidden}.task-row:hover .task-grip{visibility:visible}.task-list.dragging .task-grip{visibility:hidden}.task-check{position:relative;flex:0 0 18px;width:18px;height:18px;margin-top:1px;padding:0;border:1.5px solid var(--ink-2);border-radius:50%;background:var(--surface)}.task-check:after{content:"";position:absolute;left:5px;top:1.5px;width:4px;height:8px;border:solid var(--ink);border-width:0 1.5px 1.5px 0;transform:rotate(45deg);opacity:0}.task-check:hover:not(:disabled){background:var(--surface);border-color:var(--ink)}.task-check:hover:not(:disabled):after{opacity:.45}.task-row.done .task-check,.task-row.completing .task-check{background:var(--ink);border-color:var(--ink)}.task-row.done .task-check:after,.task-row.completing .task-check:after{border-color:#fff;opacity:1}.task-row.draft .task-check{border-style:dashed;border-color:var(--muted)}.task-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.task-line{display:flex;align-items:flex-start;gap:6px}.task-ta{width:100%;min-width:0;height:auto;padding:0;border:none;border-radius:0;background:transparent;resize:none;overflow:hidden;font-family:var(--sans);line-height:20px}.task-ta:focus{box-shadow:none}.task-title{flex:1;font-size:13px}.task-body{font-size:12px;line-height:18px;color:var(--ink-2)}.task-details{font-size:12px;line-height:18px;color:var(--muted);cursor:text}.task-details .md{font-size:12px}.task-details .md p{margin:0 0 2px}.task-row.done .task-title,.task-row.completing .task-title{text-decoration:line-through;color:var(--muted)}.task-row.completing{opacity:.55;transition:opacity .3s}.task-row .badge{margin-top:2px}.task-count{font-family:var(--mono);font-size:11px;line-height:20px;color:var(--muted);white-space:nowrap}.task-actions{display:inline-flex;align-items:center;gap:2px;margin-left:auto;line-height:20px;white-space:nowrap;visibility:hidden}.task-actions .link{font-size:12px}.task-row:hover .task-actions,.task-row:focus-within .task-actions,.task-sub:hover .task-actions{visibility:visible}.task-row.dragging{opacity:.4}.task-row.drop-before{box-shadow:0 -2px 0 0 var(--ink)}.task-row.drop-after{box-shadow:0 2px 0 0 var(--ink)}.task-empty{padding:14px 20px;color:var(--muted);font-size:12px}.task-add-hint{padding:0 10px 6px 46px;font-size:12px;color:var(--muted)}.task-done{margin-top:8px;padding-top:4px;border-top:1px solid var(--line)}.task-done-head{display:flex;align-items:center;min-height:28px;padding:0 14px 0 10px}.task-done-toggle{height:auto;padding:0 4px;gap:6px;border:none;background:none;color:var(--ink-2);font-weight:500}.task-done-toggle:hover:not(:disabled){background:none;border-color:transparent;color:var(--ink)}.task-list-pick{max-width:220px}.task-subs{display:flex;flex-direction:column;gap:4px;margin-top:6px}.task-sub{display:flex;align-items:flex-start;gap:8px;padding:4px 6px;border-radius:var(--r-ctl);background:var(--surface-2);font-size:12px}.task-sub-dot{flex:0 0 8px;width:8px;height:8px;margin-top:6px;border-radius:50%;background:var(--accent)}.task-sub.status-exported .task-sub-dot{background:var(--line-strong)}.task-sub.status-orphaned .task-sub-dot{background:var(--warn-line)}.task-sub-main{flex:1;min-width:0}.task-sub-head{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.task-sub-head .mono{font-size:11px}.task-sub-body{color:var(--muted);white-space:pre-wrap;max-height:54px;overflow:hidden}.task-sub-add{align-self:flex-start;font-size:12px}.task-sub-none{font-size:12px;color:var(--muted)}.toast{position:fixed;bottom:20px;left:50%;transform:translate(-50%);background:var(--ink);color:#fff;padding:8px 16px;border-radius:var(--r-card);box-shadow:0 6px 20px #16181d40;z-index:100;max-width:80vw}.toast-error{background:var(--del-fg)}.toast-action{height:22px;margin-left:14px;padding:0 8px;background:#ffffff24;border-color:#ffffff59;color:#fff;font-weight:500}.toast-action:hover:not(:disabled){background:#ffffff42;border-color:#ffffff80}.rail{width:360px;flex:none;border-left:1px solid var(--line);display:flex;flex-direction:column;background:var(--ground);min-height:0}.rail.rail-wide{width:440px}.rail-head{display:flex;align-items:center;gap:10px;padding:0 10px;min-height:36px;border-bottom:1px solid var(--line)}.rail-tools{display:flex;align-items:center;gap:8px;padding:0 10px;min-height:34px;border-bottom:1px solid var(--line);font-size:12px}.rail-notice{padding:6px 10px;background:var(--warn-bg);color:var(--warn-fg);border-bottom:1px solid var(--warn-line);display:flex;gap:8px;align-items:center;font-size:12px}.rail-notice .link{color:var(--warn-fg)}.rail-list{flex:1;overflow:auto;padding:10px;display:flex;flex-direction:column;gap:8px}.rail-slot{display:flex;flex-direction:column;gap:8px}.rail-foot{display:flex;flex-direction:column;gap:8px;padding:10px;border-top:1px solid var(--line);background:var(--ground)}.rail-foot-row{display:flex;align-items:center;gap:10px}.send{height:30px;padding:0 14px;background:var(--accent);border-color:var(--accent);color:#fff;font-weight:500}.send:hover:not(:disabled){background:var(--accent-strong);border-color:var(--accent-strong)}.send-n{font-family:var(--mono);font-size:11px;font-weight:400;line-height:16px;padding:0 5px;border-radius:var(--r-ctl);background:#ffffff38}.rail .comment-card{cursor:pointer}.rail .comment-card.focused{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.comment-head .file{color:var(--muted);max-width:120px;overflow:hidden;text-overflow:ellipsis}.rail .comment-editor{max-width:none}@media(prefers-reduced-motion:reduce){.chev,.card-actions,.task-row.completing,.progress-track>span{transition:none}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.action-icon{display:inline-flex;align-items:center;justify-content:center;gap:5px;vertical-align:middle;line-height:1}.action-icon>svg{flex-shrink:0}button:has(>.action-icon){display:inline-flex;align-items:center;justify-content:center;min-width:30px;min-height:30px;padding:6px}button.link:has(>.action-icon){color:var(--muted);border-radius:5px}button.link:has(>.action-icon):hover:not(:disabled){color:var(--accent);background:var(--accent-soft);text-decoration:none}button.link.danger:has(>.action-icon){color:var(--del-fg)}button.link.danger:has(>.action-icon):hover:not(:disabled){background:var(--del-bg)}.nvim>span{display:inline-flex;align-items:center;padding:4px}.nvim{position:relative}.nvim summary{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:1px solid var(--line);border-radius:6px;cursor:pointer;list-style:none}.nvim summary::-webkit-details-marker{display:none}.nvim summary:hover{background:var(--surface-3)}.nvim summary:focus-visible{outline:1px solid var(--accent)}.nvim[open] summary{background:var(--accent-soft);color:var(--accent)}.nvim-popover{position:absolute;z-index:30;left:0;top:calc(100% + 6px);display:flex;align-items:center;gap:8px;width:320px;max-width:calc(100vw - 32px);padding:12px;background:var(--surface);border:1px solid var(--line);border-radius:8px;box-shadow:0 4px 16px #1f23281a}.nvim-popover>:first-child{flex:1;min-width:0;overflow-wrap:anywhere}.tree-icon{flex:0 0 16px;color:var(--muted)}.tree-icon-folder{color:#8c9aab;fill:#8c9aab12}.tree-row{display:flex;align-items:center;gap:6px;padding:0 8px;min-height:32px;border-radius:5px;cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none;line-height:20px}.tree-row.active{background:#eaf3ff;box-shadow:inset 1px 0 var(--accent)}.tree-row.active .tree-file-open{color:var(--accent)}.tree-row:focus-within{outline:1px solid var(--accent);outline-offset:-1px}.tree-row.dir .name{font-weight:500}.tree-row.dir{width:100%;border:0;background:transparent;text-align:left;color:var(--muted)}.tree-row.dir:hover{background:var(--surface-2)}.tree-row .status{display:inline-flex;align-items:center;justify-content:center;flex:0 0 14px;height:18px;font-family:var(--sans);font-size:11px;font-weight:500;line-height:1}.comment-editor-head{font-family:var(--mono);font-size:12px;color:var(--muted);margin:-8px -10px 8px;padding:8px 19px;background:var(--surface-2);border-bottom:1px solid var(--line);border-radius:5px 5px 0 0}.block-heading{display:flex;align-items:center;gap:7px;min-height:26px}.block-summary{display:flex;align-items:center;gap:10px;margin-top:5px;font-size:10px;font-variant-numeric:tabular-nums}.block-progress{flex:1;height:3px;border-radius:3px;background:var(--surface-3);overflow:hidden}.block-progress>span{display:block;height:100%;background:var(--add-fg);border-radius:inherit}.tree-tools{display:inline-flex;gap:2px}.tree-tools button{display:grid;place-items:center;width:26px;height:26px;padding:0;border:none;background:transparent;color:var(--muted)}.tree-empty{display:flex;align-items:center;gap:8px;padding:16px 10px;color:var(--muted);font-size:12px}.block-head{flex-shrink:0;padding:9px 12px 10px;border-bottom:1px solid var(--line);background:#f8fafc;font-size:12px;white-space:nowrap}.block-indicator{width:6px;height:6px;border-radius:50%;background:#bf8700;flex-shrink:0}.block-indicator.is-staged{background:var(--add-fg)}.block-file-count{padding:0 6px;border:1px solid var(--line);border-radius:5px;color:var(--muted);font-size:10px;line-height:17px;font-variant-numeric:tabular-nums}.block-diff-counts{display:flex;gap:7px;font-family:var(--mono)}.block-reviewed{display:flex;align-items:center;gap:3px;color:var(--muted)}.block-reviewed .tree-icon{color:var(--add-fg);width:12px;height:12px;flex-basis:12px}.action-count{font-size:10px;font-weight:500;font-variant-numeric:tabular-nums;line-height:1}.icon-spinning{animation:icon-spin 1s linear infinite}.tree-file-icon{display:inline-flex;width:16px;height:16px;flex:0 0 16px}.tree-file-icon svg{width:100%;height:100%}.tree-file-open{display:flex;align-items:center;gap:7px;flex:1;min-width:0;align-self:stretch;padding:0;border:0;border-radius:0;background:transparent;text-align:left}.tree-file-open:hover:not(:disabled){background:transparent}.tree-file-open:focus-visible{outline:none}.tree-row .name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.tree-row .counts{margin-left:auto;font-family:var(--mono);font-size:11px;display:inline-flex;gap:6px;flex-shrink:0;font-variant-numeric:tabular-nums;min-width:48px;justify-content:flex-end}.tree-row .count{font-size:11px;margin-left:auto;padding:0 4px}.tree-changed{width:6px;height:6px;flex:0 0 6px;border-radius:50%;background:#bf8700}.tree-binary{color:var(--muted);font-size:9px;letter-spacing:.04em}.tree-block .block-head{display:block}.tree-tools{opacity:1;margin-left:auto}
|