@xbghc/warden 0.4.0 → 0.5.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
@@ -4,7 +4,8 @@ Local web UI for reviewing git diffs — built for reviewing changes that a codi
4
4
  with line comments you can copy back to the agent as a prompt.
5
5
 
6
6
  - Runs as a single local process per repository (`127.0.0.1` only, no auth, no database).
7
- - Diff sources: working tree, staged, working tree vs HEAD, any commit, any two refs, and git worktrees.
7
+ - Diff sources: working tree, staged, working tree vs HEAD, a branch since it forked off its base (commits and
8
+ uncommitted work together), any commit, any two refs, and git worktrees.
8
9
  - Side-by-side **Unstaged** and **Staged** file lists, so staging a hunk in your editor is what marks it reviewed.
9
10
  - GitHub-style unified / side-by-side diff with syntax highlighting, collapsed file tree, lazy per-file loading, context expansion, virtual scrolling.
10
11
  - Line comments (single line or a dragged range), Markdown, edit / delete.
@@ -14,7 +15,9 @@ with line comments you can copy back to the agent as a prompt.
14
15
  - Auto-refresh — warden watches the repository and reloads itself when you edit, stage, commit or switch branches.
15
16
  - Local issues (title, Markdown body, open/closed) that link comments.
16
17
  - Branch-scoped todos, exportable as a numbered checklist.
17
- - Commit history browser.
18
+ - Commit history browser: grouped by day, branch / tag labels, search by message, sha, author or
19
+ path (each a `git log` on the server, not a filter over the rows already loaded), and a
20
+ first-parent view that folds merged branches into their merge commits.
18
21
  - Click a line number to jump to that line in a running nvim instance (WSL2 friendly).
19
22
  - Read-only with respect to git: only whitelisted read sub-commands are ever executed.
20
23
 
@@ -45,10 +48,12 @@ Several instances on the same repository can run at the same time.
45
48
  | Working tree, uncommitted (incl. untracked) | `working` | `git diff` + `git diff --no-index /dev/null <file>` |
46
49
  | Staged | `staged` | `git diff --cached` |
47
50
  | Working tree, everything vs HEAD | `all` | `git diff HEAD` (+ untracked) |
51
+ | Branch, everything since it forked off a base (committed or not) | `base:<ref>` | `git diff $(git merge-base <ref> HEAD)` (+ untracked) |
48
52
  | One commit | `commit:<sha>` | `git diff <sha>^ <sha>` |
49
53
  | Two refs | `range:<base>..<head>` | `git diff <base>...<head>` |
50
54
  | Worktree, working tree | `worktree:<path>:working` | same, run inside the worktree |
51
55
  | Worktree, branch vs base | `worktree:<path>:range:<base>..<head>` | same, run inside the worktree |
56
+ | Worktree, everything since base | `worktree:<path>:base:<ref>` | same, run inside the worktree |
52
57
 
53
58
  Refs accept anything git can resolve (`main`, `v1.2`, `HEAD~3`, a sha). `@` is `HEAD`.
54
59
  Worktrees are discovered with `git worktree list` and share the review state of the main repository.
@@ -57,7 +62,17 @@ Worktrees are discovered with `git worktree list` and share the review state of
57
62
  selected the sidebar shows two blocks — Unstaged (`working`) and Staged (`staged`) — instead of a
58
63
  single tree, and clicking a file switches to the view it belongs to. A file that is only partly
59
64
  staged appears in both. Switching between the three keeps your comments, the draft you are typing
60
- and the current selection; only the diff is reloaded. Commit and range targets keep the single tree.
65
+ and the current selection; only the diff is reloaded. Commit, range and `base` targets keep the single tree.
66
+
67
+ `base:<ref>` is for a branch a coding agent has been working on, committing as it goes: one tree with
68
+ everything since the branch forked off `<ref>` — the commits plus whatever is still uncommitted or
69
+ untracked. The diff runs against the merge base, so commits that landed on `<ref>` after the fork are
70
+ not listed as reverted (which is what a plain `git diff <ref>` would do). The *Branch* button picks the
71
+ base for you — the main worktree's branch when a sibling worktree is under review, otherwise `main` or
72
+ `master` — and the field next to it takes any ref. Unlike the three local views, `base` keeps its own
73
+ pool of comments and a commit never deletes them: the round under review is not over when the agent
74
+ commits, so a comment whose lines changed stays *orphaned*, snippet and all, until you have checked
75
+ the fix and delete or re-attach it.
61
76
 
