reslop 0.3.1 → 0.3.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/CHANGELOG.md +28 -1
- package/README.md +30 -4
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +274 -438
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -14
- package/lib/agents/models.js +250 -383
- package/lib/agents/registry.js +437 -0
- package/lib/agents/run.js +234 -0
- package/lib/agents/script.js +93 -0
- package/lib/agents/transcript.js +143 -0
- package/lib/cli.js +2 -1
- package/lib/common/process.js +26 -1
- package/lib/dashboard/git.js +11 -1
- package/lib/dashboard/reports.js +80 -0
- package/lib/dashboard/watch.js +43 -8
- package/lib/dashboard/workspace.js +19 -20
- package/lib/dashboard/worktrees.js +145 -0
- package/lib/git/dir.js +50 -0
- package/lib/git/linked.js +152 -0
- package/lib/git/local.js +179 -0
- package/lib/git/status.js +126 -0
- package/lib/git/worktree.js +113 -36
- package/lib/input/actions.js +83 -5
- package/lib/input/keys.js +30 -1
- package/lib/input/line-edit.js +5 -1
- package/lib/render/agents.js +84 -34
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -6
- package/lib/render/frame.js +16 -2
- package/lib/render/menu.js +103 -6
- package/lib/render/name-edit.js +31 -0
- package/lib/render/repos.js +52 -8
- package/lib/render/status.js +22 -4
- package/lib/report/run.js +16 -23
- package/lib/review/agent-instructions.md +2 -2
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +220 -205
- package/lib/session/agents/away.js +48 -0
- package/lib/session/agents/job.js +23 -86
- package/lib/session/agents/jobs.js +14 -27
- package/lib/session/agents/options.js +45 -101
- package/lib/session/agents/plans.js +53 -0
- package/lib/session/agents/view.js +34 -0
- package/lib/session/branches.js +18 -1
- package/lib/session/compose.js +13 -5
- package/lib/session/dispatch.js +43 -4
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/lifecycle.js +67 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +2 -1
- package/lib/session/npm/npm.js +89 -12
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +15 -2
- package/lib/session/terminal.js +2 -2
- package/lib/session/text-menu.js +50 -0
- package/lib/session/view.js +19 -2
- package/lib/session/workspace.js +179 -38
- package/lib/session/worktrees.js +280 -0
- package/lib/term/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
package/lib/session/workspace.js
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('node:fs');
|
|
3
4
|
const path = require('node:path');
|
|
4
5
|
const { REVIEW_DIR } = require('../common/files.js');
|
|
5
6
|
const { FileIndex, HEAT_MS } = require('../dashboard/tree.js');
|
|
6
7
|
const { UNKNOWN_PATH } = require('../dashboard/watch.js');
|
|
7
8
|
const workspace = require('../dashboard/workspace.js');
|
|
8
9
|
const { emptyDiff, countCommits, readDiffStat, taskStats, pickRun } = workspace;
|
|
10
|
+
const { currentBranchName } = workspace;
|
|
9
11
|
const { readNpmSummary } = require('../dashboard/npm.js');
|
|
10
|
-
const {
|
|
12
|
+
const { reportCounts } = require('../dashboard/reports.js');
|
|
13
|
+
const { dirKey, changedFiles } = require('../git/linked.js');
|
|
14
|
+
const { resolveGitDirs } = require('../git/dir.js');
|
|
11
15
|
|
|
12
16
|
const TICK_ID = 'repos';
|
|
13
|
-
const HEAT_TIMER = 'repos-heat';
|
|
14
17
|
|
|
15
18
|
const freshBag = () => ({
|
|
16
19
|
files: [],
|
|
17
20
|
git: false,
|
|
21
|
+
diff: false,
|
|
18
22
|
runs: false,
|
|
19
23
|
tasks: false,
|
|
20
24
|
npm: false,
|
|
@@ -24,6 +28,7 @@ const freshBag = () => ({
|
|
|
24
28
|
const fullBag = () => ({
|
|
25
29
|
files: [UNKNOWN_PATH],
|
|
26
30
|
git: true,
|
|
31
|
+
diff: true,
|
|
27
32
|
runs: true,
|
|
28
33
|
tasks: true,
|
|
29
34
|
npm: true,
|
|
@@ -35,6 +40,7 @@ const mergeBag = (left, right) => {
|
|
|
35
40
|
return {
|
|
36
41
|
files: [...base.files, ...right.files],
|
|
37
42
|
git: base.git || right.git,
|
|
43
|
+
diff: base.diff || right.diff,
|
|
38
44
|
runs: base.runs || right.runs,
|
|
39
45
|
tasks: base.tasks || right.tasks,
|
|
40
46
|
npm: base.npm || right.npm,
|
|
@@ -42,9 +48,37 @@ const mergeBag = (left, right) => {
|
|
|
42
48
|
};
|
|
43
49
|
};
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
// The path inside of a directory, or null for one outside of it
|
|
52
|
+
const insideOf = (base, full) => {
|
|
53
|
+
const rel = path.relative(base, full);
|
|
54
|
+
const out = rel === '..' || rel.startsWith(`..${path.sep}`);
|
|
55
|
+
if (out || path.isAbsolute(rel)) return null;
|
|
56
|
+
return rel.split(path.sep).join('/');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const gitDirKey = (dir) => {
|
|
60
|
+
const dirs = resolveGitDirs(dir);
|
|
61
|
+
return dirs ? dirKey(dirs.gitDir) : '';
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// What a list of worktrees says of a repository
|
|
65
|
+
const rowMeta = (repo) => ({
|
|
46
66
|
name: repo.name,
|
|
67
|
+
group: repo.group ?? repo.name,
|
|
68
|
+
main: repo.main ?? true,
|
|
69
|
+
linked: repo.linked ?? false,
|
|
70
|
+
branch: repo.branch ?? '',
|
|
71
|
+
head: repo.head ?? '',
|
|
72
|
+
detached: repo.detached ?? false,
|
|
73
|
+
locked: repo.locked ?? false,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const blankRow = (repo) => ({
|
|
77
|
+
...rowMeta(repo),
|
|
47
78
|
dir: repo.dir,
|
|
79
|
+
key: dirKey(repo.dir),
|
|
80
|
+
gitDir: gitDirKey(repo.dir),
|
|
81
|
+
dirty: false,
|
|
48
82
|
index: new FileIndex(repo.dir),
|
|
49
83
|
bytes: 0,
|
|
50
84
|
exts: [],
|
|
@@ -78,12 +112,23 @@ const readFiles = async (row, bag) => {
|
|
|
78
112
|
};
|
|
79
113
|
|
|
80
114
|
const readCommits = async (row) => {
|
|
81
|
-
|
|
115
|
+
const [commits, branch] = await Promise.all([
|
|
116
|
+
countCommits(row.dir),
|
|
117
|
+
currentBranchName(row.dir),
|
|
118
|
+
]);
|
|
119
|
+
row.commits = commits;
|
|
120
|
+
row.branch = branch;
|
|
121
|
+
row.detached = !branch;
|
|
82
122
|
row.commitsReady = true;
|
|
83
123
|
};
|
|
84
124
|
|
|
85
125
|
const readDiff = async (row) => {
|
|
86
|
-
|
|
126
|
+
const [diff, changed] = await Promise.all([
|
|
127
|
+
readDiffStat(row.dir),
|
|
128
|
+
changedFiles(row.dir).catch(() => []),
|
|
129
|
+
]);
|
|
130
|
+
row.diff = diff;
|
|
131
|
+
row.dirty = changed.length > 0;
|
|
87
132
|
row.diffReady = true;
|
|
88
133
|
};
|
|
89
134
|
|
|
@@ -99,24 +144,26 @@ const readRuns = (row) => {
|
|
|
99
144
|
row.runsReady = true;
|
|
100
145
|
};
|
|
101
146
|
|
|
102
|
-
const reportCount = async (dir, kind) => {
|
|
103
|
-
const report = await collectReport(dir, kind);
|
|
104
|
-
return report ? report.size : 0;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
147
|
const readNpm = async (row) => {
|
|
108
|
-
const summary = await
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
reportCount(row.dir, 'outdated'),
|
|
148
|
+
const [summary, counts] = await Promise.all([
|
|
149
|
+
readNpmSummary(row.dir, row.npm),
|
|
150
|
+
reportCounts(row.dir),
|
|
112
151
|
]);
|
|
113
|
-
row.npm = { ...summary,
|
|
152
|
+
row.npm = { ...summary, ...counts };
|
|
114
153
|
row.npmReady = true;
|
|
115
154
|
};
|
|
116
155
|
|
|
117
|
-
const toTile = (row) => ({
|
|
156
|
+
const toTile = (row, live) => ({
|
|
118
157
|
id: row.name,
|
|
119
158
|
name: row.name,
|
|
159
|
+
group: row.group,
|
|
160
|
+
linked: row.linked,
|
|
161
|
+
branch: row.branch,
|
|
162
|
+
detached: row.detached,
|
|
163
|
+
head: row.head,
|
|
164
|
+
locked: row.locked,
|
|
165
|
+
dirty: row.dirty,
|
|
166
|
+
agents: live.get(row.key) ?? 0,
|
|
120
167
|
bytes: row.bytes,
|
|
121
168
|
exts: row.exts,
|
|
122
169
|
tasks: row.tasks,
|
|
@@ -171,13 +218,20 @@ const classifyRest = (rest) => {
|
|
|
171
218
|
return bag;
|
|
172
219
|
}
|
|
173
220
|
bag.files.push(rest);
|
|
221
|
+
bag.diff = true;
|
|
174
222
|
bag.activity = true;
|
|
175
223
|
return bag;
|
|
176
224
|
};
|
|
177
225
|
|
|
226
|
+
// The tiles of a folder on the repos screen, and the ones of the worktrees
|
|
227
|
+
// of a repository above the branches
|
|
178
228
|
class Workspace {
|
|
179
|
-
constructor(ui) {
|
|
229
|
+
constructor(ui, options = {}) {
|
|
180
230
|
this.ui = ui;
|
|
231
|
+
this.pane = options.pane ?? 'repos';
|
|
232
|
+
this.tick = this.pane === 'repos' ? TICK_ID : `${TICK_ID}-${this.pane}`;
|
|
233
|
+
this.heat = `${this.tick}-heat`;
|
|
234
|
+
this.infoOf = options.infoOf ?? (() => this.ui.workspaceInfo);
|
|
181
235
|
this.epoch = 0;
|
|
182
236
|
this.reset();
|
|
183
237
|
}
|
|
@@ -190,14 +244,46 @@ class Workspace {
|
|
|
190
244
|
this.cols = 1;
|
|
191
245
|
}
|
|
192
246
|
|
|
247
|
+
// Whether the tiles are on the screen
|
|
248
|
+
shown() {
|
|
249
|
+
return this.ui.uiOpen && this.ui.nav.pane === this.pane;
|
|
250
|
+
}
|
|
251
|
+
|
|
193
252
|
seed() {
|
|
194
|
-
const info = this.
|
|
253
|
+
const info = this.infoOf();
|
|
195
254
|
const repos = info ? info.repos : [];
|
|
196
255
|
this.rows = repos.map(blankRow);
|
|
197
256
|
this.byName = new Map(this.rows.map((row) => [row.name, row]));
|
|
198
257
|
if (this.cursor >= this.rows.length) this.cursor = 0;
|
|
199
258
|
}
|
|
200
259
|
|
|
260
|
+
// The list of worktrees changed: what is known of the rest is kept
|
|
261
|
+
adopt(repos) {
|
|
262
|
+
const focus = this.rows[this.cursor] ? this.rows[this.cursor].key : '';
|
|
263
|
+
const known = new Map(this.rows.map((row) => [row.key, row]));
|
|
264
|
+
const fresh = [];
|
|
265
|
+
this.rows = repos.map((repo) => {
|
|
266
|
+
const prev = known.get(dirKey(repo.dir));
|
|
267
|
+
if (!prev) {
|
|
268
|
+
const row = blankRow(repo);
|
|
269
|
+
fresh.push(row);
|
|
270
|
+
return row;
|
|
271
|
+
}
|
|
272
|
+
return Object.assign(prev, rowMeta(repo));
|
|
273
|
+
});
|
|
274
|
+
this.byName = new Map(this.rows.map((row) => [row.name, row]));
|
|
275
|
+
const at = this.rows.findIndex((row) => row.key === focus);
|
|
276
|
+
this.cursor = at < 0 ? Math.min(this.cursor, this.rows.length - 1) : at;
|
|
277
|
+
this.cursor = Math.max(0, this.cursor);
|
|
278
|
+
for (const row of fresh) this.kick(row, fullBag());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
focusDir(dir) {
|
|
282
|
+
const key = dirKey(dir);
|
|
283
|
+
const at = this.rows.findIndex((row) => row.key === key);
|
|
284
|
+
if (at >= 0) this.cursor = at;
|
|
285
|
+
}
|
|
286
|
+
|
|
201
287
|
stale(epoch) {
|
|
202
288
|
return epoch !== this.epoch || !this.active;
|
|
203
289
|
}
|
|
@@ -205,7 +291,7 @@ class Workspace {
|
|
|
205
291
|
publish(epoch) {
|
|
206
292
|
if (this.stale(epoch)) return;
|
|
207
293
|
this.afterDraw();
|
|
208
|
-
if (this.
|
|
294
|
+
if (this.shown()) this.ui.paint();
|
|
209
295
|
}
|
|
210
296
|
|
|
211
297
|
async settle(epoch, read) {
|
|
@@ -219,8 +305,8 @@ class Workspace {
|
|
|
219
305
|
if (bag.files.length) {
|
|
220
306
|
jobs.push(this.settle(epoch, () => readFiles(row, bag)));
|
|
221
307
|
}
|
|
222
|
-
if (bag.git)
|
|
223
|
-
|
|
308
|
+
if (bag.git) jobs.push(this.settle(epoch, () => readCommits(row)));
|
|
309
|
+
if (bag.git || bag.diff) {
|
|
224
310
|
jobs.push(this.settle(epoch, () => readDiff(row)));
|
|
225
311
|
}
|
|
226
312
|
if (bag.tasks) jobs.push(this.settle(epoch, () => readTasks(row)));
|
|
@@ -242,7 +328,7 @@ class Workspace {
|
|
|
242
328
|
}
|
|
243
329
|
if (this.stale(epoch)) break;
|
|
244
330
|
this.afterDraw();
|
|
245
|
-
if (this.
|
|
331
|
+
if (this.shown()) this.ui.paint();
|
|
246
332
|
}
|
|
247
333
|
row.running = false;
|
|
248
334
|
if (row.pending && this.active) {
|
|
@@ -250,7 +336,7 @@ class Workspace {
|
|
|
250
336
|
return;
|
|
251
337
|
}
|
|
252
338
|
this.afterDraw();
|
|
253
|
-
if (this.
|
|
339
|
+
if (this.shown()) this.ui.paint();
|
|
254
340
|
}
|
|
255
341
|
|
|
256
342
|
kick(row, bag) {
|
|
@@ -265,24 +351,65 @@ class Workspace {
|
|
|
265
351
|
for (const row of this.rows) this.kick(row, fullBag());
|
|
266
352
|
}
|
|
267
353
|
|
|
354
|
+
// Everything is read again, but npm only where there is no answer yet:
|
|
355
|
+
// the lists of packages change when their files do, and the watcher says so
|
|
356
|
+
refresh() {
|
|
357
|
+
for (const row of this.rows) {
|
|
358
|
+
this.kick(row, { ...fullBag(), npm: !row.npmReady });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// The worktree that owns a path of the watcher, and the path inside it.
|
|
363
|
+
// A worktree can be anywhere, and the state of git of a linked one is
|
|
364
|
+
// in the directory of the main one
|
|
365
|
+
placeOf(rel, root) {
|
|
366
|
+
const full = path.resolve(root, rel);
|
|
367
|
+
let best = null;
|
|
368
|
+
for (const row of this.rows) {
|
|
369
|
+
for (const [base, lead] of [
|
|
370
|
+
[row.key, ''],
|
|
371
|
+
[row.gitDir, '.git/'],
|
|
372
|
+
]) {
|
|
373
|
+
const inside = base ? insideOf(base, full) : null;
|
|
374
|
+
if (inside === null) continue;
|
|
375
|
+
if (best && base.length <= best.size) continue;
|
|
376
|
+
best = { row, rest: `${lead}${inside}`, size: base.length };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return best ?? this.placeByName(rel);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
placeByName(rel) {
|
|
383
|
+
const slash = rel.indexOf('/');
|
|
384
|
+
const name = slash < 0 ? rel : rel.slice(0, slash);
|
|
385
|
+
const row = this.byName.get(name);
|
|
386
|
+
return row ? { row, rest: slash < 0 ? '' : rel.slice(slash + 1) } : null;
|
|
387
|
+
}
|
|
388
|
+
|
|
268
389
|
notePaths(paths) {
|
|
269
390
|
if (!this.active) return;
|
|
391
|
+
const missing = this.rows.some((row) => !fs.existsSync(row.dir));
|
|
392
|
+
if (missing) {
|
|
393
|
+
this.ui.ops.hold(this.ui.worktrees.reload());
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
270
396
|
for (const rel of paths) {
|
|
271
397
|
if (rel === UNKNOWN_PATH) return void this.requestAll();
|
|
272
398
|
}
|
|
273
399
|
const found = new Map();
|
|
400
|
+
const info = this.infoOf();
|
|
401
|
+
const root = dirKey(info ? info.root : '.');
|
|
274
402
|
for (const rel of paths) {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
if (!row) continue;
|
|
403
|
+
const place = this.placeOf(rel, root);
|
|
404
|
+
if (!place) continue;
|
|
405
|
+
const { row, rest } = place;
|
|
406
|
+
const name = row.name;
|
|
280
407
|
found.set(name, mergeBag(found.get(name), classifyRest(rest)));
|
|
281
408
|
}
|
|
282
409
|
for (const [name, bag] of found) this.kick(this.byName.get(name), bag);
|
|
283
410
|
if (found.size) {
|
|
284
411
|
this.afterDraw();
|
|
285
|
-
if (this.
|
|
412
|
+
if (this.shown()) this.ui.paint();
|
|
286
413
|
}
|
|
287
414
|
}
|
|
288
415
|
|
|
@@ -302,8 +429,8 @@ class Workspace {
|
|
|
302
429
|
cancelTimers() {
|
|
303
430
|
const { progress, term } = this.ui;
|
|
304
431
|
if (!progress || !term) return;
|
|
305
|
-
progress.stop(
|
|
306
|
-
term.cancel(
|
|
432
|
+
progress.stop(this.tick);
|
|
433
|
+
term.cancel(this.heat);
|
|
307
434
|
}
|
|
308
435
|
|
|
309
436
|
hotUntil() {
|
|
@@ -316,32 +443,46 @@ class Workspace {
|
|
|
316
443
|
|
|
317
444
|
afterDraw() {
|
|
318
445
|
if (!this.active) return;
|
|
319
|
-
|
|
320
|
-
if (!visible) return void this.cancelTimers();
|
|
446
|
+
if (!this.shown()) return void this.cancelTimers();
|
|
321
447
|
const now = Date.now();
|
|
322
448
|
const until = this.hotUntil();
|
|
323
449
|
const hot = now < until;
|
|
324
450
|
const running = this.rows.some((row) => row.run?.status === 'running');
|
|
325
451
|
const loading = this.rows.some((row) => rowBusy(row));
|
|
326
452
|
const { progress, term } = this.ui;
|
|
327
|
-
if (running || hot || loading) progress.start(
|
|
328
|
-
else progress.stop(
|
|
453
|
+
if (running || hot || loading) progress.start(this.tick);
|
|
454
|
+
else progress.stop(this.tick);
|
|
329
455
|
if (!hot) return;
|
|
330
456
|
term.later(
|
|
331
|
-
|
|
457
|
+
this.heat,
|
|
332
458
|
() => {
|
|
333
|
-
if (this.ui.nav.pane ===
|
|
459
|
+
if (this.ui.nav.pane === this.pane) this.ui.paint();
|
|
334
460
|
},
|
|
335
461
|
until - now + 20,
|
|
336
462
|
);
|
|
337
463
|
}
|
|
338
464
|
|
|
465
|
+
liveAgents() {
|
|
466
|
+
const live = new Map();
|
|
467
|
+
const running = this.ui.agents ? this.ui.agents.liveByRoot() : new Map();
|
|
468
|
+
for (const [root, count] of running) live.set(dirKey(root), count);
|
|
469
|
+
return live;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// The index of the worktree that is shown, -1 for none of the tiles
|
|
473
|
+
currentIndex() {
|
|
474
|
+
const key = dirKey(this.ui.top || this.ui.cwd || '.');
|
|
475
|
+
return this.rows.findIndex((row) => row.key === key);
|
|
476
|
+
}
|
|
477
|
+
|
|
339
478
|
view() {
|
|
479
|
+
const live = this.liveAgents();
|
|
340
480
|
return {
|
|
341
481
|
now: Date.now(),
|
|
342
482
|
frame: this.ui.progressFrame,
|
|
343
483
|
cursor: this.cursor,
|
|
344
|
-
|
|
484
|
+
current: this.currentIndex(),
|
|
485
|
+
tiles: this.rows.map((row) => toTile(row, live)),
|
|
345
486
|
};
|
|
346
487
|
}
|
|
347
488
|
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const wt = require('../dashboard/worktrees.js');
|
|
4
|
+
const { readWorkspace, worktreesOf, worktreeTarget } = wt;
|
|
5
|
+
const linked = require('../git/linked.js');
|
|
6
|
+
const { addWorktree, removeWorktree, dirKey } = linked;
|
|
7
|
+
|
|
8
|
+
// Create, switch and remove the worktrees of a repository. They are tiles
|
|
9
|
+
// above the branches, and on the repos screen when a folder is opened. Each
|
|
10
|
+
// worktree is a repository of its own for everything else
|
|
11
|
+
class WorktreeController {
|
|
12
|
+
constructor(ui) {
|
|
13
|
+
this.ui = ui;
|
|
14
|
+
this.reset();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
reset() {
|
|
18
|
+
this.dropName = '';
|
|
19
|
+
this.dropRow = null;
|
|
20
|
+
this.branchName = '';
|
|
21
|
+
this.branchRow = null;
|
|
22
|
+
this.branchTarget = '';
|
|
23
|
+
this.busy = false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The tiles in use: the worktrees above the branches or the folder
|
|
27
|
+
panel() {
|
|
28
|
+
const { ui } = this;
|
|
29
|
+
return ui.nav.pane === 'branches' ? ui.trees : ui.workspace;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The tiles show only when there is more than one worktree
|
|
33
|
+
hasPanels() {
|
|
34
|
+
return this.ui.trees.rows.length > 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// One element is selected: a tile of the worktrees or a branch
|
|
38
|
+
focused() {
|
|
39
|
+
const { ui } = this;
|
|
40
|
+
return ui.nav.pane === 'branches' && ui.nav.treeFocus && this.hasPanels();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
selected() {
|
|
44
|
+
const { rows, cursor } = this.panel();
|
|
45
|
+
return rows[cursor] ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
mainOf(row) {
|
|
49
|
+
const rows = this.panel().rows.filter((it) => it.group === row.group);
|
|
50
|
+
return rows.find((it) => it.main) ?? rows[0];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
canWrite() {
|
|
54
|
+
const { ui } = this;
|
|
55
|
+
const inList = ui.nav.pane === 'repos' || ui.nav.pane === 'branches';
|
|
56
|
+
if (!inList || ui.mode !== 'review') return false;
|
|
57
|
+
if (!ui.capabilities.branches) {
|
|
58
|
+
ui.status = 'read only';
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (this.busy) {
|
|
62
|
+
ui.status = 'busy';
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Runs in work in a worktree keep it from being removed
|
|
69
|
+
workingIn(row) {
|
|
70
|
+
const { agents, npm } = this.ui;
|
|
71
|
+
const key = dirKey(row.dir);
|
|
72
|
+
const sources = [agents.liveByRoot(), npm.liveByRoot()];
|
|
73
|
+
return sources.some((live) => {
|
|
74
|
+
for (const [root, count] of live) {
|
|
75
|
+
if (count && dirKey(root) === key) return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The worktrees of the repository that is shown
|
|
82
|
+
async refreshTrees() {
|
|
83
|
+
const { ui } = this;
|
|
84
|
+
const top = ui.top;
|
|
85
|
+
if (!top) return;
|
|
86
|
+
try {
|
|
87
|
+
const seed = { ...worktreesOf(top), root: top };
|
|
88
|
+
const info = await readWorkspace(seed, ui.trees.rows);
|
|
89
|
+
if (ui.top !== top) return;
|
|
90
|
+
ui.treesInfo = { ...info, root: top };
|
|
91
|
+
} catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (ui.trees.active) ui.trees.adopt(ui.treesInfo.repos);
|
|
95
|
+
else ui.trees.start();
|
|
96
|
+
ui.trees.refresh();
|
|
97
|
+
ui.trees.focusDir(top);
|
|
98
|
+
if (!this.hasPanels()) ui.nav.treeFocus = false;
|
|
99
|
+
ui.lifecycle.watchTrees();
|
|
100
|
+
if (ui.uiOpen && ui.nav.pane === 'branches') ui.paint();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
moveTree(dx) {
|
|
104
|
+
if (this.focused()) this.ui.trees.move(dx, 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// A click on a tile selects it and shows that worktree
|
|
108
|
+
pickTile(name) {
|
|
109
|
+
const { ui } = this;
|
|
110
|
+
const at = ui.trees.rows.findIndex((row) => row.name === name);
|
|
111
|
+
if (at < 0 || ui.mode !== 'review') return;
|
|
112
|
+
ui.trees.cursor = at;
|
|
113
|
+
ui.nav.treeFocus = true;
|
|
114
|
+
ui.ops.hold(this.enterSelected());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Selecting a tile shows that worktree: its branches, files, diffs, tasks
|
|
118
|
+
async enterSelected() {
|
|
119
|
+
const { ui } = this;
|
|
120
|
+
const row = this.selected();
|
|
121
|
+
if (!row || ui.mode !== 'review' || this.busy) return;
|
|
122
|
+
if (dirKey(row.dir) === dirKey(ui.top)) return;
|
|
123
|
+
ui.flushReview();
|
|
124
|
+
const home = ui.dashboardHome;
|
|
125
|
+
ui.retarget({ cwd: row.dir, repoName: row.name, pane: 'branches' });
|
|
126
|
+
ui.dashboardHome = home;
|
|
127
|
+
try {
|
|
128
|
+
await ui.ensureRepo();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
ui.status = error instanceof Error ? error.message : `${error}`;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const loading = ui.openLoad();
|
|
134
|
+
await ui.gitBranches.open(true);
|
|
135
|
+
await this.refreshTrees();
|
|
136
|
+
if (ui.uiOpen) {
|
|
137
|
+
ui.lifecycle.startWatch();
|
|
138
|
+
if (home) ui.dashboard.start();
|
|
139
|
+
ui.paint();
|
|
140
|
+
}
|
|
141
|
+
await loading;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
onAdd() {
|
|
145
|
+
if (!this.canWrite() || !this.selected()) return;
|
|
146
|
+
this.ui.composer.openCompose('worktree', '');
|
|
147
|
+
this.ui.status = 'new worktree: branch name';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Make a worktree for a branch that already exists in the branch list
|
|
151
|
+
onBranch() {
|
|
152
|
+
const ui = this.ui;
|
|
153
|
+
if (this.focused() || !this.canWrite()) return;
|
|
154
|
+
const branch = ui.gitBranches.selected();
|
|
155
|
+
const row = this.selected();
|
|
156
|
+
if (!branch || !row) return;
|
|
157
|
+
const main = this.mainOf(row);
|
|
158
|
+
this.branchName = branch.name;
|
|
159
|
+
this.branchRow = main;
|
|
160
|
+
this.branchTarget = worktreeTarget(main, branch.name);
|
|
161
|
+
ui.mode = 'confirmCreateWorktree';
|
|
162
|
+
ui.status = '';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
cancelBranch() {
|
|
166
|
+
this.ui.mode = 'review';
|
|
167
|
+
this.branchName = '';
|
|
168
|
+
this.branchRow = null;
|
|
169
|
+
this.branchTarget = '';
|
|
170
|
+
this.ui.status = '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
confirmBranch() {
|
|
174
|
+
const row = this.branchRow;
|
|
175
|
+
const name = this.branchName;
|
|
176
|
+
this.cancelBranch();
|
|
177
|
+
if (row && name) this.ui.ops.hold(this.create(row, name));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
finishAdd() {
|
|
181
|
+
const ui = this.ui;
|
|
182
|
+
const editor = ui.composer.editor;
|
|
183
|
+
const name = editor ? editor.text.trim() : '';
|
|
184
|
+
if (!name) {
|
|
185
|
+
ui.status = 'empty branch name';
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const row = this.selected();
|
|
189
|
+
ui.composer.closeCompose();
|
|
190
|
+
if (!row) return;
|
|
191
|
+
ui.ops.hold(this.create(row, name));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async reload() {
|
|
195
|
+
const { ui } = this;
|
|
196
|
+
if (ui.nav.pane === 'branches') {
|
|
197
|
+
await this.refreshTrees();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const ws = ui.workspace;
|
|
201
|
+
const info = await readWorkspace(ui.workspaceInfo, ws.rows);
|
|
202
|
+
ui.workspaceInfo = info;
|
|
203
|
+
ws.adopt(info.repos);
|
|
204
|
+
if (ui.nav.pane === 'repos' && ui.uiOpen) ui.lifecycle.startWatch();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async create(row, name) {
|
|
208
|
+
const ui = this.ui;
|
|
209
|
+
const main = this.mainOf(row);
|
|
210
|
+
this.busy = true;
|
|
211
|
+
ui.status = 'creating worktree';
|
|
212
|
+
ui.paint();
|
|
213
|
+
try {
|
|
214
|
+
const dir = await addWorktree(main.dir, worktreeTarget(main, name), name);
|
|
215
|
+
await this.reload();
|
|
216
|
+
this.panel().focusDir(dir);
|
|
217
|
+
ui.status = `created ${name}`;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
ui.status = error.message;
|
|
220
|
+
} finally {
|
|
221
|
+
this.busy = false;
|
|
222
|
+
ui.paint();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
onDrop() {
|
|
227
|
+
const ui = this.ui;
|
|
228
|
+
const row = this.selected();
|
|
229
|
+
if (!this.canWrite() || !row) return;
|
|
230
|
+
if (row.main) {
|
|
231
|
+
ui.status = 'the main worktree stays';
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (ui.nav.pane === 'branches' && dirKey(row.dir) === dirKey(ui.top)) {
|
|
235
|
+
ui.status = 'this worktree is shown';
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (this.workingIn(row)) {
|
|
239
|
+
ui.status = 'runs are working in this worktree';
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this.dropRow = row;
|
|
243
|
+
this.dropName = `worktree ${row.name}`;
|
|
244
|
+
ui.mode = 'confirmWorktree';
|
|
245
|
+
ui.status = '';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
cancelDrop() {
|
|
249
|
+
this.ui.mode = 'review';
|
|
250
|
+
this.reset();
|
|
251
|
+
this.ui.status = '';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
confirmDrop() {
|
|
255
|
+
const row = this.dropRow;
|
|
256
|
+
this.ui.mode = 'review';
|
|
257
|
+
this.reset();
|
|
258
|
+
if (row) this.ui.ops.hold(this.remove(row));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Git refuses a dirty or locked worktree and so does this, nothing is forced
|
|
262
|
+
async remove(row) {
|
|
263
|
+
const ui = this.ui;
|
|
264
|
+
this.busy = true;
|
|
265
|
+
ui.status = 'removing worktree';
|
|
266
|
+
ui.paint();
|
|
267
|
+
try {
|
|
268
|
+
await removeWorktree(this.mainOf(row).dir, row.dir);
|
|
269
|
+
await this.reload();
|
|
270
|
+
ui.status = `removed ${row.name}`;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
ui.status = error.message;
|
|
273
|
+
} finally {
|
|
274
|
+
this.busy = false;
|
|
275
|
+
ui.paint();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = { WorktreeController };
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"chromeFg": "#d0d0e0",
|
|
25
25
|
"chromeBg": "#181824",
|
|
26
26
|
"noteBg": "#242436",
|
|
27
|
+
"popupBg": "#34445c",
|
|
28
|
+
"popupFg": "#f2f4ff",
|
|
29
|
+
"popupHotBg": "#566d90",
|
|
30
|
+
"popupDisabledFg": "#66748a",
|
|
27
31
|
"buttonFg": "#6a6a7e",
|
|
28
32
|
"buttonBg": "#2a2a40",
|
|
29
33
|
"buttonHotFg": "#ffffff",
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"chromeFg": "#24292f",
|
|
25
25
|
"chromeBg": "#f2f4f8",
|
|
26
26
|
"noteBg": "#f8fbff",
|
|
27
|
+
"popupBg": "#d0daea",
|
|
28
|
+
"popupFg": "#202c40",
|
|
29
|
+
"popupHotBg": "#9fb6da",
|
|
30
|
+
"popupDisabledFg": "#94a0b2",
|
|
27
31
|
"buttonFg": "#57606a",
|
|
28
32
|
"buttonBg": "#f7f9fc",
|
|
29
33
|
"buttonHotFg": "#0f1419",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reslop",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Interactive code review and repair planning for AI-assisted changes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"code-review",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"node": ">=18.15.0"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
|
-
"test": "reslop t -- npm run -s lint && reslop t -- node
|
|
52
|
+
"test": "reslop t -- npm run -s lint && reslop t -- node scripts/run-tests.js",
|
|
53
53
|
"lint": "reslop t -- eslint . && reslop t -- prettier -c \"**/*.js\" \"**/*.json\" \"**/*.md\"",
|
|
54
54
|
"fix": "reslop t -- eslint . --fix && reslop t -- prettier --write \"**/*.js\" \"**/*.json\" \"**/*.md\"",
|
|
55
55
|
"enable": "node scripts/enable.js",
|