reslop 0.3.2 → 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 CHANGED
@@ -2,6 +2,17 @@
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
+
5
16
  ## [0.3.2][] - 2026-10-15
6
17
 
7
18
  - Communicate with agents over the Agent Client Protocol instead of CLI
@@ -213,7 +224,8 @@
213
224
  - Feedback and todos that produce a repair plan for an agent
214
225
  - Intra-line highlighting in unified, mixed, and side-by-side layouts
215
226
 
216
- [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.3.2...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
217
229
  [0.3.2]: https://github.com/tshemsedinov/reslop/compare/v0.3.1...v0.3.2
218
230
  [0.3.1]: https://github.com/tshemsedinov/reslop/compare/v0.3.0...v0.3.1
219
231
  [0.3.0]: https://github.com/tshemsedinov/reslop/compare/v0.2.7...v0.3.0
package/README.md CHANGED
@@ -25,13 +25,14 @@ 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 agents over ACP: pick agent, model, and effort; watch progress; sessions, history, and continue or new runs
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
35
36
  - Agent defaults in `.reslop` (grouped settings sections)
36
37
  - Optional line numbers in diff and file views (`Ctrl+L`)
37
38
 
@@ -82,15 +83,22 @@ Reviews go in `.plan/YYYY-MM-DD-NN.md` with frontmatter `status`:
82
83
 
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`:
84
85
 
85
- | Agent | Command | Install |
86
- | ----------- | -------------------- | ------------------------------------------------ |
87
- | Claude Code | `claude-agent-acp` | `npm i -g @agentclientprotocol/claude-agent-acp` |
88
- | Codex | `codex-acp` | `npm i -g @agentclientprotocol/codex-acp` |
89
- | Cursor | `cursor-agent acp` | comes with the Cursor CLI |
90
- | OpenCode | `opencode acp` | `npm i -g opencode-ai` |
91
- | Antigravity | `agy_acp_server.par` | downloaded from the official ACP registry |
92
-
93
- A missing agent shows what it needs, in the warning color; `i` on that row proposes installing it. npm adapters are installed globally, while Google Antigravity's official ACP runtime is downloaded from the ACP registry into reslop's user data directory. 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.
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.
94
102
 
95
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.
96
104
 
@@ -11,6 +11,7 @@ const { choiceOf } = models;
11
11
  const { startRun, probeSession } = require('./run.js');
12
12
  const registry = require('./registry.js');
13
13
  const { installedRegistryAgent } = registry;
14
+ const { localBin, scriptUrl } = require('./script.js');
14
15
  const { resolveGitDirs } = require('../git/dir.js');
15
16
 
16
17
  const CODEX_SANDBOX = 'sandbox_workspace_write';
@@ -58,9 +59,15 @@ const defineAgent = (spec) => ({
58
59
  title: spec.title,
59
60
  commands: spec.commands,
60
61
  package: spec.package ?? '',
62
+ // The cli of the agent that an adapter starts, as { bin, package }
63
+ requires: spec.requires ?? null,
61
64
  registry: spec.registry ?? '',
65
+ // The install scripts of the vendor, as { unix, win } https urls
66
+ script: spec.script ?? null,
62
67
  installLabel: spec.installLabel ?? '',
63
68
  hint: spec.hint ?? '',
69
+ // A program of the vendor that does not serve ACP, as { bin, hint }
70
+ related: spec.related ?? null,
64
71
  mode: spec.mode ?? '',
65
72
  env: spec.env ?? (() => ({})),
66
73
  });
@@ -104,8 +111,73 @@ const AGENTS = [
104
111
  registry: 'antigravity-acp',
105
112
  installLabel: 'Google Antigravity ACP',
106
113
  hint: 'install Google Antigravity ACP',
114
+ related: {
115
+ bin: 'agy',
116
+ hint: 'agy installed; install Google Antigravity ACP',
117
+ },
107
118
  env: antigravityEnv,
108
119
  }),
120
+ defineAgent({
121
+ id: 'junie',
122
+ title: 'Junie',
123
+ commands: [['junie', '--acp=true']],
124
+ package: '@jetbrains/junie',
125
+ }),
126
+ defineAgent({
127
+ id: 'copilot',
128
+ title: 'Copilot',
129
+ commands: [['copilot', '--acp']],
130
+ package: '@github/copilot',
131
+ }),
132
+ defineAgent({
133
+ id: 'pi',
134
+ title: 'Pi',
135
+ commands: [['pi-acp']],
136
+ package: 'pi-acp',
137
+ requires: { bin: 'pi', package: '@earendil-works/pi-coding-agent' },
138
+ }),
139
+ defineAgent({
140
+ id: 'kiro',
141
+ title: 'Kiro',
142
+ commands: [['kiro-cli', 'acp', '--agent-engine=v3']],
143
+ script: {
144
+ unix: 'https://cli.kiro.dev/install',
145
+ win: 'https://cli.kiro.dev/install.ps1',
146
+ },
147
+ installLabel: 'Kiro CLI from cli.kiro.dev',
148
+ hint: 'install the Kiro CLI from cli.kiro.dev',
149
+ }),
150
+ defineAgent({
151
+ id: 'cline',
152
+ title: 'Cline',
153
+ commands: [['cline', '--acp']],
154
+ package: 'cline',
155
+ }),
156
+ // Windsurf's agent is now the Devin CLI of Cognition; the editor has no ACP
157
+ defineAgent({
158
+ id: 'devin',
159
+ title: 'Devin for Windsurf',
160
+ commands: [['devin', 'acp']],
161
+ script: {
162
+ unix: 'https://cli.devin.ai/install.sh',
163
+ win: 'https://static.devin.ai/cli/setup.ps1',
164
+ },
165
+ installLabel: 'Devin CLI from cli.devin.ai',
166
+ hint: 'install the Devin CLI, the agent of Windsurf',
167
+ related: {
168
+ bin: 'windsurf',
169
+ hint: 'windsurf installed; install the Devin CLI',
170
+ },
171
+ }),
172
+ defineAgent({
173
+ id: 'kilo',
174
+ title: 'Kilo Code',
175
+ commands: [
176
+ ['kilo', 'acp'],
177
+ ['kilocode', 'acp'],
178
+ ],
179
+ package: '@kilocode/cli',
180
+ }),
109
181
  ];
110
182
 
111
183
  const agentSpec = (id) => AGENTS.find((spec) => spec.id === id) ?? null;
@@ -139,8 +211,8 @@ const isBin = (file) => {
139
211
  }
140
212
  };
141
213
 
142
- const findBin = (name, env = process.env) => {
143
- for (const dir of pathDirs(env)) {
214
+ const findIn = (dirs, name, env) => {
215
+ for (const dir of dirs) {
144
216
  for (const file of binNames(name, env)) {
145
217
  const full = path.join(dir, file);
146
218
  if (isBin(full)) return full;
@@ -149,9 +221,19 @@ const findBin = (name, env = process.env) => {
149
221
  return '';
150
222
  };
151
223
 
224
+ const findBin = (name, env = process.env) => findIn(pathDirs(env), name, env);
225
+
226
+ // An install script may put its command in a directory missing from PATH
227
+ const commandDirs = (spec, env) => {
228
+ const dirs = pathDirs(env);
229
+ const local = spec.script ? localBin(env) : '';
230
+ return local && !dirs.includes(local) ? [...dirs, local] : dirs;
231
+ };
232
+
152
233
  const findCommand = (spec, env) => {
234
+ const dirs = commandDirs(spec, env);
153
235
  for (const [name, ...args] of spec.commands) {
154
- const bin = findBin(name, env);
236
+ const bin = findIn(dirs, name, env);
155
237
  if (bin) return { bin, args };
156
238
  }
157
239
  if (spec.registry) {
@@ -161,27 +243,44 @@ const findCommand = (spec, env) => {
161
243
  return { bin: '', args: [] };
162
244
  };
163
245
 
164
- // What a missing agent takes: an npm package or an official registry binary
165
- const installHint = (spec, env) => {
246
+ // npm packages still missing: the adapter and the cli of the agent it starts
247
+ const missingPackages = (spec, env, found) => {
248
+ const names = [];
249
+ if (!found.bin && spec.package) names.push(spec.package);
250
+ const cli = spec.requires;
251
+ if (cli && !findBin(cli.bin, env)) names.push(cli.package);
252
+ return names;
253
+ };
254
+
255
+ // What a missing agent takes: npm packages, a registry binary, or a script
256
+ const installHint = (spec, env, packages) => {
257
+ if (packages.length) return `npm i -g ${packages.join(' ')}`;
166
258
  if (spec.package) return `npm i -g ${spec.package}`;
167
- if (spec.registry && findBin('agy', env)) {
168
- return 'agy installed; install Google Antigravity ACP';
169
- }
259
+ const { related } = spec;
260
+ if (related && findBin(related.bin, env)) return related.hint;
170
261
  return spec.hint;
171
262
  };
172
263
 
173
- const detectAgents = (env = process.env) =>
174
- AGENTS.map((spec) => ({
264
+ const detectAgent = (spec, env) => {
265
+ const found = findCommand(spec, env);
266
+ const packages = missingPackages(spec, env, found);
267
+ const command = packages.length ? { bin: '', args: [] } : found;
268
+ return {
175
269
  id: spec.id,
176
270
  name: spec.name,
177
271
  spec,
178
- package: spec.package,
179
- installable: Boolean(spec.package || spec.registry),
272
+ package: packages.join(' ') || spec.package,
273
+ packages,
274
+ installable: Boolean(spec.package || spec.registry || spec.script),
180
275
  installLabel: spec.installLabel,
181
- hint: installHint(spec, env),
276
+ hint: installHint(spec, env, packages),
182
277
  catalog: null,
183
- ...findCommand(spec, env),
184
- }));
278
+ ...command,
279
+ };
280
+ };
281
+
282
+ const detectAgents = (env = process.env) =>
283
+ AGENTS.map((spec) => detectAgent(spec, env));
185
284
 
186
285
  const listModels = (row, options = {}) => {
187
286
  if (!row || !row.bin) return Promise.resolve(null);
@@ -239,34 +338,44 @@ const buildLaunch = (row, picked, reviewPath, cwd, sessionId = '') => {
239
338
  return launch;
240
339
  };
241
340
 
341
+ // A helper of reslop downloads a registry binary or runs a vendor script
342
+ const helperInstall = (row, installer, file, arg) => ({
343
+ installer,
344
+ cmd: process.execPath,
345
+ args: [path.join(__dirname, file), arg],
346
+ command: `install ${row.spec.installLabel || row.name}`,
347
+ });
348
+
242
349
  // The package lands in the global prefix of npm, where PATH finds its command
350
+ const installCommand = (row) => {
351
+ const { spec } = row;
352
+ if (spec.registry) {
353
+ return helperInstall(row, 'registry', 'registry.js', spec.registry);
354
+ }
355
+ if (spec.script) {
356
+ const url = scriptUrl(spec.script);
357
+ return helperInstall(row, 'script', 'script.js', url);
358
+ }
359
+ const names = row.packages ?? [spec.package].filter(Boolean);
360
+ if (!names.length) return null;
361
+ const args = ['install', '--global', ...names];
362
+ const command = `npm ${args.join(' ')}`;
363
+ return { installer: 'npm', cmd: npmBin(), args, command };
364
+ };
365
+
243
366
  const buildInstall = (row) => {
244
367
  if (!row) return { ok: false, error: 'no agent' };
245
368
  if (row.bin) return { ok: false, error: 'installed' };
246
- const name = row.spec.package;
247
- const registryId = row.spec.registry;
248
- if (!name && !registryId) {
249
- return { ok: false, error: row.hint || 'no installer' };
250
- }
251
- const npmArgs = name ? ['install', '--global', name] : [];
252
- const args = registryId
253
- ? [path.join(__dirname, 'registry.js'), registryId]
254
- : npmArgs;
255
- const cmd = registryId ? process.execPath : npmBin();
256
- const command = registryId
257
- ? `install ${row.spec.installLabel || row.name}`
258
- : `npm ${npmArgs.join(' ')}`;
369
+ const install = installCommand(row);
370
+ if (!install) return { ok: false, error: row.hint || 'no installer' };
259
371
  return {
260
372
  ok: true,
261
373
  id: row.id,
262
374
  name: row.name,
263
375
  kind: 'install',
264
- installer: registryId ? 'registry' : 'npm',
265
- cmd,
266
- args,
376
+ ...install,
267
377
  plan: '',
268
378
  session: '',
269
- command,
270
379
  };
271
380
  };
272
381
 
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawn } = require('node:child_process');
7
+
8
+ // Where the install scripts of agent CLIs put their command
9
+ const localBin = (env = process.env) => {
10
+ const home = env.HOME || env.USERPROFILE;
11
+ return home ? path.join(home, '.local', 'bin') : '';
12
+ };
13
+
14
+ const httpsUrl = (href) => {
15
+ if (!href) throw new Error('no install script for this platform');
16
+ const url = new URL(href);
17
+ if (url.protocol !== 'https:') {
18
+ throw new Error('install script must use https');
19
+ }
20
+ return url.href;
21
+ };
22
+
23
+ // The script a vendor publishes for this platform
24
+ const scriptUrl = (script, platform = process.platform) =>
25
+ httpsUrl(platform === 'win32' ? script.win : script.unix);
26
+
27
+ const shellOf = (file, platform) => {
28
+ if (platform !== 'win32') return ['bash', [file]];
29
+ const args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', file];
30
+ return ['powershell.exe', args];
31
+ };
32
+
33
+ // The script finds the directory it installs to on PATH and does not fail
34
+ const scriptEnv = (env) => {
35
+ const bin = localBin(env);
36
+ const key = Object.keys(env).find((name) => name.toUpperCase() === 'PATH');
37
+ const name = key ?? 'PATH';
38
+ const dirs = `${env[name] ?? ''}`.split(path.delimiter).filter(Boolean);
39
+ if (!bin || dirs.includes(bin)) return { ...env };
40
+ return { ...env, [name]: [bin, ...dirs].join(path.delimiter) };
41
+ };
42
+
43
+ const runFile = (cmd, args, env, spawnImpl) =>
44
+ new Promise((resolve, reject) => {
45
+ const stdio = ['ignore', 'inherit', 'inherit'];
46
+ const child = spawnImpl(cmd, args, { env, stdio, windowsHide: true });
47
+ child.on('error', reject);
48
+ child.on('close', (code) => resolve(code ?? 1));
49
+ });
50
+
51
+ const runScript = async (url, options = {}) => {
52
+ const fetchImpl = options.fetch ?? globalThis.fetch;
53
+ if (typeof fetchImpl !== 'function') {
54
+ throw new Error('fetch is not available');
55
+ }
56
+ const env = options.env ?? process.env;
57
+ const platform = options.platform ?? process.platform;
58
+ const log = options.log ?? (() => {});
59
+ const href = httpsUrl(url);
60
+ log(`Downloading ${href}…`);
61
+ const response = await fetchImpl(href);
62
+ if (!response || !response.ok) {
63
+ const status = response ? ` (${response.status})` : '';
64
+ throw new Error(`download failed${status}`);
65
+ }
66
+ const text = await response.text();
67
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'reslop-'));
68
+ try {
69
+ const name = platform === 'win32' ? 'install.ps1' : 'install.sh';
70
+ const file = path.join(dir, name);
71
+ await fs.promises.writeFile(file, text, { mode: 0o600 });
72
+ const [cmd, args] = shellOf(file, platform);
73
+ const spawnImpl = options.spawn ?? spawn;
74
+ return await runFile(cmd, args, scriptEnv(env), spawnImpl);
75
+ } finally {
76
+ await fs.promises.rm(dir, { recursive: true, force: true });
77
+ }
78
+ };
79
+
80
+ const main = async () => {
81
+ try {
82
+ process.exitCode = await runScript(process.argv[2], {
83
+ log: (line) => process.stdout.write(`${line}\n`),
84
+ });
85
+ } catch (error) {
86
+ process.stderr.write(`${error.message}\n`);
87
+ process.exitCode = 1;
88
+ }
89
+ };
90
+
91
+ if (require.main === module) void main();
92
+
93
+ module.exports = { localBin, scriptUrl, scriptEnv, runScript };
@@ -21,6 +21,7 @@ const { paintField, queryTones, fieldHit } = require('./field.js');
21
21
 
22
22
  const BUBBLE = '●';
23
23
  const BUBBLE_GAP = 1;
24
+ const COLUMN_GAP = 2;
24
25
  const BUBBLE_PHASE = ['key', 'add', 'sha', 'warn'];
25
26
  const PAD_W = 2;
26
27
  const TAIL = 2;
@@ -642,13 +643,29 @@ const agentLead = (item, frame) => {
642
643
  return cellSegs(segs);
643
644
  };
644
645
 
645
- const agentName = (item) => {
646
+ // The last column clips its names when the tile is narrow
647
+ const agentCells = (item, frame, last) => {
648
+ if (!item) return [];
646
649
  const tone = item.bin ? 'text' : 'muted';
647
- const name = flexCell(item.name, tone);
650
+ const name = last ? flexCell(item.name, tone) : cell(item.name, tone);
648
651
  name.gap = BUBBLE_GAP;
649
- return name;
652
+ return [agentLead(item, frame), name];
650
653
  };
651
654
 
655
+ // Two columns while both fit their widest agent, else one
656
+ const columnCount = (items, width) => {
657
+ let lead = 0;
658
+ let name = 0;
659
+ for (const item of items) {
660
+ const sessions = item.running ? ` ${item.running}` : '';
661
+ lead = Math.max(lead, visibleWidth(`${BUBBLE}${sessions}`));
662
+ name = Math.max(name, visibleWidth(item.name));
663
+ }
664
+ const entry = lead + BUBBLE_GAP + name;
665
+ return entry * 2 + COLUMN_GAP <= width ? 2 : 1;
666
+ };
667
+
668
+ // Agents fill the left column first
652
669
  const agentsBlock = (model, width, height, ctx, tile) => {
653
670
  const data = model.agents;
654
671
  if (!data || !data.ready) return waiting('detecting agents…');
@@ -656,13 +673,20 @@ const agentsBlock = (model, width, height, ctx, tile) => {
656
673
  const titleLine = titleAside(tile, aside, width);
657
674
  const items = data.items ?? [];
658
675
  if (!items.length) return notice('no agents', titleLine);
659
- const shown = items.slice(0, Math.max(0, height));
660
- const rows = shown.map((item) => [
661
- agentLead(item, ctx.frame),
662
- agentName(item),
663
- cell(item.bin ? item.model : 'missing', item.bin ? 'sha' : 'muted', 'r'),
664
- ]);
665
- return { titleLine, lines: tableLines(rows, width) };
676
+ const columns = columnCount(items, width);
677
+ const perColumn = Math.ceil(items.length / columns);
678
+ const count = Math.min(Math.max(0, height), perColumn);
679
+ const rows = [];
680
+ for (let i = 0; i < count; i++) {
681
+ const left = agentCells(items[i], ctx.frame, columns === 1);
682
+ if (columns === 1) {
683
+ rows.push(left);
684
+ continue;
685
+ }
686
+ const right = agentCells(items[i + count], ctx.frame, true);
687
+ rows.push([...left, ...right]);
688
+ }
689
+ return { titleLine, lines: tableLines(rows, width, COLUMN_GAP) };
666
690
  };
667
691
 
668
692
  module.exports = { paintBodyAgents, agentsBlock };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reslop",
3
- "version": "0.3.2",
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",