reslop 0.1.4 → 0.1.5

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
@@ -12,17 +12,16 @@ const { copyText } = require('./clipboard.js');
12
12
  const { watchResize } = require('./sys.js');
13
13
  const { fileEntries, itemPath } = require('./files.js');
14
14
  const { Editor } = require('./editor.js');
15
- const { refreshIndexPatches } = require('./diff.js');
15
+ const { refreshIndexPatches, blockAddText } = require('./diff.js');
16
16
 
17
17
  const { renderFrame, presentRows, presentCursor } = render;
18
- const { noteInnerWidth, todoInnerWidth } = render;
19
- const { QUIT_FINISH, QUIT_CONTINUE } = render;
18
+ const { noteInnerWidth, todoInnerWidth, codeInnerWidth } = render;
20
19
  const { actionFromKey, hitAction, decodeChunk } = keys;
21
- const { FILES_DISABLED } = keys;
20
+ const { disabledActions } = keys;
22
21
  const { cursorInWrap, wrapDoc, wrapMove } = wrap;
23
22
  const { AUTOSAVE_MS, listReviewNames, loadTemplates } = review;
24
23
  const { resolveReviewPath, loadReview, createStore } = review;
25
- const { noteCounts, hasNotes, setFeedback, addTodo } = review;
24
+ const { noteCounts, hasNotes, setFeedback, setCode, addTodo } = review;
26
25
  const { applyImportedNotes } = review;
27
26
  const { setTodoText, removeTodo, flushReview, mergeTodos } = review;
28
27
  const { feedbackKey, checkLabel, rankedTemplates } = review;
@@ -87,6 +86,13 @@ const itemFeedKey = (item) => {
87
86
  return feedbackKey({ file, oldStart, newStart, blockId });
88
87
  };
89
88
 
89
+ const canEditCode = (item) => {
90
+ if (!item || item.origin === 'todo') return false;
91
+ if (!item.hunk || !item.file) return false;
92
+ if (item.file.isBinary) return false;
93
+ return true;
94
+ };
95
+
90
96
  const isAbortError = (error) => !!(error && error.code === 'ABORT');
91
97
 
