@xbghc/warden 0.8.0 → 0.9.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 +43 -28
- package/dist/cli.js +45 -37
- package/dist/web/assets/index-BAWRMudD.js +198 -0
- package/dist/web/assets/index-DI521Osx.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +5 -1
- package/dist/web/assets/index-C8TtGAMN.css +0 -1
- package/dist/web/assets/index-CwQ9JHmk.js +0 -198
package/README.md
CHANGED
|
@@ -68,25 +68,29 @@ inside a removed worktree falls back to the working tree on the next load. The c
|
|
|
68
68
|
flags kept under that worktree's key stay in the state file and come back if a worktree is created
|
|
69
69
|
at the same path again.
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
The sidebar is the navigation, and the only navigation: its three tabs — *变更*, *提交*, *Worktree* —
|
|
72
|
+
each fill the left column with the controls for what the middle is showing, and the top bar carries
|
|
73
|
+
none of it. There is no target switcher: the *变更* tab's two blocks *are* the working tree, a commit
|
|
74
|
+
is picked from the *提交* list, and the *审阅整条分支* and *对比两个 ref* forms below that tab's filters
|
|
75
|
+
open the other two. The sidebar names whatever is under review in its block header and puts a
|
|
74
76
|
*返回工作区* link above it.
|
|
75
77
|
|
|
76
78
|
`working`, `staged` and `all` are the three **local views** of one worktree. While any of them is
|
|
77
|
-
selected the sidebar shows two blocks —
|
|
79
|
+
selected the sidebar shows two blocks — 未暂存 (`working`) and 已暂存 (`staged`) — instead of a
|
|
78
80
|
single tree, and clicking a file switches to the view it belongs to. A file that is only partly
|
|
79
|
-
staged appears in both.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
staged appears in both. Above them sits the one figure in the app: how many of the files under review
|
|
82
|
+
are fully staged, which is what "staged means reviewed" amounts to. Switching between the three keeps
|
|
83
|
+
your comments, the draft you are typing and the current selection; only the diff is reloaded. `all`
|
|
84
|
+
has no control of its own (a saved `lastTarget` can still restore it; *审阅整条分支* against `@` shows
|
|
85
|
+
the same diff). Commit, range and `base` targets keep the single tree, and their figure counts 已读
|
|
86
|
+
instead, since nothing can be staged there.
|
|
83
87
|
|
|
84
88
|
`base:<ref>` is for a branch a coding agent has been working on, committing as it goes: one tree with
|
|
85
89
|
everything since the branch forked off `<ref>` — the commits plus whatever is still uncommitted or
|
|
86
90
|
untracked. The diff runs against the merge base, so commits that landed on `<ref>` after the fork are
|
|
87
|
-
not listed as reverted (which is what a plain `git diff <ref>` would do). The
|
|
88
|
-
|
|
89
|
-
review, otherwise `main` or `master` — and takes any ref.
|
|
91
|
+
not listed as reverted (which is what a plain `git diff <ref>` would do). The *相对于* field under
|
|
92
|
+
*审阅整条分支* suggests the base for you — the main worktree's branch when a sibling worktree is under
|
|
93
|
+
review, otherwise `main` or `master` — and takes any ref. Under it the form names the fork point (the
|
|
90
94
|
merge base with that ref, `GET /api/fork-point`) with how many commits the branch is ahead of it and how
|
|
91
95
|
many landed on the base since, and the list marks that commit with a *分叉自* chip and a rule above it:
|
|
92
96
|
what sits above is the branch's own work. Unlike the three local views, `base` keeps its own
|
|
@@ -96,10 +100,10 @@ the fix and delete or re-attach it.
|
|
|
96
100
|
|
|
97
101
|
## Worktrees
|
|
98
102
|
|
|
99
|
-
Agents do their best work each in a worktree of its own, and the *
|
|
103
|
+
Agents do their best work each in a worktree of its own, and the sidebar's *Worktree* tab is
|
|
100
104
|
where those are made and taken down without a trip to the terminal:
|
|
101
105
|
|
|
102
|
-
- *新建 worktree
|
|
106
|
+
- *新建 worktree*, the form in the sidebar, takes a branch and a base. A name that is not a branch yet becomes one from the
|
|
103
107
|
base (`main` or `master` unless you say otherwise; any ref goes) — `git worktree add -b <branch>
|
|
104
108
|
<path> <base>`; an existing branch is checked out as it is, unless another worktree already has it.
|
|
105
109
|
The path is suggested as a sibling of the main worktree named `<repo>-<branch>` (slashes become
|
|
@@ -108,11 +112,12 @@ where those are made and taken down without a trip to the terminal:
|
|
|
108
112
|
- Each row names the checkout, its branch, and whether it is clean or how many paths `git status`
|
|
109
113
|
reports. *查看* switches the review to it (the kind of target carries over, as with the selector
|
|
110
114
|
in the top bar), *复制路径* is for the agent's prompt.
|
|
111
|
-
- *删除*
|
|
112
|
-
changes is not removed until you confirm
|
|
113
|
-
else git refuses without `--force` is put to you the same way.
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
- *删除* always asks first, in the row it would remove, and runs `git worktree remove` without
|
|
116
|
+
`--force`: a worktree with uncommitted changes is not removed until you confirm again, since those
|
|
117
|
+
changes go with it, and whatever else git refuses without `--force` is put to you the same way.
|
|
118
|
+
*一并删除分支* is offered in that same confirmation, ticked by default, and takes the branch by
|
|
119
|
+
`git branch -d`: one that is not merged is kept and the toast says why. The comments and viewed
|
|
120
|
+
flags kept under the worktree's key stay in the state file, as before.
|
|
116
121
|
- A worktree whose directory was deleted behind git's back is listed struck through; *清理* drops that
|
|
117
122
|
one entry (`git worktree remove` handles it; nothing is pruned wholesale).
|
|
118
123
|
|
|
@@ -125,7 +130,7 @@ where those are made and taken down without a trip to the terminal:
|
|
|
125
130
|
| `n` / `p` | Next / previous hunk |
|
|
126
131
|
| `s` | Stage (Unstaged view) or unstage (Staged view) the picked lines |
|
|
127
132
|
| `Ctrl+Enter` | Save the comment being edited |
|
|
128
|
-
| `Esc` | Drop the picked lines / cancel editing /
|
|
133
|
+
| `Esc` | Drop the picked lines / cancel editing / cancel re-attach mode / shut the rail / back to 变更 |
|
|
129
134
|
|
|
130
135
|
## Staging
|
|
131
136
|
|
|
@@ -156,12 +161,18 @@ Limits, each reported as a plain error rather than a half-applied patch:
|
|
|
156
161
|
|
|
157
162
|
## Comments and export
|
|
158
163
|
|
|
159
|
-
Press the `+` that appears next to a line (drag to cover several lines) and write Markdown.
|
|
164
|
+
Press the `+` that appears next to a line (drag to cover several lines) and write Markdown. What you
|
|
165
|
+
write lands in the right-hand rail, which is shut until it has something to hold: it costs 360px of
|
|
166
|
+
the code column, which is most of it once the diff is side by side on a laptop. Writing a comment,
|
|
167
|
+
focusing one or re-attaching one opens it; so does the *评论* switch at the right of the top bar,
|
|
168
|
+
which carries the count and turns violet while any comment is still waiting to go back to the agent.
|
|
169
|
+
`Esc` shuts it again.
|
|
170
|
+
|
|
160
171
|
Each comment belongs to the `old` or `new` side of the diff and has a status:
|
|
161
172
|
|
|
162
|
-
- `active` — not yet exported
|
|
163
|
-
- `exported` — copied at least once (excluded from "copy all" unless *含已导出* is checked)
|
|
164
|
-
- `orphaned` — the code it referred to no longer exists in the current diff
|
|
173
|
+
- `active` (待导出) — not yet exported
|
|
174
|
+
- `exported` (已导出) — copied at least once (excluded from "copy all" unless *含已导出* is checked)
|
|
175
|
+
- `orphaned` (已失联) — the code it referred to no longer exists in the current diff
|
|
165
176
|
|
|
166
177
|
"复制评论" copies every active comment of the current target; each comment also has a "复制此条" button.
|
|
167
178
|
The clipboard format is fixed so an agent can read it directly:
|
|
@@ -211,7 +222,7 @@ them deletes on a moved HEAD.
|
|
|
211
222
|
Comment markers in the diff belong to one view; the rail's *全部* tab lists the whole pool and
|
|
212
223
|
*此文件* lists every comment on the open file regardless of which view it currently sits in.
|
|
213
224
|
|
|
214
|
-
|
|
225
|
+
*已读* is per view, so a half-staged file can be marked read on one side and not the other. It is
|
|
215
226
|
bound to a hash of the file's diff; when the diff changes the flag is dropped and the file is marked
|
|
216
227
|
*已变化*.
|
|
217
228
|
|
|
@@ -229,9 +240,9 @@ refreshes by hand.
|
|
|
229
240
|
|
|
230
241
|
Notes attached to a branch rather than to a line of code, for the things you notice while reviewing
|
|
231
242
|
that do not belong in a comment. They are not deleted when you commit. They sit in the right-hand
|
|
232
|
-
rail next to the comments, under the
|
|
243
|
+
rail next to the comments, under the *待办* tab, as a task list that works the way Google Tasks does:
|
|
233
244
|
|
|
234
|
-
-
|
|
245
|
+
- *添加待办* opens an empty row at the top; type the title and press Enter, and the next row opens
|
|
235
246
|
right under it. Esc or Backspace on an empty row drops it.
|
|
236
247
|
- Titles and details are edited where they are: click into the text. Enter at the end of a title
|
|
237
248
|
starts the next todo under it; Backspace on an emptied title deletes the todo. The details field
|
|
@@ -244,10 +255,10 @@ rail next to the comments, under the *Todo* tab, as a task list that works the w
|
|
|
244
255
|
- The picker at the top is the list selector: the current branch, any other branch that has todos,
|
|
245
256
|
or all of them with the branch on each row. A new todo goes to the branch being shown.
|
|
246
257
|
|
|
247
|
-
Issues (the
|
|
258
|
+
Issues (the rail's third tab) are the same list: closing an issue is ticking it, a
|
|
248
259
|
*已关闭* section holds the closed ones, and the comments linked to an issue sit under its open row
|
|
249
260
|
like subtasks, each with *跳转* and *解除关联*. Tick comments in the rail and the *创建 Issue* button
|
|
250
|
-
|
|
261
|
+
switches to that tab with the add row ready and the comments attached on creation; for an existing
|
|
251
262
|
issue, open its row and use *关联选中的评论*.
|
|
252
263
|
|
|
253
264
|
Each row has its own *复制* button, and there is deliberately no "copy all": a todo is one task to
|
|
@@ -351,3 +362,7 @@ to `main` and every pull request.
|
|
|
351
362
|
## License
|
|
352
363
|
|
|
353
364
|
MIT
|
|
365
|
+
|
|
366
|
+
## Storybook
|
|
367
|
+
|
|
368
|
+
Run `pnpm storybook` for isolated review and layout components at http://127.0.0.1:6006. Stories use in-memory fixtures and include interaction checks. Run `pnpm build:storybook` to build the static preview.
|
package/dist/cli.js
CHANGED
|
@@ -2918,7 +2918,7 @@ function parseTargetKey(key) {
|
|
|
2918
2918
|
if (key.startsWith("worktree:")) {
|
|
2919
2919
|
const rest = key.slice("worktree:".length);
|
|
2920
2920
|
const m = /^(.+):(working|staged|all|commit:.+|range:.+|base:.+)$/.exec(rest);
|
|
2921
|
-
if (!m
|
|
2921
|
+
if (!m?.[1] || !m[2]) throw new TargetKeyError(`invalid worktree target key: ${key}`);
|
|
2922
2922
|
const worktree = m[1];
|
|
2923
2923
|
if (!worktree.startsWith("/")) throw new TargetKeyError(`worktree path must be absolute: ${worktree}`);
|
|
2924
2924
|
return { ...parseSuffix(m[2]), worktree };
|
|
@@ -2968,11 +2968,7 @@ function stageModeFor(t) {
|
|
|
2968
2968
|
function localViewKeys(key) {
|
|
2969
2969
|
const t = tryParseTargetKey(key);
|
|
2970
2970
|
const wt = t?.worktree ? { worktree: t.worktree } : {};
|
|
2971
|
-
return [
|
|
2972
|
-
formatTargetKey({ kind: "working", ...wt }),
|
|
2973
|
-
formatTargetKey({ kind: "staged", ...wt }),
|
|
2974
|
-
formatTargetKey({ kind: "all", ...wt })
|
|
2975
|
-
];
|
|
2971
|
+
return [formatTargetKey({ kind: "working", ...wt }), formatTargetKey({ kind: "staged", ...wt }), formatTargetKey({ kind: "all", ...wt })];
|
|
2976
2972
|
}
|
|
2977
2973
|
|
|
2978
2974
|
// packages/shared/src/order.ts
|
|
@@ -3049,7 +3045,19 @@ function parseCommitLog(stdout) {
|
|
|
3049
3045
|
// packages/server/src/git.ts
|
|
3050
3046
|
import { execFile } from "child_process";
|
|
3051
3047
|
import { existsSync } from "fs";
|
|
3052
|
-
var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
3048
|
+
var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
3049
|
+
"rev-parse",
|
|
3050
|
+
"diff",
|
|
3051
|
+
"show",
|
|
3052
|
+
"log",
|
|
3053
|
+
"worktree",
|
|
3054
|
+
"ls-files",
|
|
3055
|
+
"status",
|
|
3056
|
+
"merge-base",
|
|
3057
|
+
"rev-list",
|
|
3058
|
+
"for-each-ref",
|
|
3059
|
+
"check-ref-format"
|
|
3060
|
+
]);
|
|
3053
3061
|
var FORBIDDEN_OPTION_PREFIXES = ["--output", "--ext-diff", "--textconv", "-c", "--config-env", "--exec-path", "--git-dir", "--work-tree"];
|
|
3054
3062
|
var DEFAULT_GIT_TIMEOUT_MS = 3e4;
|
|
3055
3063
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
@@ -3269,7 +3277,7 @@ async function resolveRepo(dir) {
|
|
|
3269
3277
|
try {
|
|
3270
3278
|
const r = await runGit(["rev-parse", "--show-toplevel"], { cwd: dir });
|
|
3271
3279
|
top = r.stdout.trim();
|
|
3272
|
-
} catch
|
|
3280
|
+
} catch {
|
|
3273
3281
|
throw new HttpError(400, `${dir} is not inside a git repository`, "not_a_repo");
|
|
3274
3282
|
}
|
|
3275
3283
|
if (!top) throw new HttpError(400, `${dir} is not inside a git worktree (bare repository?)`, "not_a_repo");
|
|
@@ -3284,7 +3292,7 @@ async function listWorktreesAll(ctx) {
|
|
|
3284
3292
|
const out = [];
|
|
3285
3293
|
let cur = null;
|
|
3286
3294
|
const flush = () => {
|
|
3287
|
-
if (cur
|
|
3295
|
+
if (cur?.path) {
|
|
3288
3296
|
out.push({
|
|
3289
3297
|
path: cur.path,
|
|
3290
3298
|
head: cur.head ?? "",
|
|
@@ -3306,7 +3314,6 @@ async function listWorktreesAll(ctx) {
|
|
|
3306
3314
|
flush();
|
|
3307
3315
|
cur = { path: line.slice("worktree ".length) };
|
|
3308
3316
|
} else if (!cur) {
|
|
3309
|
-
continue;
|
|
3310
3317
|
} else if (line.startsWith("HEAD ")) cur.head = line.slice(5);
|
|
3311
3318
|
else if (line.startsWith("branch ")) cur.branch = line.slice(7).replace(/^refs\/heads\//, "");
|
|
3312
3319
|
else if (line === "detached") cur.detached = true;
|
|
@@ -3366,7 +3373,9 @@ function lineHash(content) {
|
|
|
3366
3373
|
return sha1(content.trimEnd());
|
|
3367
3374
|
}
|
|
3368
3375
|
function hunkHash(lineContents) {
|
|
3369
|
-
return sha1(
|
|
3376
|
+
return sha1(
|
|
3377
|
+
lineContents.map((l) => l.trimEnd()).join("\n").trim()
|
|
3378
|
+
);
|
|
3370
3379
|
}
|
|
3371
3380
|
|
|
3372
3381
|
// packages/server/src/diffparse.ts
|
|
@@ -3407,7 +3416,7 @@ function stripPrefix(p, prefix) {
|
|
|
3407
3416
|
function splitGitHeader(rest) {
|
|
3408
3417
|
if (rest.startsWith('"')) {
|
|
3409
3418
|
const m = /^("(?:[^"\\]|\\.)*")\s+("(?:[^"\\]|\\.)*")$/.exec(rest);
|
|
3410
|
-
if (m
|
|
3419
|
+
if (m?.[1] && m[2]) return { a: stripPrefix(m[1], "a/"), b: stripPrefix(m[2], "b/") };
|
|
3411
3420
|
}
|
|
3412
3421
|
for (let i = 0; i < rest.length; i++) {
|
|
3413
3422
|
if (rest[i] !== " ") continue;
|
|
@@ -3564,7 +3573,6 @@ function parseUnifiedDiff(text) {
|
|
|
3564
3573
|
if (line.startsWith("Binary files ") || line === "GIT binary patch") {
|
|
3565
3574
|
c.binary = true;
|
|
3566
3575
|
c.raw.push(line);
|
|
3567
|
-
continue;
|
|
3568
3576
|
}
|
|
3569
3577
|
}
|
|
3570
3578
|
flush();
|
|
@@ -3888,12 +3896,13 @@ function pickedLines(diff, selection) {
|
|
|
3888
3896
|
const picked = /* @__PURE__ */ new Map();
|
|
3889
3897
|
const changed = (h) => h.lines.map((l, i) => l.type === "context" ? -1 : i).filter((i) => i >= 0);
|
|
3890
3898
|
if (selection === void 0) {
|
|
3891
|
-
diff.hunks.
|
|
3899
|
+
for (const [i, h] of diff.hunks.entries()) picked.set(i, new Set(changed(h)));
|
|
3892
3900
|
return picked;
|
|
3893
3901
|
}
|
|
3894
3902
|
if (!Array.isArray(selection)) throw badRequest("hunks must be an array", "bad_selection");
|
|
3895
3903
|
for (const sel of selection) {
|
|
3896
|
-
if (!sel || !Number.isInteger(sel.index) || sel.index < 0 || sel.index >= diff.hunks.length)
|
|
3904
|
+
if (!sel || !Number.isInteger(sel.index) || sel.index < 0 || sel.index >= diff.hunks.length)
|
|
3905
|
+
throw badRequest(`no such hunk: ${sel?.index}`, "bad_selection");
|
|
3897
3906
|
const hunk = diff.hunks[sel.index];
|
|
3898
3907
|
const set = picked.get(sel.index) ?? /* @__PURE__ */ new Set();
|
|
3899
3908
|
picked.set(sel.index, set);
|
|
@@ -4014,13 +4023,11 @@ async function assertNewWorktreePath(ctx, worktrees, p) {
|
|
|
4014
4023
|
for (const w of worktrees) {
|
|
4015
4024
|
if (target === w.path || target.startsWith(w.path + path3.sep)) throw badRequest(`${target} is inside the worktree at ${w.path}`, "invalid_path");
|
|
4016
4025
|
}
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
st = await stat(target);
|
|
4020
|
-
} catch (e) {
|
|
4021
|
-
if (e.code === "ENOENT") return target;
|
|
4026
|
+
const st = await stat(target).catch((e) => {
|
|
4027
|
+
if (e.code === "ENOENT") return void 0;
|
|
4022
4028
|
throw e;
|
|
4023
|
-
}
|
|
4029
|
+
});
|
|
4030
|
+
if (!st) return target;
|
|
4024
4031
|
if (!st.isDirectory() || (await readdir(target)).length > 0) throw badRequest(`${target} already exists`, "path_exists");
|
|
4025
4032
|
return target;
|
|
4026
4033
|
}
|
|
@@ -4093,7 +4100,7 @@ import path4 from "path";
|
|
|
4093
4100
|
import { mkdir, open, readFile as readFile2, rename, stat as stat2, unlink, writeFile } from "fs/promises";
|
|
4094
4101
|
function dataDir() {
|
|
4095
4102
|
const xdg = process.env.XDG_DATA_HOME;
|
|
4096
|
-
const base = xdg
|
|
4103
|
+
const base = xdg?.trim() ? xdg : path4.join(os.homedir(), ".local", "share");
|
|
4097
4104
|
return path4.join(base, "warden");
|
|
4098
4105
|
}
|
|
4099
4106
|
function repoHash(repoRoot) {
|
|
@@ -4103,7 +4110,7 @@ function stateFilePath(repoRoot, baseDir = dataDir()) {
|
|
|
4103
4110
|
return path4.join(baseDir, repoHash(repoRoot), "state.json");
|
|
4104
4111
|
}
|
|
4105
4112
|
function defaultPrefs() {
|
|
4106
|
-
return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true };
|
|
4113
|
+
return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false };
|
|
4107
4114
|
}
|
|
4108
4115
|
function defaultState(repoRoot) {
|
|
4109
4116
|
return { schemaVersion: 1, repoRoot, targets: {}, issues: [], todos: [], prefs: defaultPrefs() };
|
|
@@ -4143,7 +4150,8 @@ function normalise(raw2, repoRoot) {
|
|
|
4143
4150
|
...defaultPrefs(),
|
|
4144
4151
|
...r.prefs ?? {},
|
|
4145
4152
|
nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {},
|
|
4146
|
-
autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true
|
|
4153
|
+
autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true,
|
|
4154
|
+
railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false
|
|
4147
4155
|
}
|
|
4148
4156
|
};
|
|
4149
4157
|
}
|
|
@@ -4294,7 +4302,7 @@ var LANG_BY_EXT = {
|
|
|
4294
4302
|
};
|
|
4295
4303
|
function langForPath(filePath) {
|
|
4296
4304
|
const m = /\.([A-Za-z0-9]+)$/.exec(filePath);
|
|
4297
|
-
if (!m
|
|
4305
|
+
if (!m?.[1]) return "";
|
|
4298
4306
|
return LANG_BY_EXT[m[1].toLowerCase()] ?? m[1].toLowerCase();
|
|
4299
4307
|
}
|
|
4300
4308
|
function formatCommentSection(c) {
|
|
@@ -4315,13 +4323,7 @@ function formatCommentsExport({ repoRoot, comments }) {
|
|
|
4315
4323
|
}
|
|
4316
4324
|
function formatIssueExport({ repoRoot, issue, comments }) {
|
|
4317
4325
|
const targets = [...new Set(comments.map((c) => c.targetKey))];
|
|
4318
|
-
const head = [
|
|
4319
|
-
`# Issue: ${issue.title}`,
|
|
4320
|
-
`Status: ${issue.status}`,
|
|
4321
|
-
`Target: ${targets.join(", ") || "-"}`,
|
|
4322
|
-
`Repo: ${repoRoot}`,
|
|
4323
|
-
`Count: ${comments.length}`
|
|
4324
|
-
];
|
|
4326
|
+
const head = [`# Issue: ${issue.title}`, `Status: ${issue.status}`, `Target: ${targets.join(", ") || "-"}`, `Repo: ${repoRoot}`, `Count: ${comments.length}`];
|
|
4325
4327
|
const parts = [head.join("\n")];
|
|
4326
4328
|
if (issue.body.trim()) parts.push(issue.body.trim());
|
|
4327
4329
|
parts.push(...comments.map(formatCommentSection));
|
|
@@ -4682,7 +4684,11 @@ function createApp(opts) {
|
|
|
4682
4684
|
const target = last ? tryParseTargetKey(last) : void 0;
|
|
4683
4685
|
const usable2 = !!target && (!target.worktree || info.worktrees.some((w) => w.path === target.worktree));
|
|
4684
4686
|
if (last && usable2) info.defaultTarget = last;
|
|
4685
|
-
else if (last)
|
|
4687
|
+
else if (last) {
|
|
4688
|
+
await store.update((s) => {
|
|
4689
|
+
s.prefs.lastTarget = "working";
|
|
4690
|
+
});
|
|
4691
|
+
}
|
|
4686
4692
|
return c.json(info);
|
|
4687
4693
|
});
|
|
4688
4694
|
api.get("/state", async (c) => c.json(await store.load()));
|
|
@@ -4692,6 +4698,7 @@ function createApp(opts) {
|
|
|
4692
4698
|
if (body.viewMode === "unified" || body.viewMode === "split") s.prefs.viewMode = body.viewMode;
|
|
4693
4699
|
if (typeof body.lastTarget === "string") s.prefs.lastTarget = body.lastTarget;
|
|
4694
4700
|
if (typeof body.autoRefresh === "boolean") s.prefs.autoRefresh = body.autoRefresh;
|
|
4701
|
+
if (typeof body.railOpen === "boolean") s.prefs.railOpen = body.railOpen;
|
|
4695
4702
|
if (body.nvimSocketByRoot && typeof body.nvimSocketByRoot === "object") {
|
|
4696
4703
|
s.prefs.nvimSocketByRoot = { ...s.prefs.nvimSocketByRoot, ...body.nvimSocketByRoot };
|
|
4697
4704
|
}
|
|
@@ -4781,7 +4788,8 @@ function createApp(opts) {
|
|
|
4781
4788
|
if (body.hunks !== void 0 && !Array.isArray(body.hunks)) throw badRequest("hunks must be an array", "bad_selection");
|
|
4782
4789
|
const diff = await fileDiffWithHints(ctx, body.path);
|
|
4783
4790
|
if (!diff) throw badRequest(`file ${body.path} is not part of ${key}`, "no_diff");
|
|
4784
|
-
if (diff.contentHash !== body.contentHash)
|
|
4791
|
+
if (diff.contentHash !== body.contentHash)
|
|
4792
|
+
throw new HttpError(409, "the diff changed since it was loaded; refresh and pick the lines again", "diff_changed");
|
|
4785
4793
|
const { patch, lines } = buildStagePatch(diff, mode, body.hunks);
|
|
4786
4794
|
await applyToIndex(ctx.cwd, patch, { reverse: mode === "unstage" });
|
|
4787
4795
|
void watchers.get(ctx.cwd)?.poll();
|
|
@@ -5004,7 +5012,7 @@ function createApp(opts) {
|
|
|
5004
5012
|
});
|
|
5005
5013
|
api.post("/issues", async (c) => {
|
|
5006
5014
|
const body = await c.req.json();
|
|
5007
|
-
if (!body.title
|
|
5015
|
+
if (!body.title?.trim()) throw badRequest("title is required");
|
|
5008
5016
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5009
5017
|
const issue = {
|
|
5010
5018
|
id: randomUUID(),
|
|
@@ -5097,7 +5105,7 @@ function createApp(opts) {
|
|
|
5097
5105
|
});
|
|
5098
5106
|
api.post("/todos", async (c) => {
|
|
5099
5107
|
const body = await c.req.json();
|
|
5100
|
-
if (!body.title
|
|
5108
|
+
if (!body.title?.trim()) throw badRequest("title is required");
|
|
5101
5109
|
const branch = body.branch?.trim() || await currentBranch(await knownRoot(body.root));
|
|
5102
5110
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5103
5111
|
const todo = {
|
|
@@ -5406,7 +5414,7 @@ async function startServer(opts) {
|
|
|
5406
5414
|
}
|
|
5407
5415
|
|
|
5408
5416
|
// bin/cli.ts
|
|
5409
|
-
var VERSION = true ? "0.
|
|
5417
|
+
var VERSION = true ? "0.9.0" : "dev";
|
|
5410
5418
|
function parseArgs(argv) {
|
|
5411
5419
|
const args = { repoPath: process.cwd(), open: true, help: false, version: false };
|
|
5412
5420
|
for (let i = 0; i < argv.length; i++) {
|