reslop 0.2.7 → 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.
Files changed (158) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +573 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +17 -0
  7. package/lib/agents/models.js +467 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +151 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +504 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +114 -48
  45. package/lib/{editor.js → input/editor.js} +16 -8
  46. package/lib/{keys.js → input/keys.js} +24 -45
  47. package/lib/input/line-edit.js +78 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +642 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +9 -14
  58. package/lib/render/footer.js +385 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +320 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +13 -60
  72. package/lib/render/tiles.js +109 -48
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +48 -19
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +14 -0
  81. package/lib/review/format.js +420 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +976 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -40
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +189 -147
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +86 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +93 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +78 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +250 -0
  111. package/lib/session/npm/npm.js +836 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +158 -63
  120. package/lib/session/tasks.js +321 -66
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +65 -65
  126. package/lib/session/workspace.js +39 -78
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +4 -3
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -1,9 +1,19 @@
1
1
  'use strict';
2
2
 
3
- const files = require('../files.js');
4
- const { isTasksEntry, TASKS_FILE, TASK_KINDS, taskKind } = files;
5
- const review = require('../review.js');
6
- const { addTask, setTaskText, setTaskDone, removeTask, checkLabel } = review;
3
+ const path = require('node:path');
4
+
5
+ const { clamp, trimText } = require('../common/utilities.js');
6
+ const files = require('../common/files.js');
7
+ const { isTasksEntry, TASKS_FILE, TASK_KINDS, taskKind, taskTitle } = files;
8
+ const { REVIEW_DIR } = files;
9
+ const review = require('../review/review.js');
10
+ const { checkLabel } = review;
11
+ const { orderedPlanNames, allocateReviewPath } = review;
12
+ const plans = require('./agents/plans.js');
13
+ const { planKey, planPart, alignPlanLabels } = plans;
14
+ const { MenuPick, menuRows, pickDelta } = require('../input/menu-pick.js');
15
+ const { isTextKey } = require('../input/keys.js');
16
+ const { lineKey } = require('../input/line-edit.js');
7
17
 
