@xbghc/warden 0.8.0 → 0.10.0

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