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.
Files changed (74) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +21 -3
  3. package/lib/agents/acp.js +309 -0
  4. package/lib/agents/agents.js +166 -370
  5. package/lib/agents/allow.js +141 -52
  6. package/lib/agents/allow.json +16 -13
  7. package/lib/agents/models.js +250 -376
  8. package/lib/agents/registry.js +437 -0
  9. package/lib/agents/run.js +234 -0
  10. package/lib/agents/transcript.js +143 -0
  11. package/lib/cli.js +2 -1
  12. package/lib/common/process.js +26 -1
  13. package/lib/dashboard/git.js +11 -1
  14. package/lib/dashboard/reports.js +80 -0
  15. package/lib/dashboard/watch.js +43 -8
  16. package/lib/dashboard/workspace.js +19 -20
  17. package/lib/dashboard/worktrees.js +145 -0
  18. package/lib/git/deps.js +29 -0
  19. package/lib/git/dir.js +50 -0
  20. package/lib/git/git.js +1 -1
  21. package/lib/git/linked.js +152 -0
  22. package/lib/git/local.js +179 -0
  23. package/lib/git/status.js +126 -0
  24. package/lib/git/worktree.js +137 -31
  25. package/lib/input/actions.js +83 -2
  26. package/lib/input/editor.js +9 -0
  27. package/lib/input/keys.js +39 -3
  28. package/lib/input/line-edit.js +8 -3
  29. package/lib/render/agents.js +64 -36
  30. package/lib/render/branches.js +55 -27
  31. package/lib/render/field.js +19 -5
  32. package/lib/render/footer.js +14 -4
  33. package/lib/render/frame.js +16 -2
  34. package/lib/render/log.js +5 -4
  35. package/lib/render/menu.js +103 -6
  36. package/lib/render/name-edit.js +31 -0
  37. package/lib/render/repos.js +52 -8
  38. package/lib/render/status.js +22 -4
  39. package/lib/report/render.js +4 -7
  40. package/lib/report/run.js +16 -23
  41. package/lib/review/agent-instructions.md +13 -8
  42. package/lib/review/format.js +0 -6
  43. package/lib/runs/process.js +3 -3
  44. package/lib/runs/runs.js +27 -4
  45. package/lib/session/agents/agents.js +227 -194
  46. package/lib/session/agents/away.js +48 -0
  47. package/lib/session/agents/job.js +23 -86
  48. package/lib/session/agents/jobs.js +14 -27
  49. package/lib/session/agents/options.js +45 -101
  50. package/lib/session/agents/plans.js +53 -0
  51. package/lib/session/agents/view.js +34 -0
  52. package/lib/session/branches.js +18 -2
  53. package/lib/session/compose.js +20 -6
  54. package/lib/session/dispatch.js +43 -3
  55. package/lib/session/editor-pointer.js +98 -0
  56. package/lib/session/files-pane.js +3 -0
  57. package/lib/session/import.js +12 -1
  58. package/lib/session/lifecycle.js +70 -10
  59. package/lib/session/navigation.js +5 -0
  60. package/lib/session/npm/history.js +3 -1
  61. package/lib/session/npm/npm.js +111 -13
  62. package/lib/session/ops.js +3 -0
  63. package/lib/session/pointer.js +304 -46
  64. package/lib/session/session.js +17 -2
  65. package/lib/session/tasks.js +1 -1
  66. package/lib/session/terminal.js +2 -2
  67. package/lib/session/text-menu.js +50 -0
  68. package/lib/session/view.js +19 -2
  69. package/lib/session/workspace.js +179 -38
  70. package/lib/session/worktrees.js +280 -0
  71. package/lib/term/select.js +2 -1
  72. package/lib/term/themes/dark.json +4 -0
  73. package/lib/term/themes/light.json +4 -0
  74. 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 };
@@ -151,7 +151,8 @@ const overlayRange = (line, fromCol, toCol) => {
151
151
  };
152
152
 
153
153
  const editorSelection = (view) => {
154
- const source = view?.compose || view?.codeOverlay;
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.0",
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 --test",
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",