8
18
  const TASK_TYPE_SKIP = [
9
19
  'enter',
@@ -27,23 +37,45 @@ const TASK_TYPE_SKIP = [
27
37
  'f',
28
38
  'd',
29
39
  't',
40
+ 'i',
30
41
  'b',
31
42
  'r',
32
43
  'c',
33
44
  ];
34
45
 
35
46
  const isTaskTypeKey = (key) => {
36
- if (!key || key.length !== 1 || key === ' ') return false;
37
- if (TASK_TYPE_SKIP.includes(key)) return false;
38
- if (key.startsWith('ctrl-') || key.startsWith('alt-')) return false;
39
- return key.charCodeAt(0) >= 32;
47
+ if (!isTextKey(key) || key === ' ') return false;
48
+ return !TASK_TYPE_SKIP.includes(key);
40
49
  };
41
50
 
42
- const clamp = (value, last) => Math.max(0, Math.min(value, last));
51
+ const sectionTitle = (row) => {
52
+ if (row.quote) return row.file;
53
+ return taskTitle(row.kind);
54
+ };
55
+
56
+ const idlePick = () => false;
57
+ const NEW_PLAN = '<new plan>';
58
+ const PLAN_ROWS = 8;
59
+
60
+ const planListSize = (ui) => {
61
+ const frame = ui.lastFrame;
62
+ const height = frame && frame.height ? frame.height : ui.getSize().height;
63
+ const room = Math.max(1, height - 3);
64
+ return Math.min(room, PLAN_ROWS);
65
+ };
66
+
67
+ const planOpenScroll = (cursor, extra, count, size) => {
68
+ const span = Math.abs(extra - cursor);
69
+ const end = span < size ? Math.max(cursor, extra) : cursor;
70
+ const start = Math.max(0, end - size + 1);
71
+ const maxStart = Math.max(0, count - size);
72
+ return Math.min(start, maxStart);
73
+ };
43
74
 
44
75
  class TaskPage {
45
76
  constructor(composer) {
46
77
  this.composer = composer;
78
+ this.planPick = null;
47
79
  }
48
80
 
49
81
  get ui() {
@@ -68,14 +100,46 @@ class TaskPage {
68
100
  return rows;
69
101
  }
70
102
 
103
+ quoteSections() {
104
+ const notes = this.ui.review.store;
105
+ if (!notes) return [];
106
+ const groups = new Map();
107
+ for (const [key, note] of notes.feedback) {
108
+ if (!trimText(note.text)) continue;
109
+ const file = note.file || key;
110
+ let items = groups.get(file);
111
+ if (!items) {
112
+ items = [];
113
+ groups.set(file, items);
114
+ }
115
+ items.push({ key, note });
116
+ }
117
+ const sections = [];
118
+ for (const [file, items] of groups) sections.push({ file, items });
119
+ return sections;
120
+ }
121
+
71
122
  rows() {
72
123
  const rows = [];
73
124
  for (const kind of TASK_KINDS) {
74
125
  const tasks = this.list(kind.id);
75
126
  for (const task of tasks) {
76
- rows.push({ kind: kind.id, task, draft: false });
127
+ rows.push({ quote: false, kind: kind.id, task, draft: false });
128
+ }
129
+ rows.push({ quote: false, kind: kind.id, task: null, draft: true });
130
+ }
131
+ for (const section of this.quoteSections()) {
132
+ for (const item of section.items) {
133
+ rows.push({
134
+ quote: true,
135
+ kind: section.file,
136
+ file: section.file,
137
+ key: item.key,
138
+ note: item.note,
139
+ task: null,
140
+ draft: false,
141
+ });
77
142
  }
78
- rows.push({ kind: kind.id, task: null, draft: true });
79
143
  }
80
144
  return rows;
81
145
  }
@@ -89,13 +153,17 @@ class TaskPage {
89
153
  if (!dest) return 0;
90
154
  for (let i = 0; i < rows.length; i++) {
91
155
  const row = rows[i];
156
+ if (dest.quote) {
157
+ if (row.quote && row.key === dest.key) return i;
158
+ continue;
159
+ }
92
160
  if (dest.draft) {
93
161
  if (row.draft && row.kind === dest.kind) return i;
94
162
  continue;
95
163
  }
96
164
  if (row.task && dest.task && row.task.id === dest.task.id) return i;
97
165
  }
98
- return clamp(0, rows.length - 1);
166
+ return 0;
99
167
  }
100
168
 
101
169
  editingId() {
@@ -105,7 +173,11 @@ class TaskPage {
105
173
 
106
174
  isDraftCompose() {
107
175
  const composer = this.composer;
108
- return composer.composeKind === 'tasks' && composer.composeTaskId === null;
176
+ return (
177
+ composer.composeKind === 'tasks' &&
178
+ composer.composeTaskId === null &&
179
+ !composer.composeQuoteKey
180
+ );
109
181
  }
110
182
 
111
183
  canToggle() {
@@ -113,14 +185,10 @@ class TaskPage {
113
185
  return this.ui.mode === 'review';
114
186
  }
115
187
 
116
- focused() {
117
- const row = this.rowAt(this.clampedTaskFocus());
118
- return row && row.task ? row.task : null;
119
- }
120
-
121
188
  focusedKind() {
122
189
  const row = this.rowAt(this.clampedTaskFocus());
123
- return row ? row.kind : TASK_KINDS[0].id;
190
+ if (!row || row.quote) return TASK_KINDS[0].id;
191
+ return row.kind;
124
192
  }
125
193
 
126
194
  closeEditor() {
@@ -128,26 +196,36 @@ class TaskPage {
128
196
  this.composer.closeCompose();
129
197
  }
130
198
 
131
- taskSections() {
132
- if (!this.isOpen()) return [];
199
+ rowText(row, at) {
133
200
  const editor = this.composer.editor;
134
- const editingId = this.editingId();
135
- const draft = this.isDraftCompose();
136
201
  const focus = this.clampedTaskFocus();
137
- let at = 0;
202
+ if (row.draft) {
203
+ const live = this.isDraftCompose() && at === focus && editor;
204
+ return checkLabel(live ? editor.text : '', false);
205
+ }
206
+ if (row.quote) {
207
+ const live = this.composer.composeQuoteKey === row.key && editor;
208
+ const text = live ? editor.text : row.note.text;
209
+ return checkLabel(text, row.note.done);
210
+ }
211
+ const live = this.editingId() === row.task.id && editor;
212
+ const text = live ? editor.text : row.task.text;
213
+ return checkLabel(text, row.task.done);
214
+ }
215
+
216
+ taskSections() {
217
+ if (!this.isOpen()) return [];
138
218
  const sections = [];
139
- for (const kind of TASK_KINDS) {
140
- const tasks = this.list(kind.id);
141
- const texts = [];
142
- for (const task of tasks) {
143
- const live = editingId === task.id && editor;
144
- texts.push(checkLabel(live ? editor.text : task.text, task.done));
145
- at += 1;
219
+ let current = null;
220
+ let at = 0;
221
+ for (const row of this.rows()) {
222
+ const key = row.quote ? `quote:${row.file}` : row.kind;
223
+ if (!current || current.key !== key) {
224
+ current = { key, title: sectionTitle(row), texts: [] };
225
+ sections.push(current);
146
226
  }
147
- const drafting = draft && at === focus && editor;
148
- texts.push(checkLabel(drafting ? editor.text : '', false));
227
+ current.texts.push(this.rowText(row, at));
149
228
  at += 1;
150
- sections.push({ title: kind.title, texts });
151
229
  }
152
230
  return sections;
153
231
  }
@@ -164,13 +242,13 @@ class TaskPage {
164
242
  }
165
243
 
166
244
  clampedTaskFocus() {
167
- return clamp(this.nav.tasksFocus, this.taskRowCount() - 1);
245
+ return clamp(this.nav.tasksFocus, 0, this.taskRowCount() - 1);
168
246
  }
169
247
 
170
248
  moveTaskFocus(delta) {
171
249
  const count = this.taskRowCount();
172
250
  if (!count) return;
173
- this.nav.tasksFocus = clamp(this.clampedTaskFocus() + delta, count - 1);
251
+ this.nav.tasksFocus = clamp(this.clampedTaskFocus() + delta, 0, count - 1);
174
252
  }
175
253
 
176
254
  openTasksPage() {
@@ -182,13 +260,6 @@ class TaskPage {
182
260
  this.ui.syncReviewPath();
183
261
  }
184
262
 
185
- closeTasksPage() {
186
- this.nav.tasksOpen = false;
187
- this.ui.syncReviewPath();
188
- this.ui.syncFileCursor();
189
- this.ui.lifecycle.catchUpWatch();
190
- }
191
-
192
263
  startDraftCompose() {
193
264
  this.openTasksPage();
194
265
  this.nav.tasksFocus = this.list(TASK_KINDS[0].id).length;
@@ -203,6 +274,10 @@ class TaskPage {
203
274
  this.composer.openCompose('tasks', '', null);
204
275
  return;
205
276
  }
277
+ if (row.quote) {
278
+ this.composer.openQuote(row.key, row.note.text);
279
+ return;
280
+ }
206
281
  this.composer.openCompose('tasks', row.task.text, row.task.id);
207
282
  }
208
283
 
@@ -223,7 +298,7 @@ class TaskPage {
223
298
  }
224
299
  const rows = this.rows();
225
300
  const at = this.clampedTaskFocus();
226
- const next = clamp(at + delta, rows.length - 1);
301
+ const next = clamp(at + delta, 0, rows.length - 1);
227
302
  if (next === at) return;
228
303
  const dest = rows[next];
229
304
  this.composer.commitCompose();
@@ -244,9 +319,18 @@ class TaskPage {
244
319
  const nav = this.nav;
245
320
  if (!this.isOpen()) return;
246
321
  const row = this.rowAt(this.clampedTaskFocus());
247
- if (!row || !row.task) return;
322
+ if (!row || row.draft) return;
323
+ const store = this.ui.review.store;
324
+ if (row.quote) {
325
+ store.setFeedback(row.key, { ...row.note, text: '' });
326
+ nav.tasksFocus = Math.min(nav.tasksFocus, this.rows().length - 1);
327
+ nav.tasksFocus = Math.max(0, nav.tasksFocus);
328
+ this.ui.flushReview(true);
329
+ return;
330
+ }
331
+ if (!row.task) return;
248
332
  const kind = row.kind;
249
- removeTask(this.ui.review.store, row.task.id);
333
+ store.removeTask(row.task.id);
250
334
  const rows = this.rows();
251
335
  let focus = Math.min(nav.tasksFocus, rows.length - 1);
252
336
  const at = rows[focus];
@@ -260,24 +344,51 @@ class TaskPage {
260
344
 
261
345
  toggleFocusedTask() {
262
346
  if (!this.canToggle()) return false;
263
- const task = this.focused();
264
- if (!task) return false;
265
- setTaskDone(this.ui.review.store, task.id, !task.done);
347
+ const row = this.rowAt(this.clampedTaskFocus());
348
+ if (!row || row.draft) return false;
349
+ const store = this.ui.review.store;
350
+ if (row.quote) store.setFeedbackDone(row.key, !row.note.done);
351
+ else if (row.task) store.setTaskDone(row.task.id, !row.task.done);
352
+ else return false;
266
353
  this.ui.flushReview(true);
267
354
  return true;
268
355
  }
269
356
 
357
+ followRow(dest) {
358
+ this.nav.tasksFocus = this.rowIndex(dest);
359
+ }
360
+
361
+ moveIssue(delta) {
362
+ if (!this.isOpen() || !delta) return;
363
+ const row = this.rowAt(this.clampedTaskFocus());
364
+ if (!row || row.draft) return;
365
+ this.composer.commitCompose();
366
+ const store = this.ui.review.store;
367
+ const moved = row.quote
368
+ ? store.moveFeedback(row.key, delta)
369
+ : store.moveTask(row.task.id, delta);
370
+ if (!moved) return;
371
+ this.composer.closeCompose();
372
+ this.followRow(row.quote ? { quote: true, key: row.key } : row);
373
+ this.ui.flushReview(true);
374
+ }
375
+
376
+ sameCompose(row, cursor) {
377
+ const composer = this.composer;
378
+ if (cursor !== this.clampedTaskFocus()) return false;
379
+ if (row.draft) return this.isDraftCompose();
380
+ if (row.quote) return composer.composeQuoteKey === row.key;
381
+ return row.task && row.task.id === composer.composeTaskId;
382
+ }
383
+
270
384
  onTaskHit(cursor) {
271
385
  if (!this.isOpen()) return;
386
+ this.closePlanMenu();
272
387
  const row = this.rowAt(cursor);
273
388
  if (!row) return;
274
389
  const composer = this.composer;
275
390
  if (composer.composeKind === 'tasks') {
276
- const same =
277
- cursor === this.clampedTaskFocus() &&
278
- ((row.draft && composer.composeTaskId === null) ||
279
- (row.task && row.task.id === composer.composeTaskId));
280
- if (same) return;
391
+ if (this.sameCompose(row, cursor)) return;
281
392
  this.closeEditor();
282
393
  }
283
394
  this.nav.tasksFocus = cursor;
@@ -287,15 +398,20 @@ class TaskPage {
287
398
  onTaskCheck(cursor) {
288
399
  if (!this.isOpen()) return;
289
400
  const row = this.rowAt(cursor);
290
- if (!row || !row.task) return void this.onTaskHit(cursor);
401
+ if (!row || row.draft) return void this.onTaskHit(cursor);
291
402
  const composer = this.composer;
292
- if (
403
+ const otherTask =
404
+ row.task &&
293
405
  composer.composeKind === 'tasks' &&
294
- composer.composeTaskId !== row.task.id
295
- ) {
296
- this.closeEditor();
297
- }
298
- setTaskDone(this.ui.review.store, row.task.id, !row.task.done);
406
+ composer.composeTaskId !== row.task.id;
407
+ const otherQuote =
408
+ row.quote &&
409
+ composer.composeKind === 'tasks' &&
410
+ composer.composeQuoteKey !== row.key;
411
+ if (otherTask || otherQuote) this.closeEditor();
412
+ const store = this.ui.review.store;
413
+ if (row.quote) store.setFeedbackDone(row.key, !row.note.done);
414
+ else store.setTaskDone(row.task.id, !row.task.done);
299
415
  this.nav.tasksFocus = cursor;
300
416
  this.ui.flushReview(true);
301
417
  }
@@ -314,20 +430,159 @@ class TaskPage {
314
430
  taskEditView() {
315
431
  const { composeKind, editor } = this.composer;
316
432
  if (composeKind !== 'tasks' || !editor) return null;
317
- return { cursor: editor.cursor };
433
+ return { cursor: editor.cursor, anchor: editor.anchor };
318
434
  }
319
435
 
320
436
  commitTask(text) {
321
437
  const notes = this.ui.review.store;
438
+ const key = this.composer.composeQuoteKey;
439
+ if (key) {
440
+ const note = notes.feedback.get(key);
441
+ if (!note) return;
442
+ notes.setFeedback(key, { ...note, text });
443
+ return;
444
+ }
322
445
  const id = this.composer.composeTaskId;
323
- const prev =
324
- id === null ? null : notes.tasks.find((task) => task.id === id);
325
- if (prev) return void setTaskText(notes, id, text);
446
+ const found = notes.tasks.find((task) => task.id === id);
447
+ const prev = id === null ? null : found;
448
+ if (prev) return void notes.setTaskText(id, text);
326
449
  if (!text.trim()) return;
327
450
  const kind = this.focusedKind();
328
- const todo = addTask(notes, TASKS_FILE, text, false, kind);
451
+ const todo = notes.addTask(TASKS_FILE, text, false, kind);
329
452
  this.composer.composeTaskId = todo.id;
330
453
  }
454
+
455
+ planName() {
456
+ const store = this.ui.review.store;
457
+ if (!store || !store.reviewPath) return '';
458
+ return path.basename(store.reviewPath);
459
+ }
460
+
461
+ planNames() {
462
+ const names = orderedPlanNames(this.ui.top || this.ui.cwd);
463
+ const current = this.planName();
464
+ if (current && !names.includes(current)) names.push(current);
465
+ return names;
466
+ }
467
+
468
+ planLabels() {
469
+ const root = this.ui.top || this.ui.cwd;
470
+ const dir = path.join(root, REVIEW_DIR);
471
+ const store = this.ui.review.store;
472
+ const parts = this.planNames().map((name) => planPart(dir, name, store));
473
+ const labels = alignPlanLabels(parts.filter(Boolean));
474
+ labels.push(NEW_PLAN);
475
+ return labels;
476
+ }
477
+
478
+ planMenu() {
479
+ const labels = this.planLabels();
480
+ if (!this.planPick) return labels;
481
+ return this.planPick.filter(labels);
482
+ }
483
+
484
+ planQuery() {
485
+ return this.planPick ? this.planPick.query : '';
486
+ }
487
+
488
+ planScroll() {
489
+ return this.planPick ? this.planPick.scroll : 0;
490
+ }
491
+
492
+ view(active) {
493
+ const pick = this.planPick;
494
+ return {
495
+ tasks: this.taskTexts(),
496
+ taskSections: this.taskSections(),
497
+ tasksFocus: this.clampedTaskFocus(),
498
+ taskEdit: this.taskEditView(),
499
+ planName: this.planName(),
500
+ planNames: active && pick ? this.planMenu() : [],
501
+ planOpen: this.planOpen,
502
+ planCursor: this.planCursor,
503
+ planScroll: this.planScroll(),
504
+ planQuery: this.planQuery(),
505
+ planEditor: pick ? pick.editor : null,
506
+ };
507
+ }
508
+
509
+ get planOpen() {
510
+ return this.planPick !== null;
511
+ }
512
+
513
+ get planCursor() {
514
+ return this.planPick ? this.planPick.cursor : 0;
515
+ }
516
+
517
+ closePlanMenu() {
518
+ this.planPick = null;
519
+ }
520
+
521
+ togglePlanMenu() {
522
+ if (this.planPick) return void this.closePlanMenu();
523
+ const labels = this.planLabels();
524
+ if (!labels.length) return;
525
+ const current = this.planName();
526
+ const at = labels.findIndex((item) => planKey(item) === current);
527
+ const cursor = at < 0 ? labels.length - 1 : at;
528
+ const extra = labels.length - 1;
529
+ const size = planListSize(this.ui);
530
+ this.planPick = new MenuPick('plan', cursor);
531
+ this.planPick.scroll = planOpenScroll(cursor, extra, labels.length, size);
532
+ }
533
+
534
+ movePlan(delta) {
535
+ const pick = this.planPick;
536
+ const items = this.planMenu();
537
+ if (!pick || !items.length) return;
538
+ pick.move(delta, items, idlePick);
539
+ }
540
+
541
+ planStep(key) {
542
+ const body = this.ui.lastFrame && this.ui.lastFrame.bodyH;
543
+ return pickDelta(key, menuRows(body), menuRows(body, 0.5));
544
+ }
545
+
546
+ notePlanEdit(changed) {
547
+ if (changed) this.planPick.touch();
548
+ }
549
+
550
+ onPlanKey(key) {
551
+ const pick = this.planPick;
552
+ if (!pick) return;
553
+ if (key === 'escape') return void this.closePlanMenu();
554
+ if (key === 'enter') return void this.choosePlan(pick.cursor);
555
+ if (key === ' ') return;
556
+ const handled = lineKey(pick.editor, key, this.ui, false);
557
+ if (handled) return void this.notePlanEdit(handled === 'text');
558
+ const step = this.planStep(key);
559
+ if (step) this.movePlan(step);
560
+ }
561
+
562
+ openPlan(name) {
563
+ if (!name || name === this.planName()) return;
564
+ this.composer.commitCompose();
565
+ this.composer.closeCompose();
566
+ const dir = this.ui.top || this.ui.cwd;
567
+ if (!this.ui.review.usePlan(dir, name)) return;
568
+ this.nav.tasksFocus = 0;
569
+ this.nav.scroll = 0;
570
+ this.ui.lifecycle.noteReviewStamp();
571
+ }
572
+
573
+ createPlan() {
574
+ const dir = this.ui.top || this.ui.cwd;
575
+ const reviewPath = allocateReviewPath(dir, new Date(), this.planNames());
576
+ this.openPlan(path.basename(reviewPath));
577
+ }
578
+
579
+ choosePlan(index) {
580
+ const label = this.planMenu()[index];
581
+ const name = label ? planKey(label) : '';
582
+ this.closePlanMenu();
583
+ if (name === NEW_PLAN) return void this.createPlan();
584
+ this.openPlan(name);
585
+ }
331
586
  }
332
587
 
333
588
  module.exports = { TaskPage };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const review = require('../review.js');
3
+ const review = require('../review/review.js');
4
4
  const { rankedTemplates, prefixTemplates, TEMPLATE_SHOW } = review;
5
5
 
6
6
  class TemplatePick {
@@ -1,9 +1,36 @@
1
1
  'use strict';
2
2
 
3
- const render = require('../render/render.js');
4
- const { presentRows, presentCursor } = render;
5
- const utilities = require('../utilities.js');
6
- const { listen, watchResize } = utilities;
3
+ const { presentRows, presentCursor } = require('../render/render.js');
4
+
5
+ const listen = (emitter, event, handler) => {
6
+ if (!emitter || typeof emitter.on !== 'function') return () => {};
7
+ emitter.on(event, handler);
8
+ return () => {
9
+ if (typeof emitter.removeListener === 'function') {
10
+ return void emitter.removeListener(event, handler);
11
+ }
12
+ if (typeof emitter.off === 'function') emitter.off(event, handler);
13
+ };
14
+ };
15
+
16
+ const watchResize = (stdout, proc, onResize, signal) => {
17
+ const stopStdout = listen(stdout, 'resize', onResize);
18
+ let stopProc = () => {};
19
+ try {
20
+ stopProc = listen(proc, 'SIGWINCH', onResize);
21
+ } catch {
22
+ // SIGWINCH is not available on Windows
23
+ }
24
+ const stop = () => {
25
+ stopStdout();
26
+ stopProc();
27
+ };
28
+ if (signal) {
29
+ if (signal.aborted) stop();
30
+ else signal.addEventListener('abort', stop, { once: true });
31
+ }
32
+ return stop;
33
+ };
7
34
 
8
35
  const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?1002h\x1b[?1006h';
9
36
  const LEAVE_TERM =
@@ -14,8 +41,12 @@ const sameCursor = (a, b) => {
14
41
  return a.x === b.x && a.y === b.y;
15
42
  };
16
43
 
17
- const sameSize = (a, b) =>
18
- a !== null && a.width === b.width && a.height === b.height;
44
+ const sameSize = (a, b) => {
45
+ if (a === null) return false;
46
+ const sameWidth = a.width === b.width;
47
+ const sameHeight = a.height === b.height;
48
+ return sameWidth && sameHeight;
49
+ };
19
50
 
20
51
  class Terminal {
21
52
  constructor({ stdin = null, stdout, proc = process }) {
@@ -36,10 +67,6 @@ class Terminal {
36
67
  this.lastSize = null;
37
68
  }
38
69
 
39
- hasTimer(name) {
40
- return this.timers.has(name);
41
- }
42
-
43
70
  cancel(name) {
44
71
  const id = this.timers.get(name);
45
72
  if (id === undefined) return;
@@ -87,6 +114,7 @@ class Terminal {
87
114
 
88
115
  startListening({ onData, onResize }) {
89
116
  if (this.disposed) return;
117
+ this.listenOpts = { onData, onResize };
90
118
  this.disposers.push(listen(this.stdin, 'data', onData));
91
119
  this.disposers.push(watchResize(this.stdout, this.proc, onResize));
92
120
  }
@@ -95,6 +123,19 @@ class Terminal {
95
123
  while (this.disposers.length) this.disposers.pop()();
96
124
  }
97
125
 
126
+ pause() {
127
+ this.stopListening();
128
+ this.leave();
129
+ if (this.stdin && this.stdin.pause) this.stdin.pause();
130
+ this.resetCache();
131
+ }
132
+
133
+ resume() {
134
+ if (this.disposed || this.open) return;
135
+ this.enter();
136
+ if (this.listenOpts) this.startListening(this.listenOpts);
137
+ }
138
+
98
139
  paint(frame, size, cursor) {
99
140
  if (this.disposed) return;
100
141
  const resized = !sameSize(this.lastSize, size);
@@ -107,8 +148,49 @@ class Terminal {
107
148
  }
108
149
  this.lastSize = size;
109
150
  this.lastCursor = cursor;
110
- this.stdout.write(presentRows(frame.rows, { clear: resized, cursor }));
151
+ const prev = resized || !last ? null : last.rows;
152
+ this.stdout.write(
153
+ presentRows(frame.rows, { clear: resized, cursor, prev }),
154
+ );
155
+ }
156
+ }
157
+
158
+ class Progress {
159
+ constructor(term, onTick, intervalMs) {
160
+ this.term = term;
161
+ this.onTick = onTick;
162
+ this.intervalMs = intervalMs;
163
+ this.active = new Set();
164
+ }
165
+
166
+ tick() {
167
+ if (!this.active.size) return;
168
+ this.onTick();
169
+ this.schedule();
170
+ }
171
+
172
+ schedule() {
173
+ this.term.later('progress', () => this.tick(), this.intervalMs);
174
+ }
175
+
176
+ start(id) {
177
+ this.active.add(id);
178
+ this.schedule();
179
+ }
180
+
181
+ stop(id) {
182
+ this.active.delete(id);
183
+ if (!this.active.size) this.term.cancel('progress');
184
+ }
185
+
186
+ clear() {
187
+ this.active.clear();
188
+ this.term.cancel('progress');
189
+ }
190
+
191
+ size() {
192
+ return this.active.size;
111
193
  }
112
194
  }
113
195
 
114
- module.exports = { ENTER_TERM, LEAVE_TERM, Terminal };
196
+ module.exports = { watchResize, ENTER_TERM, LEAVE_TERM, Terminal, Progress };