reslop 0.2.7 → 0.3.0

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 +17 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -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} +123 -96
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -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 +111 -48
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +15 -43
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -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 +383 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -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} +46 -14
  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 +9 -0
  81. package/lib/review/format.js +426 -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 +958 -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 -39
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +181 -145
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +85 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +75 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -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} +156 -63
  120. package/lib/session/tasks.js +320 -65
  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} +2 -2
  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,30 +1,24 @@
1
1
  'use strict';
2
2
 
3
- const wrap = require('../wrap.js');
4
- const { cursorInWrap, wrapDoc } = wrap;
3
+ const { cursorInWrap, wrapDoc } = require('../term/wrap.js');
5
4
  const render = require('../render/render.js');
6
- const { noteInnerWidth, codeInnerWidth, todoBodyWidth } = render;
7
- const commits = require('../render/commits.js');
8
- const { commitMessageWidth } = commits;
9
- const commit = require('./commit.js');
10
- const { listedCommits } = commit;
11
- const editor = require('../editor.js');
12
- const { Editor } = editor;
13
- const files = require('../files.js');
5
+ const { noteInnerWidth, codeInnerWidth, todoBodyWidth, activeDigits } = render;
6
+ const { commitMessageWidth } = require('../render/commits.js');
7
+ const { listedCommits } = require('./commit.js');
8
+ const { Editor } = require('../input/editor.js');
9
+ const { isTextKey } = require('../input/keys.js');
10
+ const files = require('../common/files.js');
14
11
  const { itemPath, isTaskItem, isTasksEntry, listPath } = files;
15
12
  const { isReadOnlyOrigin } = files;
16
- const diff = require('../diff/diff.js');
17
- const { blockAddText } = diff;
18
- const items = require('./items.js');
19
- const { itemFeedKey, canEditCode } = items;
20
- const review = require('../review.js');
21
- const { setFeedback, setCode, checkLabel, rememberTemplate } = review;
22
- const templates = require('./templates.js');
23
- const { TemplatePick } = templates;
24
- const tasks = require('./tasks.js');
25
- const { TaskPage } = tasks;
13
+ const { blockAddText } = require('../diff/diff.js');
14
+ const { itemFeedKey, canEditCode } = require('./items.js');
15
+ const review = require('../review/review.js');
16
+ const { checkLabel } = review;
17
+ const { TemplatePick } = require('./templates.js');
18
+ const { TaskPage } = require('./tasks.js');
26
19
  const unit = require('./unit.js');
27
- const clipboard = require('../clipboard.js');
20
+ const clipboard = require('../term/clipboard.js');
21
+ const { EDIT_OP, copyLine, cutLine } = require('../input/line-edit.js');
28
22
 
29
23
  const COMPOSE_EDITS = ['backspace', 'delete', 'ctrl-z', 'ctrl-y'];
30
24
  const COMPOSE_CLIP = ['ctrl-x', 'ctrl-v'];
@@ -78,17 +72,13 @@ const blankTail = (text) => {
78
72
  return count;
79
73
  };
80
74
 
