reslop 0.2.3 → 0.2.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +8 -4
  3. package/bin/reslop.js +4 -2
  4. package/lib/api.js +2 -1
  5. package/lib/capabilities.js +0 -4
  6. package/lib/cli.js +19 -10
  7. package/lib/clipboard.js +45 -4
  8. package/lib/deps-diff.js +61 -118
  9. package/lib/deps.js +166 -235
  10. package/lib/detect.js +4 -2
  11. package/lib/diff/align.js +2 -1
  12. package/lib/diff/diff.js +6 -3
  13. package/lib/diff/display.js +2 -1
  14. package/lib/diff/inline.js +68 -2
  15. package/lib/diff/patch.js +2 -1
  16. package/lib/diff/unit.js +15 -30
  17. package/lib/editor.js +91 -13
  18. package/lib/files.js +54 -27
  19. package/lib/git-branches.js +107 -154
  20. package/lib/git-deps.js +78 -155
  21. package/lib/git-worktree.js +508 -0
  22. package/lib/git.js +23 -580
  23. package/lib/github.js +9 -8
  24. package/lib/gitlab.js +5 -4
  25. package/lib/highlight/bash.js +2 -1
  26. package/lib/highlight/core.js +11 -0
  27. package/lib/highlight/css.js +2 -1
  28. package/lib/highlight/csv.js +2 -1
  29. package/lib/highlight/dot.js +3 -12
  30. package/lib/highlight/html.js +2 -1
  31. package/lib/highlight/json.js +2 -1
  32. package/lib/highlight/log.js +5 -11
  33. package/lib/highlight/txt.js +3 -12
  34. package/lib/highlight.js +2 -1
  35. package/lib/keys.js +22 -12
  36. package/lib/manifest.js +4 -6
  37. package/lib/npm-commands.js +82 -24
  38. package/lib/remote.js +21 -20
  39. package/lib/render/branches.js +9 -27
  40. package/lib/render/chrome.js +109 -35
  41. package/lib/render/commits.js +216 -216
  42. package/lib/render/diff.js +160 -47
  43. package/lib/render/files.js +9 -26
  44. package/lib/render/frame.js +97 -118
  45. package/lib/render/notes.js +74 -26
  46. package/lib/render/npm.js +104 -30
  47. package/lib/render/present.js +2 -1
  48. package/lib/render/primitives.js +128 -52
  49. package/lib/render/render.js +2 -1
  50. package/lib/render/unit.js +17 -11
  51. package/lib/report-model.js +6 -4
  52. package/lib/report-parse.js +8 -14
  53. package/lib/report-render.js +38 -21
  54. package/lib/report-run.js +12 -11
  55. package/lib/report-test.js +45 -51
  56. package/lib/report-tools.js +44 -25
  57. package/lib/review-merge.js +102 -131
  58. package/lib/review.js +69 -78
  59. package/lib/select.js +52 -0
  60. package/lib/session/actions.js +72 -87
  61. package/lib/session/branches.js +18 -52
  62. package/lib/session/change-actions.js +175 -147
  63. package/lib/session/commit.js +65 -61
  64. package/lib/session/compose.js +152 -83
  65. package/lib/session/dispatch.js +53 -46
  66. package/lib/session/edit-click.js +57 -0
  67. package/lib/session/files-pane.js +30 -51
  68. package/lib/session/items.js +73 -90
  69. package/lib/session/lifecycle.js +37 -98
  70. package/lib/session/load.js +52 -57
  71. package/lib/session/navigation.js +105 -95
  72. package/lib/session/npm.js +205 -71
  73. package/lib/session/ops.js +200 -45
  74. package/lib/session/pointer.js +33 -8
  75. package/lib/session/progress.js +9 -6
  76. package/lib/session/review.js +35 -53
  77. package/lib/session/tasks.js +292 -0
  78. package/lib/session/terminal.js +17 -19
  79. package/lib/session/unit.js +3 -4
  80. package/lib/session/update.js +76 -96
  81. package/lib/session/view.js +45 -16
  82. package/lib/session/watch.js +47 -27
  83. package/lib/session.js +66 -97
  84. package/lib/source.js +6 -3
  85. package/lib/update.js +5 -13
  86. package/lib/utilities.js +25 -3
  87. package/lib/wrap.js +18 -1
  88. package/package.json +1 -1
  89. package/lib/session/todos.js +0 -293
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { TODO_FILE } = require('./files.js');
3
+ const files = require('./files.js');
4
+ const { TASKS_FILE } = files;
4
5
 
