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.
Files changed (66) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +30 -4
  3. package/lib/agents/acp.js +309 -0
  4. package/lib/agents/agents.js +274 -438
  5. package/lib/agents/allow.js +141 -52
  6. package/lib/agents/allow.json +16 -14
  7. package/lib/agents/models.js +250 -383
  8. package/lib/agents/registry.js +437 -0
  9. package/lib/agents/run.js +234 -0
  10. package/lib/agents/script.js +93 -0
  11. package/lib/agents/transcript.js +143 -0
  12. package/lib/cli.js +2 -1
  13. package/lib/common/process.js +26 -1
  14. package/lib/dashboard/git.js +11 -1
  15. package/lib/dashboard/reports.js +80 -0
  16. package/lib/dashboard/watch.js +43 -8
  17. package/lib/dashboard/workspace.js +19 -20
  18. package/lib/dashboard/worktrees.js +145 -0
  19. package/lib/git/dir.js +50 -0
  20. package/lib/git/linked.js +152 -0
  21. package/lib/git/local.js +179 -0
  22. package/lib/git/status.js +126 -0
  23. package/lib/git/worktree.js +113 -36
  24. package/lib/input/actions.js +83 -5
  25. package/lib/input/keys.js +30 -1
  26. package/lib/input/line-edit.js +5 -1
  27. package/lib/render/agents.js +84 -34
  28. package/lib/render/branches.js +55 -27
  29. package/lib/render/field.js +19 -5
  30. package/lib/render/footer.js +14 -6
  31. package/lib/render/frame.js +16 -2
  32. package/lib/render/menu.js +103 -6
  33. package/lib/render/name-edit.js +31 -0
  34. package/lib/render/repos.js +52 -8
  35. package/lib/render/status.js +22 -4
  36. package/lib/report/run.js +16 -23
  37. package/lib/review/agent-instructions.md +2 -2
  38. package/lib/runs/process.js +3 -3
  39. package/lib/runs/runs.js +27 -4
  40. package/lib/session/agents/agents.js +220 -205
  41. package/lib/session/agents/away.js +48 -0
  42. package/lib/session/agents/job.js +23 -86
  43. package/lib/session/agents/jobs.js +14 -27
  44. package/lib/session/agents/options.js +45 -101
  45. package/lib/session/agents/plans.js +53 -0
  46. package/lib/session/agents/view.js +34 -0
  47. package/lib/session/branches.js +18 -1
  48. package/lib/session/compose.js +13 -5
  49. package/lib/session/dispatch.js +43 -4
  50. package/lib/session/editor-pointer.js +98 -0
  51. package/lib/session/files-pane.js +3 -0
  52. package/lib/session/lifecycle.js +67 -10
  53. package/lib/session/navigation.js +5 -0
  54. package/lib/session/npm/history.js +2 -1
  55. package/lib/session/npm/npm.js +89 -12
  56. package/lib/session/ops.js +3 -0
  57. package/lib/session/pointer.js +304 -46
  58. package/lib/session/session.js +15 -2
  59. package/lib/session/terminal.js +2 -2
  60. package/lib/session/text-menu.js +50 -0
  61. package/lib/session/view.js +19 -2
  62. package/lib/session/workspace.js +179 -38
  63. package/lib/session/worktrees.js +280 -0
  64. package/lib/term/themes/dark.json +4 -0
  65. package/lib/term/themes/light.json +4 -0
  66. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [Unreleased][unreleased]
4
4
 