75
+ const EDITOR_KEYS = {};
76
+ for (const key of Object.keys(EDIT_OP)) {
77
+ EDITOR_KEYS[key] = (c) => EDIT_OP[key](c.editor);
78
+ }
79
+
81
80
  const COMPOSE_SPECIAL = {
82
- backspace: (c) => c.editor.backspace(),
83
- delete: (c) => c.editor.delete(),
84
- left: (c) => c.editor.move(-1),
85
- right: (c) => c.editor.move(1),
86
- 'shift-left': (c) => c.editor.move(-1, true),
87
- 'shift-right': (c) => c.editor.move(1, true),
88
- 'ctrl-left': (c) => c.editor.moveWord(-1),
89
- 'ctrl-right': (c) => c.editor.moveWord(1),
90
- 'ctrl-shift-left': (c) => c.editor.moveWord(-1, true),
91
- 'ctrl-shift-right': (c) => c.editor.moveWord(1, true),
81
+ ...EDITOR_KEYS,
92
82
  up: (c) => c.moveLine(-1),
93
83
  down: (c) => c.moveLine(1),
94
84
  'shift-up': (c) => c.moveLine(-1, true),
@@ -118,7 +108,9 @@ class Composer {
118
108
  }
119
109
 
120
110
  reset() {
111
+ this.tasks.closePlanMenu();
121
112
  this.fileDirty = false;
113
+ this.saving = false;
122
114
  this.clear();
123
115
  }
124
116
 
@@ -128,6 +120,7 @@ class Composer {
128
120
  this.commitKind = null;
129
121
  this.commitBody = '';
130
122
  this.composeTaskId = null;
123
+ this.composeQuoteKey = null;
131
124
  this.composeBaseline = '';
132
125
  this.templateIndex = -1;
133
126
  this.templateFocus = false;
@@ -157,7 +150,8 @@ class Composer {
157
150
  if (kind !== 'code' && kind !== 'file') return;
158
151
  if (!this.editor) return;
159
152
  const width = this.viewSize().width;
160
- this.editor.reveal(codeInnerWidth(width, this.ui.layout));
153
+ const digits = activeDigits(this.ui);
154
+ this.editor.reveal(codeInnerWidth(width, this.ui.layout, digits));
161
155
  }
162
156
 
163
157
  followFileEdit() {
@@ -173,6 +167,7 @@ class Composer {
173
167
  this.ui.mode = 'compose';
174
168
  this.composeKind = kind;
175
169
  this.composeTaskId = taskId;
170
+ this.composeQuoteKey = null;
176
171
  this.composeBaseline = text;
177
172
  this.fileOpened = kind === 'file' ? text : '';
178
173
  this.editor = new Editor(text);
@@ -182,6 +177,11 @@ class Composer {
182
177
  this.ui.nav.clearSelection();
183
178
  }
184
179
 
180
+ openQuote(key, text) {
181
+ this.openCompose('tasks', text, null);
182
+ this.composeQuoteKey = key;
183
+ }
184
+
185
185
  closeCompose() {
186
186
  const ui = this.ui;
187
187
  ui.mode = 'review';
@@ -205,7 +205,7 @@ class Composer {
205
205
  commitFeedback(text) {
206
206
  const item = this.ui.current();
207
207
  if (!item || isTaskItem(item)) return;
208
- setFeedback(this.notes, itemFeedKey(item), itemNote(item, text));
208
+ this.notes.setFeedback(itemFeedKey(item), itemNote(item, text));
209
209
  }
210
210
 
211
211
  dropCodeNote(item, text) {
@@ -213,34 +213,52 @@ class Composer {
213
213
  if (!notes) return;
214
214
  const key = itemFeedKey(item);
215
215
  if (!notes.code.has(key)) return;
216
- setCode(notes, key, { ...itemNote(item, text), clear: true });
216
+ notes.setCode(key, { ...itemNote(item, text), clear: true });
217
+ }
218
+
219
+ // Edits that land in the worktree or index, not in the review notes
220
+ writesDisk() {
221
+ const kind = this.composeKind;
222
+ if (!this.editor) return false;
223
+ if (kind === 'file') return true;
224
+ return kind === 'code' && this.writesCodeFile(this.ui.current());
217
225
  }
218
226
 
219
227
  commitCode(text) {
228
+ const item = this.ui.current();
229
+ if (!canEditCode(item) || this.writesCodeFile(item)) return;
230
+ const note = itemNote(item, text);
231
+ const clear = text === blockAddText(item.hunk, item.blockId);
232
+ this.notes.setCode(itemFeedKey(item), clear ? { ...note, clear } : note);
233
+ }
234
+
235
+ async write(action) {
220
236
  const ui = this.ui;
221
- const item = ui.current();
222
- if (!canEditCode(item)) return true;
223
- const original = blockAddText(item.hunk, item.blockId);
224
- if (!this.writesCodeFile(item)) {
225
- const note = itemNote(item, text);
226
- const clear = text === original;
227
- setCode(this.notes, itemFeedKey(item), clear ? { ...note, clear } : note);
228
- return true;
237
+ ui.ignoreWatch();
238
+ try {
239
+ await ui.ops.exclusive(action);
240
+ } catch (error) {
241
+ ui.status = error.message;
242
+ return false;
229
243
  }
230
- if (text !== original) {
231
- try {
232
- ui.repo.edit(ui.top, item, text);
233
- } catch (error) {
234
- ui.status = error.message;
235
- return false;
236
- }
244
+ ui.ignoreWatch();
245
+ return true;
246
+ }
247
+
248
+ async writeCode(text) {
249
+ const ui = this.ui;
250
+ const item = ui.current();
251
+ if (text !== blockAddText(item.hunk, item.blockId)) {
252
+ const edit = (top) => ui.repo.edit(top, item, text);
253
+ const written = await this.write(edit);
254
+ if (!written) return false;
237
255
  this.fileDirty = true;
238
256
  }
239
257
  this.dropCodeNote(item, text);
240
258
  return true;
241
259
  }
242
260
 
243
- saveUnitFile(text) {
261
+ async saveUnitFile(text) {
244
262
  const ui = this.ui;
245
263
  const rel = ui.nav.reviewPath;
246
264
  if (!rel || !ui.repo.writeFile) return true;
@@ -248,48 +266,37 @@ class Composer {
248
266
  ui.status = 'read only';
249
267
  return false;
250
268
  }
251
- try {
252
- ui.repo.writeFile(ui.top, rel, text);
253
- } catch (error) {
254
- ui.status = error.message;
255
- return false;
256
- }
257
- ui.ignoreWatch();
269
+ const save = (top) => ui.repo.writeFile(top, rel, text);
270
+ const written = await this.write(save);
271
+ if (!written) return false;
272
+ ui.reads.keepText(rel, text);
258
273
  this.composeBaseline = text;
259
274
  this.fileDirty = true;
260
275
  return true;
261
276
  }
262
277
 
263
- stageEditedFile(rel) {
278
+ stageItems(rel) {
264
279
  const ui = this.ui;
265
280
  const repo = ui.repo;
266
- if (!rel || !this.canChange()) return false;
267
- if (repo.stagePath) {
268
- try {
269
- repo.stagePath(ui.top, rel);
270
- } catch (error) {
271
- ui.status = error.message;
272
- return false;
273
- }
274
- ui.ignoreWatch();
275
- return true;
276
- }
277
- if (!repo.add) return false;
278
- let staged = 0;
279
- for (const item of ui.items) {
280
- if (listPath(item) !== rel) continue;
281
- if (isReadOnlyOrigin(item.origin) || item.origin === 'staged') continue;
282
- try {
283
- repo.add(ui.top, item);
284
- } catch (error) {
285
- ui.status = error.message;
286
- return staged > 0;
287
- }
288
- ui.collection.keepOrigin(item, 'staged');
289
- staged += 1;
290
- }
291
- if (staged) ui.ignoreWatch();
292
- return staged > 0;
281
+ if (!repo.add) return;
282
+ const targets = ui.items.filter((item) => {
283
+ if (listPath(item) !== rel || isReadOnlyOrigin(item.origin)) return false;
284
+ return item.origin !== 'staged';
285
+ });
286
+ if (!targets.length) return;
287
+ for (const item of targets) ui.collection.keepOrigin(item, 'staged');
288
+ ui.ignoreWatch();
289
+ const write = async () => {
290
+ for (const item of targets) await ui.ops.add(item);
291
+ };
292
+ ui.ops.enqueueLazy({ kind: 'staged', done: '', reload: false, write });
293
+ }
294
+
295
+ async stageEditedFile(rel) {
296
+ const repo = this.ui.repo;
297
+ if (!rel || !this.canChange()) return;
298
+ if (!repo.stagePath) return void this.stageItems(rel);
299
+ await this.write((top) => repo.stagePath(top, rel));
293
300
  }
294
301
 
295
302
  finishSave(reload, doneStatus) {
@@ -300,84 +307,112 @@ class Composer {
300
307
  if (!ui.status) ui.status = doneStatus;
301
308
  return;
302
309
  }
310
+ this.fileDirty = false;
303
311
  ui.reloadAfterChange(() => {
304
312
  if (!ui.status) ui.status = doneStatus;
305
313
  });
306
314
  }
307
315
 
308
- saveFileCompose() {
316
+ // Closes the editor after a note or a block edit; reloads if a file changed
317
+ finishNote(doneStatus) {
318
+ const ui = this.ui;
319
+ if (this.fileDirty === true) return void this.finishSave(true, doneStatus);
320
+ ui.flushReview();
321
+ this.closeCompose();
322
+ ui.status = doneStatus;
323
+ }
324
+
325
+ async saveFile() {
309
326
  const rel = this.ui.nav.reviewPath;
310
327
  const opened = this.fileOpened;
311
- const text = this.editor ? this.editor.text : '';
312
- if (this.editor) this.ui.nav.unitLine = this.editor.linePos().line;
313
- if (!this.saveUnitFile(text)) return { kind: 'error' };
328
+ const text = this.editor.text;
329
+ this.ui.nav.unitLine = this.editor.linePos().line;
330
+ const saved = await this.saveUnitFile(text);
331
+ if (!saved) return false;
314
332
  const changed = text !== opened;
315
333
  const reload = this.fileDirty === true;
316
- if (changed) this.stageEditedFile(rel);
334
+ if (changed) await this.stageEditedFile(rel);
317
335
  this.finishSave(reload, changed ? 'staged' : 'saved');
318
- return { kind: 'saved' };
336
+ return true;
337
+ }
338
+
339
+ async saveCode() {
340
+ const ui = this.ui;
341
+ const item = ui.current();
342
+ const unstaged = item.origin !== 'staged';
343
+ const rel = itemPath(item);
344
+ const saved = await this.writeCode(this.editor.text);
345
+ if (!saved) return false;
346
+ const staging = unstaged && this.fileDirty === true;
347
+ if (staging) await this.stageEditedFile(rel);
348
+ this.finishNote(staging ? 'staged' : 'saved');
349
+ return true;
350
+ }
351
+
352
+ saveEdit() {
353
+ return this.composeKind === 'file' ? this.saveFile() : this.saveCode();
354
+ }
355
+
356
+ // One save at a time; `after` runs once the edit is safely on disk
357
+ async saveToDisk(after) {
358
+ if (this.saving) return;
359
+ this.saving = true;
360
+ const settle = () => {
361
+ this.saving = false;
362
+ };
363
+ const saved = await this.saveEdit().finally(settle);
364
+ if (saved && after) after();
365
+ }
366
+
367
+ // Leaves the editor whether or not the edit could be written
368
+ async writeAndClose() {
369
+ const text = this.editor.text;
370
+ if (this.composeKind === 'file') await this.saveUnitFile(text);
371
+ else await this.writeCode(text);
372
+ this.closeCompose();
319
373
  }
320
374
 
321
375
  commitCompose() {
322
376
  const kind = this.composeKind;
323
- if (!kind || !this.editor) return true;
377
+ if (!kind || !this.editor) return;
324
378
  const text = this.editor.text;
325
- if (kind === 'feedback') {
326
- if (this.notes) this.commitFeedback(text);
327
- return true;
328
- }
329
- if (kind === 'tasks') {
330
- this.tasks.commitTask(text);
331
- return true;
332
- }
333
- if (kind === 'code') return this.commitCode(text);
334
- if (kind === 'file') return this.saveUnitFile(text);
335
- return true;
379
+ if (kind === 'feedback' && this.notes) this.commitFeedback(text);
380
+ if (kind === 'tasks') this.tasks.commitTask(text);
381
+ if (kind === 'code') this.commitCode(text);
336
382
  }
337
383
 
384
+ // Notes save at once. An edit to a file answers `saving` and finishes later
338
385
  saveCompose(opts = {}) {
339
386
  const ui = this.ui;
340
387
  const kind = this.composeKind;
341
388
  if (COMMAND_KINDS.includes(kind)) return { kind };
342
- if (kind === 'file') return this.saveFileCompose();
343
- const item = ui.current();
344
- const shouldStage =
345
- kind === 'code' && this.writesCodeFile(item) && item.origin !== 'staged';
346
- const rel = itemPath(item);
389
+ if (this.writesDisk()) {
390
+ ui.ops.hold(this.saveToDisk(opts.after));
391
+ return { kind: 'saving' };
392
+ }
347
393
  const baseline = this.composeBaseline;
348
- if (this.commitCompose() === false) return { kind: 'error' };
349
- const reload = this.fileDirty === true;
394
+ this.commitCompose();
350
395
  if (kind === 'feedback' && this.notes) {
351
396
  const next = this.editor ? this.editor.text : '';
352
- rememberTemplate(this.notes, baseline, next);
353
- }
354
- const staging = shouldStage && reload;
355
- if (staging) this.stageEditedFile(rel);
356
- const doneStatus = staging ? 'staged' : 'saved';
357
- if (reload) {
358
- this.finishSave(true, doneStatus);
359
- } else {
360
- ui.flushReview();
361
- this.closeCompose();
362
- ui.status = doneStatus;
397
+ this.notes.rememberTemplate(baseline, next);
363
398
  }
399
+ this.finishNote('saved');
364
400
  if (opts.editNext === true) this.tasks.editNextTask();
365
401
  return { kind: 'saved' };
366
402
  }
367
403
 
404
+ autosaveFile(text) {
405
+ if (this.saving || text === this.composeBaseline) return;
406
+ this.ui.ops.track(this.saveUnitFile(text));
407
+ }
408
+
368
409
  autosave() {
369
410
  const kind = this.composeKind;
370
411
  const text = this.editor ? this.editor.text : '';
371
- if (kind === 'file' && this.editor) {
372
- if (text === this.composeBaseline) return;
373
- return void this.saveUnitFile(text);
374
- }
375
- const editingCode = kind === 'code';
412
+ if (kind === 'file' && this.editor) return void this.autosaveFile(text);
376
413
  const reviewKind = kind !== null && !COMMAND_KINDS.includes(kind);
377
- const fileEdit = editingCode && this.writesCodeFile(this.ui.current());
378
- if (reviewKind && !fileEdit && (editingCode || text.trim())) {
379
- this.commitCompose();
380
- }
414
+ const noted = reviewKind && !this.writesDisk();
415
+ if (noted && (kind === 'code' || text.trim())) this.commitCompose();
381
416
  if (this.notes && this.notes.dirty) this.ui.flushReview();
382
417
  }
383
418
 
@@ -538,6 +573,12 @@ class Composer {
538
573
  ui.status = 'read only';
539
574
  return;
540
575
  }
576
+ ui.ops.hold(this.editViewed());
577
+ }
578
+
579
+ async editViewed() {
580
+ const ui = this.ui;
581
+ await ui.reads.loadText(ui.nav.reviewPath);
541
582
  const at = unit.editCursor(ui);
542
583
  this.openCompose('file', at.text);
543
584
  this.fileEditRows = at.rows ?? null;
@@ -554,8 +595,8 @@ class Composer {
554
595
  ui.status = 'not a diff block';
555
596
  return;
556
597
  }
557
- unit.openUnitFile(ui, entry);
558
- this.onUnitEdit();
598
+ const edit = () => this.onUnitEdit();
599
+ ui.ops.hold(unit.showFile(ui, entry).then(edit));
559
600
  }
560
601
 
561
602
  onCode() {
@@ -563,7 +604,7 @@ class Composer {
563
604
  if (ui.nav.pane === 'files') return void this.onFileEdit();
564
605
  if (ui.nav.pane === 'unit') return void this.onUnitEdit();
565
606
  const item = ui.current();
566
- if (ui.nav.pane === 'files' || !canEditCode(item)) {
607
+ if (!canEditCode(item)) {
567
608
  ui.status = 'not a diff block';
568
609
  return;
569
610
  }
@@ -609,16 +650,11 @@ class Composer {
609
650
  }
610
651
 
611
652
  copy() {
612
- const text = this.editor.selectedText();
613
- if (!text) return;
614
- const ok = clipboard.copyText(text, this.ui.stdout);
615
- this.ui.status = ok ? 'copied' : 'copy failed';
653
+ copyLine(this.editor, this.ui);
616
654
  }
617
655
 
618
656
  cut() {
619
- if (!this.editor.hasSelect()) return;
620
- this.copy();
621
- this.editor.removeSpan();
657
+ cutLine(this.editor, this.ui);
622
658
  }
623
659
 
624
660
  paste() {
@@ -646,10 +682,7 @@ class Composer {
646
682
  this.afterEdit();
647
683
  return result ?? null;
648
684
  }
649
- if (!key.length || key.startsWith('ctrl-') || key.startsWith('alt-')) {
650
- return null;
651
- }
652
- if (key.length === 1 && key.charCodeAt(0) >= 32) {
685
+ if (isTextKey(key)) {
653
686
  this.templates.clearTemplatePick();
654
687
  this.editor.insert(key);
655
688
  this.afterEdit();
@@ -660,12 +693,15 @@ class Composer {
660
693
  applyDiskText(text) {
661
694
  const editor = this.editor;
662
695
  if (this.composeKind !== 'file' || !editor) return;
663
- if (text === editor.text) return;
696
+ // The baseline is the last text known on disk: no news, keep the typing
697
+ if (text === editor.text || text === this.composeBaseline) return;
664
698
  const cursor = editor.cursor;
665
699
  editor.text = text;
666
700
  editor.cursor = Math.min(cursor, text.length);
667
701
  editor.anchor = null;
668
702
  this.composeBaseline = text;
703
+ // An outside change is the new starting point, not an edit made here
704
+ this.fileOpened = text;
669
705
  this.fileEditRows = unit.linesFor(this.ui, this.ui.nav.reviewPath, null);
670
706
  this.syncCodeScroll();
671
707
  this.followFileEdit();
@@ -1,22 +1,15 @@
1
1
  'use strict';
2
2
 
3
- const files = require('../files.js');
4
- const { fileEntries, fileTotals } = files;
5
- const review = require('../review.js');
6
- const { noteCounts } = review;
7
- const runs = require('../runs.js');
8
- const { readRuns, settleRecord, isRunsRel } = runs;
9
- const tree = require('../dashboard/tree.js');
10
- const { FileIndex, HEAT_MS } = tree;
11
- const gitInfo = require('../dashboard/git.js');
12
- const { readGitSummary } = gitInfo;
13
- const npmInfo = require('../dashboard/npm.js');
14
- const { readNpmSummary } = npmInfo;
3
+ const { fileEntries, fileTotals } = require('../common/files.js');
4
+ const { noteCounts } = require('../review/review.js');
5
+ const { readRuns, settleRecord, isRunsRel } = require('../runs/runs.js');
6
+ const { FileIndex, HEAT_MS } = require('../dashboard/tree.js');
7
+ const { readGitSummary } = require('../dashboard/git.js');
8
+ const { readNpmSummary } = require('../dashboard/npm.js');
15
9
  const model = require('../dashboard/model.js');
16
10
  const { buildModel, minuteOf, MINUTE_MS, npmModel } = model;
17
11
  const filesPane = require('./files-pane.js');
18
- const watch = require('./watch.js');
19
- const { UNKNOWN_PATH, MODULES_STAMP } = watch;
12
+ const { UNKNOWN_PATH, MODULES_STAMP } = require('../dashboard/watch.js');
20
13
 
21
14
  const TICK_ID = 'dashboard';
22
15
  const HEAT_TIMER = 'dashboard-heat';
@@ -27,7 +20,7 @@ const BLINK_MS = 3000;
27
20
  const MINUTE_KEEP = 360;
28
21
  const SWITCHES = 4;
29
22
  const RUN_MARK = { failed: 'fail', passed: 'pass', aborted: 'abort' };
30
- const GIT_NAMES = new Set([
23
+ const GIT_NAMES = [
31
24
  'HEAD',
32
25
  'packed-refs',
33
26
  'FETCH_HEAD',
@@ -36,8 +29,8 @@ const GIT_NAMES = new Set([
36
29
  'logs',
37
30
  'rebase-merge',
38
31
  'rebase-apply',
39
- ]);
40
- const NPM_FILES = new Set(['package.json', 'package-lock.json', MODULES_STAMP]);
32
+ ];
33
+ const NPM_FILES = ['package.json', 'package-lock.json', MODULES_STAMP];
41
34
  const AGE_FAST_MS = 1000;
42
35
  const AGE_MID_MS = 30000;
43
36
  const AGE_SLOW_MS = 600000;
@@ -85,11 +78,12 @@ const classify = (paths) => {
85
78
  if (rel === UNKNOWN_PATH) {
86
79
  found.all = true;
87
80
  } else if (rel.startsWith('.git/')) {
88
- if (GIT_NAMES.has(rel.split('/')[1])) found.git = true;
81
+ const gitName = rel.split('/')[1];
82
+ if (GIT_NAMES.includes(gitName)) found.git = true;
89
83
  } else if (isRunsRel(rel)) {
90
84
  found.runs = true;
91
85
  } else {
92
- if (NPM_FILES.has(rel)) found.npm = true;
86
+ if (NPM_FILES.includes(rel)) found.npm = true;
93
87
  found.files.push(rel);
94
88
  }
95
89
  }
@@ -410,8 +404,12 @@ class Dashboard {
410
404
  }
411
405
 
412
406
  noteDisk(paths) {
413
- if (!this.active) return;
414
407
  const found = classify(paths);
408
+ const npm = this.ui.npm;
409
+ if ((found.runs || found.all) && npm && npm.refreshRecorded) {
410
+ npm.refreshRecorded();
411
+ }
412
+ if (!this.active) return;
415
413
  if (found.all) return void this.requestAll();
416
414
  const tasks = this.tasks;
417
415
  if (found.files.length) {
@@ -465,9 +463,11 @@ class Dashboard {
465
463
  npm: this.npm,
466
464
  npmExtras: ui.lifecycle.npmExtras(),
467
465
  npmRun: ui.npm.lastRun,
466
+ npmRuns: ui.npm.dashboardRuns(),
468
467
  runs: this.liveRuns(),
469
468
  notes: noteCounts(store),
470
469
  store,
470
+ agents: ui.agents.summary(),
471
471
  });
472
472
  }
473
473
 
@@ -498,8 +498,8 @@ class Dashboard {
498
498
  showFiles(scope) {
499
499
  const ui = this.ui;
500
500
  ui.showFiles();
501
- if (scope === 'file') filesPane.onFileScope(ui);
502
- else filesPane.onDiffScope(ui);
501
+ if (scope === 'file') filesPane.onFileScope(ui, true);
502
+ else filesPane.onDiffScope(ui, true);
503
503
  ui.status = '';
504
504
  }
505
505
 
@@ -513,6 +513,7 @@ class Dashboard {
513
513
  npm: () => this.openPackages(),
514
514
  run: () => ui.npm.open(),
515
515
  tasks: () => ui.composer.tasks.openTasksPage(),
516
+ agents: () => ui.agents.open(),
516
517
  };
517
518
  const opener = openers[id];
518
519
  if (!opener || ui.mode !== 'review') return;