62
77
  ## Keyboard
63
78
 
@@ -120,6 +135,9 @@ them — the comment's current view first, then `working`, `staged`, `all`. So:
120
135
  **deleted** (and unlinked from any issue), because the code they were about is now history.
121
136
  Anything still visible in Unstaged or Staged survives, including as a context line.
122
137
 
138
+ Commit, range and `base` targets each keep their own pool and only ever search themselves, and none of
139
+ them deletes on a moved HEAD.
140
+
123
141
  Comment markers in the diff belong to one view; the rail's *全部* tab lists the whole pool and
124
142
  *此文件* lists every comment on the open file regardless of which view it currently sits in.
125
143
 
@@ -201,7 +219,7 @@ move into the matching scope the first time the file is read.
201
219
 
202
220
  Single user, local only. The server binds to `127.0.0.1`, executes git only through
203
221
  `execFile('git', [...])` with an argument whitelist (`rev-parse`, `diff`, `show`, `log`, `worktree list`,
204
- `ls-files`, `status`), refuses option-looking refs and any write-capable flag, and never writes into the
222
+ `ls-files`, `status`, `merge-base`), refuses option-looking refs and any write-capable flag, and never writes into the
205
223
  repository. Requests that would need anything else get HTTP 400.
206
224
 
207
225
  ## Development
package/dist/cli.js CHANGED
@@ -2897,6 +2897,11 @@ function parseSuffix(rest) {
2897
2897
  if (!isValidRef(sha)) throw new TargetKeyError(`invalid commit ref: ${sha}`);
2898
2898
  return { kind: "commit", sha };
2899
2899
  }
