@xbghc/warden 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -11,6 +11,11 @@ with line comments you can copy back to the agent as a prompt.
|
|
|
11
11
|
releasing one keeps the directory, installed dependencies included, for the next branch.
|
|
12
12
|
- Side-by-side **Unstaged** and **Staged** file lists: staged means reviewed. Stage from the UI by dragging
|
|
13
13
|
over the lines you have read (or a hunk, or a file), and unstage the same way from the Staged view.
|
|
14
|
+
- Debug code marked in a comment is folded out of the review and left out of whole-file staging, and
|
|
15
|
+
the Staged block warns while any of it is in the index (see [Debug code](#debug-code)).
|
|
16
|
+
- Code blocks fold by indentation, as an editor does for a language it has no grammar for: the
|
|
17
|
+
chevron beside a line that heads one shuts its body, and the head then says how many lines and
|
|
18
|
+
changes it hides. A dragged pick across a shut block takes none of them; stage the hunk to take them.
|
|
14
19
|
- GitHub-style unified / side-by-side diff with syntax highlighting, collapsed file tree, lazy per-file loading, context expansion, virtual scrolling.
|
|
15
20
|
- Line comments (single line or a dragged range), Markdown, edit / delete.
|
|
16
21
|
- One click copies all comments as an agent-readable prompt to the clipboard.
|
|
@@ -72,19 +77,21 @@ for a global install. The check never delays startup and fails silently. `--no-u
|
|
|
72
77
|
| Worktree, everything since base | `worktree:<path>:base:<ref>` | same, run inside the worktree |
|
|
73
78
|
|
|
74
79
|
Refs accept anything git can resolve (`main`, `v1.2`, `HEAD~3`, a sha). `@` is `HEAD`.
|
|
75
|
-
Worktrees are discovered with `git worktree list` (and made in the *
|
|
80
|
+
Worktrees are discovered with `git worktree list` (and made in the *Worktree* view, see below) and
|
|
76
81
|
share the review state of the main repository.
|
|
77
82
|
One whose directory is gone (git lists it as *prunable*) is not offered, and a remembered target
|
|
78
83
|
inside a removed worktree falls back to the working tree on the next load. The comments and viewed
|
|
79
84
|
flags kept under a worktree's key stay in the state file until a branch is checked out into that
|
|
80
85
|
slot again, which drops them: they were about another branch.
|
|
81
86
|
|
|
82
|
-
The sidebar is the navigation, and the only navigation:
|
|
83
|
-
|
|
84
|
-
none of it. There is no target switcher:
|
|
85
|
-
is picked from the
|
|
86
|
-
open the other two.
|
|
87
|
-
|
|
87
|
+
The sidebar is the navigation, and the only navigation: the picker at its top — *工作区*, *提交历史*,
|
|
88
|
+
*Worktree* — fills the left column with the controls for what the middle is showing, and the top bar
|
|
89
|
+
carries none of it. There is no separate target switcher: *工作区*'s two blocks *are* the working
|
|
90
|
+
tree, a commit is picked from the *提交历史* list, and the *审阅整条分支* and *对比两个 ref* forms below
|
|
91
|
+
its filters open the other two. While a commit, range or branch is under review the picker gains an
|
|
92
|
+
entry naming it — *提交 89d8ba2*, *main..feature*, *分支 vs main* — and reads that while its files are
|
|
93
|
+
in front, so the control at the top of the column says what the files below belong to; *工作区* is
|
|
94
|
+
then the way back.
|
|
88
95
|
|
|
89
96
|
`working`, `staged` and `all` are the three **local views** of one worktree. While any of them is
|
|
90
97
|
selected the sidebar shows two blocks — 未暂存 (`working`) and 已暂存 (`staged`) — instead of a
|
|
@@ -115,7 +122,7 @@ the fix and delete or re-attach it.
|
|
|
115
122
|
|
|
116
123
|
## Worktrees
|
|
117
124
|
|
|
118
|
-
Agents do their best work each in a worktree of its own, and the sidebar's *Worktree*
|
|
125
|
+
Agents do their best work each in a worktree of its own, and the sidebar's *Worktree* view is
|
|
119
126
|
where those are made and taken down without a trip to the terminal. The directories are numbered
|
|
120
127
|
**slots** beside the main worktree — `<repo>-1`, `<repo>-2`, … (工位 in the UI) — rather than one
|
|
121
128
|
directory per branch: a directory is where the dependencies get installed, and a fresh one for every
|
|
@@ -170,7 +177,7 @@ directory behind, `node_modules` and all, and the next branch is checked out int
|
|
|
170
177
|
| `n` / `p` | Next / previous hunk |
|
|
171
178
|
| `s` | Stage (Unstaged view) or unstage (Staged view) the picked lines |
|
|
172
179
|
| `Ctrl+Enter` | Save the comment being edited |
|
|
173
|
-
| `Esc` | Drop the picked lines / cancel editing / cancel re-attach mode / shut the rail / back to
|
|
180
|
+
| `Esc` | Drop the picked lines / cancel editing / cancel re-attach mode / shut the rail / back to the files under review |
|
|
174
181
|
|
|
175
182
|
## Staging
|
|
176
183
|
|
|
@@ -198,6 +205,34 @@ Limits, each reported as a plain error rather than a half-applied patch:
|
|
|
198
205
|
(the agent kept editing), the server answers 409 and the page reloads instead of staging the wrong lines.
|
|
199
206
|
- If another git process holds the index lock the server retries briefly, then gives up with 409.
|
|
200
207
|
|
|
208
|
+
## Debug code
|
|
209
|
+
|
|
210
|
+
Code written to poke at a problem and not meant to be committed can be marked in a comment. There is no
|
|
211
|
+
single convention for this, so warden takes the ones in use, in any comment syntax (`//`, `/*`, `#`,
|
|
212
|
+
`--`, `<!--`, `;`, `%`) and any case, as long as the marker opens the comment:
|
|
213
|
+
|
|
214
|
+
- a block between `debug:start` and `debug:end`, or `develblock:start` and `develblock:end` (the
|
|
215
|
+
webpack-strip-block / gulp-strip-block spelling); markers included, blocks nest, and a block left
|
|
216
|
+
open runs to the end of the file;
|
|
217
|
+
- a single line tagged `nocommit`, `no-commit` or `do not commit` (optionally `@nocommit` / `!nocommit`),
|
|
218
|
+
the tag the usual pre-commit hooks look for.
|
|
219
|
+
|
|
220
|
+
With *忽略调试代码* ticked in the sidebar (the default):
|
|
221
|
+
|
|
222
|
+
- runs of debug lines in a hunk fold into one striped row that opens with a click; jumping to a comment
|
|
223
|
+
inside one opens it;
|
|
224
|
+
- *暂存文件* and *暂存此 hunk* leave debug lines out. Lines picked one by one go in regardless — that
|
|
225
|
+
is how debug code is staged on purpose. A file whose changes are all debug code is refused with a
|
|
226
|
+
note to pick the lines instead;
|
|
227
|
+
- a file whose unstaged changes are all debug code does not count as still to review.
|
|
228
|
+
|
|
229
|
+
Whether it is ticked or not, the 已暂存 heading carries a *调试代码 N* mark while N added lines in the
|
|
230
|
+
index are debug code, with the files in its tooltip.
|
|
231
|
+
|
|
232
|
+
A hunk rarely shows the marker that opened the block its lines sit in, so warden reads both sides of a
|
|
233
|
+
file whole — only for the files `git grep` finds a marker in. Marker detection is textual: a marker
|
|
234
|
+
inside a string literal that follows a comment leader counts too.
|
|
235
|
+
|
|
201
236
|
## Comments and export
|
|
202
237
|
|
|
203
238
|
Press the `+` that appears next to a line (drag to cover several lines) and write Markdown. What you
|
|
@@ -358,7 +393,8 @@ first time the file is read, and the 已读 marks those views used to keep are d
|
|
|
358
393
|
|
|
359
394
|
Single user, local only. The server binds to `127.0.0.1` and executes git only through
|
|
360
395
|
`execFile('git', [...])`. Reads go through an argument whitelist (`rev-parse`, `diff`, `show`, `log`,
|
|
361
|
-
`worktree list`, `ls-files`, `status`, `merge-base`, `rev-list`
|
|
396
|
+
`worktree list`, `ls-files`, `status`, `merge-base`, `rev-list`, `grep` without `-O` /
|
|
397
|
+
`--open-files-in-pager`) that refuses option-looking refs and any write-capable
|
|
362
398
|
flag; requests that would need anything else get HTTP 400. The writes are few and each composes its
|
|
363
399
|
own arguments. `POST /api/targets/:key/stage` runs `git apply --cached` (with `--reverse` for the
|
|
364
400
|
Staged view) on a patch the server itself builds from the diff it just produced — the patch is never
|
package/dist/cli.js
CHANGED
|
@@ -3059,7 +3059,8 @@ var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
3059
3059
|
"merge-base",
|
|
3060
3060
|
"rev-list",
|
|
3061
3061
|
"for-each-ref",
|
|
3062
|
-
"check-ref-format"
|
|
3062
|
+
"check-ref-format",
|
|
3063
|
+
"grep"
|
|
3063
3064
|
]);
|
|
3064
3065
|
var FORBIDDEN_OPTION_PREFIXES = ["--output", "--ext-diff", "--textconv", "-c", "--config-env", "--exec-path", "--git-dir", "--work-tree"];
|
|
3065
3066
|
var DEFAULT_GIT_TIMEOUT_MS = 3e4;
|
|
@@ -3082,6 +3083,15 @@ function assertAllowedGitArgs(args) {
|
|
|
3082
3083
|
if (sub === "worktree" && args[1] !== "list") {
|
|
3083
3084
|
throw new GitError(`git worktree ${args[1] ?? ""} is not allowed`, null, "", 400, "git_subcommand_forbidden");
|
|
3084
3085
|
}
|
|
3086
|
+
if (sub === "grep") {
|
|
3087
|
+
for (const arg of args.slice(1)) {
|
|
3088
|
+
if (arg === "--") break;
|
|
3089
|
+
const name = arg.split("=")[0];
|
|
3090
|
+
if (arg.startsWith("-O") || name.length > 3 && "--open-files-in-pager".startsWith(name)) {
|
|
3091
|
+
throw new GitError(`git option not allowed: ${arg}`, null, "", 400, "git_option_forbidden");
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3085
3095
|
for (const arg of args.slice(1)) {
|
|
3086
3096
|
if (arg.includes("\0")) throw new GitError("NUL byte in git argument", null, "", 400, "git_bad_argument");
|
|
3087
3097
|
for (const prefix of FORBIDDEN_OPTION_PREFIXES) {
|
|
@@ -3587,6 +3597,40 @@ function parseUnifiedDiff(text) {
|
|
|
3587
3597
|
return files;
|
|
3588
3598
|
}
|
|
3589
3599
|
|
|
3600
|
+
// packages/server/src/debug.ts
|
|
3601
|
+
var LEADER = String.raw`(?:\/\/+|\/\*+|#+|--|<!--|;+|%+)\s*`;
|
|
3602
|
+
var BLOCK_START = new RegExp(`${LEADER}(?:debug|develblock):start\\b`, "i");
|
|
3603
|
+
var BLOCK_END = new RegExp(`${LEADER}(?:debug|develblock):end\\b`, "i");
|
|
3604
|
+
var LINE_TAG = new RegExp(`${LEADER}[@!]?(?:no-?commit|do not commit)\\b`, "i");
|
|
3605
|
+
var DEBUG_MARKER_STRINGS = ["debug:start", "develblock:start", "nocommit", "no-commit", "do not commit"];
|
|
3606
|
+
function debugLineNumbers(text) {
|
|
3607
|
+
const out = /* @__PURE__ */ new Set();
|
|
3608
|
+
const lines = text.split("\n");
|
|
3609
|
+
let depth = 0;
|
|
3610
|
+
lines.forEach((line, i) => {
|
|
3611
|
+
const no = i + 1;
|
|
3612
|
+
if (BLOCK_START.test(line)) depth++;
|
|
3613
|
+
if (depth > 0 || LINE_TAG.test(line)) out.add(no);
|
|
3614
|
+
if (depth > 0 && BLOCK_END.test(line)) depth--;
|
|
3615
|
+
});
|
|
3616
|
+
return out;
|
|
3617
|
+
}
|
|
3618
|
+
function markDebugLines(diff, newSide, oldSide) {
|
|
3619
|
+
let added = 0;
|
|
3620
|
+
let deleted = 0;
|
|
3621
|
+
for (const hunk of diff.hunks) {
|
|
3622
|
+
for (const line of hunk.lines) {
|
|
3623
|
+
const debug = line.type === "del" ? !!oldSide?.has(line.oldLineNo) : !!newSide?.has(line.newLineNo);
|
|
3624
|
+
if (!debug) continue;
|
|
3625
|
+
line.debug = true;
|
|
3626
|
+
if (line.type === "add") added++;
|
|
3627
|
+
else if (line.type === "del") deleted++;
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
if (added) diff.debugAdditions = added;
|
|
3631
|
+
if (deleted) diff.debugDeletions = deleted;
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3590
3634
|
// packages/server/src/targets.ts
|
|
3591
3635
|
var DIFF_BASE_ARGS = ["diff", "--no-color", "--no-ext-diff", "-U3", "-M", "--find-renames"];
|
|
3592
3636
|
function resolveTargetContext(repo, worktrees, key) {
|
|
@@ -3722,7 +3766,8 @@ async function refForSide(ctx, side) {
|
|
|
3722
3766
|
if (side === "new") return t.sha;
|
|
3723
3767
|
return await revParse(ctx.cwd, `${t.sha}^`) ?? EMPTY_TREE_SHA;
|
|
3724
3768
|
case "range":
|
|
3725
|
-
|
|
3769
|
+
if (side === "new") return t.head;
|
|
3770
|
+
return await mergeBase(ctx.cwd, t.base, t.head) ?? t.base;
|
|
3726
3771
|
case "base":
|
|
3727
3772
|
return side === "new" ? void 0 : baseSha(ctx, t.ref);
|
|
3728
3773
|
}
|
|
@@ -3746,6 +3791,49 @@ async function getFullFile(ctx, filePath, side) {
|
|
|
3746
3791
|
throw e;
|
|
3747
3792
|
}
|
|
3748
3793
|
}
|
|
3794
|
+
var GREP_CHUNK = 500;
|
|
3795
|
+
async function pathsWithMarkers(ctx, side, paths) {
|
|
3796
|
+
const found = /* @__PURE__ */ new Set();
|
|
3797
|
+
if (paths.length === 0) return found;
|
|
3798
|
+
const ref = await refForSide(ctx, side);
|
|
3799
|
+
const args = ["grep", "-l", "-z", "-I", "-i", "-F", ...DEBUG_MARKER_STRINGS.flatMap((m) => ["-e", m])];
|
|
3800
|
+
if (ref === void 0) args.push("--untracked");
|
|
3801
|
+
else if (ref === ":0") args.push("--cached");
|
|
3802
|
+
else args.push(ref);
|
|
3803
|
+
const prefix = ref === void 0 || ref === ":0" ? "" : `${ref}:`;
|
|
3804
|
+
for (let i = 0; i < paths.length; i += GREP_CHUNK) {
|
|
3805
|
+
const chunk = paths.slice(i, i + GREP_CHUNK).map(literal);
|
|
3806
|
+
const r = await runGit([...args, "--", ...chunk], { cwd: ctx.cwd, okCodes: [0, 1] });
|
|
3807
|
+
for (const p of r.stdout.split("\0")) if (p) found.add(p.startsWith(prefix) ? p.slice(prefix.length) : p);
|
|
3808
|
+
}
|
|
3809
|
+
return found;
|
|
3810
|
+
}
|
|
3811
|
+
async function annotateDebug(ctx, files) {
|
|
3812
|
+
const text = files.filter((f) => !f.binary && f.hunks.length > 0);
|
|
3813
|
+
const oldPathOf = (f) => f.oldPath ?? f.path;
|
|
3814
|
+
const none = () => /* @__PURE__ */ new Set();
|
|
3815
|
+
const [onNew, onOld] = await Promise.all([
|
|
3816
|
+
pathsWithMarkers(
|
|
3817
|
+
ctx,
|
|
3818
|
+
"new",
|
|
3819
|
+
text.filter((f) => f.status !== "deleted").map((f) => f.path)
|
|
3820
|
+
).catch(none),
|
|
3821
|
+
pathsWithMarkers(ctx, "old", text.filter((f) => f.status !== "added").map(oldPathOf)).catch(none)
|
|
3822
|
+
]);
|
|
3823
|
+
const read = async (p, side, marked) => {
|
|
3824
|
+
if (!marked.has(p)) return void 0;
|
|
3825
|
+
const content = await getFullFile(ctx, p, side).catch(() => null);
|
|
3826
|
+
return content === null ? void 0 : debugLineNumbers(content);
|
|
3827
|
+
};
|
|
3828
|
+
await mapLimit(
|
|
3829
|
+
text.filter((f) => onNew.has(f.path) || onOld.has(oldPathOf(f))),
|
|
3830
|
+
8,
|
|
3831
|
+
async (f) => {
|
|
3832
|
+
const [n, o] = await Promise.all([read(f.path, "new", onNew), read(oldPathOf(f), "old", onOld)]);
|
|
3833
|
+
markDebugLines(f, n, o);
|
|
3834
|
+
}
|
|
3835
|
+
);
|
|
3836
|
+
}
|
|
3749
3837
|
|
|
3750
3838
|
// packages/server/src/anchor.ts
|
|
3751
3839
|
function sideLines(diff, side) {
|
|
@@ -3895,9 +3983,9 @@ function cutHunk(hunk, picked, mode) {
|
|
|
3895
3983
|
});
|
|
3896
3984
|
return out;
|
|
3897
3985
|
}
|
|
3898
|
-
function pickedLines(diff, selection) {
|
|
3986
|
+
function pickedLines(diff, selection, skipDebug) {
|
|
3899
3987
|
const picked = /* @__PURE__ */ new Map();
|
|
3900
|
-
const changed = (h) => h.lines.map((l, i) => l.type === "context" ? -1 : i).filter((i) => i >= 0);
|
|
3988
|
+
const changed = (h) => h.lines.map((l, i) => l.type === "context" || skipDebug && l.debug ? -1 : i).filter((i) => i >= 0);
|
|
3901
3989
|
if (selection === void 0) {
|
|
3902
3990
|
for (const [i, h] of diff.hunks.entries()) picked.set(i, new Set(changed(h)));
|
|
3903
3991
|
return picked;
|
|
@@ -3921,10 +4009,11 @@ function pickedLines(diff, selection) {
|
|
|
3921
4009
|
}
|
|
3922
4010
|
return picked;
|
|
3923
4011
|
}
|
|
3924
|
-
function buildStagePatch(diff, mode, selection) {
|
|
4012
|
+
function buildStagePatch(diff, mode, selection, opts = {}) {
|
|
3925
4013
|
if (diff.binary) throw badRequest("binary files cannot be staged from here; use git add", "not_stageable");
|
|
3926
4014
|
const whole = selection === void 0;
|
|
3927
|
-
const
|
|
4015
|
+
const skipDebug = !!opts.skipDebug && mode === "stage";
|
|
4016
|
+
const picked = pickedLines(diff, selection, skipDebug);
|
|
3928
4017
|
let lines = 0;
|
|
3929
4018
|
for (const set of picked.values()) lines += set.size;
|
|
3930
4019
|
const changedTotal = diff.hunks.reduce((n, h) => n + h.lines.filter((l) => l.type !== "context").length, 0);
|
|
@@ -3957,7 +4046,11 @@ function buildStagePatch(diff, mode, selection) {
|
|
|
3957
4046
|
}
|
|
3958
4047
|
if (lines === 0) {
|
|
3959
4048
|
const headerOnly = whole && (modeChange || rename2 || (diff.status === "added" || diff.status === "deleted") && diff.hunks.length === 0);
|
|
3960
|
-
if (!headerOnly)
|
|
4049
|
+
if (!headerOnly) {
|
|
4050
|
+
if (skipDebug && [...pickedLines(diff, selection, false).values()].some((set) => set.size > 0))
|
|
4051
|
+
throw badRequest("every changed line picked is debug code; pick those lines one by one to stage them anyway", "debug_only");
|
|
4052
|
+
throw badRequest("no changed lines selected", "empty_selection");
|
|
4053
|
+
}
|
|
3961
4054
|
return { patch: header.join("\n") + "\n", lines: 0 };
|
|
3962
4055
|
}
|
|
3963
4056
|
let delta = 0;
|
|
@@ -4225,7 +4318,7 @@ function stateFilePath(repoRoot, baseDir = dataDir()) {
|
|
|
4225
4318
|
return path4.join(baseDir, repoHash(repoRoot), "state.json");
|
|
4226
4319
|
}
|
|
4227
4320
|
function defaultPrefs() {
|
|
4228
|
-
return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false };
|
|
4321
|
+
return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false, ignoreDebug: true };
|
|
4229
4322
|
}
|
|
4230
4323
|
function defaultState(repoRoot) {
|
|
4231
4324
|
return { schemaVersion: 1, repoRoot, targets: {}, issues: [], todos: [], prefs: defaultPrefs() };
|
|
@@ -4266,7 +4359,8 @@ function normalise(raw2, repoRoot) {
|
|
|
4266
4359
|
...r.prefs ?? {},
|
|
4267
4360
|
nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {},
|
|
4268
4361
|
autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true,
|
|
4269
|
-
railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false
|
|
4362
|
+
railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false,
|
|
4363
|
+
ignoreDebug: typeof r.prefs?.ignoreDebug === "boolean" ? r.prefs.ignoreDebug : true
|
|
4270
4364
|
}
|
|
4271
4365
|
};
|
|
4272
4366
|
}
|
|
@@ -4863,6 +4957,7 @@ function createApp(opts) {
|
|
|
4863
4957
|
if (typeof body.lastTarget === "string") s.prefs.lastTarget = body.lastTarget;
|
|
4864
4958
|
if (typeof body.autoRefresh === "boolean") s.prefs.autoRefresh = body.autoRefresh;
|
|
4865
4959
|
if (typeof body.railOpen === "boolean") s.prefs.railOpen = body.railOpen;
|
|
4960
|
+
if (typeof body.ignoreDebug === "boolean") s.prefs.ignoreDebug = body.ignoreDebug;
|
|
4866
4961
|
if (body.nvimSocketByRoot && typeof body.nvimSocketByRoot === "object") {
|
|
4867
4962
|
s.prefs.nvimSocketByRoot = { ...s.prefs.nvimSocketByRoot, ...body.nvimSocketByRoot };
|
|
4868
4963
|
}
|
|
@@ -4874,6 +4969,7 @@ function createApp(opts) {
|
|
|
4874
4969
|
const key = c.req.param("key");
|
|
4875
4970
|
const ctx = await targetCtx(key);
|
|
4876
4971
|
const diffs = await listTargetDiffs(ctx);
|
|
4972
|
+
await annotateDebug(ctx, diffs);
|
|
4877
4973
|
listings.set(key, { at: Date.now(), files: diffs });
|
|
4878
4974
|
const notices = changedNotices.get(key) ?? /* @__PURE__ */ new Set();
|
|
4879
4975
|
changedNotices.set(key, notices);
|
|
@@ -4914,6 +5010,7 @@ function createApp(opts) {
|
|
|
4914
5010
|
const untracked = c.req.query("untracked") === "1" ? true : void 0;
|
|
4915
5011
|
const diff = await fileDiffWithHints(ctx, filePath, { oldPath, untracked });
|
|
4916
5012
|
if (!diff) throw notFound(`no diff for ${filePath} in ${key}`, "no_diff");
|
|
5013
|
+
await annotateDebug(ctx, [diff]);
|
|
4917
5014
|
return c.json(diff);
|
|
4918
5015
|
});
|
|
4919
5016
|
api.get("/targets/:key/file/full", async (c) => {
|
|
@@ -4950,11 +5047,13 @@ function createApp(opts) {
|
|
|
4950
5047
|
if (!body.path || typeof body.path !== "string") throw badRequest("missing path");
|
|
4951
5048
|
if (!body.contentHash || typeof body.contentHash !== "string") throw badRequest("contentHash is required");
|
|
4952
5049
|
if (body.hunks !== void 0 && !Array.isArray(body.hunks)) throw badRequest("hunks must be an array", "bad_selection");
|
|
5050
|
+
if (body.skipDebug !== void 0 && typeof body.skipDebug !== "boolean") throw badRequest("skipDebug must be a boolean", "bad_selection");
|
|
4953
5051
|
const diff = await fileDiffWithHints(ctx, body.path);
|
|
4954
5052
|
if (!diff) throw badRequest(`file ${body.path} is not part of ${key}`, "no_diff");
|
|
4955
5053
|
if (diff.contentHash !== body.contentHash)
|
|
4956
5054
|
throw new HttpError(409, "the diff changed since it was loaded; refresh and pick the lines again", "diff_changed");
|
|
4957
|
-
|
|
5055
|
+
if (body.skipDebug) await annotateDebug(ctx, [diff]);
|
|
5056
|
+
const { patch, lines } = buildStagePatch(diff, mode, body.hunks, { skipDebug: body.skipDebug });
|
|
4958
5057
|
await applyToIndex(ctx.cwd, patch, { reverse: mode === "unstage" });
|
|
4959
5058
|
void watchers.get(ctx.cwd)?.poll();
|
|
4960
5059
|
const res = { ok: true, lines };
|
|
@@ -5665,7 +5764,7 @@ async function startServer(opts) {
|
|
|
5665
5764
|
}
|
|
5666
5765
|
|
|
5667
5766
|
// bin/cli.ts
|
|
5668
|
-
var VERSION = true ? "0.
|
|
5767
|
+
var VERSION = true ? "0.14.0" : "dev";
|
|
5669
5768
|
function parseArgs(argv) {
|
|
5670
5769
|
const args = { repoPath: process.cwd(), open: true, updateCheck: true, help: false, version: false };
|
|
5671
5770
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff) format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff) format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:600;src:url(/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff) format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2) format("woff2"),url(/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff) format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2) format("woff2"),url(/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff) format("woff")}:root{--ground: #eef0f3;--surface: #ffffff;--surface-2: #f5f6f8;--surface-3: #e4e7ec;--line: #d9dde3;--line-strong: #b7bdc7;--ink: #16181d;--ink-2: #454b58;--muted: #626977;--accent: #5b47d9;--accent-strong: #4a38bf;--accent-soft: #ebe8fb;--accent-line: #c4bbf2;--add-bg: #e3f5e7;--add-bg-strong: #bfe9c9;--add-fg: #1e7a3a;--del-bg: #fce8e8;--del-bg-strong: #f5c2c2;--del-fg: #c42b2b;--warn-bg: #f6e7b8;--warn-soft: #fdf8e6;--warn-fg: #8a6100;--warn-line: #d9b64a;--r-ctl: 3px;--r-card: 6px;--ctl-h: 26px;--t-meta: 11px;--t-ui: 13px;--t-lead: 15px;--t-figure: 26px;--lift: 0 4px 16px rgba(20, 24, 33, .13);--sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "Noto Sans Mono CJK SC", monospace}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{font:var(--t-ui) / 1.5 var(--sans);color:var(--ink);background:var(--ground);-webkit-font-smoothing:antialiased}button,input,select,textarea{font:inherit;color:inherit}button{height:var(--ctl-h);display:inline-flex;align-items:center;gap:6px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-ctl);padding:0 10px;cursor:pointer;white-space:nowrap}button:hover:not(:disabled){border-color:var(--line-strong);background:var(--surface-2)}button:disabled{opacity:.45;cursor:default}button.primary{background:var(--ink);border-color:var(--ink);color:#fff}button.primary:hover:not(:disabled){background:#2b2f38;border-color:#2b2f38}button.accent{background:var(--accent);border-color:var(--accent);color:#fff}button.accent:hover:not(:disabled){background:var(--accent-strong);border-color:var(--accent-strong)}button.link{height:auto;background:none;border:none;color:var(--ink-2);padding:0 4px}button.link:hover:not(:disabled){background:none;color:var(--ink);text-decoration:underline;text-underline-offset:2px}button.link.danger{color:var(--del-fg)}button.link.active{color:var(--accent);font-weight:500}button.icon{padding:0 7px}button.quiet{background:none;border-color:transparent;color:var(--ink-2)}button.quiet:hover:not(:disabled){background:var(--surface-3);border-color:transparent;color:var(--ink)}body.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}button.toggle:before{content:"";width:6px;height:6px;border-radius:50%;border:1.5px solid currentColor}button.toggle[aria-pressed=true]{border-color:var(--ink)}button.toggle[aria-pressed=true]:before{background:currentColor}input,select,textarea{height:var(--ctl-h);border:1px solid var(--line);border-radius:var(--r-ctl);padding:0 8px;background:var(--surface)}select{padding-right:4px}textarea{height:auto;width:100%;resize:vertical;font-family:var(--mono);font-size:12px;line-height:1.6;padding:6px 8px}input[type=checkbox]{height:auto;margin:0;accent-color:var(--ink)}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}button:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--accent);outline-offset:1px}kbd{font-family:var(--mono);font-size:11px;background:var(--surface);border:1px solid var(--line);border-bottom-width:2px;border-radius:3px;padding:0 5px;color:var(--ink-2)}.mono{font-family:var(--mono);font-size:12px}.muted{color:var(--muted)}.small{font-size:11px}.spacer{flex:1}.add{color:var(--add-fg)}.del{color:var(--del-fg)}.check{display:inline-flex;align-items:center;gap:5px;white-space:nowrap;cursor:pointer}.badge{display:inline-flex;align-items:center;height:17px;padding:0 6px;border-radius:var(--r-ctl);font-family:var(--mono);font-size:11px;background:var(--surface-3);color:var(--ink-2);white-space:nowrap}.badge-active{background:var(--accent-soft);color:var(--accent)}.badge-exported{background:var(--surface-3);color:var(--muted)}.badge-orphaned{background:var(--warn-bg);color:var(--warn-fg)}.badge-open{background:var(--ink);color:#fff}.badge-closed{background:var(--surface-3);color:var(--muted)}.seg{display:inline-flex;gap:1px;padding:2px;background:var(--surface-3);border-radius:4px}.seg button{height:22px;padding:0 9px;background:transparent;border:1px solid transparent;border-radius:var(--r-ctl);color:var(--ink-2)}.seg button:hover:not(:disabled){background:transparent;border-color:transparent;color:var(--ink)}.seg button.active{background:var(--surface);border-color:var(--line);color:var(--ink);font-weight:500}.seg.small button{height:20px;padding:0 7px;font-size:12px}.seg button.active .tab-count{color:var(--ink)}.tabs{display:inline-flex;align-self:stretch;align-items:stretch;gap:2px}.tab{height:auto;background:none;border:none;border-radius:0;padding:0 6px;color:var(--ink-2);position:relative}.tab:hover:not(:disabled){background:none;color:var(--ink)}.tab.active{color:var(--ink);font-weight:500}.tab.active:after{content:"";position:absolute;left:6px;right:6px;bottom:-1px;height:2px;background:var(--ink)}.tab-count{font-family:var(--mono);font-size:11px;color:var(--muted)}.tab.active .tab-count{color:var(--ink)}.tabs.small .tab{padding:0 5px;font-size:12px}.error-box{color:var(--del-fg);background:var(--del-bg);padding:8px 12px;border-radius:var(--r-card);margin:8px}.empty{padding:20px 12px;text-align:center;color:var(--muted);font-size:12px;line-height:1.6}.app{display:flex;flex-direction:column;height:100%}.body{flex:1;display:flex;min-height:0}.main{flex:1;min-width:0;display:flex;flex-direction:column;background:var(--surface)}.loading-screen,.fatal{padding:48px;text-align:center;color:var(--muted)}.fatal h1{font-family:var(--mono);font-weight:500;font-size:20px;letter-spacing:-.02em;color:var(--ink)}.placeholder{padding:48px 24px;text-align:center;color:var(--muted)}.placeholder p{margin:6px 0}.topbar{display:flex;align-items:center;gap:14px;padding:0 10px 0 12px;min-height:40px;background:var(--ground);flex-wrap:wrap}.brand{display:flex;align-items:center;gap:7px}.mark{width:16px;height:16px;flex:none}.wordmark{font-family:var(--mono);font-weight:500;font-size:15px;letter-spacing:-.02em;color:var(--ink)}.scope{display:flex;align-items:center;gap:8px}.update-chip{height:auto;padding:1px 6px;border:none;font-family:var(--mono);font-size:var(--t-meta);color:var(--ink-2);background:var(--surface-3)}.update-chip:hover:not(:disabled){color:var(--ink);background:var(--line)}.repo-name{font-weight:500}.branch{font-family:var(--mono);font-size:11px;color:var(--ink-2);background:var(--surface-3);border-radius:var(--r-ctl);padding:1px 6px}.inline-form{display:inline-flex;align-items:center;gap:4px}.inline-form input{width:130px;font-family:var(--mono);font-size:12px}.topbar-right{margin-left:auto;display:flex;align-items:center;gap:6px}.nvim{display:inline-flex;align-items:center;gap:4px;max-width:300px}.nvim select{max-width:240px}.nvim-one,.nvim-idle{font-family:var(--mono);font-size:12px;color:var(--muted)}.rail-switch{margin-left:8px;background:none;border-color:transparent;color:var(--ink-2)}.rail-switch:hover:not(:disabled){background:var(--surface-3);border-color:transparent;color:var(--ink)}.rail-switch.active{background:var(--surface);border-color:var(--line);color:var(--ink)}.rail-switch-n{font-family:var(--mono);font-size:var(--t-meta);color:var(--muted)}.rail-switch.has-unexported .rail-switch-n{background:var(--accent-soft);color:var(--accent);border-radius:var(--r-ctl);padding:0 5px}.sidebar{flex:none;display:flex;flex-direction:column;background:var(--ground);overflow:hidden;position:relative}.side-nav{padding:8px 12px;flex-shrink:0}.side-view-select{width:100%;min-height:30px;font-weight:500}.side-slot,.side-form{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;align-items:stretch;gap:10px;padding:12px 12px 16px}.side-form{overflow:visible;padding:0}.side-slot>.side-form{flex:none}.field{display:flex;flex-direction:column;gap:3px;font-size:var(--t-meta);color:var(--muted)}.field input{width:100%;font-size:var(--t-ui)}.field-row{display:flex;align-items:center;gap:6px}.field-row input{min-width:0;flex:1}.side-group{display:flex;flex-direction:column;gap:8px;padding-top:12px;border-top:1px solid var(--line)}.side-group-title{font-weight:600;color:var(--ink-2)}.side-form button{justify-content:center;background:var(--surface-2);border-color:var(--line-strong);font-weight:500}.side-form button:hover:not(:disabled){background:var(--surface-3);border-color:var(--line-strong)}.side-form button.toggle{justify-content:flex-start;font-weight:400}.side-form button.toggle[aria-pressed=true]{background:var(--ink);border-color:var(--ink);color:#fff}.side-form button.link{justify-content:flex-start;background:none;border:none;font-weight:400}.side-form-status{display:flex;align-items:center;gap:8px;font-variant-numeric:tabular-nums}.side-grip{position:absolute;top:0;right:0;bottom:0;width:5px;cursor:col-resize;background:var(--line);background-clip:content-box;border-left:4px solid transparent}.side-grip:hover{background:var(--line-strong)}.tree-blocks{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column}.tree-block{display:flex;flex-direction:column;flex:none;min-width:0}.tree-block+.tree-block{margin-top:6px;border-top:1px solid var(--line)}.tree-block.empty .block-head{padding:6px 12px}.progress{flex:none;padding:10px 12px 12px}.progress-figure{display:flex;align-items:baseline;gap:6px}.progress-done{font-family:var(--mono);font-size:var(--t-figure);font-weight:500;line-height:1.1;letter-spacing:-.03em;color:var(--ink);font-variant-numeric:tabular-nums}.progress-done.complete{color:var(--add-fg)}.progress-of{font-family:var(--mono);font-size:var(--t-lead);color:var(--muted);font-variant-numeric:tabular-nums}.progress-label{font-size:var(--t-meta);color:var(--muted)}.progress-track{margin-top:7px;height:3px;background:var(--surface-3);border-radius:2px;overflow:hidden}.progress-track>span{display:block;height:100%;background:var(--ink);transition:width .25s}.sidebar-target{flex:none;display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--line);white-space:nowrap}.sidebar-target-name{font-family:var(--mono);font-size:12px;color:var(--ink-2);min-width:0;overflow:hidden;text-overflow:ellipsis}.sidebar-target .link{font-size:12px}.block-head{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:8px;padding:8px 12px 4px;background:var(--ground);font-size:12px;white-space:nowrap}.block-title{font-weight:600}.block-n{font-family:var(--mono);font-size:var(--t-meta);color:var(--muted)}.block-head .counts{font-family:var(--mono);font-size:var(--t-meta);display:inline-flex;gap:4px}.tree-tools{display:inline-flex;gap:2px;opacity:0}.tree-block:hover .tree-tools,.tree-tools:focus-within{opacity:1}.tree-tools .link{font-size:var(--t-meta);color:var(--muted)}.tree{padding:0 0 4px}.tree-row{display:flex;align-items:center;gap:6px;padding:0 10px;height:24px;cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none}.tree-row:hover{background:var(--surface-3)}.tree-row.active{background:var(--surface);box-shadow:inset 3px 0 var(--ink);font-weight:500}.tree-row.active .name{color:var(--ink)}.tree-row.viewed .name{color:var(--muted)}.tree-row .name{overflow:hidden;text-overflow:ellipsis}.tree-row.dir .name{font-weight:500;color:var(--ink-2)}.chev{display:inline-block;width:12px;font-size:10px;color:var(--muted);transition:transform .1s}.chev.open{transform:rotate(90deg)}.tree-row .counts{margin-left:auto;font-family:var(--mono);font-size:11px;display:inline-flex;gap:4px}.tree-row .count{font-family:var(--mono);font-size:11px}.viewed-box{margin:0}.status{font-family:var(--mono);font-size:11px;font-weight:500;width:14px;text-align:center}.status-added{color:var(--add-fg)}.status-modified{color:var(--warn-fg)}.status-deleted{color:var(--del-fg)}.status-renamed{color:var(--ink-2)}.changed{font-size:11px;background:var(--warn-bg);color:var(--warn-fg);border-radius:var(--r-ctl);padding:0 5px}.cc{font-family:var(--mono);font-size:11px;background:var(--accent-soft);color:var(--accent);border-radius:var(--r-ctl);padding:0 5px}.diff-panel{flex:1;display:flex;flex-direction:column;min-height:0;position:relative}.file-head{display:flex;align-items:center;gap:10px;padding:0 14px;min-height:40px;border-bottom:1px solid var(--line);background:var(--surface);flex-wrap:wrap}.file-head .path{font-family:var(--mono);font-size:var(--t-lead);letter-spacing:-.01em}.file-head .path .dir{color:var(--muted)}.file-head .path .base{font-weight:500}.file-head .counts{font-family:var(--mono);font-size:11px;display:inline-flex;gap:4px}.file-head .check{font-size:12px}.diff-scroll{flex:1;overflow:auto;font-family:var(--mono);font-size:12px;line-height:20px;tab-size:4;background:var(--surface)}.diff-scroll.selecting,.diff-scroll.picking{-webkit-user-select:none;user-select:none;cursor:row-resize}.diff-scroll.reattaching .add-btn{background:var(--warn-fg)}.vrow{will-change:transform}.row{display:flex;min-height:20px}.row.line,.cell{position:relative}.pin{position:absolute;right:0;top:0;bottom:0;width:3px;background:var(--accent)}.pin.muted{background:var(--line-strong)}.pill{position:absolute;right:10px;top:2px;height:16px;min-width:16px;padding:0 5px;justify-content:center;border:none;border-radius:8px;background:var(--accent);color:#fff;font-family:var(--sans);font-size:11px;line-height:16px}.pill:hover:not(:disabled){background:var(--accent-strong);border-color:transparent}.pill.muted{background:var(--line-strong)}.row.hunk-head{align-items:center;background:var(--surface-2);color:var(--muted);font-size:11.5px;padding:3px 12px 3px 128px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);white-space:pre-wrap}.hunk-text{flex:1;min-width:0}.hunk-stage{font-family:var(--sans);font-size:11px;color:var(--muted);white-space:nowrap}.stagec{flex:0 0 18px;position:relative;-webkit-user-select:none;user-select:none}.stage-box{position:absolute;left:4px;top:4px;width:12px;height:12px;padding:0;border:1.5px solid var(--ink-2);border-radius:2px;background:var(--surface);opacity:0;cursor:pointer;z-index:1}.stage-box:hover:not(:disabled){background:var(--surface-3);border-color:var(--ink)}.row:hover .stage-box,.cell:hover .stage-box,.stage-box.on{opacity:1}.stage-box.on,.stage-box.on:hover:not(:disabled){background:var(--ink);border-color:var(--ink)}.stage-box.on:after{content:"";position:absolute;left:3px;top:0;width:3px;height:6px;border:solid #fff;border-width:0 1.5px 1.5px 0;transform:rotate(45deg)}.row.line.picked,.cell.picked{box-shadow:inset 3px 0 var(--ink)}.row.line.add.picked,.cell.add.picked{background:var(--add-bg-strong)}.row.line.del.picked,.cell.del.picked{background:var(--del-bg-strong)}.stage-bar{position:absolute;left:50%;bottom:16px;transform:translate(-50%);display:flex;align-items:center;gap:8px;padding:6px 8px 6px 12px;background:var(--surface);border:1px solid var(--line-strong);border-radius:var(--r-card);box-shadow:0 6px 20px #16181d24;font-size:12px;white-space:nowrap;z-index:3}.stage-bar-count{margin-right:4px}.stage-bar kbd{margin-left:2px}.stage-bar .primary kbd{background:#ffffff29;border-color:#ffffff4d;color:#fff}.tree-row .stage-file{display:none;font-size:11px;padding:0 2px}.tree-row:hover .stage-file{display:inline-flex}.row.gap-row{background:var(--surface-2);justify-content:center;gap:16px;padding:2px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line);font-family:var(--sans);font-size:12px}.gap-row .link{color:var(--muted)}.row.debug-row{background:repeating-linear-gradient(-45deg,var(--surface-2) 0 6px,var(--surface) 6px 12px);border-top:1px dashed var(--line-strong);border-bottom:1px dashed var(--line-strong);padding:2px 0 2px 60px;font-family:var(--sans);font-size:12px}.row.debug-row.open{border-bottom-style:none}.debug-row .link{color:var(--muted);text-decoration:none}.debug-row .link:hover{color:var(--ink)}.row.attach{background:var(--surface-2);border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:6px 12px 6px 60px;font-family:var(--sans);font-size:13px;display:block}.gut{flex:0 0 48px;text-align:right;padding-right:6px;font-size:11px;color:var(--muted);cursor:pointer;-webkit-user-select:none;user-select:none}.gut:hover{color:var(--ink)}.addc{flex:0 0 18px;position:relative}.add-btn{position:absolute;left:0;top:2px;width:16px;height:16px;padding:0;justify-content:center;line-height:16px;border-radius:var(--r-ctl);background:var(--accent);color:#fff;border:none;font-weight:600;font-size:13px;opacity:0;cursor:pointer;z-index:1}.add-btn:hover:not(:disabled){background:var(--accent-strong);border-color:transparent}.row:hover .add-btn,.cell:hover .add-btn{opacity:1}.add-btn.static{position:static;display:inline-flex;opacity:1;width:16px;height:16px;font-size:11px;vertical-align:-3px}.foldc{flex:0 0 14px;position:relative;-webkit-user-select:none;user-select:none}.fold-btn{position:absolute;inset:0;height:auto;display:flex;align-items:center;justify-content:center;padding:0;border:0;background:none;color:var(--muted);cursor:pointer;opacity:0}.fold-btn svg{transform:rotate(90deg);transition:transform .1s}.fold-btn.shut svg{transform:none}.row.line:hover .fold-btn,.cell:hover .fold-btn,.fold-btn.shut,.fold-btn:focus-visible{opacity:1}.fold-btn:hover:not(:disabled){background:none;color:var(--ink)}.fold-chip{margin-left:8px;padding:0 6px;border:1px solid var(--line);border-radius:var(--r-ctl);background:var(--surface-2);color:var(--muted);font-family:var(--sans);font-size:var(--t-meta);line-height:14px;cursor:pointer;display:inline-block;height:16px;vertical-align:top;margin-top:2px}.fold-chip:hover{border-color:var(--line-strong);color:var(--ink)}.fold-chip .add{color:var(--add-fg)}.fold-chip .del{color:var(--del-fg)}.marker{flex:0 0 14px;text-align:center;-webkit-user-select:none;user-select:none;color:var(--muted)}.code{flex:1;min-width:0;white-space:pre-wrap;word-break:break-all;padding-right:40px}.nonl{color:var(--del-fg);margin-left:6px}.row.line.add,.cell.add{background:var(--add-bg)}.row.line.del,.cell.del{background:var(--del-bg)}.row.line.add .marker{color:var(--add-fg)}.row.line.del .marker{color:var(--del-fg)}.row.expanded,.row.pair.expanded .cell{background:var(--surface);color:var(--muted)}.row.line.selected,.cell.selected{background:var(--accent-soft)!important}.row.line.focused,.cell.focused{background:var(--accent-soft)!important;box-shadow:inset 3px 0 var(--accent)}.row.line.flash,.cell.flash{animation:flash 1.6s ease-out}@keyframes flash{0%{background:var(--accent-line)}to{background:transparent}}.row.pair{display:grid;grid-template-columns:1fr 1fr}.cell{display:flex;min-width:0;border-left:1px solid var(--line)}.cell:first-child{border-left:none}.cell.blank{background:var(--surface-2)}.diff-list.split .row.hunk-head{padding-left:78px}.comment-card{position:relative;background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:var(--r-card);padding:7px 10px 8px}.comment-card.status-exported{border-left-color:var(--line-strong)}.comment-card.status-orphaned{border-color:var(--warn-line);border-left-style:dashed;background:var(--warn-soft)}.comment-card.selected{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.comment-head{display:flex;align-items:center;gap:8px;min-height:20px;font-size:12px;color:var(--ink-2);flex-wrap:wrap}.comment-head .mono{font-size:11px}.comment-head .time{font-size:11px;color:var(--muted)}.card-actions{position:absolute;right:6px;top:5px;display:inline-flex;align-items:center;gap:2px;padding-left:8px;border-radius:var(--r-ctl);background:inherit;opacity:0;transition:opacity .1s}.card-actions .link{font-size:12px}.comment-card:hover .card-actions,.comment-card:focus-within .card-actions,.comment-card.reattaching .card-actions{opacity:1}.comment-editor{background:var(--surface);border:1px solid var(--accent);border-left-width:3px;border-radius:var(--r-card);padding:8px 10px;max-width:900px}.comment-editor-head{font-family:var(--mono);font-size:11px;color:var(--muted);margin-bottom:6px}.comment-editor textarea{border-color:var(--line)}.comment-editor-actions{display:flex;gap:6px;margin-top:8px}.comment-editor .primary{background:var(--accent);border-color:var(--accent)}.comment-editor .primary:hover:not(:disabled){background:var(--accent-strong);border-color:var(--accent-strong)}.snippet{font-family:var(--mono);font-size:11.5px;line-height:18px;background:var(--surface-2);border-radius:var(--r-ctl);padding:4px 8px;margin:6px 0;overflow-x:auto;white-space:pre}.snippet .ln{display:inline-block;width:40px;color:var(--muted);text-align:right;margin-right:10px;-webkit-user-select:none;user-select:none}.md{font-size:13px}.md p{margin:4px 0}.md pre{background:var(--surface-2);padding:6px 8px;border-radius:var(--r-ctl);overflow-x:auto}.md code{font-family:var(--mono);font-size:12px;background:var(--surface-2);padding:0 3px;border-radius:3px}.md pre code{background:none;padding:0}.md ul,.md ol{padding-left:20px;margin:4px 0}.md blockquote{margin:4px 0;padding-left:10px;border-left:3px solid var(--line);color:var(--muted)}.commits-panel{flex:1;display:flex;flex-direction:column;min-height:0}.commits-panel[hidden]{display:none}.commits-list{flex:1;overflow:auto;background:var(--surface)}.commit-day{position:sticky;top:0;z-index:1;display:flex;align-items:center;height:24px;padding:0 12px;background:var(--ground);border-bottom:1px solid var(--line);color:var(--ink-2);font-size:12px;font-weight:500}.commit-row{display:flex;align-items:center;gap:10px;height:30px;padding:0 12px;border-bottom:1px solid var(--surface-3);cursor:pointer;white-space:nowrap}.commit-row:hover{background:var(--surface-2)}.commit-row.active{background:var(--surface-2);box-shadow:inset 2px 0 var(--ink)}.commit-row .sha{flex:0 0 auto;min-width:7ch;color:var(--ink-2)}.commit-row .subject{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.commit-row .subject .pr{margin-left:6px;color:var(--muted);font-family:var(--mono);font-size:12px}.commit-row .refs{display:inline-flex;gap:4px;flex:0 1 auto;min-width:0;overflow:hidden}.ref{display:inline-block;max-width:180px;height:17px;padding:0 7px;border:1px solid var(--line-strong);border-radius:9px;color:var(--ink-2);font-family:var(--mono);font-size:11px;line-height:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ref-tag{background:var(--surface-3);border-color:var(--surface-3)}.ref-head{background:var(--ink);border-color:var(--ink);color:#fff}.ref-more{border-style:dashed;color:var(--muted)}.ref-fork{border-style:dashed;border-color:var(--ink-2);color:var(--ink)}.commit-row.fork{border-top:1px dashed var(--line-strong)}.fork-note{font-size:var(--t-meta);line-height:1.6}.fork-note .link{padding:0;font-size:var(--t-meta)}.commit-row .author{flex:0 0 110px;overflow:hidden;text-overflow:ellipsis;color:var(--ink-2);font-size:12px}.commit-row .time{flex:0 0 40px;text-align:right;color:var(--muted);font-family:var(--mono);font-size:11px}.commits-foot{display:flex;justify-content:center;align-items:center;min-height:44px;font-size:12px}.worktrees-panel{flex:1;display:flex;flex-direction:column;min-height:0;background:var(--surface)}.wt-list{flex:1;overflow:auto;max-width:740px;padding:16px 20px 24px;display:flex;flex-direction:column;gap:8px}.wt-row{display:flex;align-items:flex-start;gap:8px;padding:8px 10px;border:1px solid var(--line);border-radius:var(--r-card);background:var(--surface)}.wt-row.active{border-color:var(--line-strong);box-shadow:inset 3px 0 var(--ink)}.wt-row.gone .wt-name{color:var(--muted);text-decoration:line-through}.wt-row.free{border-style:dashed;background:var(--surface-2)}.wt-row.free .wt-name{color:var(--ink-2)}.wt-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}.wt-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.wt-name{font-weight:500;overflow-wrap:anywhere}.wt-path{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wt-actions{display:flex;align-items:center;gap:2px;flex:0 0 auto}.wt-comparison{display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:12px}.wt-commit-count{display:inline-flex;align-items:center;gap:3px;font-variant-numeric:tabular-nums}.wt-extras{display:flex;align-items:center;flex-wrap:wrap;gap:4px 10px}.wt-extras>.row-actions{margin:0;gap:2px}.wt-expanded{flex-basis:100%;min-width:0;margin-top:4px;padding:12px;background:var(--surface-2);border-radius:var(--r-card);overflow-wrap:anywhere}.wt-commit{display:flex;align-items:baseline;gap:10px;width:100%;padding:8px 0;border:0;border-bottom:1px solid var(--line);background:transparent;text-align:left;white-space:normal}.wt-commit>span{flex:1;min-width:0}.wt-commit small{color:var(--muted)}.wt-todo{padding:8px 0;border-bottom:1px solid var(--line)}.wt-todo summary{display:flex;align-items:center;gap:8px;cursor:pointer}.wt-todo.done summary>span:last-child{text-decoration:line-through;color:var(--muted)}.wt-tmux>*+*{margin-top:8px}@media(max-width:650px){.wt-commit{flex-wrap:wrap}}.wt-confirm{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:6px;padding:7px 9px;border-radius:var(--r-ctl);background:var(--warn-soft);border:1px solid var(--warn-line);color:var(--warn-fg);font-size:12px}.wt-confirm .link{color:var(--warn-fg)}.wt-confirm .link.danger{color:var(--del-fg)}.wt-empty{padding:20px 2px}.rail-list.tasks{padding:4px 0 12px;gap:0}.task-add{height:34px;margin:0 4px 2px;padding:0 10px 0 24px;justify-content:flex-start;gap:10px;border:none;border-radius:var(--r-card);background:none;color:var(--ink-2);font-weight:500}.task-add:hover:not(:disabled){background:var(--surface-3);border-color:transparent;color:var(--ink)}.task-add-plus{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background:var(--ink);color:#fff;font-size:15px;line-height:1}.task-list{display:flex;flex-direction:column}.task-row{position:relative;display:flex;align-items:flex-start;gap:6px;margin:0 4px;padding:5px 10px 5px 4px;border-radius:var(--r-card)}.task-row:hover,.task-row.open{background:var(--surface)}.task-row.open{box-shadow:0 0 0 1px var(--line)}.task-grip{flex:0 0 14px;height:20px;display:inline-flex;align-items:center;justify-content:center;color:var(--muted);cursor:grab;visibility:hidden}.task-row:hover .task-grip{visibility:visible}.task-list.dragging .task-grip{visibility:hidden}.task-check{position:relative;flex:0 0 18px;width:18px;height:18px;margin-top:1px;padding:0;border:1.5px solid var(--ink-2);border-radius:50%;background:var(--surface)}.task-check:after{content:"";position:absolute;left:5px;top:1.5px;width:4px;height:8px;border:solid var(--ink);border-width:0 1.5px 1.5px 0;transform:rotate(45deg);opacity:0}.task-check:hover:not(:disabled){background:var(--surface);border-color:var(--ink)}.task-check:hover:not(:disabled):after{opacity:.45}.task-row.done .task-check,.task-row.completing .task-check{background:var(--ink);border-color:var(--ink)}.task-row.done .task-check:after,.task-row.completing .task-check:after{border-color:#fff;opacity:1}.task-row.draft .task-check{border-style:dashed;border-color:var(--muted)}.task-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.task-line{display:flex;align-items:flex-start;gap:6px}.task-ta{width:100%;min-width:0;height:auto;padding:0;border:none;border-radius:0;background:transparent;resize:none;overflow:hidden;font-family:var(--sans);line-height:20px}.task-ta:focus{box-shadow:none}.task-title{flex:1;font-size:13px}.task-body{font-size:12px;line-height:18px;color:var(--ink-2)}.task-details{font-size:12px;line-height:18px;color:var(--muted);cursor:text}.task-details .md{font-size:12px}.task-details .md p{margin:0 0 2px}.task-row.done .task-title,.task-row.completing .task-title{text-decoration:line-through;color:var(--muted)}.task-row.completing{opacity:.55;transition:opacity .3s}.task-row .badge{margin-top:2px}.task-count{font-family:var(--mono);font-size:11px;line-height:20px;color:var(--muted);white-space:nowrap}.task-actions{display:inline-flex;align-items:center;gap:2px;margin-left:auto;line-height:20px;white-space:nowrap;visibility:hidden}.task-actions .link{font-size:12px}.task-row:hover .task-actions,.task-row:focus-within .task-actions,.task-sub:hover .task-actions{visibility:visible}.task-row.dragging{opacity:.4}.task-row.drop-before{box-shadow:0 -2px 0 0 var(--ink)}.task-row.drop-after{box-shadow:0 2px 0 0 var(--ink)}.task-empty{padding:14px 20px;color:var(--muted);font-size:12px}.task-add-hint{padding:0 10px 6px 46px;font-size:12px;color:var(--muted)}.task-done{margin-top:8px;padding-top:4px;border-top:1px solid var(--line)}.task-done-head{display:flex;align-items:center;min-height:28px;padding:0 14px 0 10px}.task-done-toggle{height:auto;padding:0 4px;gap:6px;border:none;background:none;color:var(--ink-2);font-weight:500}.task-done-toggle:hover:not(:disabled){background:none;border-color:transparent;color:var(--ink)}.task-list-pick{max-width:220px}.task-subs{display:flex;flex-direction:column;gap:4px;margin-top:6px}.task-sub{display:flex;align-items:flex-start;gap:8px;padding:4px 6px;border-radius:var(--r-ctl);background:var(--surface-2);font-size:12px}.task-sub-dot{flex:0 0 8px;width:8px;height:8px;margin-top:6px;border-radius:50%;background:var(--accent)}.task-sub.status-exported .task-sub-dot{background:var(--line-strong)}.task-sub.status-orphaned .task-sub-dot{background:var(--warn-line)}.task-sub-main{flex:1;min-width:0}.task-sub-head{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.task-sub-head .mono{font-size:11px}.task-sub-body{color:var(--muted);white-space:pre-wrap;max-height:54px;overflow:hidden}.task-sub-add{align-self:flex-start;font-size:12px}.task-sub-none{font-size:12px;color:var(--muted)}.toast{position:fixed;bottom:20px;left:50%;transform:translate(-50%);background:var(--ink);color:#fff;padding:8px 16px;border-radius:var(--r-card);box-shadow:0 6px 20px #16181d40;z-index:100;max-width:80vw}.toast-error{background:var(--del-fg)}.toast-action{height:22px;margin-left:14px;padding:0 8px;background:#ffffff24;border-color:#ffffff59;color:#fff;font-weight:500}.toast-action:hover:not(:disabled){background:#ffffff42;border-color:#ffffff80}.rail{width:360px;flex:none;border-left:1px solid var(--line);display:flex;flex-direction:column;background:var(--ground);min-height:0}.rail.rail-wide{width:440px}.rail-head{display:flex;align-items:center;gap:10px;padding:0 10px;min-height:36px;border-bottom:1px solid var(--line)}.rail-tools{display:flex;align-items:center;gap:8px;padding:0 10px;min-height:34px;border-bottom:1px solid var(--line);font-size:12px}.rail-notice{padding:6px 10px;background:var(--warn-bg);color:var(--warn-fg);border-bottom:1px solid var(--warn-line);display:flex;gap:8px;align-items:center;font-size:12px}.rail-notice .link{color:var(--warn-fg)}.rail-list{flex:1;overflow:auto;padding:10px;display:flex;flex-direction:column;gap:8px}.rail-slot{display:flex;flex-direction:column;gap:8px}.rail-foot{display:flex;flex-direction:column;gap:8px;padding:10px;border-top:1px solid var(--line);background:var(--ground)}.rail-foot-row{display:flex;align-items:center;gap:10px}.send{height:30px;padding:0 14px;background:var(--accent);border-color:var(--accent);color:#fff;font-weight:500}.send:hover:not(:disabled){background:var(--accent-strong);border-color:var(--accent-strong)}.send-n{font-family:var(--mono);font-size:11px;font-weight:400;line-height:16px;padding:0 5px;border-radius:var(--r-ctl);background:#ffffff38}.rail .comment-card{cursor:pointer}.rail .comment-card.focused{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.comment-head .file{color:var(--muted);max-width:120px;overflow:hidden;text-overflow:ellipsis}.rail .comment-editor{max-width:none}@media(prefers-reduced-motion:reduce){.chev,.card-actions,.task-row.completing,.progress-track>span{transition:none}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.action-icon{display:inline-flex;align-items:center;justify-content:center;gap:5px;vertical-align:middle;line-height:1}.action-icon>svg{flex-shrink:0}button:has(>.action-icon){display:inline-flex;align-items:center;justify-content:center;min-width:30px;min-height:30px;padding:6px}button.link:has(>.action-icon){color:var(--muted);border-radius:5px}button.link:has(>.action-icon):hover:not(:disabled){color:var(--accent);background:var(--accent-soft);text-decoration:none}button.link.danger:has(>.action-icon){color:var(--del-fg)}button.link.danger:has(>.action-icon):hover:not(:disabled){background:var(--del-bg)}.nvim>span{display:inline-flex;align-items:center;padding:4px}.nvim{position:relative}.nvim summary{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:1px solid var(--line);border-radius:6px;cursor:pointer;list-style:none}.nvim summary::-webkit-details-marker{display:none}.nvim summary:hover{background:var(--surface-3)}.nvim summary:focus-visible{outline:1px solid var(--accent)}.nvim[open] summary{background:var(--accent-soft);color:var(--accent)}.nvim-popover{position:absolute;z-index:30;left:0;top:calc(100% + 6px);display:flex;align-items:center;gap:8px;width:320px;max-width:calc(100vw - 32px);padding:12px;background:var(--surface);border:1px solid var(--line);border-radius:8px;box-shadow:0 4px 16px #1f23281a}.nvim-popover>:first-child{flex:1;min-width:0;overflow-wrap:anywhere}.tree-icon{flex:0 0 16px;color:var(--muted)}.tree-icon-folder{color:#8c9aab;fill:#8c9aab12}.tree-row{display:flex;align-items:center;gap:6px;padding:0 8px;min-height:32px;border-radius:5px;cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none;line-height:20px}.tree-row.active{background:#eaf3ff;box-shadow:inset 1px 0 var(--accent)}.tree-row.active .tree-file-open{color:var(--accent)}.tree-row:focus-within{outline:1px solid var(--accent);outline-offset:-1px}.tree-row.dir .name{font-weight:500}.tree-row.dir{width:100%;border:0;background:transparent;text-align:left;color:var(--muted)}.tree-row.dir:hover{background:var(--surface-2)}.tree-row.dir.viewed .name{font-weight:400;color:var(--muted)}.tree-row.dir.viewed .tree-icon-folder{opacity:.55}.tree-row.dir.viewed .count{display:inline-flex;align-items:center;gap:3px}.tree-row.dir.viewed .count .tree-icon{color:var(--add-fg);width:12px;height:12px;flex-basis:12px}.tree-row .status{display:inline-flex;align-items:center;justify-content:center;flex:0 0 14px;height:18px;font-family:var(--sans);font-size:11px;font-weight:500;line-height:1}.comment-editor-head{font-family:var(--mono);font-size:12px;color:var(--muted);margin:-8px -10px 8px;padding:8px 19px;background:var(--surface-2);border-bottom:1px solid var(--line);border-radius:5px 5px 0 0}.block-heading{display:flex;align-items:center;gap:7px;min-height:26px}.block-summary{display:flex;align-items:center;gap:10px;margin-top:5px;font-size:10px;font-variant-numeric:tabular-nums}.tree-tools{display:inline-flex;gap:2px}.tree-tools button{display:grid;place-items:center;width:26px;height:26px;padding:0;border:none;background:transparent;color:var(--muted)}.tree-empty{display:flex;align-items:center;gap:8px;padding:16px 10px;color:var(--muted);font-size:12px}.block-head{flex-shrink:0;padding:9px 12px 10px;border-bottom:1px solid var(--line);background:#f8fafc;font-size:12px;white-space:nowrap}.block-indicator{width:6px;height:6px;border-radius:50%;background:#bf8700;flex-shrink:0}.block-indicator.is-staged{background:var(--add-fg)}.block-warn{padding:0 6px;border:1px solid var(--warn-line);border-radius:5px;background:var(--warn-soft);color:var(--warn-fg);font-size:10px;font-weight:600;line-height:17px;white-space:nowrap}.side-toggle{flex:none;display:flex;align-items:center;gap:6px;padding:0 12px 8px;color:var(--ink-2);font-size:var(--t-meta);cursor:pointer}.block-file-count{padding:0 6px;border:1px solid var(--line);border-radius:5px;color:var(--muted);font-size:10px;line-height:17px;font-variant-numeric:tabular-nums}.block-diff-counts{display:flex;gap:7px;font-family:var(--mono)}.action-count{font-size:10px;font-weight:500;font-variant-numeric:tabular-nums;line-height:1}.icon-spinning{animation:icon-spin 1s linear infinite}.tree-file-icon{display:inline-flex;width:16px;height:16px;flex:0 0 16px}.tree-file-icon svg{width:100%;height:100%}.tree-file-open{display:flex;align-items:center;gap:7px;flex:1;min-width:0;align-self:stretch;padding:0;border:0;border-radius:0;background:transparent;text-align:left}.tree-file-open:hover:not(:disabled){background:transparent}.tree-file-open:focus-visible{outline:none}.tree-row .name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.tree-row .counts{margin-left:auto;font-family:var(--mono);font-size:11px;display:inline-flex;gap:6px;flex-shrink:0;font-variant-numeric:tabular-nums;min-width:48px;justify-content:flex-end}.tree-row .count{font-size:11px;margin-left:auto;padding:0 4px}.tree-changed{width:6px;height:6px;flex:0 0 6px;border-radius:50%;background:#bf8700}.tree-binary{color:var(--muted);font-size:9px;letter-spacing:.04em}.tree-block .block-head{display:block}.tree-tools{opacity:1;margin-left:auto}
|