reslop 0.2.6 → 0.2.7

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 (42) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/lib/ansi.js +42 -2
  3. package/lib/api.js +1 -1
  4. package/lib/cli.js +21 -1
  5. package/lib/dashboard/model.js +28 -3
  6. package/lib/dashboard/npm.js +116 -19
  7. package/lib/files.js +13 -7
  8. package/lib/find.js +61 -0
  9. package/lib/git-deps.js +1 -0
  10. package/lib/git-worktree.js +22 -4
  11. package/lib/keys.js +26 -1
  12. package/lib/render/chrome.js +257 -79
  13. package/lib/render/dash-blocks.js +297 -46
  14. package/lib/render/dash-table.js +6 -1
  15. package/lib/render/files.js +23 -4
  16. package/lib/render/frame.js +4 -0
  17. package/lib/render/notes.js +13 -2
  18. package/lib/render/packages.js +93 -0
  19. package/lib/render/primitives.js +13 -6
  20. package/lib/render/repos.js +226 -0
  21. package/lib/render/search.js +25 -0
  22. package/lib/render/tiles.js +107 -12
  23. package/lib/review.js +5 -4
  24. package/lib/session/actions.js +136 -30
  25. package/lib/session/branches.js +3 -5
  26. package/lib/session/commit.js +31 -4
  27. package/lib/session/compose.js +19 -4
  28. package/lib/session/dashboard.js +160 -15
  29. package/lib/session/dispatch.js +38 -4
  30. package/lib/session/files-pane.js +17 -2
  31. package/lib/session/find.js +94 -0
  32. package/lib/session/lifecycle.js +27 -0
  33. package/lib/session/navigation.js +2 -0
  34. package/lib/session/packages.js +143 -0
  35. package/lib/session/pointer.js +14 -0
  36. package/lib/session/tasks.js +3 -3
  37. package/lib/session/view.js +15 -2
  38. package/lib/session/watch.js +5 -0
  39. package/lib/session/workspace.js +455 -0
  40. package/lib/session.js +14 -0
  41. package/lib/workspace.js +166 -0
  42. package/package.json +1 -1
@@ -13,7 +13,7 @@ const { readGitSummary } = gitInfo;
13
13
  const npmInfo = require('../dashboard/npm.js');
14
14
  const { readNpmSummary } = npmInfo;
15
15
  const model = require('../dashboard/model.js');
16
- const { buildModel } = model;
16
+ const { buildModel, minuteOf, MINUTE_MS, npmModel } = model;
17
17
  const filesPane = require('./files-pane.js');
18
18
  const watch = require('./watch.js');
19
19
  const { UNKNOWN_PATH, MODULES_STAMP } = watch;
@@ -21,8 +21,12 @@ const { UNKNOWN_PATH, MODULES_STAMP } = watch;
21
21
  const TICK_ID = 'dashboard';
22
22
  const HEAT_TIMER = 'dashboard-heat';
23
23
  const AGE_TIMER = 'dashboard-age';
24
- const SAMPLES = 40;
24
+ const MINUTE_TIMER = 'dashboard-minute';
25
+ const BLINK_TIMER = 'dashboard-blink';
26
+ const BLINK_MS = 3000;
27
+ const MINUTE_KEEP = 360;
25
28
  const SWITCHES = 4;
