reslop 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -1
- package/README.md +21 -3
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +166 -370
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -13
- package/lib/agents/models.js +250 -376
- package/lib/agents/registry.js +437 -0
- package/lib/agents/run.js +234 -0
- package/lib/agents/transcript.js +143 -0
- package/lib/cli.js +2 -1
- package/lib/common/process.js +26 -1
- package/lib/dashboard/git.js +11 -1
- package/lib/dashboard/reports.js +80 -0
- package/lib/dashboard/watch.js +43 -8
- package/lib/dashboard/workspace.js +19 -20
- package/lib/dashboard/worktrees.js +145 -0
- package/lib/git/deps.js +29 -0
- package/lib/git/dir.js +50 -0
- package/lib/git/git.js +1 -1
- package/lib/git/linked.js +152 -0
- package/lib/git/local.js +179 -0
- package/lib/git/status.js +126 -0
- package/lib/git/worktree.js +137 -31
- package/lib/input/actions.js +83 -2
- package/lib/input/editor.js +9 -0
- package/lib/input/keys.js +39 -3
- package/lib/input/line-edit.js +8 -3
- package/lib/render/agents.js +64 -36
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -4
- package/lib/render/frame.js +16 -2
- package/lib/render/log.js +5 -4
- package/lib/render/menu.js +103 -6
- package/lib/render/name-edit.js +31 -0
- package/lib/render/repos.js +52 -8
- package/lib/render/status.js +22 -4
- package/lib/report/render.js +4 -7
- package/lib/report/run.js +16 -23
- package/lib/review/agent-instructions.md +13 -8
- package/lib/review/format.js +0 -6
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +227 -194
- package/lib/session/agents/away.js +48 -0
- package/lib/session/agents/job.js +23 -86
- package/lib/session/agents/jobs.js +14 -27
- package/lib/session/agents/options.js +45 -101
- package/lib/session/agents/plans.js +53 -0
- package/lib/session/agents/view.js +34 -0
- package/lib/session/branches.js +18 -2
- package/lib/session/compose.js +20 -6
- package/lib/session/dispatch.js +43 -3
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/import.js +12 -1
- package/lib/session/lifecycle.js +70 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +3 -1
- package/lib/session/npm/npm.js +111 -13
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +17 -2
- package/lib/session/tasks.js +1 -1
- package/lib/session/terminal.js +2 -2
- package/lib/session/text-menu.js +50 -0
- package/lib/session/view.js +19 -2
- package/lib/session/workspace.js +179 -38
- package/lib/session/worktrees.js +280 -0
- package/lib/term/select.js +2 -1
- package/lib/term/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
package/lib/session/session.js
CHANGED
|
@@ -27,6 +27,7 @@ const dispatch = require('./dispatch.js');
|
|
|
27
27
|
const { dispatchAction, confirmChoice, handleEvent } = dispatch;
|
|
28
28
|
const { Dashboard } = require('./dashboard.js');
|
|
29
29
|
const { Workspace } = require('./workspace.js');
|
|
30
|
+
const { WorktreeController } = require('./worktrees.js');
|
|
30
31
|
|
|
31
32
|
const bindAccessors = (target, owner, names) => {
|
|
32
33
|
for (const name of names) {
|
|
@@ -134,6 +135,9 @@ const assignParts = (session, options) => {
|
|
|
134
135
|
session.agents = new AgentsController(session);
|
|
135
136
|
session.dashboard = new Dashboard(session);
|
|
136
137
|
session.workspace = new Workspace(session);
|
|
138
|
+
const infoOf = () => session.treesInfo;
|
|
139
|
+
session.trees = new Workspace(session, { pane: 'branches', infoOf });
|
|
140
|
+
session.worktrees = new WorktreeController(session);
|
|
137
141
|
session.lifecycle = new Lifecycle(session);
|
|
138
142
|
};
|
|
139
143
|
|
|
@@ -181,8 +185,12 @@ class Session {
|
|
|
181
185
|
this.agents.reset();
|
|
182
186
|
this.dashboard.reset();
|
|
183
187
|
this.workspace.reset();
|
|
188
|
+
this.worktrees.reset();
|
|
189
|
+
this.trees.reset();
|
|
190
|
+
this.treesInfo = null;
|
|
184
191
|
this.top = this.cwd;
|
|
185
192
|
this.branch = '';
|
|
193
|
+
this.repositoryUrl = '';
|
|
186
194
|
this.status = '';
|
|
187
195
|
this.layout = 'unified';
|
|
188
196
|
this.lineNumbers = false;
|
|
@@ -261,7 +269,8 @@ class Session {
|
|
|
261
269
|
panel.runScroll = frame.runScroll;
|
|
262
270
|
}
|
|
263
271
|
if (typeof frame.gridCols === 'number') {
|
|
264
|
-
this.
|
|
272
|
+
const tiles = this.nav.pane === 'branches' ? this.trees : this.workspace;
|
|
273
|
+
tiles.cols = frame.gridCols;
|
|
265
274
|
}
|
|
266
275
|
if (this.nav.tasksOpen) this.nav.scroll = frame.scroll;
|
|
267
276
|
const editScroll = frame.cursor && frame.cursor.scroll;
|
|
@@ -273,6 +282,7 @@ class Session {
|
|
|
273
282
|
this.scheduleAutosave();
|
|
274
283
|
this.dashboard.afterDraw();
|
|
275
284
|
this.workspace.afterDraw();
|
|
285
|
+
this.trees.afterDraw();
|
|
276
286
|
}
|
|
277
287
|
|
|
278
288
|
scheduleAutosave() {
|
|
@@ -363,6 +373,7 @@ class Session {
|
|
|
363
373
|
if (!result) return;
|
|
364
374
|
if (result.kind === 'commit') this.commits.finishGitCommit();
|
|
365
375
|
if (result.kind === 'branch') this.gitBranches.finishCreateBranch();
|
|
376
|
+
if (result.kind === 'worktree') this.worktrees.finishAdd();
|
|
366
377
|
if (result.kind === 'npm') this.npm.finishEdit();
|
|
367
378
|
if (result.kind === 'package') this.packages.finishAdd();
|
|
368
379
|
}
|
|
@@ -384,6 +395,7 @@ class Session {
|
|
|
384
395
|
this.paths = [];
|
|
385
396
|
this.repoName = repoName;
|
|
386
397
|
this.branch = '';
|
|
398
|
+
this.repositoryUrl = '';
|
|
387
399
|
this.rev = null;
|
|
388
400
|
this.revShort = '';
|
|
389
401
|
this.change = null;
|
|
@@ -391,6 +403,8 @@ class Session {
|
|
|
391
403
|
this.collection.reset();
|
|
392
404
|
this.reads.reset();
|
|
393
405
|
this.review.reset();
|
|
406
|
+
this.npm.retarget();
|
|
407
|
+
this.agents.retarget();
|
|
394
408
|
this.nav.pane = pane;
|
|
395
409
|
this.nav.tasksOpen = false;
|
|
396
410
|
this.nav.scroll = 0;
|
|
@@ -489,7 +503,7 @@ class Session {
|
|
|
489
503
|
this.openLoad();
|
|
490
504
|
// Populate the agent model cache while the initial screen is loading.
|
|
491
505
|
// Opening the Agents pane can then use the cache instead of being the
|
|
492
|
-
// first place that
|
|
506
|
+
// first place that asks each agent for its models.
|
|
493
507
|
void this.agents.loadModels();
|
|
494
508
|
this.openUpdate();
|
|
495
509
|
this.draw();
|
|
@@ -518,6 +532,7 @@ class Session {
|
|
|
518
532
|
this.lifecycle.stopWatch();
|
|
519
533
|
this.dashboard.stop();
|
|
520
534
|
this.workspace.stop();
|
|
535
|
+
this.trees.stop();
|
|
521
536
|
this.term.stopListening();
|
|
522
537
|
this.progress.clear();
|
|
523
538
|
this.term.clearTimers();
|
package/lib/session/tasks.js
CHANGED
|
@@ -430,7 +430,7 @@ class TaskPage {
|
|
|
430
430
|
taskEditView() {
|
|
431
431
|
const { composeKind, editor } = this.composer;
|
|
432
432
|
if (composeKind !== 'tasks' || !editor) return null;
|
|
433
|
-
return { cursor: editor.cursor };
|
|
433
|
+
return { cursor: editor.cursor, anchor: editor.anchor };
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
commitTask(text) {
|
package/lib/session/terminal.js
CHANGED
|
@@ -32,9 +32,9 @@ const watchResize = (stdout, proc, onResize, signal) => {
|
|
|
32
32
|
return stop;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?
|
|
35
|
+
const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?1003h\x1b[?1006h';
|
|
36
36
|
const LEAVE_TERM =
|
|
37
|
-
'\x1b[?1006l\x1b[?
|
|
37
|
+
'\x1b[?1006l\x1b[?1003l\x1b[?7h\x1b[0 q\x1b[?12l\x1b[?25h\x1b[?1049l';
|
|
38
38
|
|
|
39
39
|
const sameCursor = (a, b) => {
|
|
40
40
|
if (!a || !b) return !a && !b;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TEXT_ACTIONS = [
|
|
4
|
+
'textCut',
|
|
5
|
+
'textCopy',
|
|
6
|
+
'textPaste',
|
|
7
|
+
'textDelete',
|
|
8
|
+
'textSelectAll',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const textEditor = (ui, cell) => {
|
|
12
|
+
if (ui.mode === 'compose') return ui.editor;
|
|
13
|
+
const statusY = ui.lastFrame?.height - 1;
|
|
14
|
+
if (cell.y !== statusY) return null;
|
|
15
|
+
if (ui.mode === 'find') return ui.nav.find?.editor;
|
|
16
|
+
if (ui.mode === 'import') return ui.nav.import?.editor;
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const textDisabled = (editor) => {
|
|
21
|
+
const off = editor.hasSelect() ? [] : ['textCut', 'textCopy'];
|
|
22
|
+
if (!editor.hasSelect() && editor.cursor >= editor.text.length) {
|
|
23
|
+
off.push('textDelete');
|
|
24
|
+
}
|
|
25
|
+
if (!editor.text) off.push('textSelectAll');
|
|
26
|
+
return off;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const TEXT_KEYS = {
|
|
30
|
+
textCut: 'ctrl-x',
|
|
31
|
+
textCopy: 'ctrl-c',
|
|
32
|
+
textPaste: 'ctrl-v',
|
|
33
|
+
textDelete: 'delete',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const runTextAction = (ui, menu, action) => {
|
|
37
|
+
const editor = menu.editor;
|
|
38
|
+
if (!editor) return false;
|
|
39
|
+
if (action === 'textSelectAll') {
|
|
40
|
+
editor.anchor = 0;
|
|
41
|
+
editor.cursor = editor.text.length;
|
|
42
|
+
if (ui.mode === 'compose') ui.composer.afterEdit();
|
|
43
|
+
} else {
|
|
44
|
+
const key = TEXT_KEYS[action];
|
|
45
|
+
if (key) ui.handleEvent({ type: 'key', key });
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
module.exports = { TEXT_ACTIONS, textEditor, textDisabled, runTextAction };
|
package/lib/session/view.js
CHANGED
|
@@ -137,6 +137,7 @@ const navView = (ui) => {
|
|
|
137
137
|
scroll: nav.scroll,
|
|
138
138
|
listScroll: nav.listScroll,
|
|
139
139
|
selection: nav.selection,
|
|
140
|
+
contextMenu: nav.contextMenu,
|
|
140
141
|
pane: nav.pane,
|
|
141
142
|
fileScope: nav.fileScope,
|
|
142
143
|
fileCursor: nav.fileCursor,
|
|
@@ -151,9 +152,22 @@ const navView = (ui) => {
|
|
|
151
152
|
};
|
|
152
153
|
|
|
153
154
|
const dropNameOf = (ui) => {
|
|
154
|
-
const { packages, gitBranches, commits, npm } = ui;
|
|
155
|
+
const { packages, gitBranches, commits, npm, worktrees } = ui;
|
|
155
156
|
const name = packages.dropName || gitBranches.dropName;
|
|
156
|
-
return name || commits.dropName || npm.dropName;
|
|
157
|
+
return name || commits.dropName || npm.dropName || worktrees.dropName;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// One tile looks selected: the one under the cursor while the worktrees have
|
|
161
|
+
// the focus, else the worktree that is shown
|
|
162
|
+
const treesView = (ui) => {
|
|
163
|
+
const model = ui.trees.view();
|
|
164
|
+
const picked = ui.worktrees.focused() ? model.cursor : model.current;
|
|
165
|
+
return { ...model, cursor: picked < 0 ? model.cursor : picked };
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const paneTrees = (ui, pane) => {
|
|
169
|
+
if (pane !== 'branches' || !ui.worktrees.hasPanels()) return null;
|
|
170
|
+
return treesView(ui);
|
|
157
171
|
};
|
|
158
172
|
|
|
159
173
|
const repoView = (ui) => {
|
|
@@ -180,6 +194,7 @@ const repoView = (ui) => {
|
|
|
180
194
|
updateFrom: updater.from,
|
|
181
195
|
updateTo: updater.to,
|
|
182
196
|
dropName: dropNameOf(ui),
|
|
197
|
+
worktreeTarget: ui.worktrees.branchTarget,
|
|
183
198
|
};
|
|
184
199
|
};
|
|
185
200
|
|
|
@@ -200,6 +215,8 @@ const screenView = (ui) => {
|
|
|
200
215
|
dashboard: pane === 'dashboard' ? ui.dashboard.view() : null,
|
|
201
216
|
packages: pane === 'packages' ? ui.dashboard.packagesView() : null,
|
|
202
217
|
repos: pane === 'repos' ? ui.workspace.view() : null,
|
|
218
|
+
trees: paneTrees(ui, pane),
|
|
219
|
+
treeFocus: ui.worktrees.focused(),
|
|
203
220
|
};
|
|
204
221
|
};
|
|
205
222
|
|
package/lib/session/workspace.js
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('node:fs');
|
|
3
4
|
const path = require('node:path');
|
|
4
5
|
const { REVIEW_DIR } = require('../common/files.js');
|
|
5
6
|
const { FileIndex, HEAT_MS } = require('../dashboard/tree.js');
|
|
6
7
|
const { UNKNOWN_PATH } = require('../dashboard/watch.js');
|
|
7
8
|
const workspace = require('../dashboard/workspace.js');
|
|
8
9
|
const { emptyDiff, countCommits, readDiffStat, taskStats, pickRun } = workspace;
|
|
10
|
+
const { currentBranchName } = workspace;
|
|
9
11
|
const { readNpmSummary } = require('../dashboard/npm.js');
|
|
10
|
-
const {
|
|
12
|
+
const { reportCounts } = require('../dashboard/reports.js');
|
|
13
|
+
const { dirKey, changedFiles } = require('../git/linked.js');
|
|
14
|
+
const { resolveGitDirs } = require('../git/dir.js');
|
|
11
15
|
|
|
12
16
|
const TICK_ID = 'repos';
|
|
13
|
-
const HEAT_TIMER = 'repos-heat';
|
|
14
17
|
|
|
15
18
|
const freshBag = () => ({
|
|
16
19
|
files: [],
|
|
17
20
|
git: false,
|
|
21
|
+
diff: false,
|
|
18
22
|
runs: false,
|
|
19
23
|
tasks: false,
|
|
20
24
|
npm: false,
|
|
@@ -24,6 +28,7 @@ const freshBag = () => ({
|
|
|
24
28
|
const fullBag = () => ({
|
|
25
29
|
files: [UNKNOWN_PATH],
|
|
26
30
|
git: true,
|
|
31
|
+
diff: true,
|
|
27
32
|
runs: true,
|
|
28
33
|
tasks: true,
|
|
29
34
|
npm: true,
|
|
@@ -35,6 +40,7 @@ const mergeBag = (left, right) => {
|
|
|
35
40
|
return {
|
|
36
41
|
files: [...base.files, ...right.files],
|
|
37
42
|
git: base.git || right.git,
|
|
43
|
+
diff: base.diff || right.diff,
|
|
38
44
|
runs: base.runs || right.runs,
|
|
39
45
|
tasks: base.tasks || right.tasks,
|
|
40
46
|
npm: base.npm || right.npm,
|
|
@@ -42,9 +48,37 @@ const mergeBag = (left, right) => {
|
|
|
42
48
|
};
|
|
43
49
|
};
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
// The path inside of a directory, or null for one outside of it
|
|
52
|
+
const insideOf = (base, full) => {
|
|
53
|
+
const rel = path.relative(base, full);
|
|
54
|
+
const out = rel === '..' || rel.startsWith(`..${path.sep}`);
|
|
55
|
+
if (out || path.isAbsolute(rel)) return null;
|
|
56
|
+
return rel.split(path.sep).join('/');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const gitDirKey = (dir) => {
|
|
60
|
+
const dirs = resolveGitDirs(dir);
|
|
61
|
+
return dirs ? dirKey(dirs.gitDir) : '';
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// What a list of worktrees says of a repository
|
|
65
|
+
const rowMeta = (repo) => ({
|
|
46
66
|
name: repo.name,
|
|
67
|
+
group: repo.group ?? repo.name,
|
|
68
|
+
main: repo.main ?? true,
|
|
69
|
+
linked: repo.linked ?? false,
|
|
70
|
+
branch: repo.branch ?? '',
|
|
71
|
+
head: repo.head ?? '',
|
|
72
|
+
detached: repo.detached ?? false,
|
|
73
|
+
locked: repo.locked ?? false,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const blankRow = (repo) => ({
|
|
77
|
+
...rowMeta(repo),
|
|
47
78
|
dir: repo.dir,
|
|
79
|
+
key: dirKey(repo.dir),
|
|
80
|
+
gitDir: gitDirKey(repo.dir),
|
|
81
|
+
dirty: false,
|
|
48
82
|
index: new FileIndex(repo.dir),
|
|
49
83
|
bytes: 0,
|
|
50
84
|
exts: [],
|
|
@@ -78,12 +112,23 @@ const readFiles = async (row, bag) => {
|
|
|
78
112
|
};
|
|
79
113
|
|
|
80
114
|
const readCommits = async (row) => {
|
|
81
|
-
|
|
115
|
+
const [commits, branch] = await Promise.all([
|
|
116
|
+
countCommits(row.dir),
|
|
117
|
+
currentBranchName(row.dir),
|
|
118
|
+
]);
|
|
119
|
+
row.commits = commits;
|
|
120
|
+
row.branch = branch;
|
|
121
|
+
row.detached = !branch;
|
|
82
122
|
row.commitsReady = true;
|
|
83
123
|
};
|
|
84
124
|
|
|
85
125
|
const readDiff = async (row) => {
|
|
86
|
-
|
|
126
|
+
const [diff, changed] = await Promise.all([
|
|
127
|
+
readDiffStat(row.dir),
|
|
128
|
+
changedFiles(row.dir).catch(() => []),
|
|
129
|
+
]);
|
|
130
|
+
row.diff = diff;
|
|
131
|
+
row.dirty = changed.length > 0;
|
|
87
132
|
row.diffReady = true;
|
|
88
133
|
};
|
|
89
134
|
|
|
@@ -99,24 +144,26 @@ const readRuns = (row) => {
|
|
|
99
144
|
row.runsReady = true;
|
|
100
145
|
};
|
|
101
146
|
|
|
102
|
-
const reportCount = async (dir, kind) => {
|
|
103
|
-
const report = await collectReport(dir, kind);
|
|
104
|
-
return report ? report.size : 0;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
147
|
const readNpm = async (row) => {
|
|
108
|
-
const summary = await
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
reportCount(row.dir, 'outdated'),
|
|
148
|
+
const [summary, counts] = await Promise.all([
|
|
149
|
+
readNpmSummary(row.dir, row.npm),
|
|
150
|
+
reportCounts(row.dir),
|
|
112
151
|
]);
|
|
113
|
-
row.npm = { ...summary,
|
|
152
|
+
row.npm = { ...summary, ...counts };
|
|
114
153
|
row.npmReady = true;
|
|
115
154
|
};
|
|
116
155
|
|
|
117
|
-
const toTile = (row) => ({
|
|
156
|
+
const toTile = (row, live) => ({
|
|
118
157
|
id: row.name,
|
|
119
158
|
name: row.name,
|
|
159
|
+
group: row.group,
|
|
160
|
+
linked: row.linked,
|
|
161
|
+
branch: row.branch,
|
|
162
|
+
detached: row.detached,
|
|
163
|
+
head: row.head,
|
|
164
|
+
locked: row.locked,
|
|
165
|
+
dirty: row.dirty,
|
|
166
|
+
agents: live.get(row.key) ?? 0,
|
|
120
167
|
bytes: row.bytes,
|
|
121
168
|
exts: row.exts,
|
|
122
169
|
tasks: row.tasks,
|
|
@@ -171,13 +218,20 @@ const classifyRest = (rest) => {
|
|
|
171
218
|
return bag;
|
|
172
219
|
}
|
|
173
220
|
bag.files.push(rest);
|
|
221
|
+
bag.diff = true;
|
|
174
222
|
bag.activity = true;
|
|
175
223
|
return bag;
|
|
176
224
|
};
|
|
177
225
|
|
|
226
|
+
// The tiles of a folder on the repos screen, and the ones of the worktrees
|
|
227
|
+
// of a repository above the branches
|
|
178
228
|
class Workspace {
|
|
179
|
-
constructor(ui) {
|
|
229
|
+
constructor(ui, options = {}) {
|
|
180
230
|
this.ui = ui;
|
|
231
|
+
this.pane = options.pane ?? 'repos';
|
|
232
|
+
this.tick = this.pane === 'repos' ? TICK_ID : `${TICK_ID}-${this.pane}`;
|
|
233
|
+
this.heat = `${this.tick}-heat`;
|
|
234
|
+
this.infoOf = options.infoOf ?? (() => this.ui.workspaceInfo);
|
|
181
235
|
this.epoch = 0;
|
|
182
236
|
this.reset();
|
|
183
237
|
}
|
|
@@ -190,14 +244,46 @@ class Workspace {
|
|
|
190
244
|
this.cols = 1;
|
|
191
245
|
}
|
|
192
246
|
|
|
247
|
+
// Whether the tiles are on the screen
|
|
248
|
+
shown() {
|
|
249
|
+
return this.ui.uiOpen && this.ui.nav.pane === this.pane;
|
|
250
|
+
}
|
|
251
|
+
|
|
193
252
|
seed() {
|
|
194
|
-
const info = this.
|
|
253
|
+
const info = this.infoOf();
|
|
195
254
|
const repos = info ? info.repos : [];
|
|
196
255
|
this.rows = repos.map(blankRow);
|
|
197
256
|
this.byName = new Map(this.rows.map((row) => [row.name, row]));
|
|
198
257
|
if (this.cursor >= this.rows.length) this.cursor = 0;
|
|
199
258
|
}
|
|
200
259
|
|
|
260
|
+
// The list of worktrees changed: what is known of the rest is kept
|
|
261
|
+
adopt(repos) {
|
|
262
|
+
const focus = this.rows[this.cursor] ? this.rows[this.cursor].key : '';
|
|
263
|
+
const known = new Map(this.rows.map((row) => [row.key, row]));
|
|
264
|
+
const fresh = [];
|
|
265
|
+
this.rows = repos.map((repo) => {
|
|
266
|
+
const prev = known.get(dirKey(repo.dir));
|
|
267
|
+
if (!prev) {
|
|
268
|
+
const row = blankRow(repo);
|
|
269
|
+
fresh.push(row);
|
|
270
|
+
return row;
|
|
271
|
+
}
|
|
272
|
+
return Object.assign(prev, rowMeta(repo));
|
|
273
|
+
});
|
|
274
|
+
this.byName = new Map(this.rows.map((row) => [row.name, row]));
|
|
275
|
+
const at = this.rows.findIndex((row) => row.key === focus);
|
|
276
|
+
this.cursor = at < 0 ? Math.min(this.cursor, this.rows.length - 1) : at;
|
|
277
|
+
this.cursor = Math.max(0, this.cursor);
|
|
278
|
+
for (const row of fresh) this.kick(row, fullBag());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
focusDir(dir) {
|
|
282
|
+
const key = dirKey(dir);
|
|
283
|
+
const at = this.rows.findIndex((row) => row.key === key);
|
|
284
|
+
if (at >= 0) this.cursor = at;
|
|
285
|
+
}
|
|
286
|
+
|
|
201
287
|
stale(epoch) {
|
|
202
288
|
return epoch !== this.epoch || !this.active;
|
|
203
289
|
}
|
|
@@ -205,7 +291,7 @@ class Workspace {
|
|
|
205
291
|
publish(epoch) {
|
|
206
292
|
if (this.stale(epoch)) return;
|
|
207
293
|
this.afterDraw();
|
|
208
|
-
if (this.
|
|
294
|
+
if (this.shown()) this.ui.paint();
|
|
209
295
|
}
|
|
210
296
|
|
|
211
297
|
async settle(epoch, read) {
|
|
@@ -219,8 +305,8 @@ class Workspace {
|
|
|
219
305
|
if (bag.files.length) {
|
|
220
306
|
jobs.push(this.settle(epoch, () => readFiles(row, bag)));
|
|
221
307
|
}
|
|
222
|
-
if (bag.git)
|
|
223
|
-
|
|
308
|
+
if (bag.git) jobs.push(this.settle(epoch, () => readCommits(row)));
|
|
309
|
+
if (bag.git || bag.diff) {
|
|
224
310
|
jobs.push(this.settle(epoch, () => readDiff(row)));
|
|
225
311
|
}
|
|
226
312
|
if (bag.tasks) jobs.push(this.settle(epoch, () => readTasks(row)));
|
|
@@ -242,7 +328,7 @@ class Workspace {
|
|
|
242
328
|
}
|
|
243
329
|
if (this.stale(epoch)) break;
|
|
244
330
|
this.afterDraw();
|
|
245
|
-
if (this.
|
|
331
|
+
if (this.shown()) this.ui.paint();
|
|
246
332
|
}
|
|
247
333
|
row.running = false;
|
|
248
334
|
if (row.pending && this.active) {
|
|
@@ -250,7 +336,7 @@ class Workspace {
|
|
|
250
336
|
return;
|
|
251
337
|
}
|
|
252
338
|
this.afterDraw();
|
|
253
|
-
if (this.
|
|
339
|
+
if (this.shown()) this.ui.paint();
|
|
254
340
|
}
|
|
255
341
|
|
|
256
342
|
kick(row, bag) {
|
|
@@ -265,24 +351,65 @@ class Workspace {
|
|
|
265
351
|
for (const row of this.rows) this.kick(row, fullBag());
|
|
266
352
|
}
|
|
267
353
|
|
|
354
|
+
// Everything is read again, but npm only where there is no answer yet:
|
|
355
|
+
// the lists of packages change when their files do, and the watcher says so
|
|
356
|
+
refresh() {
|
|
357
|
+
for (const row of this.rows) {
|
|
358
|
+
this.kick(row, { ...fullBag(), npm: !row.npmReady });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// The worktree that owns a path of the watcher, and the path inside it.
|
|
363
|
+
// A worktree can be anywhere, and the state of git of a linked one is
|
|
364
|
+
// in the directory of the main one
|
|
365
|
+
placeOf(rel, root) {
|
|
366
|
+
const full = path.resolve(root, rel);
|
|
367
|
+
let best = null;
|
|
368
|
+
for (const row of this.rows) {
|
|
369
|
+
for (const [base, lead] of [
|
|
370
|
+
[row.key, ''],
|
|
371
|
+
[row.gitDir, '.git/'],
|
|
372
|
+
]) {
|
|
373
|
+
const inside = base ? insideOf(base, full) : null;
|
|
374
|
+
if (inside === null) continue;
|
|
375
|
+
if (best && base.length <= best.size) continue;
|
|
376
|
+
best = { row, rest: `${lead}${inside}`, size: base.length };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return best ?? this.placeByName(rel);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
placeByName(rel) {
|
|
383
|
+
const slash = rel.indexOf('/');
|
|
384
|
+
const name = slash < 0 ? rel : rel.slice(0, slash);
|
|
385
|
+
const row = this.byName.get(name);
|
|
386
|
+
return row ? { row, rest: slash < 0 ? '' : rel.slice(slash + 1) } : null;
|
|
387
|
+
}
|
|
388
|
+
|
|
268
389
|
notePaths(paths) {
|
|
269
390
|
if (!this.active) return;
|
|
391
|
+
const missing = this.rows.some((row) => !fs.existsSync(row.dir));
|
|
392
|
+
if (missing) {
|
|
393
|
+
this.ui.ops.hold(this.ui.worktrees.reload());
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
270
396
|
for (const rel of paths) {
|
|
271
397
|
if (rel === UNKNOWN_PATH) return void this.requestAll();
|
|
272
398
|
}
|
|
273
399
|
const found = new Map();
|
|
400
|
+
const info = this.infoOf();
|
|
401
|
+
const root = dirKey(info ? info.root : '.');
|
|
274
402
|
for (const rel of paths) {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
if (!row) continue;
|
|
403
|
+
const place = this.placeOf(rel, root);
|
|
404
|
+
if (!place) continue;
|
|
405
|
+
const { row, rest } = place;
|
|
406
|
+
const name = row.name;
|
|
280
407
|
found.set(name, mergeBag(found.get(name), classifyRest(rest)));
|
|
281
408
|
}
|
|
282
409
|
for (const [name, bag] of found) this.kick(this.byName.get(name), bag);
|
|
283
410
|
if (found.size) {
|
|
284
411
|
this.afterDraw();
|
|
285
|
-
if (this.
|
|
412
|
+
if (this.shown()) this.ui.paint();
|
|
286
413
|
}
|
|
287
414
|
}
|
|
288
415
|
|
|
@@ -302,8 +429,8 @@ class Workspace {
|
|
|
302
429
|
cancelTimers() {
|
|
303
430
|
const { progress, term } = this.ui;
|
|
304
431
|
if (!progress || !term) return;
|
|
305
|
-
progress.stop(
|
|
306
|
-
term.cancel(
|
|
432
|
+
progress.stop(this.tick);
|
|
433
|
+
term.cancel(this.heat);
|
|
307
434
|
}
|
|
308
435
|
|
|
309
436
|
hotUntil() {
|
|
@@ -316,32 +443,46 @@ class Workspace {
|
|
|
316
443
|
|
|
317
444
|
afterDraw() {
|
|
318
445
|
if (!this.active) return;
|
|
319
|
-
|
|
320
|
-
if (!visible) return void this.cancelTimers();
|
|
446
|
+
if (!this.shown()) return void this.cancelTimers();
|
|
321
447
|
const now = Date.now();
|
|
322
448
|
const until = this.hotUntil();
|
|
323
449
|
const hot = now < until;
|
|
324
450
|
const running = this.rows.some((row) => row.run?.status === 'running');
|
|
325
451
|
const loading = this.rows.some((row) => rowBusy(row));
|
|
326
452
|
const { progress, term } = this.ui;
|
|
327
|
-
if (running || hot || loading) progress.start(
|
|
328
|
-
else progress.stop(
|
|
453
|
+
if (running || hot || loading) progress.start(this.tick);
|
|
454
|
+
else progress.stop(this.tick);
|
|
329
455
|
if (!hot) return;
|
|
330
456
|
term.later(
|
|
331
|
-
|
|
457
|
+
this.heat,
|
|
332
458
|
() => {
|
|
333
|
-
if (this.ui.nav.pane ===
|
|
459
|
+
if (this.ui.nav.pane === this.pane) this.ui.paint();
|
|
334
460
|
},
|
|
335
461
|
until - now + 20,
|
|
336
462
|
);
|
|
337
463
|
}
|
|
338
464
|
|
|
465
|
+
liveAgents() {
|
|
466
|
+
const live = new Map();
|
|
467
|
+
const running = this.ui.agents ? this.ui.agents.liveByRoot() : new Map();
|
|
468
|
+
for (const [root, count] of running) live.set(dirKey(root), count);
|
|
469
|
+
return live;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// The index of the worktree that is shown, -1 for none of the tiles
|
|
473
|
+
currentIndex() {
|
|
474
|
+
const key = dirKey(this.ui.top || this.ui.cwd || '.');
|
|
475
|
+
return this.rows.findIndex((row) => row.key === key);
|
|
476
|
+
}
|
|
477
|
+
|
|
339
478
|
view() {
|
|
479
|
+
const live = this.liveAgents();
|
|
340
480
|
return {
|
|
341
481
|
now: Date.now(),
|
|
342
482
|
frame: this.ui.progressFrame,
|
|
343
483
|
cursor: this.cursor,
|
|
344
|
-
|
|
484
|
+
current: this.currentIndex(),
|
|
485
|
+
tiles: this.rows.map((row) => toTile(row, live)),
|
|
345
486
|
};
|
|
346
487
|
}
|
|
347
488
|
|