reslop 0.3.0 → 0.3.1

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,18 @@
2
2
 
3
3
  ## [Unreleased][unreleased]
4
4
 
5
+ ## [0.3.1][] - 2026-10-13
6
+
7
+ - Allow agents to write to work with git
8
+ - Add default URL for tasks import in UI
9
+ - Implement session continue for supported agents
10
+ - Fix scroller in agents sessions list
11
+ - Multiple fixes in background agents and command runner
12
+ - Sync the lockfile when a `package.json` change is staged
13
+ - Update agent instructions to make commits
14
+ - Support Ctrl+Backspace word deletion in editors
15
+ - Support Shift-cursor selection in the tasks editor
16
+
5
17
  ## [0.3.0][] - 2026-10-12
6
18
 
7
19
  - Execute review results and plan in cli agents
@@ -187,7 +199,8 @@
187
199
  - Feedback and todos that produce a repair plan for an agent
188
200
  - Intra-line highlighting in unified, mixed, and side-by-side layouts
189
201
 
190
- [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.3.0...HEAD
202
+ [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.3.1...HEAD
203
+ [0.3.1]: https://github.com/tshemsedinov/reslop/compare/v0.3.0...v0.3.1
191
204
  [0.3.0]: https://github.com/tshemsedinov/reslop/compare/v0.2.7...v0.3.0
192
205
  [0.2.7]: https://github.com/tshemsedinov/reslop/compare/v0.2.6...v0.2.7
193
206
  [0.2.6]: https://github.com/tshemsedinov/reslop/compare/v0.2.5...v0.2.6
@@ -16,6 +16,7 @@ const LIST_MS = 5000;
16
16
  const AUTH_NEED = /authentication required/i;
17
17
  const PERMISSION_FAIL =
18
18
  /permissions?[ -]?(?:denied|denial)|denied permissions?/i;
19
+ const GITDIR_PREFIX = 'gitdir:';
19
20
 
20
21
  const plainEffort = (level) => level;
21
22
  const codexEffort = (level) => `model_reasoning_effort=${level}`;
@@ -370,6 +371,71 @@ const effortArgs = (spec, effort) => {
370
371
  return [spec.effortFlag, spec.effortValue(level)];
371
372
  };
372
373
 
374
+ const statOf = (file) => {
375
+ try {
376
+ return fs.statSync(file);
377
+ } catch {
378
+ return null;
379
+ }
380
+ };
381
+
382
+ const readFirstLine = (file) => {
383
+ try {
384
+ return fs.readFileSync(file, 'utf8').split('\n')[0] ?? '';
385
+ } catch {
386
+ return '';
387
+ }
388
+ };
389
+
390
+ const realPath = (file) => {
391
+ try {
392
+ return fs.realpathSync.native(file);
393
+ } catch {
394
+ return path.resolve(file);
395
+ }
396
+ };
397
+
398
+ const gitDirOf = (dot) => {
399
+ const line = readFirstLine(dot).trim();
400
+ if (!line.startsWith(GITDIR_PREFIX)) return '';
401
+ const raw = line.slice(GITDIR_PREFIX.length).trim();
402
+ if (!raw) return '';
403
+ return path.resolve(path.dirname(dot), raw);
404
+ };
405
+
406
+ const commonGitDir = (gitDir) => {
407
+ const raw = readFirstLine(path.join(gitDir, 'commondir')).trim();
408
+ if (!raw) return gitDir;
409
+ return path.resolve(gitDir, raw);
410
+ };
411
+
412
+ // workspace-write remounts .git read-only unless that path is itself writable.
413
+ const gitWriteRoots = (cwd) => {
414
+ if (!cwd) return [];
415
+ const dot = path.join(path.resolve(cwd), '.git');
416
+ const stat = statOf(dot);
417
+ if (!stat) return [];
418
+ if (stat.isDirectory()) return [realPath(dot)];
419
+ if (!stat.isFile()) return [];
420
+ const gitDir = gitDirOf(dot);
421
+ if (!gitDir) return [];
422
+ const roots = [realPath(gitDir), realPath(commonGitDir(gitDir))];
423
+ return [...new Set(roots)];
424
+ };
425
+
426
+ const tomlQuote = (text) => {
427
+ const escaped = text.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
428
+ return `"${escaped}"`;
429
+ };
430
+
431
+ const codexGitArgs = (cwd) => {
432
+ const roots = gitWriteRoots(cwd);
433
+ if (!roots.length) return [];
434
+ const listed = roots.map(tomlQuote).join(',');
435
+ const value = `sandbox_workspace_write.writable_roots=[${listed}]`;
436
+ return ['--sandbox', 'workspace-write', '-c', value];
437
+ };
438
+
373
439
  const paramPart = (key, value) => {
374
440
  const text = trimText(value);
375
441
  if (!text) return '';
@@ -397,8 +463,10 @@ const fastParam = (model, fast) => {
397
463
 
398
464
  const modelArg = (spec, resolved, context, fast) => {
399
465
  const parts = [paramPart('context', context)];
400
- const plain = spec.modelParams && !resolved.encoded;
401
- if (!plain) return withParams(resolved.model, parts);
466
+ const brackets = spec.modelParams && !resolved.encoded;
467
+ if (!brackets || resolved.params === false) {
468
+ return withParams(resolved.model, parts);
469
+ }
402
470
  const level = effortParam(resolved.model, resolved.effort);
403
471
  parts.push(paramPart('effort', level));
404
472
  parts.push(paramPart('fast', fastParam(resolved.model, fast)));
@@ -446,6 +514,7 @@ const buildLaunch = (row, choice, reviewPath, cwd, sessionId = '') => {
446
514
  if (!spec.modelParams && !resolved.encoded) {
447
515
  args.push(...effortArgs(spec, resolved.effort));
448
516
  }
517
+ if (spec.id === 'codex') args.push(...codexGitArgs(cwd));
449
518
  args.push(...splitArgs(picked.extra));
450
519
  if (spec.promptFlag) args.push(spec.promptFlag);
451
520
  args.push(prompt);
@@ -11,6 +11,7 @@
11
11
  "Shell(node)",
12
12
  "Shell(node --test)",
13
13
  "Shell(reslop)",
14
- "Shell(reslop t)"
14
+ "Shell(reslop t)",
15
+ "Shell(git)"
15
16
  ]
16
17
  }
@@ -342,16 +342,17 @@ const fitEffort = (levels, effort) => {
342
342
  return named ?? levels[0];
343
343
  };
344
344
 
345
- const plainResolved = (model, effort) => ({
345
+ const plainResolved = (model, effort, params = true) => ({
346
346
  model,
347
347
  encoded: false,
348
348
  effort,
349
349
  family: model,
350
350
  levels: [],
351
+ params,
351
352
  });
352
353
 
353
- const entryResolved = (entry, effort) => {
354
- if (!entry.encoded) return plainResolved(entry.model, effort);
354
+ const entryResolved = (entry, effort, params = true) => {
355
+ if (!entry.encoded) return plainResolved(entry.model, effort, params);
355
356
  const level = fitEffort(entry.levels, effort);
356
357
  return {
357
358
  model: entry.ids.get(level) ?? entry.model,
@@ -359,6 +360,7 @@ const entryResolved = (entry, effort) => {
359
360
  effort: level,
360
361
  family: entry.model,
361
362
  levels: entry.levels,
363
+ params: false,
362
364
  };
363
365
  };
364
366
 
@@ -371,6 +373,8 @@ class ModelCatalog {
371
373
  for (const entry of this.entries) {
372
374
  if (!this.byName.has(entry.model)) this.byName.set(entry.model, entry);
373
375
  }
376
+ const encoded = this.entries.some((entry) => entry.encoded);
377
+ this.bracketParams = !encoded;
374
378
  }
375
379
 
376
380
  find(name) {
@@ -391,14 +395,16 @@ class ModelCatalog {
391
395
 
392
396
  resolve(choice) {
393
397
  const effort = trimText(choice.effort) || 'default';
398
+ const params = this.bracketParams;
394
399
  const entry = this.find(choice.model);
395
400
  const prefixed = this.prefixed(entry, choice.model);
396
- if (prefixed) return entryResolved(prefixed, effort);
397
- if (entry) return entryResolved(entry, effort);
401
+ if (prefixed) return entryResolved(prefixed, effort, params);
402
+ if (entry) return entryResolved(entry, effort, params);
398
403
  const part = splitEffortModel(choice.model);
399
404
  const family = part ? this.find(modelLabel(part)) : null;
400
- if (!family) return plainResolved(choice.model, effort);
401
- return entryResolved(family, effort === 'default' ? part.effort : effort);
405
+ if (!family) return plainResolved(choice.model, effort, params);
406
+ const level = effort === 'default' ? part.effort : effort;
407
+ return entryResolved(family, level, params);
402
408
  }
403
409
 
404
410
  withFast(resolved, fast) {
@@ -407,7 +413,8 @@ class ModelCatalog {
407
413
  if (!family || (fast === true) === onFast) return resolved;
408
414
  const other = onFast ? withoutFast(family) : `${family}${FAST}`;
409
415
  const entry = this.find(other);
410
- return entry ? entryResolved(entry, resolved.effort) : resolved;
416
+ const params = resolved.params;
417
+ return entry ? entryResolved(entry, resolved.effort, params) : resolved;
411
418
  }
412
419
 
413
420
  hasWide(wide, model) {
package/lib/git/deps.js CHANGED
@@ -272,6 +272,27 @@ const finishPlan = async (top, plan, result, stage = stagePlan) => {
272
272
  await stage(top, plan);
273
273
  };
274
274
 
275
+ const manifestPlan = (top, rel) => {
276
+ const meta = depFileMeta(rel);
277
+ if (!meta || meta.kind !== 'manifest') return null;
278
+ return {
279
+ cwd: meta.dir === '.' ? top : path.join(top, meta.dir),
280
+ npmArgs: ['i'],
281
+ fail: 'npm i failed',
282
+ lockOnly: false,
283
+ lockPath: relOf(meta.dir, LOCKFILE),
284
+ manifest: rel,
285
+ };
286
+ };
287
+
288
+ const syncManifest = async (top, rel, extra = {}) => {
289
+ const plan = manifestPlan(top, rel);
290
+ if (!plan) return;
291
+ const install = extra.run ?? runNpm;
292
+ const result = await install(plan.cwd, plan.npmArgs);
293
+ await finishPlan(top, plan, result, extra.stage);
294
+ };
295
+
275
296
  const restoreOwned = (top, owned) => {
276
297
  for (const saved of owned) restoreFile(top, saved);
277
298
  };
@@ -320,6 +341,7 @@ const applyDepChange = async (top, item, mode, extra) => {
320
341
  const addItem = async (top, item, extra) => {
321
342
  if (!item.dep) {
322
343
  await worktree.addItem(top, item);
344
+ await syncManifest(top, itemPath(item), extra);
323
345
  return;
324
346
  }
325
347
  if (isStagedOrCommit(item)) return;
@@ -331,6 +353,11 @@ const addItem = async (top, item, extra) => {
331
353
  if (rels.length) await gitOk(['add', '--', ...rels], top);
332
354
  };
333
355
 
356
+ const stagePath = async (top, rel, extra) => {
357
+ await worktree.stagePath(top, rel);
358
+ await syncManifest(top, rel, extra);
359
+ };
360
+
334
361
  const unstageItem = async (top, item) => {
335
362
  if (!item.dep) {
336
363
  await worktree.unstageItem(top, item);
@@ -381,7 +408,9 @@ module.exports = {
381
408
  restoreFile,
382
409
  proposedNpmPlan,
383
410
  applyProposedUpdate,
411
+ syncManifest,
384
412
  addItem,
413
+ stagePath,
385
414
  unstageItem,
386
415
  revertItem,
387
416
  revertFile,
package/lib/git/git.js CHANGED
@@ -27,7 +27,7 @@ const REPO_METHODS = {
27
27
  revertFile: gitDeps.revertFile,
28
28
  edit: worktree.editItem,
29
29
  writeFile: worktree.writeFile,
30
- stagePath: worktree.stagePath,
30
+ stagePath: gitDeps.stagePath,
31
31
  commit: branches.commitChanges,
32
32
  updateCommit: branches.updateCommit,
33
33
  hasStaged: branches.hasStaged,
@@ -266,22 +266,50 @@ const branchName = (result) => {
266
266
  };
267
267
 
268
268
  const BRANCH_ARGS = ['rev-parse', '--abbrev-ref', 'HEAD'];
269
+ const REMOTE_ARGS = ['config', '--get', 'remote.origin.url'];
269
270
 
270
271
  const currentBranch = async (top) => branchName(await runGit(BRANCH_ARGS, top));
271
272
 
272
- const worktreeSnapshot = (top, outputs, untracked, branch) => {
273
+ const repositoryUrl = (result) => {
274
+ if (result.status !== 0) return '';
275
+ let value = result.stdout.trim();
276
+ const scp = /^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/.exec(value);
277
+ if (scp && !value.includes('://')) value = `https://${scp[1]}/${scp[2]}`;
278
+ else if (value.startsWith('git+')) value = value.slice(4);
279
+ let url;
280
+ try {
281
+ url = new URL(value);
282
+ } catch {
283
+ return '';
284
+ }
285
+ if (!['http:', 'https:', 'git:', 'ssh:'].includes(url.protocol)) return '';
286
+ url.protocol = 'https:';
287
+ url.username = '';
288
+ url.password = '';
289
+ if (url.port === '22') url.port = '';
290
+ url.search = '';
291
+ url.hash = '';
292
+ url.pathname = url.pathname.replace(/\.git\/?$/i, '').replace(/\/$/, '');
293
+ return url.toString().replace(/\/$/, '');
294
+ };
295
+
296
+ const worktreeSnapshot = (top, outputs, untracked, branch, repoUrl) => {
273
297
  const staged = itemsFromFiles(parseDiff(outputs[0]), 'staged');
274
298
  const unstaged = itemsFromFiles(parseDiff(outputs[1]), 'unstaged');
275
299
  const grouped = groupByPath([...staged, ...unstaged, ...untracked]);
276
300
  const parsed = dropReviewItems([...grouped.values()].flat());
277
- return { top, parsed, branch };
301
+ return { top, parsed, branch, repositoryUrl: repoUrl };
278
302
  };
279
303
 
280
304
  const snapshotCommit = async (top, commit, files) => {
281
305
  const parsed = dropReviewItems(itemsFromFiles(files, 'commit'));
282
- const names = [shortRev(top, commit), currentBranch(top)];
283
- const [revShort, branch] = await Promise.all(names);
284
- return { top, parsed, rev: commit, revShort, branch };
306
+ const names = [
307
+ shortRev(top, commit),
308
+ currentBranch(top),
309
+ runGit(REMOTE_ARGS, top).then(repositoryUrl),
310
+ ];
311
+ const [revShort, branch, repoUrl] = await Promise.all(names);
312
+ return { top, parsed, rev: commit, revShort, branch, repositoryUrl: repoUrl };
285
313
  };
286
314
 
287
315
  function* commitDiff(commit, paths) {
@@ -303,14 +331,15 @@ const readSnapshot = async (cwd, paths, options) => {
303
331
  const files = await runSteps(commitDiff(rev, paths), top, signal);
304
332
  return snapshotCommit(top, rev, files);
305
333
  }
306
- const commands = [...worktreeCommands(paths), BRANCH_ARGS];
334
+ const commands = [...worktreeCommands(paths), BRANCH_ARGS, REMOTE_ARGS];
307
335
  const running = commands.map((args) => runGit(args, top, { signal }));
308
336
  const results = await Promise.all(running);
337
+ const repoUrl = repositoryUrl(results.pop());
309
338
  const branch = branchName(results.pop());
310
339
  const outputs = results.map((result) => requireOk(result).stdout);
311
340
  const names = namesFromLs(outputs[2]);
312
341
  const untracked = await untrackedItems(top, names);
313
- return worktreeSnapshot(top, outputs, untracked, branch);
342
+ return worktreeSnapshot(top, outputs, untracked, branch, repoUrl);
314
343
  };
315
344
 
316
345
  const applyPatch = async (cwd, patch, flags) => {
@@ -69,6 +69,7 @@ const AGENT_ACTIONS = {
69
69
  agentFast: ['a'],
70
70
  agentContext: ['t'],
71
71
  agentReview: ['p'],
72
+ agentContinue: ['c'],
72
73
  agentStop: ['s'],
73
74
  agentRerun: ['r'],
74
75
  };
@@ -79,6 +80,7 @@ const AGENT_LIST_ACTIONS = {
79
80
  agentFast: AGENT_ACTIONS.agentFast,
80
81
  agentContext: AGENT_ACTIONS.agentContext,
81
82
  agentReview: AGENT_ACTIONS.agentReview,
83
+ agentContinue: AGENT_ACTIONS.agentContinue,
82
84
  };
83
85
 
84
86
  const DASH_TILES = [
@@ -178,6 +180,7 @@ const ACTION_LABEL = {
178
180
  agentFast: 'fast',
179
181
  agentContext: 'context',
180
182
  agentReview: 'plan',
183
+ agentContinue: 'continue',
181
184
  agentStop: 'stop',
182
185
  agentRerun: 're-run',
183
186
  quit: 'q',
@@ -139,6 +139,15 @@ class Editor {
139
139
  this.edit(next, this.cursor - size);
140
140
  }
141
141
 
142
+ backspaceWord() {
143
+ this.wantCol = null;
144
+ if (this.removeSpan()) return;
145
+ const { parts, i } = graphemeIndex(this.text, this.cursor);
146
+ const start = offsetAt(parts, wordIndex(parts, i, -1));
147
+ if (start === this.cursor) return;
148
+ this.edit(this.text.slice(0, start) + this.text.slice(this.cursor), start);
149
+ }
150
+
142
151
  delete() {
143
152
  this.wantCol = null;
144
153
  if (this.removeSpan()) return;
package/lib/input/keys.js CHANGED
@@ -45,6 +45,10 @@ const CSI_TILDE = {
45
45
  24: 'f12',
46
46
  };
47
47
 
48
+ const CSI_U = {
49
+ 127: 'backspace',
50
+ };
51
+
48
52
  const CSI_MAX = 16;
49
53
 
50
54
  const LITERAL_KEY = {
@@ -71,11 +75,12 @@ const keyAt = (key, end) => ({ event: { type: 'key', key }, end });
71
75
  const modifiedKey = (base, modText) => {
72
76
  const arrow = ARROW_KEYS.includes(base);
73
77
  const end = EDGE_KEYS.includes(base);
74
- if (!arrow && !end) return base;
75
78
  const mod = parseInt(modText, 10);
76
79
  const bits = Number.isFinite(mod) ? mod - 1 : 0;
77
80
  const ctrl = (bits & 4) !== 0;
78
81
  const shift = (bits & 1) !== 0;
82
+ if (base === 'backspace' && ctrl) return 'ctrl-backspace';
83
+ if (!arrow && !end) return base;
79
84
  if (ctrl && shift && arrow) return `ctrl-shift-${base}`;
80
85
  if (ctrl && arrow) return `ctrl-${base}`;
81
86
  if (shift) return `shift-${base}`;
@@ -84,7 +89,9 @@ const modifiedKey = (base, modText) => {
84
89
 
85
90
  const keyFromCsi = (paramsText, final) => {
86
91
  const params = paramsText ? paramsText.split(';') : [];
87
- const base = final === '~' ? CSI_TILDE[params[0]] : CSI_KEYS[final];
92
+ let base = CSI_KEYS[final];
93
+ if (final === '~') base = CSI_TILDE[params[0]];
94
+ if (final === 'u') base = CSI_U[params[0]];
88
95
  if (!base) return null;
89
96
  if (params.length < 2) return base;
90
97
  return modifiedKey(base, params[1]);
@@ -33,6 +33,7 @@ const EDIT_OP = {
33
33
  'ctrl-shift-left': (editor) => editor.moveWord(-1, true),
34
34
  'ctrl-shift-right': (editor) => editor.moveWord(1, true),
35
35
  backspace: (editor) => editor.backspace(),
36
+ 'ctrl-backspace': (editor) => editor.backspaceWord(),
36
37
  delete: (editor) => editor.delete(),
37
38
  };
38
39
 
@@ -64,8 +65,8 @@ const lineKey = (editor, key, ui, ends) => {
64
65
  return editor.text === before ? 'caret' : 'text';
65
66
  };
66
67
 
67
- const lineField = (name) => {
68
- const editor = new Editor('');
68
+ const lineField = (name, text = '') => {
69
+ const editor = new Editor(text);
69
70
  return {
70
71
  editor,
71
72
  get [name]() {
@@ -501,16 +501,15 @@ const paintAgentLog = (view, width, color, bodyH) => {
501
501
  return log;
502
502
  };
503
503
 
504
- const clipRunHits = (hits, width) => {
505
- const last = Math.max(0, width - 1);
506
- return hits.map((hit) => {
507
- if (hit.side !== 'runs') return hit;
508
- const x1 = Math.min(hit.x1 ?? last, last);
509
- return { ...hit, x1 };
510
- });
511
- };
512
-
513
- const withRunScroller = (painted, count, start, bodyH, width, color) => {
504
+ const withRunScroller = (
505
+ painted,
506
+ count,
507
+ start,
508
+ bodyH,
509
+ width,
510
+ color,
511
+ runCursor,
512
+ ) => {
514
513
  const pad = bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
515
514
  const rows = Math.max(1, bodyH - pad * 2);
516
515
  const overflow = count > rows;
@@ -520,9 +519,11 @@ const withRunScroller = (painted, count, start, bodyH, width, color) => {
520
519
  let mark = '';
521
520
  if (overflow && onText) mark = scrollMark(at, count, rows, start);
522
521
  if (overflow && !onText) mark = 'track';
523
- return withLogScroller(line, mark, width, color);
522
+ const selected = onText && runCursor === start + at;
523
+ const rowBg = selected ? THEME.checkBg : null;
524
+ return withLogScroller(line, mark, width, color, rowBg);
524
525
  });
525
- const fileHits = clipRunHits(painted.fileHits, width);
526
+ const fileHits = painted.fileHits;
526
527
  if (!overflow) return { ...painted, body, fileHits };
527
528
  const scrollBar = {
528
529
  col: width,
@@ -599,6 +600,7 @@ const paintBodyAgents = (view, width, color, bodyH, headerLines) => {
599
600
  bodyH,
600
601
  width,
601
602
  color,
603
+ runCursor,
602
604
  );
603
605
  };
604
606
 
@@ -298,6 +298,7 @@ const agentListDim = (view) => {
298
298
  if (!installed) off.push('agentFast');
299
299
  if (!installed || !sizes.length) off.push('agentContext');
300
300
  if ((view.agentPlanCount ?? 0) < 2) off.push('agentReview');
301
+ if (!view.agentCanContinue) off.push('agentContinue');
301
302
  return off;
302
303
  };
303
304
 
@@ -347,6 +348,7 @@ const FOOTER_EXTRA = {
347
348
  'agentFast',
348
349
  'agentContext',
349
350
  'agentReview',
351
+ 'agentContinue',
350
352
  ],
351
353
  tasks: ['plan', 'import', 'drop', 'check', 'taskNudge'],
352
354
  };
package/lib/render/log.js CHANGED
@@ -263,14 +263,15 @@ const paintLogRow = (view, lines, index, width, color) => {
263
263
  return fill(width, THEME.ctxFg, THEME.ctxBg, color);
264
264
  };
265
265
 
266
- const paintScrollCell = (mark, color) => {
267
- const bgRgb = mark === 'thumb' ? THEME.taskHeadBg : THEME.buttonBg;
266
+ const paintScrollCell = (mark, color, rowBg) => {
267
+ let bgRgb = rowBg ?? THEME.buttonBg;
268
+ if (mark === 'thumb') bgRgb = THEME.taskHeadBg;
268
269
  return paint(' ', THEME.chromeFg, bgRgb, color);
269
270
  };
270
271
 
271
- const withLogScroller = (line, mark, width, color) => {
272
+ const withLogScroller = (line, mark, width, color, rowBg) => {
272
273
  const text = clipAnsi(line, Math.max(0, width - 1));
273
- return `${text}${paintScrollCell(mark, color)}`;
274
+ return `${text}${paintScrollCell(mark, color, rowBg)}`;
274
275
  };
275
276
 
276
277
  const paintNpmLog = (view, width, color, bodyH) => {
@@ -3,15 +3,12 @@
3
3
  const { unquote } = require('../common/utilities.js');
4
4
  const { formatTable } = require('../runs/output.js');
5
5
 
6
- const NOTICE_HEAD =
7
- 'reslop: reduced output; passing results are omitted ' +
8
- 'and similar problems are grouped';
6
+ const NOTICE_HEAD = 'Reduced output;';
9
7
 
10
8
  const notice = (rawFile) => {
11
- let target = 'the matching raw log under `.log/`';
12
- if (rawFile) target = `the raw log \`.log/${rawFile}\``;
13
- const hint = `Agent: if this report hides a needed detail, read ${target}.`;
14
- return `${NOTICE_HEAD}\n${hint}`;
9
+ let target = 'under `.log/`';
10
+ if (rawFile) target = `\`.log/${rawFile}\``;
11
+ return `${NOTICE_HEAD} the raw log if needed ${target}.`;
15
12
  };
16
13
 
17
14
  const addRow = (rows, key, value) => {
@@ -1,9 +1,14 @@
1
- - Execute reviews with `status` `editing, partial`
2
- - Do not start or change `done` review files
3
- - Work through unchecked items. After finishing one, mark it `[x]`
4
- - Mark `[x]` only near the end, after code changes are done and tests pass
5
- - For `code` items, replace added lines at that location
6
- - Set `status` to `partial` if some items remain, or `done` if all are `[x]`
1
+ Work through unchecked items.
2
+
3
+ - Run reviews with status `editing` or `partial`
4
+ - Do not start or change reviews with status `done`
5
+ - After finishing an item, mark it `[x]`
6
+ - Mark `[x]` only after the code changes are done and tests pass
7
+ - For `code` items, replace the added lines at the given location
8
+ - Set `status` to `partial` if any items remain, or `done` if all are `[x]`
7
9
  - Do not redo items already marked `[x]`
8
10
  - Run the full check with `npm t`
9
11
  - Run specific test files with `reslop t -- node --test <files>`
12
+ - An item prefixed with `F<number>:` gets its own commit in this repo
13
+ - Leave items without that prefix unstaged, do not commit or stage them
14
+ - Do not add `Authored-by` or `Co-authored-by` unless the issue asks for it
@@ -58,10 +58,6 @@ const TASK_TITLES = Object.fromEntries([
58
58
  ...TASK_KINDS.map((kind) => [kind.title, kind.id]),
59
59
  ]);
60
60
 
61
- const AGENT_BRIEF =
62
- 'Follow the agent instructions. Work through unchecked items. ' +
63
- 'Mark [x] only near exit after tests pass.';
64
-
65
61
  const AGENT_INSTRUCTIONS = resource(__dirname, 'agent-instructions.md').trim();
66
62
 
67
63
  const canonicalStatus = (raw) => {
@@ -179,8 +175,6 @@ const reviewHeader = (notes) => {
179
175
  '',
180
176
  '## Agent instructions',
181
177
  '',
182
- AGENT_BRIEF,
183
- '',
184
178
  AGENT_INSTRUCTIONS,
185
179
  '',
186
180
  ];
@@ -91,6 +91,7 @@ const NO_LIST = {
91
91
  agentRunCursor: 0,
92
92
  agentRunScroll: 0,
93
93
  agentFocus: 'cli',
94
+ agentCanContinue: false,
94
95
  agentPlanCount: 0,
95
96
  agentView: false,
96
97
  };
@@ -378,12 +379,19 @@ class AgentsController extends RunPanel {
378
379
 
379
380
  listView(onAgents) {
380
381
  if (!onAgents) return NO_LIST;
382
+ const agentRuns = this.sessionRows();
383
+ const selected = agentRuns[this.runCursor];
384
+ const agentCanContinue =
385
+ this.focus === 'runs' &&
386
+ Boolean(continuedId(selected)) &&
387
+ selected.status !== 'running';
381
388
  return {
382
389
  agents: this.rows(),
383
- agentRuns: this.sessionRows(),
390
+ agentRuns,
384
391
  agentRunCursor: this.runCursor,
385
392
  agentRunScroll: this.runScroll,
386
393
  agentFocus: this.focus,
394
+ agentCanContinue,
387
395
  agentPlanCount: this.reviewChoices().length,
388
396
  agentView: this.viewing,
389
397
  };
@@ -938,6 +946,16 @@ class AgentsController extends RunPanel {
938
946
  this.beginStart(row, continuedId(session));
939
947
  }
940
948
 
949
+ continueSession() {
950
+ if (this.viewing || this.pick || this.focus !== 'runs') return;
951
+ const session = this.selectedSession();
952
+ const id = continuedId(session);
953
+ if (!id || session.status === 'running') return;
954
+ const row = this.prepareStart();
955
+ if (!row) return;
956
+ this.beginStart(row, id);
957
+ }
958
+
941
959
  selectedJob() {
942
960
  const row = this.selected();
943
961
  return row ? this.runningJob(row.id) : null;
@@ -111,7 +111,6 @@ class BranchController {
111
111
  const after = (extra) => this.refreshAfterWrite(extra);
112
112
  const leave = () => {
113
113
  ui.composer.closeCompose();
114
- ui.showFiles();
115
114
  };
116
115
  const done = `created ${name}`;
117
116
  ui.ops.runWrite(
@@ -20,7 +20,13 @@ const unit = require('./unit.js');
20
20
  const clipboard = require('../term/clipboard.js');
21
21
  const { EDIT_OP, copyLine, cutLine } = require('../input/line-edit.js');
22
22
 
23
- const COMPOSE_EDITS = ['backspace', 'delete', 'ctrl-z', 'ctrl-y'];
23
+ const COMPOSE_EDITS = [
24
+ 'backspace',
25
+ 'ctrl-backspace',
26
+ 'delete',
27
+ 'ctrl-z',
28
+ 'ctrl-y',
29
+ ];
24
30
  const COMPOSE_CLIP = ['ctrl-x', 'ctrl-v'];
25
31
  const COMPOSE_LEAVE = ['next', 'prev', 'tasks'];
26
32
 
@@ -116,6 +116,7 @@ const HANDLERS = {
116
116
  agentFast: (ui) => ui.agents.toggleFast(),
117
117
  agentContext: (ui) => ui.agents.openPick('context'),
118
118
  agentReview: (ui) => ui.agents.openPick('plan'),
119
+ agentContinue: (ui) => ui.agents.continueSession(),
119
120
  agentStop: (ui) => ui.agents.stop(),
120
121
  agentRerun: (ui) => ui.agents.rerun(),
121
122
  quit: (ui) => ui.onQuit(),
@@ -3,11 +3,22 @@
3
3
  const { remoteKind, selectChangeSource } = require('../source/source.js');
4
4
  const { lineKey, lineField } = require('../input/line-edit.js');
5
5
 
6
+ const changeRepositoryUrl = (change) => {
7
+ if (!change?.url || !change.repository) return '';
8
+ try {
9
+ const url = new URL(change.url);
10
+ return `${url.origin}/${change.repository}`;
11
+ } catch {
12
+ return '';
13
+ }
14
+ };
15
+
6
16
  const startImport = (ui) => {
7
17
  if (ui.mode !== 'review' || ui.pane !== 'tasks') return;
8
18
  ui.composer.tasks.closePlanMenu();
9
19
  ui.mode = 'import';
10
- ui.nav.import = lineField('url');
20
+ const defaultUrl = ui.repositoryUrl || changeRepositoryUrl(ui.change);
21
+ ui.nav.import = lineField('url', defaultUrl);
11
22
  ui.status = '';
12
23
  };
13
24
 
@@ -169,6 +169,9 @@ class Lifecycle {
169
169
 
170
170
  applySource(loaded) {
171
171
  const ui = this.ui;
172
+ if (loaded.repositoryUrl !== undefined) {
173
+ ui.repositoryUrl = loaded.repositoryUrl || '';
174
+ }
172
175
  if (loaded.rev) ui.rev = loaded.rev;
173
176
  if (loaded.revShort) ui.revShort = loaded.revShort;
174
177
  else if (ui.rev && !ui.revShort) ui.revShort = ui.rev.slice(0, 7);
@@ -189,6 +189,7 @@ const recordedItem = (record) => {
189
189
  return {
190
190
  name,
191
191
  kind: script ? 'script' : 'command',
192
+ script,
192
193
  command,
193
194
  status: externalStatus(record),
194
195
  exit: externalExit(record),
@@ -239,6 +239,7 @@ class NpmController extends RunPanel {
239
239
  const run = newRun(this.nextId, {
240
240
  name: item.name,
241
241
  kind: item.kind,
242
+ script: item.script ?? '',
242
243
  command: item.command ?? '',
243
244
  label: item.kind === 'command' ? historyName(item) : runLabel(item),
244
245
  status: item.status,
@@ -321,6 +322,25 @@ class NpmController extends RunPanel {
321
322
  this.runs = [...done, ...running];
322
323
  }
323
324
 
325
+ followAdded(added) {
326
+ const current = this.viewedRun();
327
+ if (!this.viewing || !current?.externalId || !current.script) return false;
328
+ if (current.followEnd === false) return false;
329
+ const next = added
330
+ .filter(
331
+ (run) =>
332
+ run.script === current.script &&
333
+ run.startedAt >= current.startedAt &&
334
+ sameWindow(current, run),
335
+ )
336
+ .sort((left, right) => left.startedAt - right.startedAt)
337
+ .at(-1);
338
+ if (!next) return false;
339
+ this.viewId = next.id;
340
+ this.focusRun(next);
341
+ return true;
342
+ }
343
+
324
344
  followExternal() {
325
345
  this.syncProgress();
326
346
  if (this.ui.nav.pane === 'npm') this.ui.paint();
@@ -337,8 +357,9 @@ class NpmController extends RunPanel {
337
357
  const added = [];
338
358
  let changed = this.absorbRecorded(recorded, this.knownLogs(), added);
339
359
  if (this.settleLost(recorded)) changed = true;
340
- if (this.hydrate(this.viewedRun())) changed = true;
341
360
  this.storeAdded(added);
361
+ if (this.followAdded(added)) changed = true;
362
+ if (this.hydrate(this.viewedRun())) changed = true;
342
363
  if (changed) this.followExternal();
343
364
  }
344
365
 
@@ -183,6 +183,7 @@ class Session {
183
183
  this.workspace.reset();
184
184
  this.top = this.cwd;
185
185
  this.branch = '';
186
+ this.repositoryUrl = '';
186
187
  this.status = '';
187
188
  this.layout = 'unified';
188
189
  this.lineNumbers = false;
@@ -384,6 +385,7 @@ class Session {
384
385
  this.paths = [];
385
386
  this.repoName = repoName;
386
387
  this.branch = '';
388
+ this.repositoryUrl = '';
387
389
  this.rev = null;
388
390
  this.revShort = '';
389
391
  this.change = null;
@@ -430,7 +430,7 @@ class TaskPage {
430
430
  taskEditView() {
431
431
  const { composeKind, editor } = this.composer;
432
432
  if (composeKind !== 'tasks' || !editor) return null;
433
- return { cursor: editor.cursor };
433
+ return { cursor: editor.cursor, anchor: editor.anchor };
434
434
  }
435
435
 
436
436
  commitTask(text) {
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reslop",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Interactive code review and repair planning for AI-assisted changes",
5
5
  "keywords": [
6
6
  "code-review",