29
+ const RUN_MARK = { failed: 'fail', passed: 'pass', aborted: 'abort' };
26
30
  const GIT_NAMES = new Set([
27
31
  'HEAD',
28
32
  'packed-refs',
@@ -124,7 +128,9 @@ class Dashboard {
124
128
  this.totals = null;
125
129
  this.totalsKey = '';
126
130
  this.diffDelta = null;
127
- this.samples = [];
131
+ this.minutes = new Map();
132
+ this.activityAt = 0;
133
+ this.rebaseOn = false;
128
134
  this.switches = [];
129
135
  this.branchAt = new Map();
130
136
  this.branchSha = new Map();
@@ -177,6 +183,101 @@ class Dashboard {
177
183
  progress.stop(TICK_ID);
178
184
  term.cancel(HEAT_TIMER);
179
185
  term.cancel(AGE_TIMER);
186
+ term.cancel(MINUTE_TIMER);
187
+ term.cancel(BLINK_TIMER);
188
+ }
189
+
190
+ bucket(at) {
191
+ const minute = minuteOf(at);
192
+ let newest = minute;
193
+ for (const key of this.minutes.keys()) {
194
+ if (key > newest) newest = key;
195
+ }
196
+ if (minute < newest - MINUTE_KEEP) return null;
197
+ let row = this.minutes.get(minute);
198
+ if (!row) {
199
+ row = { level: 0, added: 0, removed: 0, marks: [] };
200
+ this.minutes.set(minute, row);
201
+ }
202
+ const oldest = newest - MINUTE_KEEP;
203
+ for (const key of this.minutes.keys()) {
204
+ if (key < oldest) this.minutes.delete(key);
205
+ }
206
+ return row;
207
+ }
208
+
209
+ touchActivity(at) {
210
+ if (at > this.activityAt) this.activityAt = at;
211
+ }
212
+
213
+ heat(at, added, removed = 0) {
214
+ const plus = Math.max(0, added || 0);
215
+ const minus = Math.max(0, removed || 0);
216
+ if (!at || plus + minus <= 0) return;
217
+ const row = this.bucket(at);
218
+ if (!row) return;
219
+ row.added += plus;
220
+ row.removed += minus;
221
+ row.level += plus + minus;
222
+ this.touchActivity(at);
223
+ }
224
+
225
+ mark(at, kind) {
226
+ if (!at || !kind) return;
227
+ const row = this.bucket(at);
228
+ if (!row || row.marks.includes(kind)) return;
229
+ row.marks.push(kind);
230
+ this.touchActivity(at);
231
+ }
232
+
233
+ noteCommits(next) {
234
+ const recent = next.commits.recent;
235
+ for (const entry of recent) this.mark(entry.at, 'commit');
236
+ }
237
+
238
+ noteRebase(next, now) {
239
+ const rebasing = Boolean(next.rebase);
240
+ if (rebasing && !this.rebaseOn) this.mark(now, 'rebase');
241
+ this.rebaseOn = rebasing;
242
+ }
243
+
244
+ noteRuns(runs) {
245
+ for (const run of runs) {
246
+ const kind = RUN_MARK[run.status];
247
+ if (kind) this.mark(run.endedAt || run.startedAt, kind);
248
+ }
249
+ }
250
+
251
+ isRunLive() {
252
+ for (const run of this.liveRuns()) {
253
+ if (run.status === 'running') return true;
254
+ }
255
+ const npm = this.ui.npm.lastRun;
256
+ return Boolean(npm && npm.running);
257
+ }
258
+
259
+ isBusy() {
260
+ if (this.git && this.git.rebase) return true;
261
+ return this.isRunLive();
262
+ }
263
+
264
+ liveActivity(now) {
265
+ if (this.isBusy()) return true;
266
+ return now - this.activityAt < BLINK_MS;
267
+ }
268
+
269
+ activityView(now) {
270
+ const minutes = [];
271
+ for (const [at, row] of this.minutes) {
272
+ minutes.push({
273
+ at,
274
+ level: row.level,
275
+ added: row.added,
276
+ removed: row.removed,
277
+ marks: row.marks.slice(),
278
+ });
279
+ }
280
+ return { minutes, live: this.liveActivity(now) };
180
281
  }
181
282
 
182
283
  afterDraw() {
@@ -184,17 +285,21 @@ class Dashboard {
184
285
  if (!this.visible()) return void this.cancelTimers();
185
286
  const { progress, term } = this.ui;
186
287
  const now = Date.now();
288
+ if (this.isBusy()) this.touchActivity(now);
187
289
  const until = this.hotUntil();
188
290
  const hot = now < until;
189
- const runs = this.liveRuns();
190
- const running = runs.some((run) => run.status === 'running');
191
- if (running || hot) progress.start(TICK_ID);
291
+ const live = this.liveActivity(now);
292
+ if (live || hot) progress.start(TICK_ID);
192
293
  else progress.stop(TICK_ID);
193
294
  const repaint = () => this.paintIfVisible();
194
295
  if (hot) term.later(HEAT_TIMER, repaint, until - now + 20);
195
- const at = this.youngest(runs);
296
+ const at = this.youngest(this.liveRuns());
196
297
  const delay = at ? ageDelay(now - at) : AGE_SLOW_MS;
197
298
  term.later(AGE_TIMER, repaint, delay);
299
+ const minuteLeft = MINUTE_MS - (now % MINUTE_MS) + 20;
300
+ term.later(MINUTE_TIMER, repaint, minuteLeft);
301
+ const blinkLeft = this.activityAt + BLINK_MS - now;
302
+ if (blinkLeft > 0) term.later(BLINK_TIMER, repaint, blinkLeft + 20);
198
303
  }
199
304
 
200
305
  async runFiles() {
@@ -209,7 +314,12 @@ class Dashboard {
209
314
  const after = index.summary().total;
210
315
  const lines = after.lines - before.lines;
211
316
  const bytes = after.bytes - before.bytes;
212
- if (lines || bytes) this.fileDelta = { lines, at: Date.now() };
317
+ if (!lines && !bytes) return;
318
+ const at = Date.now();
319
+ this.fileDelta = { lines, at };
320
+ const plus = lines > 0 ? lines : 0;
321
+ const minus = lines < 0 ? -lines : 0;
322
+ this.heat(at, plus || (minus ? 0 : 1), minus);
213
323
  }
214
324
 
215
325
  noteShas(next, now) {
@@ -243,9 +353,12 @@ class Dashboard {
243
353
  const moved = this.noteShas(next, now);
244
354
  const switched = this.noteSwitch(next, now);
245
355
  if (moved || switched) this.marks.branches = now;
356
+ if (switched) this.mark(now, 'branch');
246
357
  const sha = next.commits.last ? next.commits.last.sha : '';
247
358
  if (this.lastSha && sha !== this.lastSha) this.marks.commit = now;
248
359
  this.lastSha = sha;
360
+ this.noteCommits(next);
361
+ this.noteRebase(next, now);
249
362
  this.git = next;
250
363
  }
251
364
 
@@ -259,12 +372,17 @@ class Dashboard {
259
372
  (prev.deps !== next.deps ||
260
373
  prev.dev !== next.dev ||
261
374
  prev.modules.stamp !== next.modules.stamp);
262
- if (changed) this.marks.npm = Date.now();
375
+ if (changed) {
376
+ const now = Date.now();
377
+ this.marks.npm = now;
378
+ this.mark(now, 'npm');
379
+ }
263
380
  this.npm = next;
264
381
  }
265
382
 
266
383
  runRuns() {
267
384
  this.runs = readRuns(this.root());
385
+ this.noteRuns(this.runs);
268
386
  }
269
387
 
270
388
  noteLoaded() {
@@ -276,12 +394,14 @@ class Dashboard {
276
394
  if (prev) {
277
395
  const added = totals.added - prev.added;
278
396
  const removed = totals.removed - prev.removed;
279
- this.diffDelta = { added, removed, at: Date.now() };
397
+ const at = Date.now();
398
+ this.diffDelta = { added, removed, at };
399
+ const plus = Math.max(0, added);
400
+ const minus = Math.max(0, removed);
401
+ this.heat(at, plus || (minus ? 0 : 1), minus);
280
402
  }
281
403
  this.totals = totals;
282
404
  this.totalsKey = key;
283
- this.samples.push(totals.added + totals.removed);
284
- if (this.samples.length > SAMPLES) this.samples.shift();
285
405
  }
286
406
 
287
407
  requestAll() {
@@ -325,17 +445,18 @@ class Dashboard {
325
445
 
326
446
  view() {
327
447
  const ui = this.ui;
448
+ const now = Date.now();
328
449
  const entries = fileEntries(ui.items);
329
450
  const store = ui.review.store;
330
451
  return buildModel({
331
- now: Date.now(),
452
+ now,
332
453
  frame: ui.progressFrame,
333
454
  busy: ui.ops.busy,
334
455
  index: this.index,
335
456
  fileDelta: this.fileDelta,
336
457
  entries,
337
458
  totals: this.totals ?? fileTotals(entries),
338
- samples: this.samples,
459
+ activity: this.activityView(now),
339
460
  diffDelta: this.diffDelta,
340
461
  git: this.git,
341
462
  switches: this.switches,
@@ -350,6 +471,30 @@ class Dashboard {
350
471
  });
351
472
  }
352
473
 
474
+ packagesView() {
475
+ return npmModel({
476
+ npm: this.npm,
477
+ npmExtras: this.ui.lifecycle.npmExtras(),
478
+ npmRun: this.ui.npm.lastRun,
479
+ });
480
+ }
481
+
482
+ packageCount() {
483
+ const data = this.packagesView();
484
+ if (!data.ready || !data.hasManifest) return 1;
485
+ const listed = data.modules && data.modules.packages;
486
+ return listed ? listed.length : 0;
487
+ }
488
+
489
+ openPackages() {
490
+ const ui = this.ui;
491
+ ui.nav.pane = 'packages';
492
+ ui.nav.packagesCursor = 0;
493
+ ui.nav.resetListScroll('packages');
494
+ ui.nav.clearSelection();
495
+ ui.status = '';
496
+ }
497
+
353
498
  showFiles(scope) {
354
499
  const ui = this.ui;
355
500
  ui.showFiles();
@@ -365,7 +510,7 @@ class Dashboard {
365
510
  diffs: () => this.showFiles('diff'),
366
511
  commits: () => ui.commits.onCommit(),
367
512
  branches: () => ui.gitBranches.onBranch(),
368
- npm: () => ui.npm.open(),
513
+ npm: () => this.openPackages(),
369
514
  run: () => ui.npm.open(),
370
515
  tasks: () => ui.composer.tasks.openTasksPage(),
371
516
  };
@@ -6,12 +6,15 @@ const commit = require('./commit.js');
6
6
  const { hasStagedChanges, listedCommits } = commit;
7
7
  const actions = require('./actions.js');
8
8
  const { LIST_PANES, DASH_BLOCKS, DASH_ALLOWED, CONFIRM_MODE } = actions;
9
- const { disabledActions, actionFromKey, confirmKey } = actions;
9
+ const { REPOS_ALLOWED } = actions;
10
+ const { disabledActions, actionFromKey, confirmKey, lineHasDiff } = actions;
10
11
  const changeActions = require('./change-actions.js');
11
12
  const { onCommand } = changeActions;
12
13
  const filesPane = require('./files-pane.js');
13
14
  const pointer = require('./pointer.js');
14
15
  const { handlePointer } = pointer;
16
+ const finder = require('./find.js');
17
+ const { FIND_PANES, startFind, onFindKey } = finder;
15
18
 
16
19
  const currentTarget = (ui) => {
17
20
  if (ui.pane === 'files') return ui.fileCursorEntry();
@@ -22,7 +25,14 @@ const currentTarget = (ui) => {
22
25
  const unstagedCurrent = !ui.rev && !ui.revShort;
23
26
  return { ...entry, canCommit, unstagedCurrent };
24
27
  }
25
- return ui.current();
28
+ if (ui.pane !== 'unit') return ui.current();
29
+ const item = ui.current();
30
+ const lines = ui.view().unitLines ?? [];
31
+ if (!lines.length) return item;
32
+ const line = lines[ui.nav.unitLine] ?? null;
33
+ const lineDiff = lineHasDiff(line);
34
+ if (!item) return { lineDiff };
35
+ return { ...item, lineDiff };
26
36
  };
27
37
 
28
38
  const scrollBy = (fraction) => (ui) =>
@@ -45,7 +55,10 @@ const HANDLERS = {
45
55
  layout: filesPane.onLayout,
46
56
  theme: filesPane.onTheme,
47
57
  files: filesPane.onFiles,
48
- open: filesPane.onOpenFile,
58
+ open: (ui) => {
59
+ if (ui.pane === 'repos') return void ui.workspace.openSelected();
60
+ filesPane.onOpenFile(ui);
61
+ },
49
62
  file: filesPane.onFileScope,
50
63
  diff: filesPane.onDiffScope,
51
64
  back: filesPane.onEscape,
@@ -63,10 +76,12 @@ const HANDLERS = {
63
76
  check: (ui) => ui.composer.tasks.toggleFocusedTask(),
64
77
  removeTask: (ui) => {
65
78
  if (ui.pane === 'npm') return void ui.npm.askDrop();
79
+ if (ui.pane === 'packages') return;
66
80
  ui.composer.tasks.removeFocusedTask();
67
81
  },
68
82
  reload: (ui) => ui.lifecycle.onReload(),
69
83
  commit: (ui) => ui.commits.onCommit(),
84
+ newCommit: (ui) => ui.commits.onInsert(),
70
85
  amend: (ui) => ui.commits.onAmend(),
71
86
  reword: (ui) => ui.commits.onReword(),
72
87
  apply: (ui) => ui.commits.onApply(),
@@ -86,11 +101,19 @@ const HANDLERS = {
86
101
  npmEdit: (ui) => ui.npm.edit(),
87
102
  npmNew: (ui) => ui.npm.create(),
88
103
  npmDrop: (ui) => ui.npm.askDrop(),
104
+ packageNew: (ui) => ui.packages.create(),
105
+ packageDrop: (ui) => ui.packages.askDrop(),
106
+ packageWanted: (ui) => ui.packages.updateWanted(),
107
+ packageLatest: (ui) => ui.packages.updateLatest(),
89
108
  npmLogs: (ui) => ui.npm.askLogs(),
90
109
  npmStop: (ui) => ui.npm.closeView(),
91
110
  npmVerbose: (ui) => ui.npm.toggleVerbose(),
92
111
  npmRerun: (ui) => ui.npm.rerun(),
93
112
  quit: (ui) => ui.onQuit(),
113
+ repoLeft: (ui) => ui.workspace.move(-1, 0),
114
+ repoRight: (ui) => ui.workspace.move(1, 0),
115
+ repoUp: (ui) => ui.workspace.move(0, -1),
116
+ repoDown: (ui) => ui.workspace.move(0, 1),
94
117
  ...DASH_HANDLERS,
95
118
  };
96
119
 
@@ -102,6 +125,7 @@ const dispatchAction = (ui, action) => {
102
125
  if (result && result.kind === 'error') return;
103
126
  }
104
127
  const pane = ui.pane;
128
+ if (pane === 'repos' && !REPOS_ALLOWED.includes(action)) return;
105
129
  if (pane === 'dashboard' && !DASH_ALLOWED.includes(action)) return;
106
130
  if (LIST_PANES.includes(pane) && (action === 'next' || action === 'prev')) {
107
131
  return void filesPane.onScroll(ui, action === 'next' ? 1 : -1);
@@ -123,11 +147,13 @@ const CONFIRMATIONS = {
123
147
  },
124
148
  confirmDrop: {
125
149
  confirm: (ui) => {
150
+ if (ui.packages.dropName) return void ui.packages.confirmDrop();
126
151
  if (ui.npm.dropName) return void ui.npm.confirmDrop();
127
152
  if (ui.commits.dropName) return void ui.commits.confirmDropCommit();
128
153
  ui.gitBranches.confirmDropBranch();
129
154
  },
130
155
  cancel: (ui) => {
156
+ if (ui.packages.dropName) return void ui.packages.cancelDrop();
131
157
  if (ui.npm.dropName) return void ui.npm.cancelDrop();
132
158
  if (ui.commits.dropName) return void ui.commits.cancelDropCommit();
133
159
  ui.gitBranches.cancelDropBranch();
@@ -137,6 +163,12 @@ const CONFIRMATIONS = {
137
163
  force: (ui) => ui.gitBranches.onForcePush(),
138
164
  cancel: (ui) => ui.gitBranches.cancelPush(),
139
165
  },
166
+ confirmCommit: {
167
+ commit: (ui) => ui.commits.chooseCommit('commit'),
168
+ amend: (ui) => ui.commits.chooseCommit('amend'),
169
+ fixup: (ui) => ui.commits.chooseCommit('fixup'),
170
+ cancel: (ui) => ui.commits.cancelCommitChoice(),
171
+ },
140
172
  };
141
173
 
142
174
  const confirmChoice = (ui, key) => {
@@ -173,11 +205,13 @@ const handleKey = (ui, key) => {
173
205
  if (ui.mode === 'compose') {
174
206
  return void ui.runComposeCommand(ui.composer.handleKey(key));
175
207
  }
208
+ if (ui.mode === 'find') return void onFindKey(ui, key);
209
+ if (key === '/' && FIND_PANES.includes(ui.pane)) return void startFind(ui);
176
210
  const onNpmKey = npmKey(ui, key);
177
211
  if (onNpmKey) return void onNpmKey(ui);
178
212
  if (key === 'escape') return void filesPane.onEscape(ui);
179
213
  const tasks = ui.composer.tasks;
180
- const toggle = key === 'x' || key === ' ';
214
+ const toggle = key === ' ';
181
215
  if (toggle && tasks.toggleFocusedTask()) return;
182
216
  if (tasks.typeIntoTask(key)) return;
183
217
  const action = actionFromKey(key, ui.pane, ui.fileScope);
@@ -35,7 +35,8 @@ const showFiles = (ui) => {
35
35
  nav.tasksOpen === true ||
36
36
  nav.pane === 'branches' ||
37
37
  nav.pane === 'commits' ||
38
- nav.pane === 'npm';
38
+ nav.pane === 'npm' ||
39
+ nav.pane === 'packages';
39
40
  nav.pane = 'files';
40
41
  nav.resetListScroll('files');
41
42
  nav.clearSelection();
@@ -66,7 +67,7 @@ const showDashboard = (ui) => {
66
67
  ui.dashboard.enter();
67
68
  };
68
69
 
69
- const HOME_CHILDREN = ['files', 'branches', 'commits', 'npm'];
70
+ const HOME_CHILDREN = ['files', 'branches', 'commits', 'npm', 'packages'];
70
71
 
71
72
  const returnsHome = (ui) => {
72
73
  if (!ui.dashboardHome) return false;
@@ -75,6 +76,10 @@ const returnsHome = (ui) => {
75
76
  };
76
77
 
77
78
  const onEscape = (ui) => {
79
+ if (ui.nav.pane === 'repos') return void ui.onQuit();
80
+ if (ui.nav.pane === 'dashboard' && ui.workspaceRoot) {
81
+ return void ui.workspace.show();
82
+ }
78
83
  if (ui.nav.pane === 'dashboard') return void ui.onQuit();
79
84
  const commitDiff = ui.nav.pane === 'diff' && viewingCommit(ui);
80
85
  if (returnsHome(ui) || (ui.dashboardHome && commitDiff)) {
@@ -101,6 +106,7 @@ const onFiles = (ui) => {
101
106
  };
102
107
 
103
108
  const onOpenFile = (ui) => {
109
+ if (ui.nav.pane === 'packages') return;
104
110
  if (ui.nav.pane === 'branches') {
105
111
  return void ui.gitBranches.onCheckoutBranch();
106
112
  }
@@ -203,6 +209,15 @@ const onScroll = (ui, delta) => {
203
209
  return void ui.commits.onCommitMove(delta);
204
210
  }
205
211
  if (ui.nav.pane === 'npm') return void ui.npm.move(delta);
212
+ if (ui.nav.pane === 'packages') {
213
+ const count = ui.dashboard.packageCount();
214
+ const nav = ui.nav;
215
+ const next = nav.packagesCursor + delta;
216
+ const last = Math.max(0, count - 1);
217
+ nav.packagesCursor = Math.max(0, Math.min(last, next));
218
+ ui.status = '';
219
+ return;
220
+ }
206
221
  if (ui.nav.tasksOpen && ui.composer.tasks.taskRowCount() > 1) {
207
222
  return void ui.composer.tasks.moveTaskFocus(delta);
208
223
  }
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ const files = require('../files.js');
4
+ const { isTasksEntry, TASKS_FILE } = files;
5
+ const find = require('../find.js');
6
+ const { fileMatches } = find;
7
+
8
+ const FIND_PANES = ['files', 'diff'];
9
+
10
+ const isTypeKey = (key) =>
11
+ Boolean(key) && key.length === 1 && key.charCodeAt(0) >= 32;
12
+
13
+ const startFind = (ui) => {
14
+ if (ui.mode !== 'review') return;
15
+ if (!FIND_PANES.includes(ui.pane)) return;
16
+ ui.mode = 'find';
17
+ ui.nav.find = { query: '', at: 0 };
18
+ ui.status = '';
19
+ };
20
+
21
+ const stopFind = (ui) => {
22
+ ui.mode = 'review';
23
+ ui.nav.find = null;
24
+ ui.status = '';
25
+ };
26
+
27
+ const focusMatch = (ui, hit, jump) => {
28
+ const entry = ui.fileList()[hit.index];
29
+ if (!entry) return;
30
+ const moved = ui.nav.fileCursor !== hit.index;
31
+ ui.nav.fileCursor = hit.index;
32
+ if (isTasksEntry(entry)) {
33
+ ui.nav.reviewPath = TASKS_FILE;
34
+ return;
35
+ }
36
+ ui.nav.reviewPath = entry.path;
37
+ if (ui.nav.pane !== 'diff') {
38
+ ui.nav.index = entry.firstIndex;
39
+ return;
40
+ }
41
+ if (!moved && !jump) return;
42
+ ui.nav.index = entry.openIndex ?? entry.firstIndex;
43
+ ui.nav.scroll = 0;
44
+ ui.nav.clearSelection();
45
+ };
46
+
47
+ const showHit = (ui, jump) => {
48
+ const state = ui.nav.find;
49
+ if (!state) return;
50
+ const hits = fileMatches(ui.fileList(), state.query);
51
+ if (!hits.length) return;
52
+ const count = hits.length;
53
+ let at = state.at % count;
54
+ if (at < 0) at += count;
55
+ state.at = at;
56
+ focusMatch(ui, hits[at], jump);
57
+ };
58
+
59
+ const typeFind = (ui, key) => {
60
+ const state = ui.nav.find;
61
+ state.query += key;
62
+ state.at = 0;
63
+ showHit(ui, true);
64
+ };
65
+
66
+ const eraseFind = (ui) => {
67
+ const state = ui.nav.find;
68
+ if (!state.query) return void stopFind(ui);
69
+ state.query = state.query.slice(0, -1);
70
+ state.at = 0;
71
+ showHit(ui, true);
72
+ };
73
+
74
+ const moveFind = (ui, delta) => {
75
+ const state = ui.nav.find;
76
+ if (!state.query) return;
77
+ state.at += delta;
78
+ showHit(ui, false);
79
+ };
80
+
81
+ const onFindKey = (ui, key) => {
82
+ if (!ui.nav.find) {
83
+ ui.mode = 'review';
84
+ return;
85
+ }
86
+ if (key === 'ctrl-c') return void ui.onQuit();
87
+ if (key === 'escape' || key === 'enter') return void stopFind(ui);
88
+ if (key === 'backspace') return void eraseFind(ui);
89
+ if (key === 'up' || key === 'left') return void moveFind(ui, -1);
90
+ if (key === 'down' || key === 'right') return void moveFind(ui, 1);
91
+ if (isTypeKey(key)) typeFind(ui, key);
92
+ };
93
+
94
+ module.exports = { FIND_PANES, startFind, stopFind, onFindKey };
@@ -163,6 +163,7 @@ class Lifecycle {
163
163
 
164
164
  applyLoaded(loaded) {
165
165
  const ui = this.ui;
166
+ if (ui.nav.pane === 'repos') return false;
166
167
  const { loader, collection, nav } = ui;
167
168
  if (loaded.generation !== undefined) {
168
169
  if (!loader.isCurrent(loaded.generation, ui.done)) return false;
@@ -325,6 +326,11 @@ class Lifecycle {
325
326
 
326
327
  openLoad() {
327
328
  const loader = this.ui.loader;
329
+ if (this.ui.nav.pane === 'repos') {
330
+ loader.didLoad = true;
331
+ loader.promise = Promise.resolve();
332
+ return loader.promise;
333
+ }
328
334
  if (loader.promise) return loader.promise;
329
335
  if (loader.didLoad && !loader.pendingExtras) {
330
336
  loader.promise = Promise.resolve();
@@ -335,6 +341,10 @@ class Lifecycle {
335
341
  }
336
342
 
337
343
  async loadReady() {
344
+ if (this.ui.nav.pane === 'repos') {
345
+ this.ui.loader.didLoad = true;
346
+ return;
347
+ }
338
348
  this.ui.ensureRepo();
339
349
  await this.openLoad();
340
350
  if (this.ui.loadError) throw this.ui.loadError;
@@ -407,6 +417,7 @@ class Lifecycle {
407
417
  }
408
418
 
409
419
  onDiskChange() {
420
+ if (this.ui.nav.pane === 'repos') return;
410
421
  if (Date.now() < this.ignoreWatchUntil) return;
411
422
  if (!this.shouldDeferWatch()) {
412
423
  return void this.refreshFromRepo({ keepView: true, quiet: true });
@@ -465,6 +476,22 @@ class Lifecycle {
465
476
 
466
477
  startWatch() {
467
478
  this.stopWatch();
479
+ const ui = this.ui;
480
+ if (ui.nav.pane === 'repos') {
481
+ const root = ui.workspaceRoot;
482
+ if (!root) return;
483
+ const info = ui.workspaceInfo;
484
+ const repos = info ? info.repos : [];
485
+ const paths = ['.'];
486
+ for (const repo of repos) paths.push(path.join(repo.name, '.git'));
487
+ this.diskWatcher = new DiskWatcher({
488
+ root,
489
+ paths,
490
+ matchReview: (rel) => rel.includes('/.review/'),
491
+ onPaths: (rels) => ui.workspace.notePaths(rels),
492
+ });
493
+ return;
494
+ }
468
495
  const root = this.root;
469
496
  if (!isWorktree(this.ui) || !root) return;
470
497
  this.diskWatcher = new DiskWatcher({
@@ -20,6 +20,7 @@ class Navigation {
20
20
  this.branchCursor = 0;
21
21
  this.commitCursor = 0;
22
22
  this.npmCursor = 0;
23
+ this.packagesCursor = 0;
23
24
  this.scroll = 0;
24
25
  this.listScrollByPane = freshListScroll();
25
26
  this.reviewPath = null;
@@ -31,6 +32,7 @@ class Navigation {
31
32
  this.lastClick = null;
32
33
  this.tasksOpen = false;
33
34
  this.tasksFocus = 0;
35
+ this.find = null;
34
36
  }
35
37
 
36
38
  clampIndex(length) {