5
+ ## [0.3.3][] - 2026-10-16
6
+
7
+ - Add Junie by JetBrains support
8
+ - Add GitHub Copilot support
9
+ - Add Pi coding agent support with its ACP adapter
10
+ - Add Kiro by AWS support, installed with its vendor script
11
+ - Add Cline support
12
+ - Add Windsurf support through Devin CLI, its current agent
13
+ - Add Kilo Code support
14
+ - Show agents in two columns on the dashboard, without models
15
+
16
+ ## [0.3.2][] - 2026-10-15
17
+
18
+ - Communicate with agents over the Agent Client Protocol instead of CLI
19
+ - Listed models from the agent session, with effort, context, fast
20
+ - Answer agent permission requests
21
+ - Installing a missing agent's and ACP for them
22
+ - Add Google Antigravity's support
23
+ - Started a new agent session or continued the selected one
24
+ - Showed a plan heading or task as the session caption
25
+ - Support git worktrees at branches screen
26
+ - Added pop-up menus on tables and UI controls
27
+ - Add new hotkeys for better UX
28
+ - Improve command and agent background runners
29
+
5
30
  ## [0.3.1][] - 2026-10-13
6
31
 
7
32
  - Allow agents to write to work with git
@@ -199,7 +224,9 @@
199
224
  - Feedback and todos that produce a repair plan for an agent
200
225
  - Intra-line highlighting in unified, mixed, and side-by-side layouts
201
226
 
