reslop 0.3.0 → 0.3.2
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 +29 -1
- package/README.md +21 -3
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +166 -370
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -13
- package/lib/agents/models.js +250 -376
- package/lib/agents/registry.js +437 -0
- package/lib/agents/run.js +234 -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/deps.js +29 -0
- package/lib/git/dir.js +50 -0
- package/lib/git/git.js +1 -1
- 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 +137 -31
- package/lib/input/actions.js +83 -2
- package/lib/input/editor.js +9 -0
- package/lib/input/keys.js +39 -3
- package/lib/input/line-edit.js +8 -3
- package/lib/render/agents.js +64 -36
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -4
- package/lib/render/frame.js +16 -2
- package/lib/render/log.js +5 -4
- 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/render.js +4 -7
- package/lib/report/run.js +16 -23
- package/lib/review/agent-instructions.md +13 -8
- package/lib/review/format.js +0 -6
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +227 -194
- 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 -2
- package/lib/session/compose.js +20 -6
- package/lib/session/dispatch.js +43 -3
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/import.js +12 -1
- package/lib/session/lifecycle.js +70 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +3 -1
- package/lib/session/npm/npm.js +111 -13
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +17 -2
- package/lib/session/tasks.js +1 -1
- 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/select.js +2 -1
- package/lib/term/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
|
@@ -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 };
|
package/lib/term/select.js
CHANGED
|
@@ -151,7 +151,8 @@ const overlayRange = (line, fromCol, toCol) => {
|
|
|
151
151
|
};
|
|
152
152
|
|
|
153
153
|
const editorSelection = (view) => {
|
|
154
|
-
const
|
|
154
|
+
const taskEdit = view?.pane === 'tasks' ? view.taskEdit : null;
|
|
155
|
+
const source = view?.compose || view?.codeOverlay || taskEdit;
|
|
155
156
|
if (!source || typeof source.anchor !== 'number') return null;
|
|
156
157
|
const cursor = source.cursor ?? 0;
|
|
157
158
|
if (source.anchor === cursor) return null;
|
|
@@ -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.2",
|
|
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",
|