5
6
  const flatText = (text) => `${text ?? ''}`.trim().replace(/\n/g, ' ');
6
7
 
@@ -29,7 +30,7 @@ const copyMap = (map) => {
29
30
 
30
31
  const snapshotSide = (store) => ({
31
32
  status: store.status,
32
- todos: snapList(store.todos),
33
+ tasks: snapList(store.tasks),
33
34
  feedback: copyMap(store.feedback),
34
35
  code: copyMap(store.code),
35
36
  });
@@ -67,83 +68,12 @@ const snapText = (snap) => snap.text;
67
68
  const flags = (n) => new Array(n).fill(false);
68
69
 
69
70
  const adoptTodo = (store, snap) => {
70
- const id = store.nextTodoId;
71
- store.nextTodoId += 1;
72
- const file = snap.file || TODO_FILE;
71
+ const id = store.nextTaskId;
72
+ store.nextTaskId += 1;
73
+ const file = snap.file || TASKS_FILE;
73
74
  return { id, file, text: snap.text, done: snap.done };
74
75
  };
75
76
 
76
- const findMerged = (merged, alias, text) => {
77
- const linked = alias.get(text);
78
- if (linked) {
79
- const at = merged.indexOf(linked);
80
- if (at >= 0) return at;
81
- }
82
- for (let i = 0; i < merged.length; i++) {
83
- if (liveText(merged[i]) === text) return i;
84
- }
85
- return -1;
86
- };
87
-
88
- const insertTheir = (store, merged, theirs, theirKeep, alias) => {
89
- for (let i = 0; i < theirs.length; i++) {
90
- if (!theirKeep[i]) continue;
91
- let at = merged.length;
92
- let placed = false;
93
- for (let j = i - 1; j >= 0 && !placed; j--) {
94
- const prev = findMerged(merged, alias, theirs[j].text);
95
- if (prev < 0) continue;
96
- at = prev + 1;
97
- placed = true;
98
- }
99
- for (let j = i + 1; j < theirs.length && !placed; j++) {
100
- const next = findMerged(merged, alias, theirs[j].text);
101
- if (next < 0) continue;
102
- at = next;
103
- placed = true;
104
- }
105
- const item = adoptTodo(store, theirs[i]);
106
- merged.splice(at, 0, item);
107
- alias.set(theirs[i].text, item);
108
- }
109
- return merged;
110
- };
111
-
112
- const keepOurs = (ours, kept, alias, index, text) => {
113
- kept[index] = true;
114
- alias.set(text, ours[index]);
115
- };
116
-
117
- const mergeBaseTodo = (
118
- base,
119
- ours,
120
- theirs,
121
- used,
122
- kept,
123
- dropped,
124
- theirKeep,
125
- alias,
126
- ) => {
127
- const text = base.text;
128
- const oi = claim(ours, used.ours, text, liveText);
129
- const ti = claim(theirs, used.theirs, text, snapText);
130
- if (oi >= 0 && ti >= 0) {
131
- const done = pickDone(base.done, ours[oi].done === true, theirs[ti].done);
132
- ours[oi] = withDone(ours[oi], done);
133
- keepOurs(ours, kept, alias, oi, text);
134
- return;
135
- }
136
- if (oi >= 0) {
137
- if (ours[oi].done === base.done) {
138
- dropped[oi] = true;
139
- return;
140
- }
141
- keepOurs(ours, kept, alias, oi, text);
142
- return;
143
- }
144
- if (ti >= 0 && theirs[ti].done !== base.done) theirKeep[ti] = true;
145
- };
146
-
147
77
  const replaceTodo = (todo, snap) => ({
148
78
  id: todo.id,
149
79
  file: todo.file,
@@ -151,68 +81,109 @@ const replaceTodo = (todo, snap) => ({
151
81
  done: snap.done,
152
82
  });
153
83
 
154
- const fillGaps = (merged, gaps, theirs, theirKeep, alias) => {
155
- let filled = 0;
156
- for (let i = 0; i < theirs.length && filled < gaps.length; i++) {
157
- if (!theirKeep[i]) continue;
158
- const gap = gaps[filled];
159
- const item = replaceTodo(gap.todo, theirs[i]);
160
- merged.splice(gap.at, 0, item);
161
- alias.set(theirs[i].text, item);
162
- theirKeep[i] = false;
163
- filled += 1;
84
+ class TodoMerge {
85
+ constructor(store, theirs) {
86
+ this.store = store;
87
+ this.ours = store.tasks.filter((todo) => liveText(todo));
88
+ this.theirs = theirs;
89
+ this.usedOurs = flags(this.ours.length);
90
+ this.usedTheirs = flags(theirs.length);
91
+ this.kept = flags(this.ours.length);
92
+ this.dropped = flags(this.ours.length);
93
+ this.theirKeep = flags(theirs.length);
94
+ this.alias = new Map();
95
+ }
96
+
97
+ keepOurs(index, text) {
98
+ this.kept[index] = true;
99
+ this.alias.set(text, this.ours[index]);
100
+ }
101
+
102
+ mergeBase(base) {
103
+ const { ours, theirs } = this;
104
+ const text = base.text;
105
+ const oi = claim(ours, this.usedOurs, text, liveText);
106
+ const ti = claim(theirs, this.usedTheirs, text, snapText);
107
+ if (oi >= 0 && ti >= 0) {
108
+ const done = pickDone(base.done, ours[oi].done === true, theirs[ti].done);
109
+ ours[oi] = withDone(ours[oi], done);
110
+ this.keepOurs(oi, text);
111
+ } else if (oi >= 0) {
112
+ if (ours[oi].done === base.done) this.dropped[oi] = true;
113
+ else this.keepOurs(oi, text);
114
+ } else if (ti >= 0 && theirs[ti].done !== base.done) {
115
+ this.theirKeep[ti] = true;
116
+ }
164
117
  }
165
- };
166
118
 
167
- const mergeFreshTodo = (ours, theirs, used, kept, alias, index) => {
168
- const text = liveText(ours[index]);
169
- const ti = claim(theirs, used.theirs, text, snapText);
170
- if (ti < 0) {
171
- kept[index] = true;
172
- return;
119
+ mergeFresh(index) {
120
+ const { ours, theirs } = this;
121
+ const text = liveText(ours[index]);
122
+ const ti = claim(theirs, this.usedTheirs, text, snapText);
123
+ if (ti < 0) {
124
+ this.kept[index] = true;
125
+ return;
126
+ }
127
+ const done = ours[index].done === true || theirs[ti].done === true;
128
+ ours[index] = withDone(ours[index], done);
129
+ this.keepOurs(index, text);
173
130
  }
174
- const done = ours[index].done === true || theirs[ti].done === true;
175
- ours[index] = withDone(ours[index], done);
176
- keepOurs(ours, kept, alias, index, text);
177
- };
178
131
 
179
- const mergeTodos = (store, base, theirs) => {
180
- const ours = [];
181
- for (const todo of store.todos) {
182
- if (!liveText(todo)) continue;
183
- ours.push(todo);
184
- }
185
- const used = { ours: flags(ours.length), theirs: flags(theirs.length) };
186
- const kept = flags(ours.length);
187
- const dropped = flags(ours.length);
188
- const theirKeep = flags(theirs.length);
189
- const alias = new Map();
190
- for (const item of base) {
191
- mergeBaseTodo(item, ours, theirs, used, kept, dropped, theirKeep, alias);
132
+ findMerged(merged, text) {
133
+ const linked = this.alias.get(text);
134
+ const at = linked ? merged.indexOf(linked) : -1;
135
+ if (at >= 0) return at;
136
+ return merged.findIndex((todo) => liveText(todo) === text);
192
137
  }
193
- for (let i = 0; i < ours.length; i++) {
194
- if (used.ours[i]) continue;
195
- mergeFreshTodo(ours, theirs, used, kept, alias, i);
138
+
139
+ theirPlace(merged, index) {
140
+ const { theirs } = this;
141
+ for (let j = index - 1; j >= 0; j--) {
142
+ const prev = this.findMerged(merged, theirs[j].text);
143
+ if (prev >= 0) return prev + 1;
144
+ }
145
+ for (let j = index + 1; j < theirs.length; j++) {
146
+ const next = this.findMerged(merged, theirs[j].text);
147
+ if (next >= 0) return next;
148
+ }
149
+ return merged.length;
196
150
  }
197
- for (let i = 0; i < theirs.length; i++) {
198
- if (!used.theirs[i]) theirKeep[i] = true;
151
+
152
+ place(merged, at, index, item) {
153
+ merged.splice(at, 0, item);
154
+ this.alias.set(this.theirs[index].text, item);
155
+ this.theirKeep[index] = false;
199
156
  }
200
- const merged = [];
201
- const gaps = [];
202
- let cursor = 0;
203
- for (let i = 0; i < ours.length; i++) {
204
- if (kept[i]) {
205
- merged.push(ours[i]);
206
- cursor += 1;
207
- continue;
157
+
158
+ merge(base) {
159
+ const { ours, theirs } = this;
160
+ for (const item of base) this.mergeBase(item);
161
+ for (let i = 0; i < ours.length; i++) {
162
+ if (!this.usedOurs[i]) this.mergeFresh(i);
163
+ }
164
+ for (let i = 0; i < theirs.length; i++) {
165
+ if (!this.usedTheirs[i]) this.theirKeep[i] = true;
208
166
  }
209
- if (!dropped[i]) continue;
210
- gaps.push({ at: cursor, todo: ours[i] });
211
- cursor += 1;
167
+ const merged = [];
168
+ const gaps = [];
169
+ for (let i = 0; i < ours.length; i++) {
170
+ const at = merged.length + gaps.length;
171
+ if (this.kept[i]) merged.push(ours[i]);
172
+ else if (this.dropped[i]) gaps.push({ at, task: ours[i] });
173
+ }
174
+ for (let i = 0; i < theirs.length && gaps.length; i++) {
175
+ if (!this.theirKeep[i]) continue;
176
+ const gap = gaps.shift();
177
+ this.place(merged, gap.at, i, replaceTodo(gap.task, theirs[i]));
178
+ }
179
+ for (let i = 0; i < theirs.length; i++) {
180
+ if (!this.theirKeep[i]) continue;
181
+ const at = this.theirPlace(merged, i);
182
+ this.place(merged, at, i, adoptTodo(this.store, theirs[i]));
183
+ }
184
+ return merged;
212
185
  }
213
- fillGaps(merged, gaps, theirs, theirKeep, alias);
214
- store.todos = insertTheir(store, merged, theirs, theirKeep, alias);
215
- };
186
+ }
216
187
 
217
188
  const sameNote = (left, right) =>
218
189
  left.text === right.text && !!left.done === !!right.done;
@@ -256,9 +227,9 @@ const mergeMap = (ours, base, theirs) => {
256
227
 
257
228
  const mergeReview = (store, disk) => {
258
229
  const base = store.baseline;
259
- const baseTodos = base ? base.todos : [];
230
+ const baseTodos = base?.tasks ?? base?.todos ?? [];
260
231
  const baseStatus = base ? base.status : store.status;
261
- mergeTodos(store, baseTodos, snapList(disk.todos));
232
+ store.tasks = new TodoMerge(store, snapList(disk.tasks)).merge(baseTodos);
262
233
  const feedback = base ? base.feedback : new Map();
263
234
  const code = base ? base.code : new Map();
264
235
  mergeMap(store.feedback, feedback, disk.feedback);
package/lib/review.js CHANGED
@@ -3,28 +3,32 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
- const { TODO_FILE, REVIEW_DIR } = require('./files.js');
7
- const { captureBaseline, mergeReview } = require('./review-merge.js');
6
+ const files = require('./files.js');
7
+ const { TASKS_FILE, REVIEW_DIR } = files;
8
+ const reviewMerge = require('./review-merge.js');
9
+ const { captureBaseline, mergeReview } = reviewMerge;
10
+ const utilities = require('./utilities.js');
11
+ const { pad2, dateStamp } = utilities;
8
12
 
9
13
  const createStore = (reviewPath, templates = []) => ({
10
14
  reviewPath,
11
15
  dirty: false,
12
- nextTodoId: 1,
16
+ nextTaskId: 1,
13
17
  feedback: new Map(),
14
18
  code: new Map(),
15
- todos: [],
19
+ tasks: [],
16
20
  templates: [...templates],
17
21
  status: 'editing',
18
22
  baseline: null,
19
23
  });
20
24
 
21
25
  const noteCounts = (store) => {
22
- const counts = { feedback: 0, todo: 0, todoDone: 0, code: 0 };
26
+ const counts = { feedback: 0, tasks: 0, tasksDone: 0, code: 0 };
23
27
  if (!store) return counts;
24
- for (const todo of store.todos) {
25
- if (!todo.text.trim()) continue;
26
- counts.todo += 1;
27
- if (todo.done) counts.todoDone += 1;
28
+ for (const task of store.tasks) {
29
+ if (!task.text.trim()) continue;
30
+ counts.tasks += 1;
31
+ if (task.done) counts.tasksDone += 1;
28
32
  }
29
33
  for (const note of store.feedback.values()) {
30
34
  if (note.text.trim()) counts.feedback += 1;
@@ -35,21 +39,20 @@ const noteCounts = (store) => {
35
39
 
36
40
  const hasNotes = (store) => {
37
41
  const counts = noteCounts(store);
38
- return counts.feedback > 0 || counts.todo > 0 || counts.code > 0;
42
+ return counts.feedback > 0 || counts.tasks > 0 || counts.code > 0;
39
43
  };
40
44
 
41
- const setFeedback = (store, key, note) => {
42
- const text = note.text ?? '';
43
- const trimmed = text.trim();
44
- const prev = store.feedback.get(key);
45
- if (!trimmed) {
45
+ const putNote = (store, notes, key, note, clear) => {
46
+ const prev = notes.get(key);
47
+ if (clear) {
46
48
  if (!prev) return;
47
- store.feedback.delete(key);
49
+ notes.delete(key);
48
50
  store.dirty = true;
49
51
  return;
50
52
  }
53
+ const text = note.text ?? '';
51
54
  if (prev && prev.text === text) return;
52
- store.feedback.set(key, {
55
+ notes.set(key, {
53
56
  file: note.file,
54
57
  oldStart: note.oldStart ?? 0,
55
58
  newStart: note.newStart ?? 0,
@@ -62,42 +65,28 @@ const setFeedback = (store, key, note) => {
62
65
  store.dirty = true;
63
66
  };
64
67
 
68
+ const setFeedback = (store, key, note) => {
69
+ const clear = !(note.text ?? '').trim();
70
+ putNote(store, store.feedback, key, note, clear);
71
+ };
72
+
65
73
  const setCode = (store, key, note) => {
66
- const prev = store.code.get(key);
67
- if (note.clear) {
68
- if (!prev) return;
69
- store.code.delete(key);
70
- store.dirty = true;
71
- return;
72
- }
73
- const text = note.text ?? '';
74
- if (prev && prev.text === text) return;
75
- store.code.set(key, {
76
- file: note.file,
77
- oldStart: note.oldStart ?? 0,
78
- newStart: note.newStart ?? 0,
79
- blockId: note.blockId ?? 0,
80
- origin: note.origin ?? '',
81
- header: note.header ?? '',
82
- text,
83
- done: note.done ?? prev?.done ?? false,
84
- });
85
- store.dirty = true;
74
+ putNote(store, store.code, key, note, Boolean(note.clear));
86
75
  };
87
76
 
88
- const addTodo = (store, file, text = '') => {
89
- const id = store.nextTodoId;
90
- store.nextTodoId += 1;
77
+ const addTask = (store, file, text = '') => {
78
+ const id = store.nextTaskId;
79
+ store.nextTaskId += 1;
91
80
  const todo = { id, file, text, done: false };
92
- store.todos.push(todo);
81
+ store.tasks.push(todo);
93
82
  store.dirty = true;
94
83
  return todo;
95
84
  };
96
85
 
97
- const removeTodo = (store, id) => {
86
+ const removeTask = (store, id) => {
98
87
  const next = [];
99
88
  let found = false;
100
- for (const todo of store.todos) {
89
+ for (const todo of store.tasks) {
101
90
  if (todo.id === id) {
102
91
  found = true;
103
92
  continue;
@@ -105,28 +94,28 @@ const removeTodo = (store, id) => {
105
94
  next.push(todo);
106
95
  }
107
96
  if (!found) return false;
108
- store.todos = next;
97
+ store.tasks = next;
109
98
  store.dirty = true;
110
99
  return true;
111
100
  };
112
101
 
113
- const setTodoText = (store, id, text) => {
114
- const i = store.todos.findIndex((todo) => todo.id === id);
102
+ const setTaskText = (store, id, text) => {
103
+ const i = store.tasks.findIndex((todo) => todo.id === id);
115
104
  if (i < 0) return;
116
- const prev = store.todos[i];
105
+ const prev = store.tasks[i];
117
106
  const trimmed = text.trim();
118
- if (!trimmed) return void removeTodo(store, id);
107
+ if (!trimmed) return void removeTask(store, id);
119
108
  if (prev.text === text) return;
120
- store.todos[i] = { id: prev.id, file: prev.file, text, done: prev.done };
109
+ store.tasks[i] = { id: prev.id, file: prev.file, text, done: prev.done };
121
110
  store.dirty = true;
122
111
  };
123
112
 
124
- const setTodoDone = (store, id, done) => {
125
- const i = store.todos.findIndex((todo) => todo.id === id);
113
+ const setTaskDone = (store, id, done) => {
114
+ const i = store.tasks.findIndex((todo) => todo.id === id);
126
115
  if (i < 0) return;
127
- const prev = store.todos[i];
116
+ const prev = store.tasks[i];
128
117
  if (prev.done === done) return;
129
- store.todos[i] = { id: prev.id, file: prev.file, text: prev.text, done };
118
+ store.tasks[i] = { id: prev.id, file: prev.file, text: prev.text, done };
130
119
  store.dirty = true;
131
120
  };
132
121
 
@@ -162,9 +151,9 @@ const applyImportedNotes = (store, imported) => {
162
151
  done,
163
152
  });
164
153
  }
165
- const todos = imported.todos ?? [];
154
+ const todos = imported.tasks ?? imported.todos ?? [];
166
155
  for (const todo of todos) {
167
- const added = addTodo(store, todo.file || '', todo.text ?? '');
156
+ const added = addTask(store, todo.file || '', todo.text ?? '');
168
157
  if (todo.done) added.done = true;
169
158
  }
170
159
  };
@@ -231,6 +220,8 @@ const SECTION_LINE = {
231
220
  '### Todo': 'todo',
232
221
  '### Feedback': 'feedback',
233
222
  };
223
+ const BACKLOG_HEAD = 'Backlog';
224
+ const LEGACY_BACKLOG = 'TODOs';
234
225
 
235
226
  const AGENT_INSTRUCTIONS = [
236
227
  '- Execute reviews with `status` `ready, partial, editing`',
@@ -240,6 +231,8 @@ const AGENT_INSTRUCTIONS = [
240
231
  '- If not `editing`, set `status` to `partial` if some remain, ' +
241
232
  'or `done` if all are `[x]`',
242
233
  '- Do not redo items already marked `[x]`',
234
+ '- Run the full check with `npm t`',
235
+ '- Run specific test files with `reslop t -- node --test <files>`',
243
236
  ].join('\n');
244
237
 
245
238
  const toInt = (text) => parseInt(text, 10);
@@ -269,9 +262,9 @@ const parseFrontmatterStatus = (text) => {
269
262
  const isDoneMark = (mark) => mark === 'x' || mark === 'X';
270
263
 
271
264
  const pushTodo = (builder, file, text, done) => {
272
- const id = builder.nextTodoId;
273
- builder.nextTodoId += 1;
274
- builder.todos.push({ id, file, text, done });
265
+ const id = builder.nextTaskId;
266
+ builder.nextTaskId += 1;
267
+ builder.tasks.push({ id, file, text, done });
275
268
  };
276
269
 
277
270
  const checkboxLine = (text, done) => {
@@ -346,7 +339,7 @@ const parseFeedSuffix = (textItem) => {
346
339
  const fileGroup = (groups, file) => {
347
340
  let group = groups.get(file);
348
341
  if (!group) {
349
- group = { todos: [], feedback: [], code: [] };
342
+ group = { tasks: [], feedback: [], code: [] };
350
343
  groups.set(file, group);
351
344
  }
352
345
  return group;
@@ -354,9 +347,9 @@ const fileGroup = (groups, file) => {
354
347
 
355
348
  const groupReviewNotes = (notes) => {
356
349
  const groups = new Map();
357
- for (const todo of notes.todos) {
350
+ for (const todo of notes.tasks) {
358
351
  if (!todo.text.trim()) continue;
359
- fileGroup(groups, TODO_FILE).todos.push(todo);
352
+ fileGroup(groups, TASKS_FILE).tasks.push(todo);
360
353
  }
361
354
  for (const key of notes.feedback.keys()) {
362
355
  const note = notes.feedback.get(key);
@@ -407,12 +400,19 @@ const emitCodeItem = (lines, note) => {
407
400
  lines.push(fence);
408
401
  };
409
402
 
403
+ const sectionTitle = (file) => (file === TASKS_FILE ? BACKLOG_HEAD : file);
404
+
405
+ const backlogFile = (name) => {
406
+ if (name === BACKLOG_HEAD || name === LEGACY_BACKLOG) return TASKS_FILE;
407
+ return name;
408
+ };
409
+
410
410
  const emitReviewGroups = (lines, groups) => {
411
411
  for (const file of groups.keys()) {
412
412
  const group = groups.get(file);
413
- lines.push(`## ${file}`);
413
+ lines.push(`## ${sectionTitle(file)}`);
414
414
  lines.push('');
415
- for (const todo of group.todos) {
415
+ for (const todo of group.tasks) {
416
416
  lines.push(checkboxLine(todo.text, todo.done));
417
417
  }
418
418
  for (const item of group.feedback) {
@@ -458,9 +458,9 @@ const flushPendingFeedback = (builder, pending) => {
458
458
 
459
459
  const headingFile = (line) => {
460
460
  const quoted = FILE_QUOTE.exec(line);
461
- if (quoted) return quoted[1];
461
+ if (quoted) return backlogFile(quoted[1]);
462
462
  if (line.startsWith('## ') && line !== '## Agent instructions') {
463
- return line.slice(3);
463
+ return backlogFile(line.slice(3));
464
464
  }
465
465
  return null;
466
466
  };
@@ -642,15 +642,6 @@ const TEMPLATES_FILE = 'templates.json';
642
642
  const AUTOSAVE_MS = 3000;
643
643
  const REVIEW_NAME = /^(\d{4}-\d{2}-\d{2})-(\d+)\.md$/;
644
644
 
645
- const pad2 = (n) => `${n}`.padStart(2, '0');
646
-
647
- const dateStamp = (date) => {
648
- const y = date.getFullYear();
649
- const month = pad2(date.getMonth() + 1);
650
- const day = pad2(date.getDate());
651
- return `${y}-${month}-${day}`;
652
- };
653
-
654
645
  const allocateReviewPath = (dir, date, existingNames) => {
655
646
  const stamp = dateStamp(date);
656
647
  let max = -1;
@@ -798,10 +789,10 @@ module.exports = {
798
789
  rememberTemplate,
799
790
  setFeedback,
800
791
  setCode,
801
- addTodo,
802
- removeTodo,
803
- setTodoText,
804
- setTodoDone,
792
+ addTask,
793
+ removeTask,
794
+ setTaskText,
795
+ setTaskDone,
805
796
  checkLabel,
806
797
  feedbackKey,
807
798
  applyImportedNotes,
package/lib/select.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const ansi = require('./ansi.js');
4
4
 
5
5
  const { ESC, RESET, THEME, seq, stripAnsi, graphemes, graphemeWidth } = ansi;
6
+ const { visibleWidth } = ansi;
6
7
 
7
8
  const CSI_FINAL = /[@-~]/;
8
9
 
@@ -177,6 +178,56 @@ const overlayRange = (line, fromCol, toCol) => {
177
178
  return out;
178
179
  };
179
180
 
181
+ const selectionOf = (source) => {
182
+ if (!source || typeof source.anchor !== 'number') return null;
183
+ const cursor = source.cursor ?? 0;
184
+ if (source.anchor === cursor) return null;
185
+ return { anchor: source.anchor, cursor };
186
+ };
187
+
188
+ const editorSelection = (view) => {
189
+ if (!view) return null;
190
+ if (view.compose) return selectionOf(view.compose);
191
+ return selectionOf(view.codeOverlay);
192
+ };
193
+
194
+ const spanCols = (hit, lo, hi) => {
195
+ const start = hit.start ?? 0;
196
+ const from = Math.max(lo, start);
197
+ const to = Math.min(hi, hit.limit ?? start);
198
+ if (to <= from) return null;
199
+ const text = hit.text ?? '';
200
+ const head = Math.max(0, from - start);
201
+ const tail = Math.max(0, to - start);
202
+ const c0 = visibleWidth(text.slice(0, head));
203
+ const c1 = visibleWidth(text.slice(0, tail));
204
+ const scroll = hit.scroll ?? 0;
205
+ const room = Math.max(0, (hit.x1 ?? hit.textX ?? 1) - (hit.textX ?? 1));
206
+ const vis0 = Math.max(c0, scroll);
207
+ const vis1 = Math.min(c1, scroll + room);
208
+ if (vis1 <= vis0) return null;
209
+ const origin = (hit.textX ?? 1) - 1;
210
+ return { from: origin + vis0 - scroll, to: origin + vis1 - scroll };
211
+ };
212
+
213
+ const markEditorSelection = (rows, view, hits, color) => {
214
+ if (!color || !hits) return rows;
215
+ const sel = editorSelection(view);
216
+ if (!sel) return rows;
217
+ const lo = Math.min(sel.anchor, sel.cursor);
218
+ const hi = Math.max(sel.anchor, sel.cursor);
219
+ const next = rows.slice();
220
+ for (const hit of hits) {
221
+ if (!hit.live || !hit.y) continue;
222
+ const range = spanCols(hit, lo, hi);
223
+ if (!range) continue;
224
+ const i = hit.y - 1;
225
+ if (i < 0 || i >= next.length) continue;
226
+ next[i] = overlayRange(next[i], range.from, range.to);
227
+ }
228
+ return next;
229
+ };
230
+
180
231
  const overlayRows = (rows, sel) => {
181
232
  if (!isRange(sel)) return rows;
182
233
  const { length } = rows;
@@ -193,4 +244,5 @@ module.exports = {
193
244
  extractText,
194
245
  overlayRange,
195
246
  overlayRows,
247
+ markEditorSelection,
196
248
  };