@xbghc/warden 0.13.0 → 0.14.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
@@ -11,6 +11,11 @@ with line comments you can copy back to the agent as a prompt.
11
11
  releasing one keeps the directory, installed dependencies included, for the next branch.
12
12
  - Side-by-side **Unstaged** and **Staged** file lists: staged means reviewed. Stage from the UI by dragging
13
13
  over the lines you have read (or a hunk, or a file), and unstage the same way from the Staged view.
14
+ - Debug code marked in a comment is folded out of the review and left out of whole-file staging, and
15
+ the Staged block warns while any of it is in the index (see [Debug code](#debug-code)).
16
+ - Code blocks fold by indentation, as an editor does for a language it has no grammar for: the
17
+ chevron beside a line that heads one shuts its body, and the head then says how many lines and
18
+ changes it hides. A dragged pick across a shut block takes none of them; stage the hunk to take them.
14
19
  - GitHub-style unified / side-by-side diff with syntax highlighting, collapsed file tree, lazy per-file loading, context expansion, virtual scrolling.
15
20
  - Line comments (single line or a dragged range), Markdown, edit / delete.
16
21
  - One click copies all comments as an agent-readable prompt to the clipboard.
@@ -72,19 +77,21 @@ for a global install. The check never delays startup and fails silently. `--no-u
72
77
  | Worktree, everything since base | `worktree:<path>:base:<ref>` | same, run inside the worktree |
73
78
 
74
79
  Refs accept anything git can resolve (`main`, `v1.2`, `HEAD~3`, a sha). `@` is `HEAD`.
75
- Worktrees are discovered with `git worktree list` (and made in the *Worktrees* tab, see below) and
80
+ Worktrees are discovered with `git worktree list` (and made in the *Worktree* view, see below) and
76
81
  share the review state of the main repository.
77
82
  One whose directory is gone (git lists it as *prunable*) is not offered, and a remembered target
78
83
  inside a removed worktree falls back to the working tree on the next load. The comments and viewed
79
84
  flags kept under a worktree's key stay in the state file until a branch is checked out into that
80
85
  slot again, which drops them: they were about another branch.
81
86
 
82
- The sidebar is the navigation, and the only navigation: its three tabs — *变更*, *提交*, *Worktree* —
83
- each fill the left column with the controls for what the middle is showing, and the top bar carries
84
- none of it. There is no target switcher: the *变更* tab's two blocks *are* the working tree, a commit
85
- is picked from the *提交* list, and the *审阅整条分支* and *对比两个 ref* forms below that tab's filters
86
- open the other two. The sidebar names whatever is under review in its block header and puts a
87
- *返回工作区* link above it.
87
+ The sidebar is the navigation, and the only navigation: the picker at its top — *工作区*, *提交历史*,
88
+ *Worktree* — fills the left column with the controls for what the middle is showing, and the top bar
89
+ carries none of it. There is no separate target switcher: *工作区*'s two blocks *are* the working
90
+ tree, a commit is picked from the *提交历史* list, and the *审阅整条分支* and *对比两个 ref* forms below
91
+ its filters open the other two. While a commit, range or branch is under review the picker gains an
92
+ entry naming it — *提交 89d8ba2*, *main..feature*, *分支 vs main* — and reads that while its files are
93
+ in front, so the control at the top of the column says what the files below belong to; *工作区* is
94
+ then the way back.
88
95
 
89
96
  `working`, `staged` and `all` are the three **local views** of one worktree. While any of them is
90
97
  selected the sidebar shows two blocks — 未暂存 (`working`) and 已暂存 (`staged`) — instead of a
@@ -115,7 +122,7 @@ the fix and delete or re-attach it.
115
122
 
116
123
  ## Worktrees
117
124
 
118
- Agents do their best work each in a worktree of its own, and the sidebar's *Worktree* tab is
125
+ Agents do their best work each in a worktree of its own, and the sidebar's *Worktree* view is
119
126
  where those are made and taken down without a trip to the terminal. The directories are numbered
120
127
  **slots** beside the main worktree — `<repo>-1`, `<repo>-2`, … (工位 in the UI) — rather than one
121
128
  directory per branch: a directory is where the dependencies get installed, and a fresh one for every
@@ -170,7 +177,7 @@ directory behind, `node_modules` and all, and the next branch is checked out int
170
177
  | `n` / `p` | Next / previous hunk |
171
178
  | `s` | Stage (Unstaged view) or unstage (Staged view) the picked lines |
172
179
  | `Ctrl+Enter` | Save the comment being edited |
173
- | `Esc` | Drop the picked lines / cancel editing / cancel re-attach mode / shut the rail / back to 变更 |
180
+ | `Esc` | Drop the picked lines / cancel editing / cancel re-attach mode / shut the rail / back to the files under review |
174
181
 
175
182
  ## Staging
176
183
 
@@ -198,6 +205,34 @@ Limits, each reported as a plain error rather than a half-applied patch:
198
205
  (the agent kept editing), the server answers 409 and the page reloads instead of staging the wrong lines.
199
206
  - If another git process holds the index lock the server retries briefly, then gives up with 409.
200
207
 
208
+ ## Debug code
209
+
210
+ Code written to poke at a problem and not meant to be committed can be marked in a comment. There is no
211
+ single convention for this, so warden takes the ones in use, in any comment syntax (`//`, `/*`, `#`,
212
+ `--`, `<!--`, `;`, `%`) and any case, as long as the marker opens the comment:
213
+
214
+ - a block between `debug:start` and `debug:end`, or `develblock:start` and `develblock:end` (the
215
+ webpack-strip-block / gulp-strip-block spelling); markers included, blocks nest, and a block left
216
+ open runs to the end of the file;
217
+ - a single line tagged `nocommit`, `no-commit` or `do not commit` (optionally `@nocommit` / `!nocommit`),
218
+ the tag the usual pre-commit hooks look for.
219
+
220
+ With *忽略调试代码* ticked in the sidebar (the default):
221
+
222
+ - runs of debug lines in a hunk fold into one striped row that opens with a click; jumping to a comment
223
+ inside one opens it;
224
+ - *暂存文件* and *暂存此 hunk* leave debug lines out. Lines picked one by one go in regardless — that
225
+ is how debug code is staged on purpose. A file whose changes are all debug code is refused with a
226
+ note to pick the lines instead;
227
+ - a file whose unstaged changes are all debug code does not count as still to review.
228
+
229
+ Whether it is ticked or not, the 已暂存 heading carries a *调试代码 N* mark while N added lines in the
230
+ index are debug code, with the files in its tooltip.
231
+
232
+ A hunk rarely shows the marker that opened the block its lines sit in, so warden reads both sides of a
233
+ file whole — only for the files `git grep` finds a marker in. Marker detection is textual: a marker
234
+ inside a string literal that follows a comment leader counts too.
235
+
201
236
  ## Comments and export
202
237
 
203
238
  Press the `+` that appears next to a line (drag to cover several lines) and write Markdown. What you
@@ -358,7 +393,8 @@ first time the file is read, and the 已读 marks those views used to keep are d
358
393
 
359
394
  Single user, local only. The server binds to `127.0.0.1` and executes git only through
360
395
  `execFile('git', [...])`. Reads go through an argument whitelist (`rev-parse`, `diff`, `show`, `log`,
361
- `worktree list`, `ls-files`, `status`, `merge-base`, `rev-list`) that refuses option-looking refs and any write-capable
396
+ `worktree list`, `ls-files`, `status`, `merge-base`, `rev-list`, `grep` without `-O` /
397
+ `--open-files-in-pager`) that refuses option-looking refs and any write-capable
362
398
  flag; requests that would need anything else get HTTP 400. The writes are few and each composes its
