reslop 0.1.2 → 0.1.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/lib/session.js CHANGED
@@ -82,6 +82,8 @@ const itemFeedKey = (item) => {
82
82
  return feedbackKey({ file, oldStart, newStart, blockId });
83
83
  };
84
84
 
85
+ const isAbortError = (error) => !!(error && error.code === 'ABORT');
86
+
85
87
  const sameCursor = (a, b) => {
86
88
  if (!a && !b) return true;
87
89
  if (!a || !b) return false;
@@ -128,6 +130,10 @@ const HANDLERS = {
128
130
  quit: (session) => session.onQuit(),
129
131
  scrollUp: (session) => session.onScroll(-1),
130
132
  scrollDown: (session) => session.onScroll(1),
133
+ pageUp: (session) => session.onScroll(session.scrollStep(-1)),
134
+ pageDown: (session) => session.onScroll(session.scrollStep(1)),
135
+ halfUp: (session) => session.onScroll(session.scrollStep(-0.5)),
136
+ halfDown: (session) => session.onScroll(session.scrollStep(0.5)),
131
137
  };
132
138
 
133
139
  const KEY_MODE = {
@@ -233,6 +239,16 @@ class Session {
233
239
  this.saveTimer = null;
234
240
  this.blinkTimer = null;
235
241
  this.blinkOn = true;
242
+ this.loadGen = 0;
243
+ this.loadAbort = null;
244
+ this.loadPromise = null;
245
+ this.busy = '';
246
+ this.emptyReview = false;
247
+ this.loadError = null;
248
+ this.didLoad = false;
249
+ this.pendingExtras = false;
250
+ this.uiOpen = false;
251
+ this.finishReadLoop = null;
236
252
  }
237
253
 
238
254
  constructor(options) {
@@ -240,14 +256,35 @@ class Session {
240
256
  this.resetState();
241
257
  }
242
258
 
243
- load() {
244
- const here = itemKey(this.items[this.index]);
259
+ loadOptions(extra = {}) {
245
260
  const commit = this.rev;
246
261
  const audit = this.audit;
247
262
  const outdatedMap = this.outdatedMap;
248
263
  const auditMap = this.auditMap;
249
- const options = { commit, audit, outdatedMap, auditMap };
250
- const loaded = this.repo.load(this.cwd, this.paths, options);
264
+ return { commit, audit, outdatedMap, auditMap, ...extra };
265
+ }
266
+
267
+ shouldDeferExtras() {
268
+ if (!this.audit) return false;
269
+ if (this.outdatedMap !== undefined && this.auditMap !== undefined) {
270
+ return false;
271
+ }
272
+ return true;
273
+ }
274
+
275
+ bumpLoad() {
276
+ this.loadGen += 1;
277
+ if (this.loadAbort) this.loadAbort.abort();
278
+ this.loadAbort = new AbortController();
279
+ }
280
+
281
+ ensureRepo() {
282
+ if (!this.repo.toplevel) return;
283
+ this.top = this.repo.toplevel(this.cwd);
284
+ }
285
+
286
+ applyLoaded(loaded) {
287
+ const here = itemKey(this.items[this.index]);
251
288
  this.top = loaded.top ?? this.cwd;
252
289
  const items = loaded.items;
253
290
  this.items = items.filter((item) => !this.dismissed.has(itemKey(item)));
@@ -277,6 +314,121 @@ class Session {
277
314
  this.clampIndex();
278
315
  this.syncReviewPath();
279
316
  this.syncFileCursor();
317
+ this.didLoad = true;
318
+ this.pendingExtras = loaded.pending === true;
319
+ }
320
+
321
+ load() {
322
+ const loaded = this.repo.load(this.cwd, this.paths, this.loadOptions());
323
+ this.applyLoaded(loaded);
324
+ }
325
+
326
+ paint() {
327
+ if (!this.uiOpen) return;
328
+ this.draw();
329
+ }
330
+
331
+ viewStatus() {
332
+ if (this.status) return this.status;
333
+ return this.busy;
334
+ }
335
+
336
+ finishIfEmpty(kind) {
337
+ if (this.items.length) return;
338
+ if (this.notes && hasNotes(this.notes)) this.flushReview();
339
+ this.done = true;
340
+ this.exitCode = 0;
341
+ this.status = 'nothing to review';
342
+ this.emptyReview = kind === 'open';
343
+ if (this.finishReadLoop) this.finishReadLoop();
344
+ }
345
+
346
+ failOpen(error) {
347
+ if (isAbortError(error)) return;
348
+ this.loadError = error;
349
+ this.done = true;
350
+ this.exitCode = 1;
351
+ this.busy = '';
352
+ if (this.finishReadLoop) this.finishReadLoop();
353
+ }
354
+
355
+ async followExtras(loaded, gen, kind) {
356
+ if (!this.repo.loadExtras) {
357
+ this.busy = '';
358
+ this.pendingExtras = false;
359
+ this.paint();
360
+ this.finishIfEmpty(kind);
361
+ return;
362
+ }
363
+ const signal = this.loadAbort ? this.loadAbort.signal : undefined;
364
+ const extra = this.loadOptions({
365
+ paths: this.paths,
366
+ signal,
367
+ });
368
+ let next;
369
+ try {
370
+ next = await this.repo.loadExtras(loaded, extra);
371
+ } catch (error) {
372
+ if (gen !== this.loadGen) return;
373
+ this.failOpen(error);
374
+ return;
375
+ }
376
+ if (gen !== this.loadGen || this.done) return;
377
+ this.applyLoaded(next);
378
+ this.busy = '';
379
+ this.paint();
380
+ this.finishIfEmpty(kind);
381
+ }
382
+
383
+ openLoad() {
384
+ if (this.loadPromise) return this.loadPromise;
385
+ if (this.didLoad && !this.pendingExtras) {
386
+ this.loadPromise = Promise.resolve();
387
+ return this.loadPromise;
388
+ }
389
+ this.bumpLoad();
390
+ this.loadPromise = this.runOpenLoad();
391
+ return this.loadPromise;
392
+ }
393
+
394
+ async runOpenLoad() {
395
+ const gen = this.loadGen;
396
+ const signal = this.loadAbort.signal;
397
+ const options = this.loadOptions({
398
+ deferExtras: this.shouldDeferExtras(),
399
+ signal,
400
+ });
401
+ this.busy = 'loading';
402
+ this.paint();
403
+ let loaded;
404
+ try {
405
+ if (this.repo.loadAsync) {
406
+ loaded = await this.repo.loadAsync(this.cwd, this.paths, options);
407
+ } else {
408
+ loaded = this.repo.load(this.cwd, this.paths, options);
409
+ }
410
+ } catch (error) {
411
+ if (gen !== this.loadGen) return;
412
+ this.failOpen(error);
413
+ return;
414
+ }
415
+ if (gen !== this.loadGen || this.done) return;
416
+ this.applyLoaded(loaded);
417
+ if (loaded.pending) {
418
+ this.busy = 'checking npm';
419
+ this.paint();
420
+ await this.followExtras(loaded, gen, 'open');
421
+ return;
422
+ }
423
+ this.busy = '';
424
+ this.paint();
425
+ this.finishIfEmpty('open');
426
+ }
427
+
428
+ async loadReady() {
429
+ this.ensureRepo();
430
+ await this.openLoad();
431
+ if (this.loadError) throw this.loadError;
280
432
  }
281
433
 
282
434
  initReview() {
@@ -393,7 +545,7 @@ class Session {
393
545
  index: this.index,
394
546
  total: this.items.length,
395
547
  scroll: this.scroll,
396
- status: this.status,
548
+ status: this.viewStatus(),
397
549
  layout: this.layout,
398
550
  counts: this.counts(),
399
551
  selection: this.selection,
@@ -466,20 +618,25 @@ class Session {
466
618
 
467
619
  reloadAfterChange() {
468
620
  const here = this.index;
469
- this.load();
470
- if (!this.items.length) {
471
- if (hasNotes(this.notes)) this.flushReview();
472
- this.done = true;
473
- this.exitCode = 0;
474
- this.status = 'nothing to review';
475
- return;
476
- }
621
+ this.loadPromise = null;
622
+ this.bumpLoad();
623
+ const options = this.loadOptions({
624
+ deferExtras: this.shouldDeferExtras(),
625
+ });
626
+ const loaded = this.repo.load(this.cwd, this.paths, options);
627
+ this.applyLoaded(loaded);
477
628
  this.index = here;
478
629
  this.clampIndex();
479
630
  this.syncReviewPath();
480
631
  this.syncFileCursor();
481
632
  this.scroll = 0;
482
633
  this.clearSelection();
634
+ if (loaded.pending) {
635
+ this.busy = 'checking npm';
636
+ this.followExtras(loaded, this.loadGen, 'reload');
637
+ return;
638
+ }
639
+ this.finishIfEmpty('reload');
483
640
  }
484
641
 
485
642
  clearSelection() {
@@ -578,12 +735,13 @@ class Session {
578
735
  onFileAdd() {
579
736
  const items = this.fileActionItems();
580
737
  if (!this.guardFileGit(items)) return;
738
+ const rel = itemPath(items[0]);
581
739
  const targets = items.filter((item) => item.origin !== 'staged');
582
740
  if (!targets.length) {
583
741
  this.status = 'already staged';
742
+ this.selectNextFileAfter(rel);
584
743
  return;
585
744
  }
586
- const rel = itemPath(targets[0]);
587
745
  const proposed = targets.some((item) => needsReload(item));
588
746
  const ok = this.applyFileGit(
589
747
  targets,
@@ -598,12 +756,13 @@ class Session {
598
756
  onFileUnstage() {
599
757
  const items = this.fileActionItems();
600
758
  if (!this.guardFileGit(items)) return;
759
+ const rel = itemPath(items[0]);
601
760
  const targets = items.filter((item) => item.origin === 'staged');
602
761
  if (!targets.length) {
603
762
  this.status = 'not staged';
763
+ this.selectNextFileAfter(rel);
604
764
  return;
605
765
  }
606
- const rel = itemPath(targets[0]);
607
766
  const ok = this.applyFileGit(
608
767
  targets,
609
768
  'unstaged',
@@ -1267,13 +1426,12 @@ class Session {
1267
1426
  this.status = 'nothing to review';
1268
1427
  return;
1269
1428
  }
1270
- const next = this.fileCursor + delta;
1271
- if (next < 0) {
1272
- this.status = 'first file';
1273
- return;
1274
- }
1275
- if (next >= files.length) {
1276
- this.status = 'last file';
1429
+ const next = Math.min(
1430
+ files.length - 1,
1431
+ Math.max(0, this.fileCursor + delta),
1432
+ );
1433
+ if (next === this.fileCursor) {
1434
+ this.status = next <= 0 ? 'first file' : 'last file';
1277
1435
  return;
1278
1436
  }
1279
1437
  this.fileCursor = next;
@@ -1283,6 +1441,16 @@ class Session {
1283
1441
  this.status = '';
1284
1442
  }
1285
1443
 
1444
+ scrollStep(fraction) {
1445
+ const bodyH = this.lastFrame?.bodyH;
1446
+ const size = this.lastSize ?? this.getSize();
1447
+ const fallback = Math.max(1, (size.height ?? 24) - 4);
1448
+ const height = Math.max(1, bodyH ?? fallback);
1449
+ const n = Math.max(1, Math.floor(height * Math.abs(fraction)));
1450
+ if (fraction < 0) return -n;
1451
+ return n;
1452
+ }
1453
+
1286
1454
  onScroll(delta) {
1287
1455
  if (this.pane === 'files') {
1288
1456
  this.onFileMove(delta);
@@ -1428,12 +1596,20 @@ class Session {
1428
1596
  }
1429
1597
 
1430
1598
  async startUi() {
1599
+ if (this.didLoad && !this.items.length) {
1600
+ this.emptyReview = true;
1601
+ return 0;
1602
+ }
1603
+ this.uiOpen = true;
1431
1604
  this.enterTerm();
1432
1605
  try {
1606
+ this.openLoad();
1433
1607
  this.draw();
1434
1608
  await this.readLoop();
1435
1609
  } finally {
1610
+ this.uiOpen = false;
1436
1611
  this.leaveTerm();
1612
+ if (this.loadAbort) this.loadAbort.abort();
1437
1613
  }
1438
1614
  return this.exitCode;
1439
1615
  }
@@ -1442,7 +1618,10 @@ class Session {
1442
1618
  return new Promise((resolve) => {
1443
1619
  this.abortController = new AbortController();
1444
1620
  const { signal } = this.abortController;
1621
+ let settled = false;
1445
1622
  const finish = () => {
1623
+ if (settled) return;
1624
+ settled = true;
1446
1625
  if (this.saveTimer !== null) {
1447
1626
  this.clearInterval(this.saveTimer);
1448
1627
  this.saveTimer = null;
@@ -1452,8 +1631,14 @@ class Session {
1452
1631
  this.blinkTimer = null;
1453
1632
  }
1454
1633
  this.abortController.abort();
1634
+ this.finishReadLoop = null;
1455
1635
  resolve();
1456
1636
  };
1637
+ this.finishReadLoop = finish;
1638
+ if (this.done) {
1639
+ finish();
1640
+ return;
1641
+ }
1457
1642
  const onData = (chunk) => {
1458
1643
  this.pushInput(chunk.toString('utf8'));
1459
1644
  if (this.done) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reslop",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Interactive code review and repair planning for AI-assisted changes",
5
5
  "keywords": [
6
6
  "code-review",