2900
+ if (rest.startsWith("base:")) {
2901
+ const ref = rest.slice("base:".length);
2902
+ if (!isValidRef(ref)) throw new TargetKeyError(`invalid base ref: ${ref}`);
2903
+ return { kind: "base", ref };
2904
+ }
2900
2905
  if (rest.startsWith("range:")) {
2901
2906
  const spec = rest.slice("range:".length);
2902
2907
  const idx = spec.indexOf("..");
@@ -2912,7 +2917,7 @@ function parseTargetKey(key) {
2912
2917
  if (typeof key !== "string" || !key) throw new TargetKeyError("empty target key");
2913
2918
  if (key.startsWith("worktree:")) {
2914
2919
  const rest = key.slice("worktree:".length);
2915
- const m = /^(.+):(working|staged|all|commit:.+|range:.+)$/.exec(rest);
2920
+ const m = /^(.+):(working|staged|all|commit:.+|range:.+|base:.+)$/.exec(rest);
2916
2921
  if (!m || !m[1] || !m[2]) throw new TargetKeyError(`invalid worktree target key: ${key}`);
2917
2922
  const worktree = m[1];
2918
2923
  if (!worktree.startsWith("/")) throw new TargetKeyError(`worktree path must be absolute: ${worktree}`);
@@ -2934,6 +2939,9 @@ function formatTargetKey(t) {
2934
2939
  case "range":
2935
2940
  suffix = `range:${t.base}..${t.head}`;
2936
2941
  break;
2942
+ case "base":
2943
+ suffix = `base:${t.ref}`;
2944
+ break;
2937
2945
  }
2938
2946
  return t.worktree ? `worktree:${t.worktree}:${suffix}` : suffix;
2939
2947
  }
@@ -2980,9 +2988,44 @@ function notFound(message, code = "not_found") {
2980
2988
  return new HttpError(404, message, code);
2981
2989
  }
2982
2990
 
2991
+ // packages/server/src/commits.ts
2992
+ var SEP = "";
2993
+ var COMMIT_FORMAT = `%H${SEP}%h${SEP}%an${SEP}%ae${SEP}%aI${SEP}%P${SEP}%D${SEP}%s`;
2994
+ function parseDecorations(raw2) {
2995
+ const refs = [];
2996
+ let head = false;
2997
+ for (const part of raw2.split(", ")) {
2998
+ const p = part.trim();
2999
+ if (!p || p === "grafted" || p === "replaced") continue;
3000
+ if (p.endsWith("/HEAD")) continue;
3001
+ if (p === "HEAD") head = true;
3002
+ else if (p.startsWith("HEAD -> ")) {
3003
+ head = true;
3004
+ refs.unshift({ name: p.slice("HEAD -> ".length), kind: "branch" });
3005
+ } else if (p.startsWith("tag: ")) refs.push({ name: p.slice("tag: ".length), kind: "tag" });
3006
+ else refs.push({ name: p, kind: "branch" });
3007
+ }
3008
+ return { refs, head };
3009
+ }
3010
+ function parseCommitLog(stdout) {
3011
+ return stdout.split("\n").filter(Boolean).map((line) => {
3012
+ const [sha = "", shortSha = "", author = "", email = "", date = "", parents = "", decorations = "", ...rest] = line.split(SEP);
3013
+ return {
3014
+ sha,
3015
+ shortSha,
3016
+ author,
3017
+ email,
3018
+ date,
3019
+ parents: parents.split(" ").filter(Boolean),
3020
+ ...parseDecorations(decorations),
3021
+ subject: rest.join(SEP)
3022
+ };
3023
+ });
3024
+ }
3025
+
2983
3026
  // packages/server/src/git.ts
2984
3027
  import { execFile } from "child_process";
2985
- var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set(["rev-parse", "diff", "show", "log", "worktree", "ls-files", "status"]);
3028
+ var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set(["rev-parse", "diff", "show", "log", "worktree", "ls-files", "status", "merge-base"]);
2986
3029
  var FORBIDDEN_OPTION_PREFIXES = ["--output", "--ext-diff", "--textconv", "-c", "--config-env", "--exec-path", "--git-dir", "--work-tree"];
2987
3030
  var DEFAULT_GIT_TIMEOUT_MS = 3e4;
2988
3031
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
@@ -3077,6 +3120,14 @@ async function revParse(cwd, ref) {
3077
3120
  return void 0;
3078
3121
  }
3079
3122
  }
3123
+ async function mergeBase(cwd, a, b) {
3124
+ try {
3125
+ const r = await runGit(["merge-base", a, b], { cwd });
3126
+ return r.stdout.trim() || void 0;
3127
+ } catch {
3128
+ return void 0;
3129
+ }
3130
+ }
3080
3131
 
3081
3132
  // packages/server/src/repo.ts
3082
3133
  import path from "path";
@@ -3150,11 +3201,16 @@ async function currentBranch(cwd) {
3150
3201
  const sha = head.stdout.trim();
3151
3202
  return sha ? sha.slice(0, 7) : "HEAD";
3152
3203
  }
3204
+ async function detectDefaultBase(cwd) {
3205
+ for (const ref of ["main", "master"]) if (await refExists(cwd, ref)) return ref;
3206
+ return void 0;
3207
+ }
3153
3208
  async function getRepoInfo(ctx, defaultTarget) {
3154
- const [branch, headRes, worktrees] = await Promise.all([
3209
+ const [branch, headRes, worktrees, defaultBase] = await Promise.all([
3155
3210
  currentBranch(ctx.root),
3156
3211
  runGit(["rev-parse", "HEAD"], { cwd: ctx.root }).catch(() => ({ stdout: "" })),
3157
- listWorktrees(ctx)
3212
+ listWorktrees(ctx),
3213
+ detectDefaultBase(ctx.root)
3158
3214
  ]);
3159
3215
  return {
3160
3216
  root: ctx.root,
@@ -3162,7 +3218,8 @@ async function getRepoInfo(ctx, defaultTarget) {
3162
3218
  branch,
3163
3219
  head: headRes.stdout.trim(),
3164
3220
  worktrees,
3165
- defaultTarget
3221
+ defaultTarget,
3222
+ ...defaultBase ? { defaultBase } : {}
3166
3223
  };
3167
3224
  }
3168
3225
 
@@ -3410,6 +3467,13 @@ function resolveTargetContext(repo, worktrees, key) {
3410
3467
  async function hasHead(cwd) {
3411
3468
  return refExists(cwd, "HEAD");
3412
3469
  }
3470
+ async function baseSha(ctx, ref) {
3471
+ if (!await refExists(ctx.cwd, ref)) throw badRequest(`unknown ref: ${ref}`, "unknown_ref");
3472
+ if (!await hasHead(ctx.cwd)) return EMPTY_TREE_SHA;
3473
+ const sha = await mergeBase(ctx.cwd, ref, "HEAD");
3474
+ if (!sha) throw badRequest(`${ref} and HEAD share no history`, "no_merge_base");
3475
+ return sha;
3476
+ }
3413
3477
  async function diffArgs(ctx) {
3414
3478
  const t = ctx.target;
3415
3479
  switch (t.kind) {
@@ -3429,10 +3493,12 @@ async function diffArgs(ctx) {
3429
3493
  if (!await refExists(ctx.cwd, t.head)) throw badRequest(`unknown ref: ${t.head}`, "unknown_ref");
3430
3494
  return [...DIFF_BASE_ARGS, `${t.base}...${t.head}`];
3431
3495
  }
3496
+ case "base":
3497
+ return [...DIFF_BASE_ARGS, await baseSha(ctx, t.ref)];
3432
3498
  }
3433
3499
  }
3434
3500
  function includesUntracked(t) {
3435
- return t.kind === "working" || t.kind === "all";
3501
+ return t.kind === "working" || t.kind === "all" || t.kind === "base";
3436
3502
  }
3437
3503
  async function listUntracked(cwd) {
3438
3504
  const r = await runGit(["ls-files", "--others", "--exclude-standard", "-z"], { cwd });
@@ -3512,6 +3578,8 @@ async function refForSide(ctx, side) {
3512
3578
  return await revParse(ctx.cwd, `${t.sha}^`) ?? EMPTY_TREE_SHA;
3513
3579
  case "range":
3514
3580
  return side === "new" ? t.head : t.base;
3581
+ case "base":
3582
+ return side === "new" ? void 0 : baseSha(ctx, t.ref);
3515
3583
  }
3516
3584
  }
3517
3585
  async function getFullFile(ctx, filePath, side) {
@@ -4689,23 +4757,29 @@ function createApp(opts) {
4689
4757
  api.get("/commits", async (c) => {
4690
4758
  const limitRaw = Number(c.req.query("limit") ?? 200);
4691
4759
  const limit = Math.min(Math.max(Number.isFinite(limitRaw) ? Math.floor(limitRaw) : 200, 1), 1e3);
4692
- const before = c.req.query("before") || void 0;
4760
+ const offsetRaw = Number(c.req.query("offset") ?? 0);
4761
+ const offset = Number.isFinite(offsetRaw) && offsetRaw > 0 ? Math.floor(offsetRaw) : 0;
4693
4762
  const filePath = c.req.query("path") || void 0;
4694
4763
  const ref = c.req.query("ref") || void 0;
4695
4764
  const rootParam = c.req.query("root") || void 0;
4765
+ const q = (c.req.query("q") ?? "").trim();
4766
+ const author = (c.req.query("author") ?? "").trim();
4767
+ const firstParent = c.req.query("firstParent") === "1";
4696
4768
  let cwd = repo.root;
4697
4769
  if (rootParam) {
4698
4770
  const wt = (await worktrees()).find((w) => w.path === rootParam);
4699
4771
  if (!wt && rootParam !== repo.root) throw badRequest("unknown root", "unknown_worktree");
4700
4772
  cwd = rootParam;
4701
4773
  }
4702
- if (before && !isValidRef(before)) throw badRequest("invalid before ref");
4703
4774
  if (ref && !isValidRef(ref)) throw badRequest("invalid ref");
4704
4775
  if (filePath && (filePath.startsWith("/") || filePath.split("/").includes(".."))) throw badRequest("invalid path");
4705
- const SEP = "";
4706
- const args = ["log", `--max-count=${limit + 1}`, `--format=%H${SEP}%h${SEP}%an${SEP}%ae${SEP}%aI${SEP}%P${SEP}%s`];
4707
- if (before) args.push(before);
4708
- else if (ref) args.push(ref);
4776
+ if (q.length > 200 || author.length > 200) throw badRequest("search text too long");
4777
+ const args = ["log", `--max-count=${limit + 1}`, `--skip=${offset}`, `--format=${COMMIT_FORMAT}`];
4778
+ if (firstParent) args.push("--first-parent");
4779
+ if (q || author) args.push("--fixed-strings", "--regexp-ignore-case");
4780
+ if (q) args.push(`--grep=${q}`);
4781
+ if (author) args.push(`--author=${author}`);
4782
+ if (ref) args.push(ref);
4709
4783
  if (filePath) args.push("--", filePath);
4710
4784
  let stdout = "";
4711
4785
  try {
@@ -4714,13 +4788,17 @@ function createApp(opts) {
4714
4788
  if (e instanceof HttpError && e.code === "git_failed") stdout = "";
4715
4789
  else throw e;
4716
4790
  }
4717
- let commits = stdout.split("\n").filter(Boolean).map((line) => {
4718
- const [sha = "", shortSha = "", author = "", email = "", date = "", parents = "", ...rest] = line.split(SEP);
4719
- return { sha, shortSha, author, email, date, parents: parents.split(" ").filter(Boolean), subject: rest.join(SEP) };
4720
- });
4721
- if (before) commits = commits.filter((x) => x.sha !== before && !x.sha.startsWith(before));
4722
- const hasMore = commits.length > limit;
4723
- const res = { commits: commits.slice(0, limit), hasMore };
4791
+ const rows = parseCommitLog(stdout);
4792
+ const hasMore = rows.length > limit;
4793
+ let commits = rows.slice(0, limit);
4794
+ if (q && offset === 0 && /^[0-9a-f]{4,40}$/i.test(q)) {
4795
+ const sha = await revParse(cwd, `${q}^{commit}`);
4796
+ if (sha && !commits.some((x) => x.sha === sha)) {
4797
+ const hit = parseCommitLog((await runGit(["log", "--max-count=1", `--format=${COMMIT_FORMAT}`, sha], { cwd })).stdout)[0];
4798
+ if (hit) commits = [hit, ...commits];
4799
+ }
4800
+ }
4801
+ const res = { commits, hasMore };
4724
4802
  return c.json(res);
4725
4803
  });
4726
4804
  api.get("/events", async (c) => {
@@ -4900,7 +4978,7 @@ async function startServer(opts) {
4900
4978
  }
4901
4979
 
4902
4980
  // bin/cli.ts
4903
- var VERSION = true ? "0.4.0" : "dev";
4981
+ var VERSION = true ? "0.5.0" : "dev";
4904
4982
  function parseArgs(argv) {
4905
4983
  const args = { repoPath: process.cwd(), open: true, help: false, version: false };
4906
4984
  for (let i = 0; i < argv.length; i++) {
@@ -1 +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;--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:13px/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.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 12px;min-height:42px;background:var(--ground);border-bottom:1px solid var(--line);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}.target{display:flex;align-items:center;gap:8px}.target-label{font-family:var(--mono);font-size:12px;color:var(--muted);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.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;align-self:stretch;display:flex;align-items:stretch;gap:14px}.tools{display:flex;align-items:center;gap:8px}.nvim{display:inline-flex;align-items:center;gap:4px;max-width:320px}.nvim select{max-width:260px}.nvim-one{font-family:var(--mono);font-size:12px;color:var(--ink-2)}.nvim .muted{font-size:12px}.sidebar{width:280px;min-width:200px;border-right:1px solid var(--line);display:flex;flex-direction:column;background:var(--ground);resize:horizontal;overflow:hidden}.tree-block{display:flex;flex-direction:column;flex:0 1 auto;min-height:66px;min-width:0}.tree-block+.tree-block{border-top:1px solid var(--line)}.block-head{display:flex;align-items:center;gap:8px;padding:8px 10px 4px;font-size:12px;white-space:nowrap}.block-title{font-weight:600}.block-n{font-family:var(--mono);font-size:11px;color:var(--muted)}.block-head .counts{font-family:var(--mono);font-size:11px;display:inline-flex;gap:4px}.block-viewed{font-family:var(--mono);font-size:11px;color:var(--muted)}.block-progress{height:2px;margin:0 10px 4px;background:var(--line);border-radius:1px;overflow:hidden}.block-progress>span{display:block;height:100%;background:var(--ink);transition:width .2s}.tree-tools{display:inline-flex;gap:2px}.tree-tools .link{font-size:11px;color:var(--muted)}.tree{flex:1 1 auto;min-height:0;overflow:auto;padding:2px 0 6px}.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 2px 0 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}.file-head{display:flex;align-items:center;gap:10px;padding:0 12px;min-height:38px;border-bottom:1px solid var(--line);background:var(--surface);flex-wrap:wrap}.file-head .path{font-family:var(--mono);font-size:12px}.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{-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{background:var(--surface-2);color:var(--muted);font-size:11.5px;padding:3px 12px 3px 110px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);white-space:pre-wrap}.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:60px}.comment-card,.todo-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,.todo-card:hover .card-actions,.todo-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-tools{display:flex;align-items:center;gap:10px;padding:6px 12px;border-bottom:1px solid var(--line);background:var(--ground);flex-wrap:wrap}.commits-tools input{width:260px}.commits-tools .muted{font-size:12px}.commits-list{flex:1;overflow:auto;background:var(--surface)}.commit-row{display:flex;align-items:center;gap:12px;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{color:var(--ink-2);flex:0 0 70px}.commit-row .subject{flex:1;overflow:hidden;text-overflow:ellipsis}.commit-row .author{flex:0 0 120px;overflow:hidden;text-overflow:ellipsis;font-size:12px}.commit-row .date{flex:0 0 150px;font-family:var(--mono);font-size:11px}.issues-drawer{width:440px;border-left:1px solid var(--line);display:flex;flex-direction:column;background:var(--ground);min-height:0}.drawer-head{display:flex;align-items:center;gap:10px;padding:0 10px;min-height:36px;border-bottom:1px solid var(--line)}.drawer-title{font-weight:600}.issue-form{display:flex;flex-direction:column;gap:6px;padding:10px;border-bottom:1px solid var(--line);background:var(--surface)}.row-actions{display:flex;gap:6px}.issue-list{flex:1;overflow:auto}.issue-row{display:flex;align-items:center;gap:8px;min-height:34px;padding:4px 10px;border-bottom:1px solid var(--line);cursor:pointer}.issue-row:hover{background:var(--surface-3)}.issue-row .title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.issue-row .muted{font-size:12px}.issue-detail{flex:1;overflow:auto;padding:12px;background:var(--surface)}.issue-detail h3{margin:8px 0;font-size:15px;font-weight:600;display:flex;align-items:center;gap:8px}.issue-detail-head{display:flex;align-items:center;gap:4px}.section-title{font-weight:600;margin:16px 0 6px;display:flex;align-items:center;gap:8px}.linked-comment{border:1px solid var(--line);border-left:3px solid var(--line-strong);border-radius:var(--r-card);padding:6px 8px;margin-bottom:6px}.linked-comment.status-active{border-left-color:var(--accent)}.linked-comment.status-orphaned{border-color:var(--warn-line);border-left-style:dashed;background:var(--warn-soft)}.linked-head{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:12px}.linked-body{margin-top:4px;white-space:pre-wrap;color:var(--muted);max-height:80px;overflow:hidden}.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)}.rail{width:360px;min-width:300px;border-left:1px solid var(--line);display:flex;flex-direction:column;background:var(--ground);min-height:0}.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}.sidebar.dual{overflow:hidden}.todo-add input{width:100%}.todo-head{display:flex;align-items:flex-start;gap:8px;min-height:20px;cursor:pointer}.todo-head input[type=checkbox]{margin-top:3px}.todo-head .title{flex:1;min-width:0;line-height:20px;overflow-wrap:anywhere}.todo-head .badge{margin-top:1px}.todo-head .time{font-size:11px;line-height:20px;color:var(--muted)}.todo-head .card-actions{position:static;padding-left:0;line-height:20px}.todo-card.status-done{border-left-color:var(--line-strong)}.todo-card.status-done .title{text-decoration:line-through;color:var(--muted)}.todo-body{margin-top:6px;padding-left:21px}.todo-body p:first-child{margin-top:0}.todo-edit{display:flex;flex-direction:column;gap:6px;margin-top:8px}@media(prefers-reduced-motion:reduce){.chev,.card-actions,.block-progress>span{transition:none}}
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;--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:13px/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.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 12px;min-height:42px;background:var(--ground);border-bottom:1px solid var(--line);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}.target{display:flex;align-items:center;gap:8px}.target-label{font-family:var(--mono);font-size:12px;color:var(--muted);max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.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;align-self:stretch;display:flex;align-items:stretch;gap:14px}.tools{display:flex;align-items:center;gap:8px}.nvim{display:inline-flex;align-items:center;gap:4px;max-width:320px}.nvim select{max-width:260px}.nvim-one{font-family:var(--mono);font-size:12px;color:var(--ink-2)}.nvim .muted{font-size:12px}.sidebar{width:280px;min-width:200px;border-right:1px solid var(--line);display:flex;flex-direction:column;background:var(--ground);resize:horizontal;overflow:hidden}.tree-block{display:flex;flex-direction:column;flex:0 1 auto;min-height:66px;min-width:0}.tree-block+.tree-block{border-top:1px solid var(--line)}.block-head{display:flex;align-items:center;gap:8px;padding:8px 10px 4px;font-size:12px;white-space:nowrap}.block-title{font-weight:600}.block-n{font-family:var(--mono);font-size:11px;color:var(--muted)}.block-head .counts{font-family:var(--mono);font-size:11px;display:inline-flex;gap:4px}.block-viewed{font-family:var(--mono);font-size:11px;color:var(--muted)}.block-progress{height:2px;margin:0 10px 4px;background:var(--line);border-radius:1px;overflow:hidden}.block-progress>span{display:block;height:100%;background:var(--ink);transition:width .2s}.tree-tools{display:inline-flex;gap:2px}.tree-tools .link{font-size:11px;color:var(--muted)}.tree{flex:1 1 auto;min-height:0;overflow:auto;padding:2px 0 6px}.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 2px 0 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}.file-head{display:flex;align-items:center;gap:10px;padding:0 12px;min-height:38px;border-bottom:1px solid var(--line);background:var(--surface);flex-wrap:wrap}.file-head .path{font-family:var(--mono);font-size:12px}.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{-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{background:var(--surface-2);color:var(--muted);font-size:11.5px;padding:3px 12px 3px 110px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);white-space:pre-wrap}.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:60px}.comment-card,.todo-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,.todo-card:hover .card-actions,.todo-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-tools{display:flex;align-items:center;gap:8px;padding:6px 12px;border-bottom:1px solid var(--line);background:var(--ground);flex-wrap:wrap}.commits-tools input{width:140px}.commits-tools input.search{width:260px}.commits-tools input.path{font-size:12px}.commits-count{margin-left:auto;font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}.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)}.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}.issues-drawer{width:440px;border-left:1px solid var(--line);display:flex;flex-direction:column;background:var(--ground);min-height:0}.drawer-head{display:flex;align-items:center;gap:10px;padding:0 10px;min-height:36px;border-bottom:1px solid var(--line)}.drawer-title{font-weight:600}.issue-form{display:flex;flex-direction:column;gap:6px;padding:10px;border-bottom:1px solid var(--line);background:var(--surface)}.row-actions{display:flex;gap:6px}.issue-list{flex:1;overflow:auto}.issue-row{display:flex;align-items:center;gap:8px;min-height:34px;padding:4px 10px;border-bottom:1px solid var(--line);cursor:pointer}.issue-row:hover{background:var(--surface-3)}.issue-row .title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.issue-row .muted{font-size:12px}.issue-detail{flex:1;overflow:auto;padding:12px;background:var(--surface)}.issue-detail h3{margin:8px 0;font-size:15px;font-weight:600;display:flex;align-items:center;gap:8px}.issue-detail-head{display:flex;align-items:center;gap:4px}.section-title{font-weight:600;margin:16px 0 6px;display:flex;align-items:center;gap:8px}.linked-comment{border:1px solid var(--line);border-left:3px solid var(--line-strong);border-radius:var(--r-card);padding:6px 8px;margin-bottom:6px}.linked-comment.status-active{border-left-color:var(--accent)}.linked-comment.status-orphaned{border-color:var(--warn-line);border-left-style:dashed;background:var(--warn-soft)}.linked-head{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:12px}.linked-body{margin-top:4px;white-space:pre-wrap;color:var(--muted);max-height:80px;overflow:hidden}.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)}.rail{width:360px;min-width:300px;border-left:1px solid var(--line);display:flex;flex-direction:column;background:var(--ground);min-height:0}.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}.sidebar.dual{overflow:hidden}.todo-add input{width:100%}.todo-head{display:flex;align-items:flex-start;gap:8px;min-height:20px;cursor:pointer}.todo-head input[type=checkbox]{margin-top:3px}.todo-head .title{flex:1;min-width:0;line-height:20px;overflow-wrap:anywhere}.todo-head .badge{margin-top:1px}.todo-head .time{font-size:11px;line-height:20px;color:var(--muted)}.todo-head .card-actions{position:static;padding-left:0;line-height:20px}.todo-card.status-done{border-left-color:var(--line-strong)}.todo-card.status-done .title{text-decoration:line-through;color:var(--muted)}.todo-body{margin-top:6px;padding-left:21px}.todo-body p:first-child{margin-top:0}.todo-edit{display:flex;flex-direction:column;gap:6px;margin-top:8px}@media(prefers-reduced-motion:reduce){.chev,.card-actions,.block-progress>span{transition:none}}