202
- [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.3.1...HEAD
227
+ [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.3.3...HEAD
228
+ [0.3.3]: https://github.com/tshemsedinov/reslop/compare/v0.3.2...v0.3.3
229
+ [0.3.2]: https://github.com/tshemsedinov/reslop/compare/v0.3.1...v0.3.2
203
230
  [0.3.1]: https://github.com/tshemsedinov/reslop/compare/v0.3.0...v0.3.1
204
231
  [0.3.0]: https://github.com/tshemsedinov/reslop/compare/v0.2.7...v0.3.0
205
232
  [0.2.7]: https://github.com/tshemsedinov/reslop/compare/v0.2.6...v0.2.7
package/README.md CHANGED
@@ -25,14 +25,15 @@ Review → Plan → Repair → Verify
25
25
  - Auto-reload local diffs when files change, keeping the current screen
26
26
  - Import GitHub PR and GitLab MR review comments, and GitHub/GitLab issues, into tasks and the local plan
27
27
  - Commits list: brief and full, view a commit's diff, commit, update, amend, apply, edit, fixup, and delete
28
- - Branches: checkout, create, rebase, delete, pull, and push
28
+ - Branches: checkout, create, rebase, delete, pull, and push; git worktrees as tiles above the branches
29
+ - Pop-up menus on tables and controls
29
30
  - npm scripts and bins: run, edit, and reorder; double-click to run; clean logs older than 5 days
30
31
  - Reduced test and lint output for agents: failures and a short summary, not passing noise
31
32
  - Review, renew, audit and update npm dependencies in `package.json` and the lockfile
32
33
  - Auto-update reslop patch and minor releases; confirm a new major
33
34
  - Work with git, npm, and fetch in the background; run tests in the background while you review
34
- - Execute the plan in local CLI agents: pick agent, model, and effort; watch progress; sessions, history, and continue or new runs
35
- - Agent defaults and extras in `.reslop` (grouped settings sections)
35
+ - Execute the plan in local agents over ACP: pick agent, model, and effort; watch progress; sessions, history, and continue or new runs. The dashboard lists agents in two columns, without models
36
+ - Agent defaults in `.reslop` (grouped settings sections)
36
37
  - Optional line numbers in diff and file views (`Ctrl+L`)
37
38
 
38
39
  ## Install
@@ -47,6 +48,8 @@ Requires Node.js `>=18.15.0`.
47
48
  ## Usage
48
49
 
49
50
  - `reslop` live dashboard of this repository; press a block key to open its screen
51
+ - `reslop folder` tiles of the repositories in a folder, each with its git worktrees
52
+ - git worktrees of a repository are tiles above its branches when there are several: `←` `→` select a tile, `↑` `↓` go through the branches, `enter` or a click shows that worktree, `insert` and `delete` create and remove one
50
53
  - `reslop path/file` show only that path or file (starts on the file list)
51
54
  - `reslop 7ac260c` show that commit (read-only)
52
55
  - `reslop https://github.com/metarhia/metacom/pull/555` GitHub/GitLab PR/MR
@@ -64,7 +67,7 @@ Prefix each program in an npm script with `reslop t --`. Keep `&&` between those
64
67
 
65
68
  The report is Markdown: failures, diagnostics, and a short summary. Passing results are omitted and similar problems are grouped. The reduced report is saved as `.log/<name>.log` and the raw output as `.log/<name>.raw`. The report names the raw file for an agent to read when a detail is missing. Set `RESLOP_OUTPUT=raw` to pass the command through unchanged. The review screen runs scripts in raw mode and applies its own filter once.
66
69
 
67
- Run the full check with `npm t`. Run one or more test files with `reslop t -- node --test <files>`. New reviews include those two commands in the agent instructions.
70
+ `npm t` runs lint and the tests that do not start a process. Run a test file that starts git, node, or npm with `node --test <file>`. New reviews include both commands in the agent instructions.
68
71
 
69
72
  `reslop t` records each run as `.log/<name>.json` beside those files so that the dashboard can show commands started by an agent in another terminal.
70
73
 
@@ -76,6 +79,29 @@ Reviews go in `.plan/YYYY-MM-DD-NN.md` with frontmatter `status`:
76
79
 
77
80
  `ready` and `pending` in older reviews are read as `editing`.
78
81
 
82
+ ## Agents
83
+
84
+ reslop runs a plan in an agent over the [Agent Client Protocol](https://agentclientprotocol.com). An agent is listed as available when the command that serves ACP is on `PATH`:
85
+
86
+ | Agent | Command | Install |
87
+ | ----------- | -------------------- | ------------------------------------------------- |
88
+ | Claude Code | `claude-agent-acp` | `npm i -g @agentclientprotocol/claude-agent-acp` |
89
+ | Codex | `codex-acp` | `npm i -g @agentclientprotocol/codex-acp` |
90
+ | Cursor | `cursor-agent acp` | comes with the Cursor CLI |
91
+ | OpenCode | `opencode acp` | `npm i -g opencode-ai` |
92
+ | Antigravity | `agy_acp_server.par` | downloaded from the official ACP registry |
93
+ | Junie | `junie --acp=true` | `npm i -g @jetbrains/junie` |
94
+ | Copilot | `copilot --acp` | `npm i -g @github/copilot` |
95
+ | Pi | `pi-acp` and `pi` | `npm i -g pi-acp @earendil-works/pi-coding-agent` |
96
+ | Kiro | `kiro-cli acp` | install script from cli.kiro.dev |
97
+ | Cline | `cline --acp` | `npm i -g cline` |
98
+ | Windsurf | `devin acp` | install script from cli.devin.ai |
99
+ | Kilo Code | `kilo acp` | `npm i -g @kilocode/cli` |
100
+
101
+ A missing agent shows what it needs, in the warning color; `i` on that row proposes installing it. npm adapters are installed globally, together with the agent's own CLI when the adapter runs it and it is not on `PATH`, while Google Antigravity's official ACP runtime is downloaded from the ACP registry into reslop's user data directory. Windsurf is listed as `devin`: Cognition's Devin CLI is now the agent of Windsurf, and the Windsurf editor does not serve ACP. An agent CLI published with an install script, such as Kiro's or Devin's, is installed by running the vendor's script, and its command is also found in `~/.local/bin`. reslop also discovers that runtime on `PATH` and in Zed's registry directory. After confirmation, reslop lists the agent as soon as the install finishes and does not restart. [docs/acp-adapters.md](docs/acp-adapters.md) describes the integrations.
102
+
103
+ Models are the ones the agent lists; efforts, context sizes, and fast mode are the ones it offers for the picked model. When a run ends, the session is set back to what it was, so the defaults of the agent's own CLI stay as they were. A run has nobody to ask, so reslop answers permission requests itself: edits inside the repository and `npm`, `npx`, `node`, `reslop`, and `git` commands, piped through read-only filters such as `grep` or `tail`, are allowed; everything else is refused.
104
+
79
105
  ## Future
80
106
 
81
107
  - Search, blame, and file history in the review
@@ -0,0 +1,309 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('node:child_process');
4
+ const { jsonParse, isHashObject } = require('metautil');
5
+
6
+ const PROTOCOL_VERSION = 1;
7
+ const METHOD_NOT_FOUND = -32601;
8
+ const INTERNAL_ERROR = -32603;
9
+ const AUTH_REQUIRED = -32000;
10
+ const STDERR_KEEP = 2000;
11
+ const SECRET_AUTH = ['env_var', 'terminal'];
12
+
13
+ const errorText = (error) => {
14
+ const data = isHashObject(error.data) ? error.data : {};
15
+ const detail = data.message ?? data.details ?? error.data;
16
+ const text = typeof detail === 'string' ? detail.trim() : '';
17
+ return text || `${error.message ?? 'agent error'}`;
18
+ };
19
+
20
+ class AcpError extends Error {
21
+ constructor(error) {
22
+ super(errorText(error));
23
+ this.code = error.code;
24
+ this.data = error.data;
25
+ }
26
+ }
27
+
28
+ const rpcError = (message, code) => Object.assign(new Error(message), { code });
29
+
30
+ const methodMissing = (method) =>
31
+ rpcError(`Method not found: ${method}`, METHOD_NOT_FOUND);
32
+
33
+ const exitMessage = (status, stderr) => {
34
+ const code = typeof status === 'number' ? ` with code ${status}` : '';
35
+ const tail = stderr.trim();
36
+ return tail ? `agent exited${code}\n${tail}` : `agent exited${code}`;
37
+ };
38
+
39
+ // JSON-RPC over the stdio of one agent process, a message per line
40
+ class AcpConnection {
41
+ constructor(handlers = {}) {
42
+ this.onNotify = handlers.notify ?? (() => {});
43
+ this.onRequest = handlers.request ?? null;
44
+ this.child = null;
45
+ this.pending = new Map();
46
+ this.nextId = 1;
47
+ this.buffer = '';
48
+ this.stderr = '';
49
+ this.failure = null;
50
+ }
51
+
52
+ start(cmd, args, options) {
53
+ const stdio = ['pipe', 'pipe', 'pipe'];
54
+ const child = spawn(cmd, args, { ...options, stdio });
55
+ this.child = child;
56
+ child.stdout.setEncoding('utf8');
57
+ child.stdout.on('data', (chunk) => this.read(chunk));
58
+ child.stderr.setEncoding('utf8');
59
+ child.stderr.on('data', (chunk) => {
60
+ this.stderr = `${this.stderr}${chunk}`.slice(-STDERR_KEEP);
61
+ });
62
+ // A closed pipe is reported by the exit of the process
63
+ child.stdin.on('error', () => {});
64
+ child.on('error', (error) => this.fail(error));
65
+ child.on('close', (status) => {
66
+ this.fail(new Error(exitMessage(status, this.stderr)));
67
+ });
68
+ return this;
69
+ }
70
+
71
+ fail(error) {
72
+ if (this.failure) return;
73
+ this.failure = error;
74
+ for (const waiter of this.pending.values()) waiter.reject(error);
75
+ this.pending.clear();
76
+ }
77
+
78
+ send(message) {
79
+ const stdin = this.child && this.child.stdin;
80
+ if (this.failure || !stdin || !stdin.writable) return;
81
+ stdin.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`);
82
+ }
83
+
84
+ request(method, params) {
85
+ if (this.failure) return Promise.reject(this.failure);
86
+ const id = this.nextId;
87
+ this.nextId += 1;
88
+ return new Promise((resolve, reject) => {
89
+ this.pending.set(id, { resolve, reject });
90
+ this.send({ id, method, params });
91
+ });
92
+ }
93
+
94
+ notify(method, params) {
95
+ this.send({ method, params });
96
+ }
97
+
98
+ read(chunk) {
99
+ const lines = `${this.buffer}${chunk}`.split('\n');
100
+ this.buffer = lines.pop();
101
+ for (const line of lines) {
102
+ const message = line.trim() ? jsonParse(line) : null;
103
+ if (isHashObject(message)) this.dispatch(message);
104
+ }
105
+ }
106
+
107
+ dispatch(message) {
108
+ const { id, method } = message;
109
+ if (typeof method !== 'string') return void this.settle(message);
110
+ if (id === undefined || id === null) {
111
+ return void this.onNotify(method, message.params ?? {});
112
+ }
113
+ this.answer(id, method, message.params ?? {});
114
+ }
115
+
116
+ settle(message) {
117
+ const waiter = this.pending.get(message.id);
118
+ if (!waiter) return;
119
+ this.pending.delete(message.id);
120
+ if (message.error) waiter.reject(new AcpError(message.error));
121
+ else waiter.resolve(message.result ?? {});
122
+ }
123
+
124
+ async answer(id, method, params) {
125
+ try {
126
+ if (!this.onRequest) throw methodMissing(method);
127
+ const result = await this.onRequest(method, params);
128
+ this.send({ id, result: result ?? {} });
129
+ } catch (error) {
130
+ const code = typeof error.code === 'number' ? error.code : INTERNAL_ERROR;
131
+ this.send({ id, error: { code, message: error.message } });
132
+ }
133
+ }
134
+ }
135
+
136
+ const authMethod = (init) => {
137
+ const methods = Array.isArray(init.authMethods) ? init.authMethods : [];
138
+ const open = (method) => {
139
+ if (!isHashObject(method) || typeof method.id !== 'string') return false;
140
+ if (SECRET_AUTH.includes(method.type)) return false;
141
+ const meta = isHashObject(method._meta) ? method._meta : {};
142
+ return !Object.keys(meta).some((key) => /api.?key/i.test(key));
143
+ };
144
+ return methods.find(open) ?? null;
145
+ };
146
+
147
+ const sessionState = (sessionId, result) => ({
148
+ sessionId,
149
+ configOptions: Array.isArray(result.configOptions)
150
+ ? result.configOptions
151
+ : [],
152
+ models: isHashObject(result.models) ? result.models : null,
153
+ modes: isHashObject(result.modes) ? result.modes : null,
154
+ });
155
+
156
+ // One agent session: opened or continued, configured, then prompted
157
+ class AcpSession {
158
+ constructor(connection, cwd, info) {
159
+ this.connection = connection;
160
+ this.cwd = cwd;
161
+ this.info = info;
162
+ this.init = {};
163
+ this.state = sessionState('', {});
164
+ this.replaying = false;
165
+ this.undone = [];
166
+ this.onAuth = () => {};
167
+ }
168
+
169
+ get id() {
170
+ return this.state.sessionId;
171
+ }
172
+
173
+ request(method, params = {}) {
174
+ const sessionId = this.id;
175
+ return this.connection.request(method, { sessionId, ...params });
176
+ }
177
+
178
+ async handshake() {
179
+ this.init = await this.connection.request('initialize', {
180
+ protocolVersion: PROTOCOL_VERSION,
181
+ clientCapabilities: {
182
+ fs: { readTextFile: false, writeTextFile: false },
183
+ terminal: false,
184
+ // Cursor lists the settings of a model only to a client that asks
185
+ _meta: { parameterizedModelPicker: true },
186
+ },
187
+ clientInfo: this.info,
188
+ });
189
+ }
190
+
191
+ // Signing in can open a browser, so only a run is allowed to start it
192
+ async signedIn(call, login) {
193
+ try {
194
+ return await call();
195
+ } catch (error) {
196
+ const asked = login && error.code === AUTH_REQUIRED;
197
+ const method = asked ? authMethod(this.init) : null;
198
+ if (!method) throw error;
199
+ this.onAuth(method);
200
+ await this.connection.request('authenticate', { methodId: method.id });
201
+ return call();
202
+ }
203
+ }
204
+
205
+ continueMethod() {
206
+ const caps = this.init.agentCapabilities ?? {};
207
+ const session = caps.sessionCapabilities ?? {};
208
+ if (isHashObject(session.resume)) return 'session/resume';
209
+ if (caps.loadSession === true) return 'session/load';
210
+ throw new Error('agent cannot continue a session');
211
+ }
212
+
213
+ async open(sessionId = '', login = false) {
214
+ await this.handshake();
215
+ const params = { cwd: this.cwd, mcpServers: [] };
216
+ if (!sessionId) {
217
+ const start = () => this.connection.request('session/new', params);
218
+ const result = await this.signedIn(start, login);
219
+ this.state = sessionState(`${result.sessionId ?? ''}`, result);
220
+ return this.state;
221
+ }
222
+ const method = this.continueMethod();
223
+ const resume = () =>
224
+ this.connection.request(method, { ...params, sessionId });
225
+ // A loaded session replays its history as updates before it answers
226
+ this.replaying = true;
227
+ try {
228
+ const result = await this.signedIn(resume, login);
229
+ this.state = sessionState(sessionId, result);
230
+ } finally {
231
+ this.replaying = false;
232
+ }
233
+ return this.state;
234
+ }
235
+
236
+ // The request that puts back what a change is about to replace
237
+ undoOf(step) {
238
+ const { state } = this;
239
+ const { configId, modelId, modeId } = step.params;
240
+ const option = state.configOptions.find(
241
+ (item) => isHashObject(item) && item.id === configId,
242
+ );
243
+ const params = {};
244
+ if (option) Object.assign(params, { configId, value: option.currentValue });
245
+ if (modelId && state.models) params.modelId = state.models.currentModelId;
246
+ if (modeId && state.modes) params.modeId = state.modes.currentModeId;
247
+ return Object.keys(params).length ? { method: step.method, params } : null;
248
+ }
249
+
250
+ async apply(step) {
251
+ const result = await this.request(step.method, step.params);
252
+ const state = { ...this.state };
253
+ if (Array.isArray(result.configOptions)) {
254
+ state.configOptions = result.configOptions;
255
+ }
256
+ const { modelId, modeId } = step.params;
257
+ if (modelId && state.models) {
258
+ state.models = { ...state.models, currentModelId: modelId };
259
+ }
260
+ if (modeId && state.modes) {
261
+ state.modes = { ...state.modes, currentModeId: modeId };
262
+ }
263
+ this.state = state;
264
+ }
265
+
266
+ async change(step) {
267
+ if (!step) return;
268
+ const undo = this.undoOf(step);
269
+ await this.apply(step);
270
+ if (undo) this.undone.push(undo);
271
+ }
272
+
273
+ // An agent can keep what a session was set to as the defaults of the user
274
+ async revert() {
275
+ const steps = this.undone.reverse();
276
+ this.undone = [];
277
+ for (const step of steps) {
278
+ try {
279
+ await this.apply(step);
280
+ } catch {
281
+ // an agent that is gone has nothing left to put back
282
+ }
283
+ }
284
+ }
285
+
286
+ absorb(update) {
287
+ if (update.sessionUpdate !== 'config_option_update') return;
288
+ if (!Array.isArray(update.configOptions)) return;
289
+ this.state = { ...this.state, configOptions: update.configOptions };
290
+ }
291
+
292
+ cancel() {
293
+ const sessionId = this.id;
294
+ if (sessionId) this.connection.notify('session/cancel', { sessionId });
295
+ }
296
+
297
+ prompt(text) {
298
+ return this.request('session/prompt', {
299
+ prompt: [{ type: 'text', text }],
300
+ });
301
+ }
302
+ }
303
+
304
+ module.exports = {
305
+ AcpConnection,
306
+ AcpSession,
307
+ methodMissing,
308
+ authMethod,
309
+ };