92
98
  const sameCursor = (a, b) => {
@@ -96,12 +102,26 @@ const sameCursor = (a, b) => {
96
102
  };
97
103
 
98
104
  const QUIT_CONFIRM = {
99
- [QUIT_FINISH]: (session) => session.finishQuit('ready'),
100
- [QUIT_CONTINUE]: (session) => session.finishQuit('editing'),
105
+ f: (session) => session.finishQuit('ready'),
106
+ c: (session) => session.finishQuit('editing'),
107
+ d: (session) => session.discardQuit(),
101
108
  'ctrl-c': (session) => session.finishQuit('ready'),
102
109
  escape: (session) => session.cancelQuit(),
103
110
  };
104
111
 
112
+ const COMMIT_CONFIRM = {
113
+ c: (session) => session.startGitCommit('commit'),
114
+ a: (session) => session.startGitCommit('amend'),
115
+ f: (session) => session.startGitCommit('fixup'),
116
+ escape: (session) => session.cancelCommit(),
117
+ };
118
+
119
+ const COMMIT_STATUS = {
120
+ commit: 'committed',
121
+ amend: 'amended',
122
+ fixup: 'fixup',
123
+ };
124
+
105
125
  const COMPOSE_EDITS = ['backspace', 'delete', 'ctrl-z', 'ctrl-y'];
106
126
 
107
127
  const COMPOSE_SPECIAL = {
@@ -117,7 +137,7 @@ const COMPOSE_SPECIAL = {
117
137
  'ctrl-e': (session) => session.editor.end(),
118
138
  'ctrl-z': (session) => session.editor.undoEdit(),
119
139
  'ctrl-y': (session) => session.editor.redoEdit(),
120
- tab: (session) => session.applyTemplate(),
140
+ tab: (session) => session.composeTab(),
121
141
  };
122
142
 
123
143
  const HANDLERS = {
@@ -130,6 +150,9 @@ const HANDLERS = {
130
150
  files: (session) => session.onFiles(),
131
151
  feedback: (session) => session.onFeedback(),
132
152
  todo: (session) => session.onTodo(),
153
+ code: (session) => session.onCode(),
154
+ reload: (session) => session.onReload(),
155
+ commit: (session) => session.onCommit(),
133
156
  open: (session) => session.onOpenFile(),
134
157
  removeTodo: (session) => session.removeFocusedTodo(),
135
158
  quit: (session) => session.onQuit(),
@@ -143,9 +166,12 @@ const HANDLERS = {
143
166
 
144
167
  const KEY_MODE = {
145
168
  confirmQuit: (session, key) => session.onConfirmQuit(key),
169
+ confirmCommit: (session, key) => session.onConfirmCommit(key),
146
170
  compose: (session, key) => session.handleComposeKey(key),
147
171
  };
148
172
 
173
+ const COMPOSE_LEAVE = ['next', 'prev', 'todo'];
174
+
149
175
  const MOUSE_KIND = {
150
176
  wheelUp: (session) => session.onScroll(-1),
151
177
  wheelDown: (session) => session.onScroll(1),
@@ -200,6 +226,7 @@ class Session {
200
226
  this.readFileSync = options.readFileSync ?? fs.readFileSync;
201
227
  this.writeFileSync = options.writeFileSync ?? fs.writeFileSync;
202
228
  this.mkdirSync = options.mkdirSync ?? fs.mkdirSync;
229
+ this.unlinkSync = options.unlinkSync ?? fs.unlinkSync;
203
230
  this.now = options.now ?? systemNow;
204
231
  this.forceNewReview = options.newReview === true;
205
232
  this.readOnly = options.readOnly === true;
@@ -236,6 +263,7 @@ class Session {
236
263
  this.mode = 'review';
237
264
  this.editor = null;
238
265
  this.composeKind = null;
266
+ this.commitKind = null;
239
267
  this.composeTodoId = null;
240
268
  this.composeBaseline = '';
241
269
  this.todoFocus = 0;
@@ -530,6 +558,7 @@ class Session {
530
558
  pr: 0,
531
559
  todo: 0,
532
560
  feedback: 0,
561
+ code: 0,
533
562
  };
534
563
  for (const item of this.items) {
535
564
  if (!Object.hasOwn(counts, item.origin)) continue;
@@ -538,6 +567,7 @@ class Session {
538
567
  const notes = noteCounts(this.notes);
539
568
  counts.feedback = notes.feedback;
540
569
  counts.todo = notes.todo;
570
+ counts.code = notes.code;
541
571
  return counts;
542
572
  }
543
573
 
@@ -569,6 +599,7 @@ class Session {
569
599
  todos: this.todoTexts(),
570
600
  todoFocus: this.clampedTodoFocus(),
571
601
  todoEdit: this.todoEditView(),
602
+ codeOverlay: this.codeOverlayView(),
572
603
  templates,
573
604
  templateIndex: this.clampedTemplateIndex(templates),
574
605
  };
@@ -601,18 +632,18 @@ class Session {
601
632
 
602
633
  dispatch(action) {
603
634
  if (this.mode === 'confirmQuit') return;
635
+ if (this.mode === 'confirmCommit') return;
604
636
  if (this.mode === 'compose') {
605
- if (action !== 'next' && action !== 'prev' && action !== 'todo') {
606
- return;
607
- }
637
+ if (!COMPOSE_LEAVE.includes(action)) return;
608
638
  this.commitCompose();
609
639
  this.closeCompose();
610
640
  }
611
641
  if (this.pane === 'files') {
612
642
  if (action === 'next') return void this.onFileMove(1);
613
643
  if (action === 'prev') return void this.onFileMove(-1);
614
- if (FILES_DISABLED.includes(action)) return;
615
644
  }
645
+ const hidden = disabledActions(this.pane, this.current());
646
+ if (hidden.includes(action)) return;
616
647
  const handler = HANDLERS[action];
617
648
  if (!handler) return;
618
649
  handler(this);
@@ -622,8 +653,7 @@ class Session {
622
653
  return this.items[this.index] ?? null;
623
654
  }
624
655
 
625
- reloadAfterChange() {
626
- const here = this.index;
656
+ refreshFromRepo() {
627
657
  this.loadPromise = null;
628
658
  this.bumpLoad();
629
659
  const options = this.loadOptions({
@@ -631,10 +661,6 @@ class Session {
631
661
  });
632
662
  const loaded = this.repo.load(this.cwd, this.paths, options);
633
663
  this.applyLoaded(loaded);
634
- this.index = here;
635
- this.clampIndex();
636
- this.syncReviewPath();
637
- this.syncFileCursor();
638
664
  this.scroll = 0;
639
665
  this.clearSelection();
640
666
  if (loaded.pending) {
@@ -645,6 +671,22 @@ class Session {
645
671
  this.finishIfEmpty('reload');
646
672
  }
647
673
 
674
+ reloadAfterChange() {
675
+ const here = this.index;
676
+ this.refreshFromRepo();
677
+ this.index = here;
678
+ this.clampIndex();
679
+ this.syncReviewPath();
680
+ this.syncFileCursor();
681
+ }
682
+
683
+ onReload() {
684
+ this.dismissed.clear();
685
+ this.status = '';
686
+ this.refreshFromRepo();
687
+ if (!this.done && !this.pendingExtras) this.status = 'reloaded';
688
+ }
689
+
648
690
  clearSelection() {
649
691
  this.selection = null;
650
692
  this.mouseAnchor = null;
@@ -795,7 +837,7 @@ class Session {
795
837
  let ok = false;
796
838
  try {
797
839
  this.repo.revertFile(this.top, rel, items);
798
- this.status = 'reverted';
840
+ this.status = 'dropped';
799
841
  ok = true;
800
842
  } catch (error) {
801
843
  this.status = error.message;
@@ -849,7 +891,7 @@ class Session {
849
891
  if (!this.guardWritable(item)) return;
850
892
  this.applyGit(
851
893
  item,
852
- 'reverted',
894
+ 'dropped',
853
895
  (top, current) => this.repo.revert(top, current),
854
896
  () => {
855
897
  if (needsReload(item)) this.dismissed.add(itemKey(item));
@@ -911,6 +953,23 @@ class Session {
911
953
  this.exitCode = 0;
912
954
  }
913
955
 
956
+ removeReviewFile() {
957
+ const file = this.notes && this.notes.reviewPath;
958
+ if (!file) return;
959
+ try {
960
+ this.unlinkSync(file);
961
+ } catch {
962
+ // ignore missing review file
963
+ }
964
+ }
965
+
966
+ discardQuit() {
967
+ if (this.notes) this.notes.dirty = false;
968
+ if (!this.didResumeReview) this.removeReviewFile();
969
+ this.done = true;
970
+ this.exitCode = 0;
971
+ }
972
+
914
973
  cancelQuit() {
915
974
  this.mode = 'review';
916
975
  this.status = '';
@@ -922,6 +981,69 @@ class Session {
922
981
  if (handler) handler(this);
923
982
  }
924
983
 
984
+ canCommit() {
985
+ if (this.readOnly || this.rev || this.change) {
986
+ this.status = 'read only';
987
+ return false;
988
+ }
989
+ for (const item of this.items) {
990
+ if (isReadOnlyOrigin(item.origin)) {
991
+ this.status = 'read only';
992
+ return false;
993
+ }
994
+ }
995
+ return true;
996
+ }
997
+
998
+ onCommit() {
999
+ if (!this.canCommit()) return;
1000
+ this.mode = 'confirmCommit';
1001
+ this.status = '';
1002
+ }
1003
+
1004
+ cancelCommit() {
1005
+ this.mode = 'review';
1006
+ this.commitKind = null;
1007
+ this.status = '';
1008
+ }
1009
+
1010
+ startGitCommit(kind) {
1011
+ this.commitKind = kind;
1012
+ let text = '';
1013
+ if (kind === 'fixup') text = 'HEAD';
1014
+ if (kind === 'amend' && this.repo.lastMessage) {
1015
+ text = this.repo.lastMessage(this.top) ?? '';
1016
+ }
1017
+ this.openCompose('commit', text);
1018
+ }
1019
+
1020
+ onConfirmCommit(key) {
1021
+ const commitKey = key.length === 1 ? key.toLowerCase() : key;
1022
+ const handler = COMMIT_CONFIRM[commitKey];
1023
+ if (handler) handler(this);
1024
+ }
1025
+
1026
+ finishGitCommit() {
1027
+ const kind = this.commitKind;
1028
+ const message = this.editor ? this.editor.text : '';
1029
+ if (kind !== 'fixup' && !message.trim()) {
1030
+ this.status = 'empty commit message';
1031
+ return;
1032
+ }
1033
+ try {
1034
+ this.repo.commit(this.top, kind, message);
1035
+ } catch (error) {
1036
+ this.status = error.message;
1037
+ return;
1038
+ }
1039
+ this.closeCompose();
1040
+ this.dismissed.clear();
1041
+ this.refreshFromRepo();
1042
+ if (!this.done && !this.pendingExtras) {
1043
+ this.status = COMMIT_STATUS[kind] ?? 'committed';
1044
+ }
1045
+ }
1046
+
925
1047
  onFeedback() {
926
1048
  if (this.pane === 'files') {
927
1049
  this.status = 'not a diff block';
@@ -952,6 +1074,22 @@ class Session {
952
1074
  this.createTodo(file);
953
1075
  }
954
1076
 
1077
+ onCode() {
1078
+ if (this.pane === 'files') {
1079
+ this.status = 'not a diff block';
1080
+ return;
1081
+ }
1082
+ const item = this.current();
1083
+ if (!canEditCode(item)) {
1084
+ this.status = 'not a diff block';
1085
+ return;
1086
+ }
1087
+ const prev = this.notes.code.get(itemFeedKey(item));
1088
+ const original = blockAddText(item.hunk, item.blockId);
1089
+ const text = prev ? prev.text : original;
1090
+ this.openCompose('code', text);
1091
+ }
1092
+
955
1093
  todoPageIndex(file) {
956
1094
  return this.items.findIndex(
957
1095
  (item) => item.origin === 'todo' && itemPath(item) === file,
@@ -1084,6 +1222,29 @@ class Session {
1084
1222
  return;
1085
1223
  }
1086
1224
  if (this.composeKind === 'todo') this.commitTodo(text);
1225
+ if (this.composeKind === 'code') this.commitCode(text);
1226
+ }
1227
+
1228
+ commitCode(text) {
1229
+ const item = this.current();
1230
+ if (!canEditCode(item)) return;
1231
+ const original = blockAddText(item.hunk, item.blockId);
1232
+ const key = itemFeedKey(item);
1233
+ const hunk = item.hunk;
1234
+ const note = {
1235
+ file: itemPath(item),
1236
+ oldStart: hunk.oldStart ?? 0,
1237
+ newStart: hunk.newStart ?? 0,
1238
+ blockId: item.blockId ?? 0,
1239
+ origin: item.origin,
1240
+ header: hunk.header ?? '',
1241
+ text,
1242
+ };
1243
+ if (text === original) {
1244
+ setCode(this.notes, key, { ...note, clear: true });
1245
+ return;
1246
+ }
1247
+ setCode(this.notes, key, note);
1087
1248
  }
1088
1249
 
1089
1250
  commitTodo(text) {
@@ -1114,6 +1275,7 @@ class Session {
1114
1275
  this.mode = 'review';
1115
1276
  this.editor = null;
1116
1277
  this.composeKind = null;
1278
+ this.commitKind = null;
1117
1279
  this.composeTodoId = null;
1118
1280
  this.composeBaseline = '';
1119
1281
  this.templateIndex = -1;
@@ -1174,7 +1336,9 @@ class Session {
1174
1336
 
1175
1337
  composeView() {
1176
1338
  if (this.mode !== 'compose' || !this.editor) return null;
1177
- if (this.composeKind !== 'feedback') return null;
1339
+ if (this.composeKind !== 'feedback' && this.composeKind !== 'commit') {
1340
+ return null;
1341
+ }
1178
1342
  const kind = this.composeKind;
1179
1343
  const text = this.editor.text;
1180
1344
  const cursor = this.editor.cursor;
@@ -1187,6 +1351,34 @@ class Session {
1187
1351
  return { cursor: this.editor.cursor };
1188
1352
  }
1189
1353
 
1354
+ liveCodeText() {
1355
+ const item = this.current();
1356
+ if (!canEditCode(item)) return null;
1357
+ if (this.mode === 'compose' && this.composeKind === 'code' && this.editor) {
1358
+ return this.editor.text;
1359
+ }
1360
+ if (!this.notes) return null;
1361
+ const note = this.notes.code.get(itemFeedKey(item));
1362
+ if (!note) return null;
1363
+ return note.text;
1364
+ }
1365
+
1366
+ codeOverlayView() {
1367
+ const text = this.liveCodeText();
1368
+ if (text === null) return null;
1369
+ const editing = this.mode === 'compose' && this.composeKind === 'code';
1370
+ const cursor = editing && this.editor ? this.editor.cursor : null;
1371
+ return { text, keepEmpty: editing, cursor };
1372
+ }
1373
+
1374
+ composeTab() {
1375
+ if (this.composeKind === 'code') {
1376
+ this.editor.insert('\t');
1377
+ return;
1378
+ }
1379
+ this.applyTemplate();
1380
+ }
1381
+
1190
1382
  fileTodos(rel) {
1191
1383
  if (!this.notes || !rel) return [];
1192
1384
  const todos = [];
@@ -1268,6 +1460,10 @@ class Session {
1268
1460
  }
1269
1461
 
1270
1462
  saveCompose() {
1463
+ if (this.composeKind === 'commit') {
1464
+ this.finishGitCommit();
1465
+ return;
1466
+ }
1271
1467
  this.commitCompose();
1272
1468
  if (this.composeKind === 'feedback') {
1273
1469
  const next = this.editor ? this.editor.text : '';
@@ -1280,7 +1476,10 @@ class Session {
1280
1476
 
1281
1477
  autosave() {
1282
1478
  const text = this.editor ? this.editor.text : '';
1283
- if (this.mode === 'compose' && text.trim()) this.commitCompose();
1479
+ const editingCode = this.mode === 'compose' && this.composeKind === 'code';
1480
+ if (this.mode === 'compose' && (editingCode || text.trim())) {
1481
+ this.commitCompose();
1482
+ }
1284
1483
  if (this.notes && this.notes.dirty) this.flushReview();
1285
1484
  }
1286
1485
 
@@ -1306,6 +1505,11 @@ class Session {
1306
1505
  this.moveTodoLine(delta, todoInnerWidth(size.width));
1307
1506
  return;
1308
1507
  }
1508
+ if (this.composeKind === 'code') {
1509
+ const inner = codeInnerWidth(size.width, this.layout);
1510
+ this.editor.moveLine(delta, inner);
1511
+ return;
1512
+ }
1309
1513
  const width = noteInnerWidth(size.width);
1310
1514
  const shown = this.shownTemplates();
1311
1515
  if (shown.length && this.composeKind === 'feedback') {
@@ -1355,10 +1559,20 @@ class Session {
1355
1559
  this.selectTemplate(this.templateIndex);
1356
1560
  return;
1357
1561
  }
1562
+ if (this.composeKind === 'code') {
1563
+ this.editor.insert('\n');
1564
+ this.resetBlink();
1565
+ return;
1566
+ }
1358
1567
  this.saveCompose();
1359
1568
  return;
1360
1569
  }
1361
1570
  if (key === 'escape' || key === 'ctrl-s') {
1571
+ if (this.composeKind === 'commit' && key === 'escape') {
1572
+ this.closeCompose();
1573
+ this.status = '';
1574
+ return;
1575
+ }
1362
1576
  this.saveCompose();
1363
1577
  return;
1364
1578
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reslop",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Interactive code review and repair planning for AI-assisted changes",
5
5
  "keywords": [
6
6
  "code-review",