@xbghc/warden 0.6.0 → 0.7.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 +79 -19
- package/dist/cli.js +302 -41
- package/dist/web/assets/index-Cr74Kv2S.css +1 -0
- package/dist/web/assets/index-DNRG_SPg.js +198 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-Bi9N5cIP.js +0 -197
- package/dist/web/assets/index-pyKaNQqX.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// bin/cli.ts
|
|
4
4
|
import path8 from "path";
|
|
5
5
|
import { spawn } from "child_process";
|
|
6
|
-
import { existsSync } from "fs";
|
|
6
|
+
import { existsSync as existsSync2 } from "fs";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
|
|
9
9
|
// packages/server/src/index.ts
|
|
@@ -2960,6 +2960,11 @@ function commentScopeKey(key) {
|
|
|
2960
2960
|
if (!t || !isLocalTarget(t)) return key;
|
|
2961
2961
|
return t.worktree ? `worktree:${t.worktree}:local` : "local";
|
|
2962
2962
|
}
|
|
2963
|
+
function stageModeFor(t) {
|
|
2964
|
+
if (t.kind === "working") return "stage";
|
|
2965
|
+
if (t.kind === "staged") return "unstage";
|
|
2966
|
+
return void 0;
|
|
2967
|
+
}
|
|
2963
2968
|
function localViewKeys(key) {
|
|
2964
2969
|
const t = tryParseTargetKey(key);
|
|
2965
2970
|
const wt = t?.worktree ? { worktree: t.worktree } : {};
|
|
@@ -2970,6 +2975,24 @@ function localViewKeys(key) {
|
|
|
2970
2975
|
];
|
|
2971
2976
|
}
|
|
2972
2977
|
|
|
2978
|
+
// packages/shared/src/order.ts
|
|
2979
|
+
function insertAfter(list, item, after) {
|
|
2980
|
+
const idx = after === void 0 ? -1 : list.findIndex((x) => x.id === after);
|
|
2981
|
+
if (idx < 0) return [item, ...list];
|
|
2982
|
+
return [...list.slice(0, idx + 1), item, ...list.slice(idx + 1)];
|
|
2983
|
+
}
|
|
2984
|
+
function moveBefore(list, id, before) {
|
|
2985
|
+
const item = list.find((x) => x.id === id);
|
|
2986
|
+
if (!item) return null;
|
|
2987
|
+
if (before === id) return [...list];
|
|
2988
|
+
const rest = list.filter((x) => x.id !== id);
|
|
2989
|
+
if (before === null) return [...rest, item];
|
|
2990
|
+
const idx = rest.findIndex((x) => x.id === before);
|
|
2991
|
+
if (idx < 0) return null;
|
|
2992
|
+
rest.splice(idx, 0, item);
|
|
2993
|
+
return rest;
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2973
2996
|
// packages/server/src/errors.ts
|
|
2974
2997
|
var HttpError = class extends Error {
|
|
2975
2998
|
constructor(status, message, code) {
|
|
@@ -3025,7 +3048,8 @@ function parseCommitLog(stdout) {
|
|
|
3025
3048
|
|
|
3026
3049
|
// packages/server/src/git.ts
|
|
3027
3050
|
import { execFile } from "child_process";
|
|
3028
|
-
|
|
3051
|
+
import { existsSync } from "fs";
|
|
3052
|
+
var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set(["rev-parse", "diff", "show", "log", "worktree", "ls-files", "status", "merge-base", "rev-list"]);
|
|
3029
3053
|
var FORBIDDEN_OPTION_PREFIXES = ["--output", "--ext-diff", "--textconv", "-c", "--config-env", "--exec-path", "--git-dir", "--work-tree"];
|
|
3030
3054
|
var DEFAULT_GIT_TIMEOUT_MS = 3e4;
|
|
3031
3055
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
@@ -3087,6 +3111,10 @@ async function runGit(args, opts) {
|
|
|
3087
3111
|
return;
|
|
3088
3112
|
}
|
|
3089
3113
|
if (err.code === "ENOENT") {
|
|
3114
|
+
if (opts.cwd && !existsSync(opts.cwd)) {
|
|
3115
|
+
reject(new GitError(`working directory no longer exists: ${opts.cwd}`, null, "", 400, "unknown_worktree"));
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3090
3118
|
reject(new GitError("git executable not found in PATH", null, "", 500, "git_missing"));
|
|
3091
3119
|
return;
|
|
3092
3120
|
}
|
|
@@ -3104,6 +3132,64 @@ async function runGit(args, opts) {
|
|
|
3104
3132
|
);
|
|
3105
3133
|
});
|
|
3106
3134
|
}
|
|
3135
|
+
async function applyToIndex(cwd, patch, opts = {}) {
|
|
3136
|
+
for (let attempt = 1; ; attempt++) {
|
|
3137
|
+
try {
|
|
3138
|
+
await applyOnce(cwd, patch, opts);
|
|
3139
|
+
return;
|
|
3140
|
+
} catch (e) {
|
|
3141
|
+
if (!(e instanceof GitError && e.code === "index_locked") || attempt >= 3) throw e;
|
|
3142
|
+
await new Promise((r) => setTimeout(r, 150 * attempt));
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
async function applyOnce(cwd, patch, opts) {
|
|
3147
|
+
const args = ["apply", "--cached", "--whitespace=nowarn"];
|
|
3148
|
+
if (opts.reverse) args.push("--reverse");
|
|
3149
|
+
args.push("-");
|
|
3150
|
+
await new Promise((resolve, reject) => {
|
|
3151
|
+
const child = execFile(
|
|
3152
|
+
"git",
|
|
3153
|
+
args,
|
|
3154
|
+
{
|
|
3155
|
+
cwd,
|
|
3156
|
+
timeout: opts.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS,
|
|
3157
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
3158
|
+
encoding: "utf8",
|
|
3159
|
+
env: { ...process.env, LC_ALL: "C", GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" },
|
|
3160
|
+
windowsHide: true
|
|
3161
|
+
},
|
|
3162
|
+
(error, _stdout, stderr) => {
|
|
3163
|
+
const err = error;
|
|
3164
|
+
if (!err) {
|
|
3165
|
+
resolve();
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
if (err.killed || err.signal === "SIGTERM") {
|
|
3169
|
+
reject(new GitError("git apply timed out", null, stderr, 504, "git_timeout"));
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
if (err.code === "ENOENT") {
|
|
3173
|
+
if (!existsSync(cwd)) {
|
|
3174
|
+
reject(new GitError(`working directory no longer exists: ${cwd}`, null, "", 400, "unknown_worktree"));
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
reject(new GitError("git executable not found in PATH", null, "", 500, "git_missing"));
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
const code = typeof err.code === "number" ? err.code : -1;
|
|
3181
|
+
const first = stderr.trim().split("\n")[0] || err.message;
|
|
3182
|
+
if (/index\.lock/.test(stderr)) {
|
|
3183
|
+
reject(new GitError("the index is locked by another git process; try again", code, stderr, 409, "index_locked"));
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
reject(new GitError(first.replace(/^error: /, ""), code, stderr, 409, "apply_failed"));
|
|
3187
|
+
}
|
|
3188
|
+
);
|
|
3189
|
+
child.stdin?.on("error", () => void 0);
|
|
3190
|
+
child.stdin?.end(patch);
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3107
3193
|
async function refExists(cwd, ref) {
|
|
3108
3194
|
try {
|
|
3109
3195
|
await runGit(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], { cwd });
|
|
@@ -3159,7 +3245,7 @@ async function listWorktrees(ctx) {
|
|
|
3159
3245
|
const out = [];
|
|
3160
3246
|
let cur = null;
|
|
3161
3247
|
const flush = () => {
|
|
3162
|
-
if (cur && cur.path) {
|
|
3248
|
+
if (cur && cur.path && !cur.prunable) {
|
|
3163
3249
|
out.push({
|
|
3164
3250
|
path: cur.path,
|
|
3165
3251
|
head: cur.head ?? "",
|
|
@@ -3185,6 +3271,7 @@ async function listWorktrees(ctx) {
|
|
|
3185
3271
|
else if (line.startsWith("branch ")) cur.branch = line.slice(7).replace(/^refs\/heads\//, "");
|
|
3186
3272
|
else if (line === "detached") cur.detached = true;
|
|
3187
3273
|
else if (line === "bare") cur.bare = true;
|
|
3274
|
+
else if (line.startsWith("prunable")) cur.prunable = true;
|
|
3188
3275
|
}
|
|
3189
3276
|
flush();
|
|
3190
3277
|
for (const wt of out) {
|
|
@@ -3387,11 +3474,11 @@ function parseUnifiedDiff(text) {
|
|
|
3387
3474
|
continue;
|
|
3388
3475
|
}
|
|
3389
3476
|
if (line.startsWith("--- ")) {
|
|
3390
|
-
c.fromMinus = stripPrefix(line.slice(4), "a/");
|
|
3477
|
+
c.fromMinus = stripPrefix(line.slice(4).replace(/\t$/, ""), "a/");
|
|
3391
3478
|
continue;
|
|
3392
3479
|
}
|
|
3393
3480
|
if (line.startsWith("+++ ")) {
|
|
3394
|
-
c.fromPlus = stripPrefix(line.slice(4), "b/");
|
|
3481
|
+
c.fromPlus = stripPrefix(line.slice(4).replace(/\t$/, ""), "b/");
|
|
3395
3482
|
continue;
|
|
3396
3483
|
}
|
|
3397
3484
|
if (line.startsWith("new file mode ")) {
|
|
@@ -3464,6 +3551,10 @@ function resolveTargetContext(repo, worktrees, key) {
|
|
|
3464
3551
|
}
|
|
3465
3552
|
return { key, target, cwd };
|
|
3466
3553
|
}
|
|
3554
|
+
function assertRepoPath(p) {
|
|
3555
|
+
if (!p || p.startsWith("/") || p.split("/").includes("..")) throw badRequest(`invalid path: ${p}`, "invalid_path");
|
|
3556
|
+
}
|
|
3557
|
+
var literal = (p) => `:(literal)${p}`;
|
|
3467
3558
|
async function hasHead(cwd) {
|
|
3468
3559
|
return refExists(cwd, "HEAD");
|
|
3469
3560
|
}
|
|
@@ -3491,6 +3582,7 @@ async function diffArgs(ctx) {
|
|
|
3491
3582
|
case "range": {
|
|
3492
3583
|
if (!await refExists(ctx.cwd, t.base)) throw badRequest(`unknown ref: ${t.base}`, "unknown_ref");
|
|
3493
3584
|
if (!await refExists(ctx.cwd, t.head)) throw badRequest(`unknown ref: ${t.head}`, "unknown_ref");
|
|
3585
|
+
if (!await mergeBase(ctx.cwd, t.base, t.head)) throw badRequest(`${t.base} and ${t.head} share no history`, "no_merge_base");
|
|
3494
3586
|
return [...DIFF_BASE_ARGS, `${t.base}...${t.head}`];
|
|
3495
3587
|
}
|
|
3496
3588
|
case "base":
|
|
@@ -3505,7 +3597,7 @@ async function listUntracked(cwd) {
|
|
|
3505
3597
|
return r.stdout.split("\0").filter(Boolean);
|
|
3506
3598
|
}
|
|
3507
3599
|
async function isUntracked(cwd, file) {
|
|
3508
|
-
const r = await runGit(["ls-files", "--others", "--exclude-standard", "-z", "--", file], { cwd });
|
|
3600
|
+
const r = await runGit(["ls-files", "--others", "--exclude-standard", "-z", "--", literal(file)], { cwd });
|
|
3509
3601
|
return r.stdout.split("\0").filter(Boolean).includes(file);
|
|
3510
3602
|
}
|
|
3511
3603
|
async function untrackedDiff(cwd, file) {
|
|
@@ -3551,15 +3643,14 @@ function toSummary(f) {
|
|
|
3551
3643
|
return rest;
|
|
3552
3644
|
}
|
|
3553
3645
|
async function getFileDiff(ctx, filePath, hints = {}) {
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
}
|
|
3646
|
+
assertRepoPath(filePath);
|
|
3647
|
+
if (hints.oldPath) assertRepoPath(hints.oldPath);
|
|
3557
3648
|
if (includesUntracked(ctx.target) && (hints.untracked || await isUntracked(ctx.cwd, filePath))) {
|
|
3558
3649
|
return untrackedDiff(ctx.cwd, filePath);
|
|
3559
3650
|
}
|
|
3560
3651
|
const args = await diffArgs(ctx);
|
|
3561
|
-
const pathspec = [filePath];
|
|
3562
|
-
if (hints.oldPath && hints.oldPath !== filePath) pathspec.push(hints.oldPath);
|
|
3652
|
+
const pathspec = [literal(filePath)];
|
|
3653
|
+
if (hints.oldPath && hints.oldPath !== filePath) pathspec.push(literal(hints.oldPath));
|
|
3563
3654
|
const r = await runGit([...args, "--", ...pathspec], { cwd: ctx.cwd });
|
|
3564
3655
|
const files = parseUnifiedDiff(r.stdout);
|
|
3565
3656
|
return files.find((f) => f.path === filePath) ?? files[0];
|
|
@@ -3583,9 +3674,7 @@ async function refForSide(ctx, side) {
|
|
|
3583
3674
|
}
|
|
3584
3675
|
}
|
|
3585
3676
|
async function getFullFile(ctx, filePath, side) {
|
|
3586
|
-
|
|
3587
|
-
throw badRequest(`invalid path: ${filePath}`, "invalid_path");
|
|
3588
|
-
}
|
|
3677
|
+
assertRepoPath(filePath);
|
|
3589
3678
|
const ref = await refForSide(ctx, side);
|
|
3590
3679
|
if (ref === void 0) {
|
|
3591
3680
|
try {
|
|
@@ -3722,6 +3811,122 @@ function reanchorComment(comment, diff, now = (/* @__PURE__ */ new Date()).toISO
|
|
|
3722
3811
|
};
|
|
3723
3812
|
}
|
|
3724
3813
|
|
|
3814
|
+
// packages/server/src/patch.ts
|
|
3815
|
+
function quotePath(p) {
|
|
3816
|
+
if (!/[\s"\\\x00-\x1f\x7f]|[^\x00-\x7f]/.test(p)) return p;
|
|
3817
|
+
let out = '"';
|
|
3818
|
+
for (const byte of Buffer.from(p, "utf8")) {
|
|
3819
|
+
if (byte === 34) out += '\\"';
|
|
3820
|
+
else if (byte === 92) out += "\\\\";
|
|
3821
|
+
else if (byte === 10) out += "\\n";
|
|
3822
|
+
else if (byte === 9) out += "\\t";
|
|
3823
|
+
else if (byte < 32 || byte >= 127) out += "\\" + byte.toString(8).padStart(3, "0");
|
|
3824
|
+
else out += String.fromCharCode(byte);
|
|
3825
|
+
}
|
|
3826
|
+
return out + '"';
|
|
3827
|
+
}
|
|
3828
|
+
function cutHunk(hunk, picked, mode) {
|
|
3829
|
+
const out = [];
|
|
3830
|
+
hunk.lines.forEach((l, i) => {
|
|
3831
|
+
const noNewline = !!l.noNewline;
|
|
3832
|
+
if (l.type === "context") out.push({ ch: " ", content: l.content, noNewline });
|
|
3833
|
+
else if (picked.has(i)) out.push({ ch: l.type === "add" ? "+" : "-", content: l.content, noNewline });
|
|
3834
|
+
else if (l.type === "del" === (mode === "stage")) out.push({ ch: " ", content: l.content, noNewline });
|
|
3835
|
+
});
|
|
3836
|
+
out.forEach((l, j) => {
|
|
3837
|
+
if (!l.noNewline) return;
|
|
3838
|
+
const rest = out.slice(j + 1);
|
|
3839
|
+
const ok = l.ch === " " ? rest.length === 0 : l.ch === "-" ? rest.every((r) => r.ch === "+") : rest.every((r) => r.ch === "-");
|
|
3840
|
+
if (!ok) throw badRequest("the selection splits the missing newline at the end of the file; include the neighbouring changed lines", "newline_split");
|
|
3841
|
+
});
|
|
3842
|
+
return out;
|
|
3843
|
+
}
|
|
3844
|
+
function pickedLines(diff, selection) {
|
|
3845
|
+
const picked = /* @__PURE__ */ new Map();
|
|
3846
|
+
const changed = (h) => h.lines.map((l, i) => l.type === "context" ? -1 : i).filter((i) => i >= 0);
|
|
3847
|
+
if (selection === void 0) {
|
|
3848
|
+
diff.hunks.forEach((h, i) => picked.set(i, new Set(changed(h))));
|
|
3849
|
+
return picked;
|
|
3850
|
+
}
|
|
3851
|
+
if (!Array.isArray(selection)) throw badRequest("hunks must be an array", "bad_selection");
|
|
3852
|
+
for (const sel of selection) {
|
|
3853
|
+
if (!sel || !Number.isInteger(sel.index) || sel.index < 0 || sel.index >= diff.hunks.length) throw badRequest(`no such hunk: ${sel?.index}`, "bad_selection");
|
|
3854
|
+
const hunk = diff.hunks[sel.index];
|
|
3855
|
+
const set = picked.get(sel.index) ?? /* @__PURE__ */ new Set();
|
|
3856
|
+
picked.set(sel.index, set);
|
|
3857
|
+
if (sel.lines === void 0) {
|
|
3858
|
+
for (const i of changed(hunk)) set.add(i);
|
|
3859
|
+
continue;
|
|
3860
|
+
}
|
|
3861
|
+
if (!Array.isArray(sel.lines)) throw badRequest("lines must be an array of indices", "bad_selection");
|
|
3862
|
+
for (const i of sel.lines) {
|
|
3863
|
+
if (!Number.isInteger(i) || i < 0 || i >= hunk.lines.length) throw badRequest(`no such line in hunk ${sel.index}: ${i}`, "bad_selection");
|
|
3864
|
+
if (hunk.lines[i].type !== "context") set.add(i);
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
return picked;
|
|
3868
|
+
}
|
|
3869
|
+
function buildStagePatch(diff, mode, selection) {
|
|
3870
|
+
if (diff.binary) throw badRequest("binary files cannot be staged from here; use git add", "not_stageable");
|
|
3871
|
+
const whole = selection === void 0;
|
|
3872
|
+
const picked = pickedLines(diff, selection);
|
|
3873
|
+
let lines = 0;
|
|
3874
|
+
for (const set of picked.values()) lines += set.size;
|
|
3875
|
+
const changedTotal = diff.hunks.reduce((n, h) => n + h.lines.filter((l) => l.type !== "context").length, 0);
|
|
3876
|
+
const full = lines === changedTotal;
|
|
3877
|
+
const modeChange = !!(diff.oldMode && diff.newMode && diff.oldMode !== diff.newMode);
|
|
3878
|
+
const rename2 = diff.status === "renamed" && !!diff.oldPath && diff.oldPath !== diff.path;
|
|
3879
|
+
const oldPath = diff.oldPath ?? diff.path;
|
|
3880
|
+
const newPath = diff.path;
|
|
3881
|
+
const gitLine = (a, b) => `diff --git ${quotePath(`a/${a}`)} ${quotePath(`b/${b}`)}`;
|
|
3882
|
+
const from = (p) => p === null ? "--- /dev/null" : `--- ${quotePath(`a/${p}`)}`;
|
|
3883
|
+
const to = (p) => p === null ? "+++ /dev/null" : `+++ ${quotePath(`b/${p}`)}`;
|
|
3884
|
+
const header = [];
|
|
3885
|
+
if (diff.status === "added" && (mode === "stage" || full)) {
|
|
3886
|
+
header.push(gitLine(newPath, newPath), `new file mode ${diff.newMode ?? "100644"}`);
|
|
3887
|
+
if (lines > 0) header.push(from(null), to(newPath));
|
|
3888
|
+
} else if (diff.status === "deleted" && full) {
|
|
3889
|
+
header.push(gitLine(oldPath, oldPath), `deleted file mode ${diff.oldMode ?? "100644"}`);
|
|
3890
|
+
if (lines > 0) header.push(from(oldPath), to(null));
|
|
3891
|
+
} else if (diff.status === "deleted" && mode === "unstage") {
|
|
3892
|
+
throw badRequest("a staged deletion can only be unstaged as a whole", "whole_file_only");
|
|
3893
|
+
} else if (rename2 && full) {
|
|
3894
|
+
header.push(gitLine(oldPath, newPath));
|
|
3895
|
+
if (whole && modeChange) header.push(`old mode ${diff.oldMode}`, `new mode ${diff.newMode}`);
|
|
3896
|
+
header.push(`rename from ${quotePath(oldPath)}`, `rename to ${quotePath(newPath)}`);
|
|
3897
|
+
if (lines > 0) header.push(from(oldPath), to(newPath));
|
|
3898
|
+
} else {
|
|
3899
|
+
header.push(gitLine(newPath, newPath));
|
|
3900
|
+
if (whole && modeChange) header.push(`old mode ${diff.oldMode}`, `new mode ${diff.newMode}`);
|
|
3901
|
+
if (lines > 0) header.push(from(newPath), to(newPath));
|
|
3902
|
+
}
|
|
3903
|
+
if (lines === 0) {
|
|
3904
|
+
const headerOnly = whole && (modeChange || rename2 || (diff.status === "added" || diff.status === "deleted") && diff.hunks.length === 0);
|
|
3905
|
+
if (!headerOnly) throw badRequest("no changed lines selected", "empty_selection");
|
|
3906
|
+
return { patch: header.join("\n") + "\n", lines: 0 };
|
|
3907
|
+
}
|
|
3908
|
+
let delta = 0;
|
|
3909
|
+
const hunks = [];
|
|
3910
|
+
diff.hunks.forEach((hunk, index) => {
|
|
3911
|
+
const set = picked.get(index);
|
|
3912
|
+
if (!set || set.size === 0) return;
|
|
3913
|
+
const out = cutHunk(hunk, set, mode);
|
|
3914
|
+
const oldLines = out.filter((l) => l.ch !== "+").length;
|
|
3915
|
+
const newLines = out.filter((l) => l.ch !== "-").length;
|
|
3916
|
+
const oldBegin = hunk.oldLines === 0 ? hunk.oldStart + 1 : hunk.oldStart;
|
|
3917
|
+
const oldStart = oldLines === 0 ? oldBegin - 1 : oldBegin;
|
|
3918
|
+
const newStart = newLines === 0 ? oldBegin + delta - 1 : oldBegin + delta;
|
|
3919
|
+
delta += newLines - oldLines;
|
|
3920
|
+
const text = [`@@ -${oldStart},${oldLines} +${newStart},${newLines} @@${hunk.header ? " " + hunk.header : ""}`];
|
|
3921
|
+
for (const l of out) {
|
|
3922
|
+
text.push(l.ch + l.content);
|
|
3923
|
+
if (l.noNewline) text.push("\");
|
|
3924
|
+
}
|
|
3925
|
+
hunks.push(text.join("\n"));
|
|
3926
|
+
});
|
|
3927
|
+
return { patch: [...header, ...hunks].join("\n") + "\n", lines };
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3725
3930
|
// packages/server/src/state.ts
|
|
3726
3931
|
import os from "os";
|
|
3727
3932
|
import path3 from "path";
|
|
@@ -3751,6 +3956,8 @@ function ensureTarget(state, key) {
|
|
|
3751
3956
|
}
|
|
3752
3957
|
return t;
|
|
3753
3958
|
}
|
|
3959
|
+
var usable = (v) => !!v && typeof v === "object" && typeof v.id === "string";
|
|
3960
|
+
var usableComment = (v) => usable(v) && typeof v.anchor === "object" && !!v.anchor;
|
|
3754
3961
|
function normalise(raw2, repoRoot) {
|
|
3755
3962
|
const base = defaultState(repoRoot);
|
|
3756
3963
|
if (!raw2 || typeof raw2 !== "object") return base;
|
|
@@ -3761,7 +3968,7 @@ function normalise(raw2, repoRoot) {
|
|
|
3761
3968
|
if (!v || typeof v !== "object") continue;
|
|
3762
3969
|
targets[k] = {
|
|
3763
3970
|
viewed: typeof v.viewed === "object" && v.viewed ? v.viewed : {},
|
|
3764
|
-
comments: Array.isArray(v.comments) ? v.comments : [],
|
|
3971
|
+
comments: Array.isArray(v.comments) ? v.comments.filter(usableComment) : [],
|
|
3765
3972
|
...typeof v.head === "string" ? { head: v.head } : {}
|
|
3766
3973
|
};
|
|
3767
3974
|
}
|
|
@@ -3770,8 +3977,8 @@ function normalise(raw2, repoRoot) {
|
|
|
3770
3977
|
schemaVersion: 1,
|
|
3771
3978
|
repoRoot: r.repoRoot ?? repoRoot,
|
|
3772
3979
|
targets,
|
|
3773
|
-
issues: Array.isArray(r.issues) ? r.issues : [],
|
|
3774
|
-
todos: Array.isArray(r.todos) ? r.todos : [],
|
|
3980
|
+
issues: Array.isArray(r.issues) ? r.issues.filter(usable) : [],
|
|
3981
|
+
todos: Array.isArray(r.todos) ? r.todos.filter(usable) : [],
|
|
3775
3982
|
prefs: {
|
|
3776
3983
|
...defaultPrefs(),
|
|
3777
3984
|
...r.prefs ?? {},
|
|
@@ -4166,7 +4373,9 @@ var RepoWatcher = class {
|
|
|
4166
4373
|
}
|
|
4167
4374
|
async snapshot() {
|
|
4168
4375
|
const [statusRes, headRes, branch] = await Promise.all([
|
|
4169
|
-
|
|
4376
|
+
// Untracked files one by one: folded into their directory, an edit inside a directory the
|
|
4377
|
+
// agent just created would leave the directory's own mtime and size untouched.
|
|
4378
|
+
runGit(["status", "--porcelain", "-z", "--untracked-files=all"], { cwd: this.root }),
|
|
4170
4379
|
runGit(["rev-parse", "HEAD"], { cwd: this.root }).catch(() => ({ stdout: "" })),
|
|
4171
4380
|
// The same label getRepoInfo reports, so the branch identity the client filters todos by
|
|
4172
4381
|
// never flips between `HEAD` and a short sha depending on which of the two spoke last.
|
|
@@ -4275,13 +4484,6 @@ function createApp(opts) {
|
|
|
4275
4484
|
throw e;
|
|
4276
4485
|
}
|
|
4277
4486
|
};
|
|
4278
|
-
const decodeKey = (raw2) => {
|
|
4279
|
-
try {
|
|
4280
|
-
return decodeURIComponent(raw2);
|
|
4281
|
-
} catch {
|
|
4282
|
-
throw badRequest("malformed target key", "invalid_target");
|
|
4283
|
-
}
|
|
4284
|
-
};
|
|
4285
4487
|
const fileDiffWithHints = async (ctx, filePath, explicit) => {
|
|
4286
4488
|
const cached = listings.get(ctx.key);
|
|
4287
4489
|
const hint = cached && Date.now() - cached.at < LISTING_HINT_TTL_MS ? cached.files.find((f) => f.path === filePath) : void 0;
|
|
@@ -4308,8 +4510,13 @@ function createApp(opts) {
|
|
|
4308
4510
|
api.get("/ping", (c) => c.text(opts.instanceToken ?? ""));
|
|
4309
4511
|
api.get("/repo", async (c) => {
|
|
4310
4512
|
const state = await store.load();
|
|
4311
|
-
const info = await getRepoInfo(repo,
|
|
4513
|
+
const info = await getRepoInfo(repo, "working");
|
|
4312
4514
|
worktreeCache = { at: Date.now(), list: info.worktrees };
|
|
4515
|
+
const last = state.prefs.lastTarget;
|
|
4516
|
+
const target = last ? tryParseTargetKey(last) : void 0;
|
|
4517
|
+
const usable2 = !!target && (!target.worktree || info.worktrees.some((w) => w.path === target.worktree));
|
|
4518
|
+
if (last && usable2) info.defaultTarget = last;
|
|
4519
|
+
else if (last) await store.update((s) => void (s.prefs.lastTarget = "working"));
|
|
4313
4520
|
return c.json(info);
|
|
4314
4521
|
});
|
|
4315
4522
|
api.get("/state", async (c) => c.json(await store.load()));
|
|
@@ -4327,7 +4534,7 @@ function createApp(opts) {
|
|
|
4327
4534
|
return c.json(prefs);
|
|
4328
4535
|
});
|
|
4329
4536
|
api.get("/targets/:key/files", async (c) => {
|
|
4330
|
-
const key =
|
|
4537
|
+
const key = c.req.param("key");
|
|
4331
4538
|
const ctx = await targetCtx(key);
|
|
4332
4539
|
const diffs = await listTargetDiffs(ctx);
|
|
4333
4540
|
listings.set(key, { at: Date.now(), files: diffs });
|
|
@@ -4363,7 +4570,7 @@ function createApp(opts) {
|
|
|
4363
4570
|
return c.json(res);
|
|
4364
4571
|
});
|
|
4365
4572
|
api.get("/targets/:key/file", async (c) => {
|
|
4366
|
-
const key =
|
|
4573
|
+
const key = c.req.param("key");
|
|
4367
4574
|
const filePath = c.req.query("path");
|
|
4368
4575
|
if (!filePath) throw badRequest("missing path");
|
|
4369
4576
|
const ctx = await targetCtx(key);
|
|
@@ -4374,7 +4581,7 @@ function createApp(opts) {
|
|
|
4374
4581
|
return c.json(diff);
|
|
4375
4582
|
});
|
|
4376
4583
|
api.get("/targets/:key/file/full", async (c) => {
|
|
4377
|
-
const key =
|
|
4584
|
+
const key = c.req.param("key");
|
|
4378
4585
|
const filePath = c.req.query("path");
|
|
4379
4586
|
if (!filePath) throw badRequest("missing path");
|
|
4380
4587
|
const side = c.req.query("side") === "old" ? "old" : "new";
|
|
@@ -4384,7 +4591,7 @@ function createApp(opts) {
|
|
|
4384
4591
|
return c.json(res);
|
|
4385
4592
|
});
|
|
4386
4593
|
api.put("/targets/:key/viewed", async (c) => {
|
|
4387
|
-
const key =
|
|
4594
|
+
const key = c.req.param("key");
|
|
4388
4595
|
await targetCtx(key);
|
|
4389
4596
|
const body = await c.req.json();
|
|
4390
4597
|
if (!body.path) throw badRequest("missing path");
|
|
@@ -4397,13 +4604,31 @@ function createApp(opts) {
|
|
|
4397
4604
|
changedNotices.get(key)?.delete(body.path);
|
|
4398
4605
|
return c.json({ viewed });
|
|
4399
4606
|
});
|
|
4607
|
+
api.post("/targets/:key/stage", async (c) => {
|
|
4608
|
+
const key = c.req.param("key");
|
|
4609
|
+
const ctx = await targetCtx(key);
|
|
4610
|
+
const mode = stageModeFor(ctx.target);
|
|
4611
|
+
if (!mode) throw badRequest("only the working (stage) and staged (unstage) views can be staged from", "not_stageable");
|
|
4612
|
+
const body = await c.req.json();
|
|
4613
|
+
if (!body.path || typeof body.path !== "string") throw badRequest("missing path");
|
|
4614
|
+
if (!body.contentHash || typeof body.contentHash !== "string") throw badRequest("contentHash is required");
|
|
4615
|
+
if (body.hunks !== void 0 && !Array.isArray(body.hunks)) throw badRequest("hunks must be an array", "bad_selection");
|
|
4616
|
+
const diff = await fileDiffWithHints(ctx, body.path);
|
|
4617
|
+
if (!diff) throw badRequest(`file ${body.path} is not part of ${key}`, "no_diff");
|
|
4618
|
+
if (diff.contentHash !== body.contentHash) throw new HttpError(409, "the diff changed since it was loaded; refresh and pick the lines again", "diff_changed");
|
|
4619
|
+
const { patch, lines } = buildStagePatch(diff, mode, body.hunks);
|
|
4620
|
+
await applyToIndex(ctx.cwd, patch, { reverse: mode === "unstage" });
|
|
4621
|
+
void watchers.get(ctx.cwd)?.poll();
|
|
4622
|
+
const res = { ok: true, lines };
|
|
4623
|
+
return c.json(res);
|
|
4624
|
+
});
|
|
4400
4625
|
api.get("/targets/:key/comments", async (c) => {
|
|
4401
|
-
const key =
|
|
4626
|
+
const key = c.req.param("key");
|
|
4402
4627
|
const state = await store.load();
|
|
4403
4628
|
return c.json({ comments: state.targets[commentScopeKey(key)]?.comments ?? [] });
|
|
4404
4629
|
});
|
|
4405
4630
|
api.post("/targets/:key/comments", async (c) => {
|
|
4406
|
-
const key =
|
|
4631
|
+
const key = c.req.param("key");
|
|
4407
4632
|
const ctx = await targetCtx(key);
|
|
4408
4633
|
const body = await c.req.json();
|
|
4409
4634
|
if (!body.filePath || typeof body.body !== "string" || !body.body.trim()) throw badRequest("filePath and body are required");
|
|
@@ -4436,7 +4661,7 @@ function createApp(opts) {
|
|
|
4436
4661
|
return c.json(comment, 201);
|
|
4437
4662
|
});
|
|
4438
4663
|
api.patch("/targets/:key/comments/:id", async (c) => {
|
|
4439
|
-
const key =
|
|
4664
|
+
const key = c.req.param("key");
|
|
4440
4665
|
const id = c.req.param("id");
|
|
4441
4666
|
const ctx = await targetCtx(key);
|
|
4442
4667
|
const body = await c.req.json();
|
|
@@ -4481,7 +4706,7 @@ function createApp(opts) {
|
|
|
4481
4706
|
return c.json(updated);
|
|
4482
4707
|
});
|
|
4483
4708
|
api.delete("/targets/:key/comments/:id", async (c) => {
|
|
4484
|
-
const key =
|
|
4709
|
+
const key = c.req.param("key");
|
|
4485
4710
|
const id = c.req.param("id");
|
|
4486
4711
|
const removed = await store.update((s) => {
|
|
4487
4712
|
const t = s.targets[commentScopeKey(key)];
|
|
@@ -4495,7 +4720,7 @@ function createApp(opts) {
|
|
|
4495
4720
|
return c.json({ ok: true });
|
|
4496
4721
|
});
|
|
4497
4722
|
api.post("/targets/:key/comments/reanchor", async (c) => {
|
|
4498
|
-
const key =
|
|
4723
|
+
const key = c.req.param("key");
|
|
4499
4724
|
const ctx = await targetCtx(key);
|
|
4500
4725
|
const scope = commentScopeKey(key);
|
|
4501
4726
|
const local = isLocalTarget(ctx.target);
|
|
@@ -4619,16 +4844,28 @@ function createApp(opts) {
|
|
|
4619
4844
|
id: randomUUID(),
|
|
4620
4845
|
title: body.title.trim(),
|
|
4621
4846
|
body: typeof body.body === "string" ? body.body : "",
|
|
4622
|
-
status: "open",
|
|
4847
|
+
status: body.status === "closed" ? "closed" : "open",
|
|
4623
4848
|
commentIds: Array.isArray(body.commentIds) ? body.commentIds.filter((x) => typeof x === "string") : [],
|
|
4624
4849
|
createdAt: now,
|
|
4625
4850
|
updatedAt: now
|
|
4626
4851
|
};
|
|
4627
4852
|
await store.update((s) => {
|
|
4628
|
-
s.issues.
|
|
4853
|
+
s.issues = insertAfter(s.issues, issue, typeof body.after === "string" ? body.after : void 0);
|
|
4629
4854
|
});
|
|
4630
4855
|
return c.json(issue, 201);
|
|
4631
4856
|
});
|
|
4857
|
+
api.post("/issues/:id/move", async (c) => {
|
|
4858
|
+
const id = c.req.param("id");
|
|
4859
|
+
const body = await c.req.json();
|
|
4860
|
+
if (body.before !== null && typeof body.before !== "string") throw badRequest("before must be an id or null");
|
|
4861
|
+
const issues = await store.update((s) => {
|
|
4862
|
+
const next = moveBefore(s.issues, id, body.before);
|
|
4863
|
+
if (!next) throw notFound("issue not found");
|
|
4864
|
+
s.issues = next;
|
|
4865
|
+
return s.issues;
|
|
4866
|
+
});
|
|
4867
|
+
return c.json({ issues });
|
|
4868
|
+
});
|
|
4632
4869
|
api.patch("/issues/:id", async (c) => {
|
|
4633
4870
|
const id = c.req.param("id");
|
|
4634
4871
|
const body = await c.req.json();
|
|
@@ -4702,15 +4939,28 @@ function createApp(opts) {
|
|
|
4702
4939
|
branch,
|
|
4703
4940
|
title: body.title.trim(),
|
|
4704
4941
|
body: typeof body.body === "string" ? body.body : "",
|
|
4705
|
-
status: "open",
|
|
4942
|
+
status: body.status === "done" ? "done" : "open",
|
|
4706
4943
|
createdAt: now,
|
|
4707
4944
|
updatedAt: now
|
|
4708
4945
|
};
|
|
4709
4946
|
await store.update((s) => {
|
|
4710
|
-
s.todos.
|
|
4947
|
+
s.todos = insertAfter(s.todos, todo, typeof body.after === "string" ? body.after : void 0);
|
|
4711
4948
|
});
|
|
4712
4949
|
return c.json(todo, 201);
|
|
4713
4950
|
});
|
|
4951
|
+
api.post("/todos/:id/move", async (c) => {
|
|
4952
|
+
const id = c.req.param("id");
|
|
4953
|
+
const body = await c.req.json();
|
|
4954
|
+
if (body.before !== null && typeof body.before !== "string") throw badRequest("before must be an id or null");
|
|
4955
|
+
const todos = await store.update((s) => {
|
|
4956
|
+
const next = moveBefore(s.todos, id, body.before);
|
|
4957
|
+
if (!next) throw notFound("todo not found");
|
|
4958
|
+
s.todos = next;
|
|
4959
|
+
return s.todos;
|
|
4960
|
+
});
|
|
4961
|
+
const res = { todos };
|
|
4962
|
+
return c.json(res);
|
|
4963
|
+
});
|
|
4714
4964
|
api.patch("/todos/:id", async (c) => {
|
|
4715
4965
|
const id = c.req.param("id");
|
|
4716
4966
|
const body = await c.req.json();
|
|
@@ -4782,6 +5032,17 @@ function createApp(opts) {
|
|
|
4782
5032
|
const res = { commits, hasMore };
|
|
4783
5033
|
return c.json(res);
|
|
4784
5034
|
});
|
|
5035
|
+
api.get("/fork-point", async (c) => {
|
|
5036
|
+
const base = (c.req.query("base") ?? "").trim();
|
|
5037
|
+
if (!isValidRef(base)) throw badRequest("invalid base ref");
|
|
5038
|
+
const cwd = await knownRoot(c.req.query("root") || void 0);
|
|
5039
|
+
if (!await refExists(cwd, base)) throw badRequest(`unknown ref: ${base}`, "unknown_ref");
|
|
5040
|
+
const sha = await refExists(cwd, "HEAD") ? await mergeBase(cwd, base, "HEAD") : void 0;
|
|
5041
|
+
if (!sha) throw badRequest(`${base} and HEAD share no history`, "no_merge_base");
|
|
5042
|
+
const counts = (await runGit(["rev-list", "--left-right", "--count", `${base}...HEAD`], { cwd })).stdout.trim().split(/\s+/);
|
|
5043
|
+
const res = { base, sha, ahead: Number(counts[1] ?? 0), behind: Number(counts[0] ?? 0) };
|
|
5044
|
+
return c.json(res);
|
|
5045
|
+
});
|
|
4785
5046
|
api.get("/events", async (c) => {
|
|
4786
5047
|
const root = c.req.query("root") || repo.root;
|
|
4787
5048
|
const known = root === repo.root || root === repo.commonRoot || (await worktrees()).some((w) => w.path === root);
|
|
@@ -4959,7 +5220,7 @@ async function startServer(opts) {
|
|
|
4959
5220
|
}
|
|
4960
5221
|
|
|
4961
5222
|
// bin/cli.ts
|
|
4962
|
-
var VERSION = true ? "0.
|
|
5223
|
+
var VERSION = true ? "0.7.0" : "dev";
|
|
4963
5224
|
function parseArgs(argv) {
|
|
4964
5225
|
const args = { repoPath: process.cwd(), open: true, help: false, version: false };
|
|
4965
5226
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -5029,7 +5290,7 @@ function resolveWebDir() {
|
|
|
5029
5290
|
// bin/cli.ts (tsx dev) -> dist/web
|
|
5030
5291
|
].filter((p) => !!p);
|
|
5031
5292
|
for (const dir of candidates) {
|
|
5032
|
-
if (
|
|
5293
|
+
if (existsSync2(path8.join(dir, "index.html"))) return dir;
|
|
5033
5294
|
}
|
|
5034
5295
|
return void 0;
|
|
5035
5296
|
}
|
|
@@ -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;--sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "Noto Sans Mono CJK SC", monospace}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{font:13px/1.5 var(--sans);color:var(--ink);background:var(--ground);-webkit-font-smoothing:antialiased}button,input,select,textarea{font:inherit;color:inherit}button{height:var(--ctl-h);display:inline-flex;align-items:center;gap:6px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-ctl);padding:0 10px;cursor:pointer;white-space:nowrap}button:hover:not(:disabled){border-color:var(--line-strong);background:var(--surface-2)}button:disabled{opacity:.45;cursor:default}button.primary{background:var(--ink);border-color:var(--ink);color:#fff}button.primary:hover:not(:disabled){background:#2b2f38;border-color:#2b2f38}button.accent{background:var(--accent);border-color:var(--accent);color:#fff}button.accent:hover:not(:disabled){background:var(--accent-strong);border-color:var(--accent-strong)}button.link{height:auto;background:none;border:none;color:var(--ink-2);padding:0 4px}button.link:hover:not(:disabled){background:none;color:var(--ink);text-decoration:underline;text-underline-offset:2px}button.link.danger{color:var(--del-fg)}button.link.active{color:var(--accent);font-weight:500}button.icon{padding:0 7px}button.toggle:before{content:"";width:6px;height:6px;border-radius:50%;border:1.5px solid currentColor}button.toggle[aria-pressed=true]{border-color:var(--ink)}button.toggle[aria-pressed=true]:before{background:currentColor}input,select,textarea{height:var(--ctl-h);border:1px solid var(--line);border-radius:var(--r-ctl);padding:0 8px;background:var(--surface)}select{padding-right:4px}textarea{height:auto;width:100%;resize:vertical;font-family:var(--mono);font-size:12px;line-height:1.6;padding:6px 8px}input[type=checkbox]{height:auto;margin:0;accent-color:var(--ink)}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}button:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--accent);outline-offset:1px}kbd{font-family:var(--mono);font-size:11px;background:var(--surface);border:1px solid var(--line);border-bottom-width:2px;border-radius:3px;padding:0 5px;color:var(--ink-2)}.mono{font-family:var(--mono);font-size:12px}.muted{color:var(--muted)}.small{font-size:11px}.spacer{flex:1}.add{color:var(--add-fg)}.del{color:var(--del-fg)}.check{display:inline-flex;align-items:center;gap:5px;white-space:nowrap;cursor:pointer}.badge{display:inline-flex;align-items:center;height:17px;padding:0 6px;border-radius:var(--r-ctl);font-family:var(--mono);font-size:11px;background:var(--surface-3);color:var(--ink-2);white-space:nowrap}.badge-active{background:var(--accent-soft);color:var(--accent)}.badge-exported{background:var(--surface-3);color:var(--muted)}.badge-orphaned{background:var(--warn-bg);color:var(--warn-fg)}.badge-open{background:var(--ink);color:#fff}.badge-closed{background:var(--surface-3);color:var(--muted)}.seg{display:inline-flex;gap:1px;padding:2px;background:var(--surface-3);border-radius:4px}.seg button{height:22px;padding:0 9px;background:transparent;border:1px solid transparent;border-radius:var(--r-ctl);color:var(--ink-2)}.seg button:hover:not(:disabled){background:transparent;border-color:transparent;color:var(--ink)}.seg button.active{background:var(--surface);border-color:var(--line);color:var(--ink);font-weight:500}.seg.small button{height:20px;padding:0 7px;font-size:12px}.seg button.active .tab-count{color:var(--ink)}.tabs{display:inline-flex;align-self:stretch;align-items:stretch;gap:2px}.tab{height:auto;background:none;border:none;border-radius:0;padding:0 6px;color:var(--ink-2);position:relative}.tab:hover:not(:disabled){background:none;color:var(--ink)}.tab.active{color:var(--ink);font-weight:500}.tab.active:after{content:"";position:absolute;left:6px;right:6px;bottom:-1px;height:2px;background:var(--ink)}.tab-count{font-family:var(--mono);font-size:11px;color:var(--muted)}.tab.active .tab-count{color:var(--ink)}.tabs.small .tab{padding:0 5px;font-size:12px}.error-box{color:var(--del-fg);background:var(--del-bg);padding:8px 12px;border-radius:var(--r-card);margin:8px}.empty{padding:20px 12px;text-align:center;color:var(--muted);font-size:12px;line-height:1.6}.app{display:flex;flex-direction:column;height:100%}.body{flex:1;display:flex;min-height:0}.main{flex:1;min-width:0;display:flex;flex-direction:column;background:var(--surface)}.loading-screen,.fatal{padding:48px;text-align:center;color:var(--muted)}.fatal h1{font-family:var(--mono);font-weight:500;font-size:20px;letter-spacing:-.02em;color:var(--ink)}.placeholder{padding:48px 24px;text-align:center;color:var(--muted)}.placeholder p{margin:6px 0}.topbar{display:flex;align-items:center;gap:14px;padding:0 12px;min-height:42px;background:var(--ground);border-bottom:1px solid var(--line);flex-wrap:wrap}.brand{display:flex;align-items:center;gap:7px}.mark{width:16px;height:16px;flex:none}.wordmark{font-family:var(--mono);font-weight:500;font-size:15px;letter-spacing:-.02em;color:var(--ink)}.scope{display:flex;align-items:center;gap:8px}.repo-name{font-weight:500}.branch{font-family:var(--mono);font-size:11px;color:var(--ink-2);background:var(--surface-3);border-radius:var(--r-ctl);padding:1px 6px}.inline-form{display:inline-flex;align-items:center;gap:4px}.inline-form input{width:130px;font-family:var(--mono);font-size:12px}.topbar-right{margin-left:auto;align-self:stretch;display:flex;align-items:stretch;gap:14px}.tools{display:flex;align-items:center;gap:8px}.nvim{display:inline-flex;align-items:center;gap:4px;max-width:320px}.nvim select{max-width:260px}.nvim-one{font-family:var(--mono);font-size:12px;color:var(--ink-2)}.nvim .muted{font-size:12px}.sidebar{width:280px;min-width:200px;border-right:1px solid var(--line);display:flex;flex-direction:column;background:var(--ground);resize:horizontal;overflow:hidden}.tree-block{display:flex;flex-direction:column;flex:0 1 auto;min-height:66px;min-width:0}.tree-block+.tree-block{border-top:1px solid var(--line)}.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{display:flex;align-items:center;gap:8px;padding:8px 10px 4px;font-size:12px;white-space:nowrap}.block-title{font-weight:600}.block-n{font-family:var(--mono);font-size:11px;color:var(--muted)}.block-head .counts{font-family:var(--mono);font-size:11px;display:inline-flex;gap:4px}.block-viewed{font-family:var(--mono);font-size:11px;color:var(--muted)}.block-progress{height:2px;margin:0 10px 4px;background:var(--line);border-radius:1px;overflow:hidden}.block-progress>span{display:block;height:100%;background:var(--ink);transition:width .2s}.tree-tools{display:inline-flex;gap:2px}.tree-tools .link{font-size:11px;color:var(--muted)}.tree{flex:1 1 auto;min-height:0;overflow:auto;padding:2px 0 6px}.tree-row{display:flex;align-items:center;gap:6px;padding:0 10px;height:24px;cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none}.tree-row:hover{background:var(--surface-3)}.tree-row.active{background:var(--surface);box-shadow:inset 2px 0 var(--ink)}.tree-row.viewed .name{color:var(--muted)}.tree-row .name{overflow:hidden;text-overflow:ellipsis}.tree-row.dir .name{font-weight:500;color:var(--ink-2)}.chev{display:inline-block;width:12px;font-size:10px;color:var(--muted);transition:transform .1s}.chev.open{transform:rotate(90deg)}.tree-row .counts{margin-left:auto;font-family:var(--mono);font-size:11px;display:inline-flex;gap:4px}.tree-row .count{font-family:var(--mono);font-size:11px}.viewed-box{margin:0}.status{font-family:var(--mono);font-size:11px;font-weight:500;width:14px;text-align:center}.status-added{color:var(--add-fg)}.status-modified{color:var(--warn-fg)}.status-deleted{color:var(--del-fg)}.status-renamed{color:var(--ink-2)}.changed{font-size:11px;background:var(--warn-bg);color:var(--warn-fg);border-radius:var(--r-ctl);padding:0 5px}.cc{font-family:var(--mono);font-size:11px;background:var(--accent-soft);color:var(--accent);border-radius:var(--r-ctl);padding:0 5px}.diff-panel{flex:1;display:flex;flex-direction:column;min-height:0;position:relative}.file-head{display:flex;align-items:center;gap:10px;padding:0 12px;min-height:38px;border-bottom:1px solid var(--line);background:var(--surface);flex-wrap:wrap}.file-head .path{font-family:var(--mono);font-size:12px}.file-head .path .dir{color:var(--muted)}.file-head .path .base{font-weight:500}.file-head .counts{font-family:var(--mono);font-size:11px;display:inline-flex;gap:4px}.file-head .check{font-size:12px}.diff-scroll{flex:1;overflow:auto;font-family:var(--mono);font-size:12px;line-height:20px;tab-size:4;background:var(--surface)}.diff-scroll.selecting,.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-tools{display:flex;align-items:center;gap:8px;padding:6px 12px;border-bottom:1px solid var(--line);background:var(--ground);flex-wrap:wrap}.commits-tools input{width:140px}.commits-tools input.search{width:260px}.commits-tools input.path{font-size:12px}.commits-count{margin-left:auto;font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}.commits-compare{display:flex;align-items:center;gap:20px;padding:6px 12px;border-bottom:1px solid var(--line);background:var(--ground);flex-wrap:wrap}.compare-label{font-size:12px;color:var(--ink-2);margin-right:2px}.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{margin-left:6px;font-size:12px;white-space:nowrap}.fork-note .link{padding:0;font-size:12px}.commit-row .author{flex:0 0 110px;overflow:hidden;text-overflow:ellipsis;color:var(--ink-2);font-size:12px}.commit-row .time{flex:0 0 40px;text-align:right;color:var(--muted);font-family:var(--mono);font-size:11px}.commits-foot{display:flex;justify-content:center;align-items:center;min-height:44px;font-size:12px}.issues-drawer{width:440px;border-left:1px solid var(--line);display:flex;flex-direction:column;background:var(--ground);min-height:0}.drawer-head{display:flex;align-items:center;gap:10px;padding:0 10px;min-height:36px;border-bottom:1px solid var(--line)}.drawer-title{font-weight:600}.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;min-width:300px;border-left:1px solid var(--line);display:flex;flex-direction:column;background:var(--ground);min-height:0}.rail-head{display:flex;align-items:center;gap:10px;padding:0 10px;min-height:36px;border-bottom:1px solid var(--line)}.rail-tools{display:flex;align-items:center;gap:8px;padding:0 10px;min-height:34px;border-bottom:1px solid var(--line);font-size:12px}.rail-notice{padding:6px 10px;background:var(--warn-bg);color:var(--warn-fg);border-bottom:1px solid var(--warn-line);display:flex;gap:8px;align-items:center;font-size:12px}.rail-notice .link{color:var(--warn-fg)}.rail-list{flex:1;overflow:auto;padding:10px;display:flex;flex-direction:column;gap:8px}.rail-slot{display:flex;flex-direction:column;gap:8px}.rail-foot{display:flex;flex-direction:column;gap:8px;padding:10px;border-top:1px solid var(--line);background:var(--ground)}.rail-foot-row{display:flex;align-items:center;gap:10px}.send{height:30px;padding:0 14px;background:var(--accent);border-color:var(--accent);color:#fff;font-weight:500}.send:hover:not(:disabled){background:var(--accent-strong);border-color:var(--accent-strong)}.send-n{font-family:var(--mono);font-size:11px;font-weight:400;line-height:16px;padding:0 5px;border-radius:var(--r-ctl);background:#ffffff38}.rail .comment-card{cursor:pointer}.rail .comment-card.focused{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.comment-head .file{color:var(--muted);max-width:120px;overflow:hidden;text-overflow:ellipsis}.rail .comment-editor{max-width:none}.sidebar.dual{overflow:hidden}@media(prefers-reduced-motion:reduce){.chev,.card-actions,.task-row.completing,.block-progress>span{transition:none}}
|