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 +14 -1
- package/lib/agents/agents.js +71 -2
- package/lib/agents/allow.json +2 -1
- package/lib/agents/models.js +15 -8
- package/lib/git/deps.js +29 -0
- package/lib/git/git.js +1 -1
- package/lib/git/worktree.js +36 -7
- package/lib/input/actions.js +3 -0
- package/lib/input/editor.js +9 -0
- package/lib/input/keys.js +9 -2
- package/lib/input/line-edit.js +3 -2
- package/lib/render/agents.js +14 -12
- package/lib/render/footer.js +2 -0
- package/lib/render/log.js +5 -4
- package/lib/report/render.js +4 -7
- package/lib/review/agent-instructions.md +11 -6
- package/lib/review/format.js +0 -6
- package/lib/session/agents/agents.js +19 -1
- package/lib/session/branches.js +0 -1
- package/lib/session/compose.js +7 -1
- package/lib/session/dispatch.js +1 -0
- package/lib/session/import.js +12 -1
- package/lib/session/lifecycle.js +3 -0
- package/lib/session/npm/history.js +1 -0
- package/lib/session/npm/npm.js +22 -1
- package/lib/session/session.js +2 -0
- package/lib/session/tasks.js +1 -1
- package/lib/term/select.js +2 -1
- package/package.json +1 -1
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.
|
|
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
|
package/lib/agents/agents.js
CHANGED
|
@@ -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
|
|
401
|
-
if (!
|
|
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);
|
package/lib/agents/allow.json
CHANGED
package/lib/agents/models.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
30
|
+
stagePath: gitDeps.stagePath,
|
|
31
31
|
commit: branches.commitChanges,
|
|
32
32
|
updateCommit: branches.updateCommit,
|
|
33
33
|
hasStaged: branches.hasStaged,
|
package/lib/git/worktree.js
CHANGED
|
@@ -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
|
|
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 = [
|
|
283
|
-
|
|
284
|
-
|
|
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) => {
|
package/lib/input/actions.js
CHANGED
|
@@ -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',
|
package/lib/input/editor.js
CHANGED
|
@@ -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
|
-
|
|
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]);
|
package/lib/input/line-edit.js
CHANGED
|
@@ -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]() {
|
package/lib/render/agents.js
CHANGED
|
@@ -501,16 +501,15 @@ const paintAgentLog = (view, width, color, bodyH) => {
|
|
|
501
501
|
return log;
|
|
502
502
|
};
|
|
503
503
|
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
package/lib/render/footer.js
CHANGED
|
@@ -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
|
-
|
|
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) => {
|
package/lib/report/render.js
CHANGED
|
@@ -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 = '
|
|
12
|
-
if (rawFile) target =
|
|
13
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
-
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
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
|
package/lib/review/format.js
CHANGED
|
@@ -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
|
|
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;
|
package/lib/session/branches.js
CHANGED
package/lib/session/compose.js
CHANGED
|
@@ -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 = [
|
|
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
|
|
package/lib/session/dispatch.js
CHANGED
|
@@ -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(),
|
package/lib/session/import.js
CHANGED
|
@@ -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.
|
|
20
|
+
const defaultUrl = ui.repositoryUrl || changeRepositoryUrl(ui.change);
|
|
21
|
+
ui.nav.import = lineField('url', defaultUrl);
|
|
11
22
|
ui.status = '';
|
|
12
23
|
};
|
|
13
24
|
|
package/lib/session/lifecycle.js
CHANGED
|
@@ -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);
|
package/lib/session/npm/npm.js
CHANGED
|
@@ -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
|
|
package/lib/session/session.js
CHANGED
|
@@ -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;
|
package/lib/session/tasks.js
CHANGED
|
@@ -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) {
|
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;
|