@xbghc/warden 0.16.1 → 0.16.3

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
@@ -75,6 +75,11 @@ you did not>"`.
75
75
  | `s` | Stage (Unstaged view) or unstage (Staged view) the picked lines |
76
76
  | `Ctrl+Enter` | Save the comment being edited |
77
77
  | `Esc` | Drop the picked lines / cancel editing / cancel re-attach mode / shut the rail / back to the files under review |
78
+ | Click a line number | Open that line in nvim (see [Editor and platform](docs/environment.md)) |
79
+ | `Alt`+click a line number | Copy the place as `src/a.ts:12` — `src/a.ts:12(修改前)` for a line of the old side |
80
+ | `Alt`+`Shift`+click a line number | Copy the range from the line last copied, `src/a.ts:12-18` |
81
+
82
+ The copy button beside the path in the file header copies the path, relative to the repository.
78
83
 
79
84
  ## Documentation
80
85
 
package/dist/cli.js CHANGED
@@ -4066,6 +4066,15 @@ async function listUntracked(cwd) {
4066
4066
  const r = await runGit(["ls-files", "--others", "--exclude-standard", "-z"], { cwd });
4067
4067
  return r.stdout.split("\0").filter(Boolean);
4068
4068
  }
4069
+ async function listUnmerged(cwd, file) {
4070
+ const r = await runGit(["ls-files", "--unmerged", "-z", ...file ? ["--", literal(file)] : []], { cwd });
4071
+ return [...new Set(r.stdout.split("\0").flatMap((e) => e.includes(" ") ? [e.slice(e.indexOf(" ") + 1)] : []))];
4072
+ }
4073
+ async function conflictDiff(ctx, file) {
4074
+ const base = await hasHead(ctx.cwd) ? "HEAD" : EMPTY_TREE_SHA;
4075
+ const f = parseUnifiedDiff(await runDiff(ctx, [...DIFF_BASE_ARGS, base, "--", literal(file)])).find((d) => d.path === file);
4076
+ return f ? { ...f, conflicted: true } : void 0;
4077
+ }
4069
4078
  async function isUntracked(cwd, file) {
4070
4079
  const r = await runGit(["ls-files", "--others", "--exclude-standard", "-z", "--", literal(file)], { cwd });
4071
4080
  return r.stdout.split("\0").filter(Boolean).includes(file);
@@ -4099,6 +4108,15 @@ async function mapLimit(items, limit, fn) {
4099
4108
  async function listTargetDiffs(ctx) {
4100
4109
  const args = await diffArgs(ctx);
4101
4110
  const files = parseUnifiedDiff(await runDiff(ctx, [...args, "--"]));
4111
+ if (ctx.target.kind === "working") {
4112
+ for (const path13 of await listUnmerged(ctx.cwd)) {
4113
+ const d = await conflictDiff(ctx, path13);
4114
+ if (!d) continue;
4115
+ const at = files.findIndex((f) => f.path === path13);
4116
+ if (at >= 0) files[at] = d;
4117
+ else files.push(d);
4118
+ }
4119
+ }
4102
4120
  if (includesUntracked(ctx.target)) {
4103
4121
  const untracked = await listUntracked(ctx.cwd);
4104
4122
  const extra = await mapLimit(untracked, 8, (f) => untrackedDiff(ctx.cwd, f).catch(() => void 0));
@@ -4117,6 +4135,7 @@ async function getFileDiff(ctx, filePath, hints = {}) {
4117
4135
  if (includesUntracked(ctx.target) && (hints.untracked || await isUntracked(ctx.cwd, filePath))) {
4118
4136
  return untrackedDiff(ctx.cwd, filePath);
4119
4137
  }
4138
+ if (ctx.target.kind === "working" && (await listUnmerged(ctx.cwd, filePath)).includes(filePath)) return conflictDiff(ctx, filePath);
4120
4139
  const args = await diffArgs(ctx);
4121
4140
  const pathspec = [literal(filePath)];
4122
4141
  if (hints.oldPath && hints.oldPath !== filePath) pathspec.push(literal(hints.oldPath));
@@ -4263,6 +4282,12 @@ function matchesAt(lines, start, hashes) {
4263
4282
  }
4264
4283
  return true;
4265
4284
  }
4285
+ var DISTINCTIVE_CHARS = 24;
4286
+ function distinctive(lines, start, count) {
4287
+ let chars = 0;
4288
+ for (let i = start; i < start + count; i++) chars += lines[i].content.replace(/\s+/g, "").length;
4289
+ return chars >= DISTINCTIVE_CHARS;
4290
+ }
4266
4291
  function contextScore(lines, start, count, anchor) {
4267
4292
  let score = 0;
4268
4293
  const before = anchor.contextBefore;
@@ -4291,7 +4316,10 @@ function locateAnchor(diff, side, anchor) {
4291
4316
  if (matchesAt(lines, i, anchor.lineHashes)) candidates.push(i);
4292
4317
  }
4293
4318
  if (candidates.length === 0) return void 0;
4294
- if (candidates.length === 1) return candidates[0];
4319
+ if (candidates.length === 1) {
4320
+ const c = candidates[0];
4321
+ return contextScore(lines, c, n, anchor) > 0 || distinctive(lines, c, n) ? c : void 0;
4322
+ }
4295
4323
  let best = [];
4296
4324
  let bestScore = -1;
4297
4325
  for (const c of candidates) {
@@ -4832,9 +4860,16 @@ async function commentsIn(state, worktreeRoot, mainRoot) {
4832
4860
  return out;
4833
4861
  }
4834
4862
  async function takeFeedback(store, opts) {
4835
- if (opts.peek) return (await commentsIn(await store.load(), opts.worktreeRoot, opts.mainRoot)).filter(awaitsAgent);
4836
- return store.update(async (s) => {
4837
- const ids = new Set((await commentsIn(s, opts.worktreeRoot, opts.mainRoot)).filter(awaitsAgent).map((c) => c.id));
4863
+ const waiting = (await commentsIn(await store.load(), opts.worktreeRoot, opts.mainRoot)).filter(awaitsAgent);
4864
+ if (opts.peek) return waiting;
4865
+ return markHandedOver(
4866
+ store,
4867
+ waiting.map((c) => c.id)
4868
+ );
4869
+ }
4870
+ async function markHandedOver(store, commentIds) {
4871
+ return store.update((s) => {
4872
+ const ids = new Set(commentIds);
4838
4873
  const now = (/* @__PURE__ */ new Date()).toISOString();
4839
4874
  const taken = [];
4840
4875
  for (const t of Object.values(s.targets)) {
@@ -5465,6 +5500,7 @@ function createApp(opts) {
5465
5500
  if (body.skipDebug !== void 0 && typeof body.skipDebug !== "boolean") throw badRequest("skipDebug must be a boolean", "bad_selection");
5466
5501
  const diff = await fileDiffWithHints(ctx, body.path);
5467
5502
  if (!diff) throw badRequest(`file ${body.path} is not part of ${key}`, "no_diff");
5503
+ if (diff.conflicted) throw new HttpError(409, `${body.path} is in conflict; resolve it and git add it in the terminal`, "conflicted");
5468
5504
  if (diff.contentHash !== body.contentHash)
5469
5505
  throw new HttpError(409, "the diff changed since it was loaded; refresh and pick the lines again", "diff_changed");
5470
5506
  if (body.skipDebug) await annotateDebug(ctx, [diff]);
@@ -5519,20 +5555,20 @@ function createApp(opts) {
5519
5555
  const body = await c.req.json();
5520
5556
  const scope = commentScopeKey(key);
5521
5557
  const state = await store.load();
5522
- const existing = state.targets[scope]?.comments.find((x) => x.id === id);
5523
- if (!existing) throw notFound("comment not found");
5558
+ const existing2 = state.targets[scope]?.comments.find((x) => x.id === id);
5559
+ if (!existing2) throw notFound("comment not found");
5524
5560
  let patch = {};
5525
5561
  if (typeof body.body === "string") {
5526
5562
  if (!body.body.trim()) throw badRequest("body must not be empty");
5527
5563
  patch.body = body.body;
5528
5564
  }
5529
5565
  if (body.startLine !== void 0 || body.side !== void 0) {
5530
- const side = body.side ?? existing.side;
5531
- const start = Number(body.startLine ?? existing.startLine);
5532
- const end = Number(body.endLine ?? body.startLine ?? existing.endLine);
5566
+ const side = body.side ?? existing2.side;
5567
+ const start = Number(body.startLine ?? existing2.startLine);
5568
+ const end = Number(body.endLine ?? body.startLine ?? existing2.endLine);
5533
5569
  if (side !== "old" && side !== "new") throw badRequest("side must be old|new");
5534
- const diff = await fileDiffWithHints(ctx, existing.filePath);
5535
- if (!diff) throw badRequest(`file ${existing.filePath} is not part of ${key}`, "no_diff");
5570
+ const diff = await fileDiffWithHints(ctx, existing2.filePath);
5571
+ if (!diff) throw badRequest(`file ${existing2.filePath} is not part of ${key}`, "no_diff");
5536
5572
  const anchored = buildAnchor(diff, side, start, end);
5537
5573
  if (!anchored) throw badRequest("selection is not fully visible in the diff on that side", "bad_selection");
5538
5574
  patch = {
@@ -5542,7 +5578,7 @@ function createApp(opts) {
5542
5578
  endLine: anchored.endLine,
5543
5579
  codeSnippet: anchored.snippet,
5544
5580
  anchor: anchored.anchor,
5545
- status: existing.exportedAt ? "exported" : "active",
5581
+ status: existing2.exportedAt ? "exported" : "active",
5546
5582
  // Re-attaching in a view moves the comment to it.
5547
5583
  targetKey: key
5548
5584
  };
@@ -5551,7 +5587,12 @@ function createApp(opts) {
5551
5587
  const t = ensureTarget(s, scope);
5552
5588
  const idx = t.comments.findIndex((x) => x.id === id);
5553
5589
  if (idx < 0) throw notFound("comment not found");
5554
- const next = { ...t.comments[idx], ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
5590
+ const prev = t.comments[idx];
5591
+ const next = { ...prev, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
5592
+ if (patch.body !== void 0 && patch.body !== prev.body && next.exportedAt) {
5593
+ delete next.exportedAt;
5594
+ if (next.status === "exported") next.status = "active";
5595
+ }
5555
5596
  t.comments[idx] = next;
5556
5597
  return next;
5557
5598
  });
@@ -5678,30 +5719,34 @@ function createApp(opts) {
5678
5719
  const res = { comments: result };
5679
5720
  return c.json(res);
5680
5721
  });
5681
- api.post("/comments/export", async (c) => {
5682
- const body = await c.req.json();
5683
- if (!Array.isArray(body.commentIds)) throw badRequest("commentIds required");
5684
- const ids = body.commentIds.filter((x) => typeof x === "string");
5685
- let handed = [];
5686
- const res = await store.update((s) => {
5722
+ const handOver = async (ids) => {
5723
+ const handed = await store.update((s) => {
5687
5724
  const now = (/* @__PURE__ */ new Date()).toISOString();
5688
- const selected = [];
5725
+ const out = [];
5689
5726
  for (const id of ids) {
5690
5727
  const found = findComment(s, id);
5691
5728
  if (!found) continue;
5692
5729
  const next = { ...found.comment, exportedAt: now, updatedAt: now };
5693
5730
  if (next.status === "active") next.status = "exported";
5694
5731
  s.targets[found.targetKey].comments[found.index] = next;
5695
- selected.push(next);
5732
+ out.push(next);
5696
5733
  }
5697
- handed = selected;
5698
- return {
5699
- text: formatCommentsExport({ repoRoot: repo.root, comments: selected, replyCommand: opts.replyCommand }),
5700
- count: selected.length,
5701
- commentIds: selected.map((x) => x.id)
5702
- };
5734
+ return out;
5703
5735
  });
5704
5736
  checkpointHandoff(handed);
5737
+ return handed;
5738
+ };
5739
+ const existing = (state, ids) => ids.flatMap((id) => findComment(state, id)?.comment ?? []);
5740
+ api.post("/comments/export", async (c) => {
5741
+ const body = await c.req.json();
5742
+ if (!Array.isArray(body.commentIds)) throw badRequest("commentIds required");
5743
+ const ids = body.commentIds.filter((x) => typeof x === "string");
5744
+ const comments = body.preview ? existing(await store.load(), ids) : await handOver(ids);
5745
+ const res = {
5746
+ text: formatCommentsExport({ repoRoot: repo.root, comments, replyCommand: opts.replyCommand }),
5747
+ count: comments.length,
5748
+ commentIds: comments.map((x) => x.id)
5749
+ };
5705
5750
  return c.json(res);
5706
5751
  });
5707
5752
  const knownRoot = async (rootParam) => {
@@ -5776,26 +5821,17 @@ function createApp(opts) {
5776
5821
  });
5777
5822
  api.post("/todos/:id/export", async (c) => {
5778
5823
  const id = c.req.param("id");
5779
- const handed = [];
5780
- const res = await store.update((s) => {
5781
- const todo = s.todos.find((t) => t.id === id);
5782
- if (!todo) throw notFound("todo not found");
5783
- const now = (/* @__PURE__ */ new Date()).toISOString();
5784
- for (const cid of todo.commentIds ?? []) {
5785
- const found = findComment(s, cid);
5786
- if (!found) continue;
5787
- const next = { ...found.comment, exportedAt: now, updatedAt: now };
5788
- if (next.status === "active") next.status = "exported";
5789
- s.targets[found.targetKey].comments[found.index] = next;
5790
- handed.push(next);
5791
- }
5792
- return {
5793
- text: formatTodoExport({ todo, comments: handed, replyCommand: opts.replyCommand }),
5794
- count: handed.length,
5795
- commentIds: handed.map((x) => x.id)
5796
- };
5797
- });
5798
- checkpointHandoff(handed);
5824
+ const body = await c.req.json().catch(() => ({})) ?? {};
5825
+ const state = await store.load();
5826
+ const todo = state.todos.find((t) => t.id === id);
5827
+ if (!todo) throw notFound("todo not found");
5828
+ const ids = todo.commentIds ?? [];
5829
+ const comments = body.preview ? existing(state, ids) : await handOver(ids);
5830
+ const res = {
5831
+ text: formatTodoExport({ todo, comments, replyCommand: opts.replyCommand }),
5832
+ count: comments.length,
5833
+ commentIds: comments.map((x) => x.id)
5834
+ };
5799
5835
  return c.json(res);
5800
5836
  });
5801
5837
  api.delete("/todos/:id", async (c) => {
@@ -6233,18 +6269,21 @@ async function feedback(args, replyCommand) {
6233
6269
  const unknown = args.find((a) => a !== "--peek");
6234
6270
  if (unknown) throw new Error(`unknown argument: ${unknown}`);
6235
6271
  const { store, root, commonRoot } = await openStore();
6236
- const peek = args.includes("--peek");
6237
- const comments = await takeFeedback(store, { worktreeRoot: root, mainRoot: commonRoot, peek });
6272
+ const comments = await takeFeedback(store, { worktreeRoot: root, mainRoot: commonRoot, peek: true });
6238
6273
  if (comments.length === 0) {
6239
6274
  console.log("No review comments are waiting for you.");
6240
6275
  return;
6241
6276
  }
6242
- if (!peek) {
6243
- await takeCheckpoint(store, root, root === commonRoot ? void 0 : root, { handoff: true }).catch((e) => {
6244
- console.error(`warden feedback: no checkpoint taken: ${e instanceof Error ? e.message : e}`);
6245
- });
6246
- }
6247
- process.stdout.write(formatCommentsExport({ repoRoot: root, comments, replyCommand }));
6277
+ const text = formatCommentsExport({ repoRoot: root, comments, replyCommand });
6278
+ await new Promise((resolve, reject) => process.stdout.write(text, (e) => e ? reject(e) : resolve()));
6279
+ if (args.includes("--peek")) return;
6280
+ await markHandedOver(
6281
+ store,
6282
+ comments.map((c) => c.id)
6283
+ );
6284
+ await takeCheckpoint(store, root, root === commonRoot ? void 0 : root, { handoff: true }).catch((e) => {
6285
+ console.error(`warden feedback: no checkpoint taken: ${e instanceof Error ? e.message : e}`);
6286
+ });
6248
6287
  }
6249
6288
  async function reply(args) {
6250
6289
  const [id, ...words] = args;
@@ -6271,7 +6310,7 @@ async function runAgentCommand(argv, replyCommand) {
6271
6310
  }
6272
6311
 
6273
6312
  // bin/cli.ts
6274
- var VERSION = true ? "0.16.1" : "dev";
6313
+ var VERSION = true ? "0.16.3" : "dev";
6275
6314
  function parseArgs(argv) {
6276
6315
  const args = { repoPath: process.cwd(), open: true, updateCheck: true, help: false, version: false };
6277
6316
  for (let i = 0; i < argv.length; i++) {