363
399
  own arguments. `POST /api/targets/:key/stage` runs `git apply --cached` (with `--reverse` for the
364
400
  Staged view) on a patch the server itself builds from the diff it just produced — the patch is never
package/dist/cli.js CHANGED
@@ -3059,7 +3059,8 @@ var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set([
3059
3059
  "merge-base",
3060
3060
  "rev-list",
3061
3061
  "for-each-ref",
3062
- "check-ref-format"
3062
+ "check-ref-format",
3063
+ "grep"
3063
3064
  ]);
3064
3065
  var FORBIDDEN_OPTION_PREFIXES = ["--output", "--ext-diff", "--textconv", "-c", "--config-env", "--exec-path", "--git-dir", "--work-tree"];
3065
3066
  var DEFAULT_GIT_TIMEOUT_MS = 3e4;
@@ -3082,6 +3083,15 @@ function assertAllowedGitArgs(args) {
3082
3083
  if (sub === "worktree" && args[1] !== "list") {
3083
3084
  throw new GitError(`git worktree ${args[1] ?? ""} is not allowed`, null, "", 400, "git_subcommand_forbidden");
3084
3085
  }
3086
+ if (sub === "grep") {
3087
+ for (const arg of args.slice(1)) {
3088
+ if (arg === "--") break;
3089
+ const name = arg.split("=")[0];
3090
+ if (arg.startsWith("-O") || name.length > 3 && "--open-files-in-pager".startsWith(name)) {
3091
+ throw new GitError(`git option not allowed: ${arg}`, null, "", 400, "git_option_forbidden");
3092
+ }
3093
+ }
3094
+ }
3085
3095
  for (const arg of args.slice(1)) {
3086
3096
  if (arg.includes("\0")) throw new GitError("NUL byte in git argument", null, "", 400, "git_bad_argument");
3087
3097
  for (const prefix of FORBIDDEN_OPTION_PREFIXES) {
@@ -3587,6 +3597,40 @@ function parseUnifiedDiff(text) {
3587
3597
  return files;
3588
3598
  }
3589
3599
 
3600
+ // packages/server/src/debug.ts
3601
+ var LEADER = String.raw`(?:\/\/+|\/\*+|#+|--|<!--|;+|%+)\s*`;
3602
+ var BLOCK_START = new RegExp(`${LEADER}(?:debug|develblock):start\\b`, "i");
3603
+ var BLOCK_END = new RegExp(`${LEADER}(?:debug|develblock):end\\b`, "i");
3604
+ var LINE_TAG = new RegExp(`${LEADER}[@!]?(?:no-?commit|do not commit)\\b`, "i");
3605
+ var DEBUG_MARKER_STRINGS = ["debug:start", "develblock:start", "nocommit", "no-commit", "do not commit"];
3606
+ function debugLineNumbers(text) {
3607
+ const out = /* @__PURE__ */ new Set();
3608
+ const lines = text.split("\n");
3609
+ let depth = 0;
3610
+ lines.forEach((line, i) => {
3611
+ const no = i + 1;
3612
+ if (BLOCK_START.test(line)) depth++;
3613
+ if (depth > 0 || LINE_TAG.test(line)) out.add(no);
3614
+ if (depth > 0 && BLOCK_END.test(line)) depth--;
3615
+ });
3616
+ return out;
3617
+ }
3618
+ function markDebugLines(diff, newSide, oldSide) {
3619
+ let added = 0;
3620
+ let deleted = 0;
3621
+ for (const hunk of diff.hunks) {
3622
+ for (const line of hunk.lines) {
3623
+ const debug = line.type === "del" ? !!oldSide?.has(line.oldLineNo) : !!newSide?.has(line.newLineNo);
3624
+ if (!debug) continue;
3625
+ line.debug = true;
3626
+ if (line.type === "add") added++;
3627
+ else if (line.type === "del") deleted++;
3628
+ }
3629
+ }
3630
+ if (added) diff.debugAdditions = added;
3631
+ if (deleted) diff.debugDeletions = deleted;
3632
+ }
3633
+
3590
3634
  // packages/server/src/targets.ts
3591
3635
  var DIFF_BASE_ARGS = ["diff", "--no-color", "--no-ext-diff", "-U3", "-M", "--find-renames"];
3592
3636
  function resolveTargetContext(repo, worktrees, key) {
@@ -3722,7 +3766,8 @@ async function refForSide(ctx, side) {
3722
3766
  if (side === "new") return t.sha;
3723
3767
  return await revParse(ctx.cwd, `${t.sha}^`) ?? EMPTY_TREE_SHA;
3724
3768
  case "range":
3725
- return side === "new" ? t.head : t.base;
3769
+ if (side === "new") return t.head;
3770
+ return await mergeBase(ctx.cwd, t.base, t.head) ?? t.base;
3726
3771
  case "base":
3727
3772
  return side === "new" ? void 0 : baseSha(ctx, t.ref);
3728
3773
  }
@@ -3746,6 +3791,49 @@ async function getFullFile(ctx, filePath, side) {
3746
3791
  throw e;
3747
3792
  }
3748
3793
  }
3794
+ var GREP_CHUNK = 500;
3795
+ async function pathsWithMarkers(ctx, side, paths) {
3796
+ const found = /* @__PURE__ */ new Set();
3797
+ if (paths.length === 0) return found;
3798
+ const ref = await refForSide(ctx, side);
3799
+ const args = ["grep", "-l", "-z", "-I", "-i", "-F", ...DEBUG_MARKER_STRINGS.flatMap((m) => ["-e", m])];
3800
+ if (ref === void 0) args.push("--untracked");
3801
+ else if (ref === ":0") args.push("--cached");
3802
+ else args.push(ref);
3803
+ const prefix = ref === void 0 || ref === ":0" ? "" : `${ref}:`;
3804
+ for (let i = 0; i < paths.length; i += GREP_CHUNK) {
3805
+ const chunk = paths.slice(i, i + GREP_CHUNK).map(literal);
3806
+ const r = await runGit([...args, "--", ...chunk], { cwd: ctx.cwd, okCodes: [0, 1] });
3807
+ for (const p of r.stdout.split("\0")) if (p) found.add(p.startsWith(prefix) ? p.slice(prefix.length) : p);
3808
+ }
3809
+ return found;
3810
+ }
3811
+ async function annotateDebug(ctx, files) {
3812
+ const text = files.filter((f) => !f.binary && f.hunks.length > 0);
3813
+ const oldPathOf = (f) => f.oldPath ?? f.path;
3814
+ const none = () => /* @__PURE__ */ new Set();
3815
+ const [onNew, onOld] = await Promise.all([
3816
+ pathsWithMarkers(
3817
+ ctx,
3818
+ "new",
3819
+ text.filter((f) => f.status !== "deleted").map((f) => f.path)
3820
+ ).catch(none),
3821
+ pathsWithMarkers(ctx, "old", text.filter((f) => f.status !== "added").map(oldPathOf)).catch(none)
3822
+ ]);
3823
+ const read = async (p, side, marked) => {
3824
+ if (!marked.has(p)) return void 0;
3825
+ const content = await getFullFile(ctx, p, side).catch(() => null);
3826
+ return content === null ? void 0 : debugLineNumbers(content);
3827
+ };
3828
+ await mapLimit(
3829
+ text.filter((f) => onNew.has(f.path) || onOld.has(oldPathOf(f))),
3830
+ 8,
3831
+ async (f) => {
3832
+ const [n, o] = await Promise.all([read(f.path, "new", onNew), read(oldPathOf(f), "old", onOld)]);
3833
+ markDebugLines(f, n, o);
3834
+ }
3835
+ );
3836
+ }
3749
3837
 
3750
3838
  // packages/server/src/anchor.ts
3751
3839
  function sideLines(diff, side) {
@@ -3895,9 +3983,9 @@ function cutHunk(hunk, picked, mode) {
3895
3983
  });
3896
3984
  return out;
3897
3985
  }
3898
- function pickedLines(diff, selection) {
3986
+ function pickedLines(diff, selection, skipDebug) {
3899
3987
  const picked = /* @__PURE__ */ new Map();
3900
- const changed = (h) => h.lines.map((l, i) => l.type === "context" ? -1 : i).filter((i) => i >= 0);
3988
+ const changed = (h) => h.lines.map((l, i) => l.type === "context" || skipDebug && l.debug ? -1 : i).filter((i) => i >= 0);
3901
3989
  if (selection === void 0) {
3902
3990
  for (const [i, h] of diff.hunks.entries()) picked.set(i, new Set(changed(h)));
3903
3991
  return picked;
@@ -3921,10 +4009,11 @@ function pickedLines(diff, selection) {
3921
4009
  }
3922
4010
  return picked;
3923
4011
  }
3924
- function buildStagePatch(diff, mode, selection) {
4012
+ function buildStagePatch(diff, mode, selection, opts = {}) {
3925
4013
  if (diff.binary) throw badRequest("binary files cannot be staged from here; use git add", "not_stageable");
3926
4014
  const whole = selection === void 0;
3927
- const picked = pickedLines(diff, selection);
4015
+ const skipDebug = !!opts.skipDebug && mode === "stage";
4016
+ const picked = pickedLines(diff, selection, skipDebug);
3928
4017
  let lines = 0;
3929
4018
  for (const set of picked.values()) lines += set.size;
3930
4019
  const changedTotal = diff.hunks.reduce((n, h) => n + h.lines.filter((l) => l.type !== "context").length, 0);
@@ -3957,7 +4046,11 @@ function buildStagePatch(diff, mode, selection) {
3957
4046
  }
3958
4047
  if (lines === 0) {
3959
4048
  const headerOnly = whole && (modeChange || rename2 || (diff.status === "added" || diff.status === "deleted") && diff.hunks.length === 0);
3960
- if (!headerOnly) throw badRequest("no changed lines selected", "empty_selection");
4049
+ if (!headerOnly) {
4050
+ if (skipDebug && [...pickedLines(diff, selection, false).values()].some((set) => set.size > 0))
4051
+ throw badRequest("every changed line picked is debug code; pick those lines one by one to stage them anyway", "debug_only");
4052
+ throw badRequest("no changed lines selected", "empty_selection");
4053
+ }
3961
4054
  return { patch: header.join("\n") + "\n", lines: 0 };
3962
4055
  }
3963
4056
  let delta = 0;
@@ -4225,7 +4318,7 @@ function stateFilePath(repoRoot, baseDir = dataDir()) {
4225
4318
  return path4.join(baseDir, repoHash(repoRoot), "state.json");
4226
4319
  }
4227
4320
  function defaultPrefs() {
4228
- return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false };
4321
+ return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false, ignoreDebug: true };
4229
4322
  }
4230
4323
  function defaultState(repoRoot) {
4231
4324
  return { schemaVersion: 1, repoRoot, targets: {}, issues: [], todos: [], prefs: defaultPrefs() };
@@ -4266,7 +4359,8 @@ function normalise(raw2, repoRoot) {
4266
4359
  ...r.prefs ?? {},
4267
4360
  nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {},
4268
4361
  autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true,
4269
- railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false
4362
+ railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false,
4363
+ ignoreDebug: typeof r.prefs?.ignoreDebug === "boolean" ? r.prefs.ignoreDebug : true
4270
4364
  }
4271
4365
  };
4272
4366
  }
@@ -4863,6 +4957,7 @@ function createApp(opts) {
4863
4957
  if (typeof body.lastTarget === "string") s.prefs.lastTarget = body.lastTarget;
4864
4958
  if (typeof body.autoRefresh === "boolean") s.prefs.autoRefresh = body.autoRefresh;
4865
4959
  if (typeof body.railOpen === "boolean") s.prefs.railOpen = body.railOpen;
4960
+ if (typeof body.ignoreDebug === "boolean") s.prefs.ignoreDebug = body.ignoreDebug;
4866
4961
  if (body.nvimSocketByRoot && typeof body.nvimSocketByRoot === "object") {
4867
4962
  s.prefs.nvimSocketByRoot = { ...s.prefs.nvimSocketByRoot, ...body.nvimSocketByRoot };
4868
4963
  }
@@ -4874,6 +4969,7 @@ function createApp(opts) {
4874
4969
  const key = c.req.param("key");
4875
4970
  const ctx = await targetCtx(key);
4876
4971
  const diffs = await listTargetDiffs(ctx);
4972
+ await annotateDebug(ctx, diffs);
4877
4973
  listings.set(key, { at: Date.now(), files: diffs });
4878
4974
  const notices = changedNotices.get(key) ?? /* @__PURE__ */ new Set();
4879
4975
  changedNotices.set(key, notices);
@@ -4914,6 +5010,7 @@ function createApp(opts) {
4914
5010
  const untracked = c.req.query("untracked") === "1" ? true : void 0;
4915
5011
  const diff = await fileDiffWithHints(ctx, filePath, { oldPath, untracked });
4916
5012
  if (!diff) throw notFound(`no diff for ${filePath} in ${key}`, "no_diff");
5013
+ await annotateDebug(ctx, [diff]);
4917
5014
  return c.json(diff);
4918
5015
  });
4919
5016
  api.get("/targets/:key/file/full", async (c) => {
@@ -4950,11 +5047,13 @@ function createApp(opts) {
4950
5047
  if (!body.path || typeof body.path !== "string") throw badRequest("missing path");
4951
5048
  if (!body.contentHash || typeof body.contentHash !== "string") throw badRequest("contentHash is required");
4952
5049
  if (body.hunks !== void 0 && !Array.isArray(body.hunks)) throw badRequest("hunks must be an array", "bad_selection");
5050
+ if (body.skipDebug !== void 0 && typeof body.skipDebug !== "boolean") throw badRequest("skipDebug must be a boolean", "bad_selection");
4953
5051
  const diff = await fileDiffWithHints(ctx, body.path);
4954
5052
  if (!diff) throw badRequest(`file ${body.path} is not part of ${key}`, "no_diff");
4955
5053
  if (diff.contentHash !== body.contentHash)
4956
5054
  throw new HttpError(409, "the diff changed since it was loaded; refresh and pick the lines again", "diff_changed");
4957
- const { patch, lines } = buildStagePatch(diff, mode, body.hunks);
5055
+ if (body.skipDebug) await annotateDebug(ctx, [diff]);
5056
+ const { patch, lines } = buildStagePatch(diff, mode, body.hunks, { skipDebug: body.skipDebug });
4958
5057
  await applyToIndex(ctx.cwd, patch, { reverse: mode === "unstage" });
4959
5058
  void watchers.get(ctx.cwd)?.poll();
4960
5059
  const res = { ok: true, lines };
@@ -5665,7 +5764,7 @@ async function startServer(opts) {
5665
5764
  }
5666
5765
 
5667
5766
  // bin/cli.ts
5668
- var VERSION = true ? "0.13.0" : "dev";
5767
+ var VERSION = true ? "0.14.0" : "dev";
5669
5768
  function parseArgs(argv) {
5670
5769
  const args = { repoPath: process.cwd(), open: true, updateCheck: true, help: false, version: false };
5671
5770
  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}.update-chip{height:auto;padding:1px 6px;border:none;font-family:var(--mono);font-size:var(--t-meta);color:var(--ink-2);background:var(--surface-3)}.update-chip:hover:not(:disabled){color:var(--ink);background:var(--line)}.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.debug-row{background:repeating-linear-gradient(-45deg,var(--surface-2) 0 6px,var(--surface) 6px 12px);border-top:1px dashed var(--line-strong);border-bottom:1px dashed var(--line-strong);padding:2px 0 2px 60px;font-family:var(--sans);font-size:12px}.row.debug-row.open{border-bottom-style:none}.debug-row .link{color:var(--muted);text-decoration:none}.debug-row .link:hover{color:var(--ink)}.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}.foldc{flex:0 0 14px;position:relative;-webkit-user-select:none;user-select:none}.fold-btn{position:absolute;inset:0;height:auto;display:flex;align-items:center;justify-content:center;padding:0;border:0;background:none;color:var(--muted);cursor:pointer;opacity:0}.fold-btn svg{transform:rotate(90deg);transition:transform .1s}.fold-btn.shut svg{transform:none}.row.line:hover .fold-btn,.cell:hover .fold-btn,.fold-btn.shut,.fold-btn:focus-visible{opacity:1}.fold-btn:hover:not(:disabled){background:none;color:var(--ink)}.fold-chip{margin-left:8px;padding:0 6px;border:1px solid var(--line);border-radius:var(--r-ctl);background:var(--surface-2);color:var(--muted);font-family:var(--sans);font-size:var(--t-meta);line-height:14px;cursor:pointer;display:inline-block;height:16px;vertical-align:top;margin-top:2px}.fold-chip:hover{border-color:var(--line-strong);color:var(--ink)}.fold-chip .add{color:var(--add-fg)}.fold-chip .del{color:var(--del-fg)}.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.blank{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-row.free{border-style:dashed;background:var(--surface-2)}.wt-row.free .wt-name{color:var(--ink-2)}.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;align-items:center;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.dir.viewed .name{font-weight:400;color:var(--muted)}.tree-row.dir.viewed .tree-icon-folder{opacity:.55}.tree-row.dir.viewed .count{display:inline-flex;align-items:center;gap:3px}.tree-row.dir.viewed .count .tree-icon{color:var(--add-fg);width:12px;height:12px;flex-basis:12px}.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}.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-warn{padding:0 6px;border:1px solid var(--warn-line);border-radius:5px;background:var(--warn-soft);color:var(--warn-fg);font-size:10px;font-weight:600;line-height:17px;white-space:nowrap}.side-toggle{flex:none;display:flex;align-items:center;gap:6px;padding:0 12px 8px;color:var(--ink-2);font-size:var(--t-meta);cursor:pointer}.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)}.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}