reslop 0.3.1 → 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 +16 -1
- package/README.md +21 -3
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +163 -436
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -14
- package/lib/agents/models.js +250 -383
- 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/dir.js +50 -0
- 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 +113 -36
- package/lib/input/actions.js +83 -5
- package/lib/input/keys.js +30 -1
- package/lib/input/line-edit.js +5 -1
- package/lib/render/agents.js +50 -24
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -6
- package/lib/render/frame.js +16 -2
- 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/run.js +16 -23
- package/lib/review/agent-instructions.md +2 -2
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +220 -205
- 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 -1
- package/lib/session/compose.js +13 -5
- package/lib/session/dispatch.js +43 -4
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/lifecycle.js +67 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +2 -1
- package/lib/session/npm/npm.js +89 -12
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +15 -2
- 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/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
package/lib/session/branches.js
CHANGED
|
@@ -47,7 +47,7 @@ class BranchController {
|
|
|
47
47
|
ui.refreshFromRepo({ keepEmpty: true, ...extra });
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
async open() {
|
|
50
|
+
async open(focusTrees = false) {
|
|
51
51
|
const ui = this.ui;
|
|
52
52
|
const loaded = await this.loadBranches();
|
|
53
53
|
if (!loaded) return;
|
|
@@ -55,9 +55,11 @@ class BranchController {
|
|
|
55
55
|
const nav = ui.nav;
|
|
56
56
|
nav.branchCursor = current < 0 ? 0 : current;
|
|
57
57
|
nav.pane = 'branches';
|
|
58
|
+
nav.treeFocus = focusTrees;
|
|
58
59
|
nav.resetListScroll('branches');
|
|
59
60
|
ui.status = '';
|
|
60
61
|
nav.clearSelection();
|
|
62
|
+
void ui.worktrees.refreshTrees();
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
runWrite(name, args, kind, done) {
|
|
@@ -76,8 +78,23 @@ class BranchController {
|
|
|
76
78
|
ui.ops.hold(this.open());
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
// The worktrees above the branches share the cursor keys with them: one
|
|
82
|
+
// element is selected, up from the first branch reaches the worktrees
|
|
83
|
+
moveBetweenTrees(delta) {
|
|
84
|
+
const nav = this.ui.nav;
|
|
85
|
+
if (!this.ui.worktrees.hasPanels()) return false;
|
|
86
|
+
if (nav.treeFocus) {
|
|
87
|
+
if (delta > 0) nav.treeFocus = false;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (delta >= 0 || nav.branchCursor > 0) return false;
|
|
91
|
+
nav.treeFocus = true;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
onBranchMove(delta) {
|
|
80
96
|
const nav = this.ui.nav;
|
|
97
|
+
if (this.moveBetweenTrees(delta)) return void (this.ui.status = '');
|
|
81
98
|
if (!this.branches.length) {
|
|
82
99
|
this.ui.status = 'nothing to review';
|
|
83
100
|
return;
|
package/lib/session/compose.js
CHANGED
|
@@ -6,7 +6,7 @@ const { noteInnerWidth, codeInnerWidth, todoBodyWidth, activeDigits } = render;
|
|
|
6
6
|
const { commitMessageWidth } = require('../render/commits.js');
|
|
7
7
|
const { listedCommits } = require('./commit.js');
|
|
8
8
|
const { Editor } = require('../input/editor.js');
|
|
9
|
-
const { isTextKey } = require('../input/keys.js');
|
|
9
|
+
const { isTextKey, isNewlineKey } = require('../input/keys.js');
|
|
10
10
|
const files = require('../common/files.js');
|
|
11
11
|
const { itemPath, isTaskItem, isTasksEntry, listPath } = files;
|
|
12
12
|
const { isReadOnlyOrigin } = files;
|
|
@@ -39,10 +39,18 @@ const KIND = {
|
|
|
39
39
|
branch: { enter: 'save', tab: 'template', escape: 'cancel', move: 'note' },
|
|
40
40
|
npm: { enter: 'save', tab: 'insert-tab', escape: 'cancel', move: 'note' },
|
|
41
41
|
package: { enter: 'save', tab: 'insert-tab', escape: 'cancel', move: 'note' },
|
|
42
|
+
worktree: { enter: 'save', tab: 'template', escape: 'cancel', move: 'note' },
|
|
42
43
|
};
|
|
43
44
|
|
|
44
|
-
const COMMAND_KINDS = ['commit', 'branch', 'npm', 'package'];
|
|
45
|
-
const NOTE_VIEWS = [
|
|
45
|
+
const COMMAND_KINDS = ['commit', 'branch', 'npm', 'package', 'worktree'];
|
|
46
|
+
const NOTE_VIEWS = [
|
|
47
|
+
'feedback',
|
|
48
|
+
'commit',
|
|
49
|
+
'branch',
|
|
50
|
+
'npm',
|
|
51
|
+
'package',
|
|
52
|
+
'worktree',
|
|
53
|
+
];
|
|
46
54
|
|
|
47
55
|
const itemNote = (item, text) => {
|
|
48
56
|
const hunk = item.hunk;
|
|
@@ -674,8 +682,8 @@ class Composer {
|
|
|
674
682
|
}
|
|
675
683
|
|
|
676
684
|
handleKey(key) {
|
|
677
|
-
if (key === 'enter') {
|
|
678
|
-
const result = this.enter();
|
|
685
|
+
if (key === 'enter' || isNewlineKey(key)) {
|
|
686
|
+
const result = isNewlineKey(key) ? this.insertNewline() : this.enter();
|
|
679
687
|
this.syncCodeScroll();
|
|
680
688
|
return result;
|
|
681
689
|
}
|
package/lib/session/dispatch.js
CHANGED
|
@@ -8,7 +8,7 @@ const { REPOS_ALLOWED } = actions;
|
|
|
8
8
|
const { disabledActions, actionFromKey, confirmKey, lineHasDiff } = actions;
|
|
9
9
|
const { onCommand } = require('./change-actions.js');
|
|
10
10
|
const filesPane = require('./files-pane.js');
|
|
11
|
-
const { handlePointer } = require('./pointer.js');
|
|
11
|
+
const { handlePointer, handleContextKey } = require('./pointer.js');
|
|
12
12
|
const { FIND_PANES, startFind, onFindKey } = require('./find.js');
|
|
13
13
|
const { startImport, stopImport, onImportKey } = require('./import.js');
|
|
14
14
|
const { unitBodyLines } = require('./view.js');
|
|
@@ -109,6 +109,7 @@ const HANDLERS = {
|
|
|
109
109
|
packageLatest: (ui) => ui.packages.updateLatest(),
|
|
110
110
|
npmLogs: (ui) => ui.npm.askLogs(),
|
|
111
111
|
npmStop: (ui) => ui.npm.stop(),
|
|
112
|
+
npmSolve: (ui) => ui.npm.solve(),
|
|
112
113
|
npmVerbose: (ui) => ui.npm.toggleVerbose(),
|
|
113
114
|
npmRerun: (ui) => ui.npm.rerun(),
|
|
114
115
|
agentModel: (ui) => ui.agents.openPick('model'),
|
|
@@ -116,10 +117,16 @@ const HANDLERS = {
|
|
|
116
117
|
agentFast: (ui) => ui.agents.toggleFast(),
|
|
117
118
|
agentContext: (ui) => ui.agents.openPick('context'),
|
|
118
119
|
agentReview: (ui) => ui.agents.openPick('plan'),
|
|
119
|
-
|
|
120
|
+
agentInstall: (ui) => ui.agents.proposeInstall(),
|
|
121
|
+
agentStart: (ui) => ui.agents.startSelected(),
|
|
120
122
|
agentStop: (ui) => ui.agents.stop(),
|
|
121
123
|
agentRerun: (ui) => ui.agents.rerun(),
|
|
122
124
|
quit: (ui) => ui.onQuit(),
|
|
125
|
+
treeLeft: (ui) => ui.worktrees.moveTree(-1),
|
|
126
|
+
treeRight: (ui) => ui.worktrees.moveTree(1),
|
|
127
|
+
worktreeNew: (ui) => ui.worktrees.onAdd(),
|
|
128
|
+
worktreeDrop: (ui) => ui.worktrees.onDrop(),
|
|
129
|
+
worktreeBranch: (ui) => ui.worktrees.onBranch(),
|
|
123
130
|
repoLeft: (ui) => ui.workspace.move(-1, 0),
|
|
124
131
|
repoRight: (ui) => ui.workspace.move(1, 0),
|
|
125
132
|
repoUp: (ui) => ui.workspace.move(0, -1),
|
|
@@ -127,7 +134,22 @@ const HANDLERS = {
|
|
|
127
134
|
...DASH_HANDLERS,
|
|
128
135
|
};
|
|
129
136
|
|
|
130
|
-
|
|
137
|
+
// The keys of a branch do the same to the selected tile of the worktrees
|
|
138
|
+
const TREE_ACTIONS = {
|
|
139
|
+
newBranch: 'worktreeNew',
|
|
140
|
+
drop: 'worktreeDrop',
|
|
141
|
+
};
|
|
142
|
+
const TREE_OFF = ['rebase', 'commit'];
|
|
143
|
+
|
|
144
|
+
const forSelection = (ui, action) => {
|
|
145
|
+
if (!ui.worktrees.focused()) return action;
|
|
146
|
+
if (TREE_OFF.includes(action)) return null;
|
|
147
|
+
return TREE_ACTIONS[action] ?? action;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const dispatchAction = (ui, picked) => {
|
|
151
|
+
const action = forSelection(ui, picked);
|
|
152
|
+
if (!action) return;
|
|
131
153
|
if (CONFIRM_MODE.includes(ui.mode)) return;
|
|
132
154
|
if (ui.mode === 'import') stopImport(ui);
|
|
133
155
|
if (action === 'lines' && (ui.pane === 'diff' || ui.pane === 'unit')) {
|
|
@@ -159,6 +181,10 @@ const CONFIRMATIONS = {
|
|
|
159
181
|
accept: (ui) => ui.updater.accept(),
|
|
160
182
|
decline: (ui) => ui.updater.decline(),
|
|
161
183
|
},
|
|
184
|
+
confirmAgentInstall: {
|
|
185
|
+
confirm: (ui) => ui.agents.confirmInstall(),
|
|
186
|
+
cancel: (ui) => ui.agents.cancelInstall(),
|
|
187
|
+
},
|
|
162
188
|
confirmDrop: {
|
|
163
189
|
confirm: (ui) => {
|
|
164
190
|
if (ui.packages.dropName) return void ui.packages.confirmDrop();
|
|
@@ -173,6 +199,14 @@ const CONFIRMATIONS = {
|
|
|
173
199
|
ui.gitBranches.cancelDropBranch();
|
|
174
200
|
},
|
|
175
201
|
},
|
|
202
|
+
confirmWorktree: {
|
|
203
|
+
confirm: (ui) => ui.worktrees.confirmDrop(),
|
|
204
|
+
cancel: (ui) => ui.worktrees.cancelDrop(),
|
|
205
|
+
},
|
|
206
|
+
confirmCreateWorktree: {
|
|
207
|
+
confirm: (ui) => ui.worktrees.confirmBranch(),
|
|
208
|
+
cancel: (ui) => ui.worktrees.cancelBranch(),
|
|
209
|
+
},
|
|
176
210
|
confirmPush: {
|
|
177
211
|
force: (ui) => ui.gitBranches.onForcePush(),
|
|
178
212
|
cancel: (ui) => ui.gitBranches.cancelPush(),
|
|
@@ -194,7 +228,10 @@ const confirmChoice = (ui, key) => {
|
|
|
194
228
|
|
|
195
229
|
const NPM_VIEW_KEYS = {
|
|
196
230
|
escape: (ui) => ui.npm.closeView(),
|
|
197
|
-
s: (ui) =>
|
|
231
|
+
s: (ui) => {
|
|
232
|
+
if (ui.npm.viewedRunning()) ui.npm.stop();
|
|
233
|
+
else ui.npm.solve();
|
|
234
|
+
},
|
|
198
235
|
r: (ui) => ui.npm.rerun(),
|
|
199
236
|
v: (ui) => ui.npm.toggleVerbose(),
|
|
200
237
|
e: () => {},
|
|
@@ -254,7 +291,9 @@ const planMenuKey = (ui, key) => {
|
|
|
254
291
|
};
|
|
255
292
|
|
|
256
293
|
const handleKey = (ui, key) => {
|
|
294
|
+
ui.nav.editDrag = null;
|
|
257
295
|
if (CONFIRM_MODE.includes(ui.mode)) return void confirmChoice(ui, key);
|
|
296
|
+
if (handleContextKey(ui, key)) return;
|
|
258
297
|
if (planMenuKey(ui, key)) return;
|
|
259
298
|
if (ui.pane === 'tasks' && (key === 'ctrl-up' || key === 'ctrl-down')) {
|
|
260
299
|
if (ui.mode === 'import') stopImport(ui);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { indexAtRow } = require('../term/wrap.js');
|
|
4
|
+
|
|
5
|
+
const hitAt = (hits, cell) =>
|
|
6
|
+
hits.find((hit) => {
|
|
7
|
+
if (hit.y !== cell.y) return false;
|
|
8
|
+
const x0 = hit.x0 ?? hit.textX;
|
|
9
|
+
return cell.x >= x0 && (hit.x1 === undefined || cell.x < hit.x1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const offsetOf = (hit, cellX) => {
|
|
13
|
+
const col = Math.max(0, cellX - hit.textX) + (hit.scroll || 0);
|
|
14
|
+
const at = indexAtRow(hit.start || 0, hit.text, col);
|
|
15
|
+
return Math.min(at, hit.limit ?? Infinity);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const focusField = (ui, hit) => {
|
|
19
|
+
if (ui.composeKind !== 'npm' || !hit.field) return true;
|
|
20
|
+
if (hit.field === ui.npm.editField) return true;
|
|
21
|
+
return ui.npm.focusField(hit.field);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const activeEditor = (ui) => {
|
|
25
|
+
if (ui.pane === 'agents' && ui.agents.pick) return ui.agents.pick.editor;
|
|
26
|
+
if (ui.pane === 'tasks' && ui.composer.tasks.planOpen) {
|
|
27
|
+
return ui.composer.tasks.planPick.editor;
|
|
28
|
+
}
|
|
29
|
+
if (ui.mode === 'find') return ui.nav.find?.editor;
|
|
30
|
+
if (ui.mode === 'import') return ui.nav.import?.editor;
|
|
31
|
+
return ui.mode === 'compose' ? ui.editor : null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const editorHits = (ui, editor) =>
|
|
35
|
+
(ui.lastFrame?.editHits ?? []).filter(
|
|
36
|
+
(hit) => (hit.editor ?? ui.editor) === editor,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const revealEditor = (ui) => {
|
|
40
|
+
if (ui.mode === 'compose') ui.composer.afterEdit();
|
|
41
|
+
ui.paint();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const beginEditDrag = (ui, cell, extend = false) => {
|
|
45
|
+
const editor = activeEditor(ui);
|
|
46
|
+
if (!editor) return false;
|
|
47
|
+
const hit = hitAt(editorHits(ui, editor), cell);
|
|
48
|
+
if (!hit) return false;
|
|
49
|
+
if (!focusField(ui, hit)) {
|
|
50
|
+
ui.paint();
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
const previous = editor.anchor ?? editor.cursor;
|
|
54
|
+
const anchor = extend ? previous : offsetOf(hit, cell.x);
|
|
55
|
+
editor.place(offsetOf(hit, cell.x));
|
|
56
|
+
if (extend && anchor !== editor.cursor) editor.anchor = anchor;
|
|
57
|
+
ui.nav.clearSelection();
|
|
58
|
+
ui.nav.editDrag = { editor, anchor, field: hit.field, cell };
|
|
59
|
+
revealEditor(ui);
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const moveEditDrag = (ui, cell, release = false) => {
|
|
64
|
+
const drag = ui.nav.editDrag;
|
|
65
|
+
if (!drag) return false;
|
|
66
|
+
if (release && drag.cell.x === cell.x && drag.cell.y === cell.y) {
|
|
67
|
+
ui.nav.editDrag = null;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (activeEditor(ui) === drag.editor) {
|
|
71
|
+
const hits = editorHits(ui, drag.editor).filter(
|
|
72
|
+
(hit) => hit.field === drag.field,
|
|
73
|
+
);
|
|
74
|
+
const hit = hits.reduce((nearest, candidate) => {
|
|
75
|
+
if (!nearest) return candidate;
|
|
76
|
+
return Math.abs(candidate.y - cell.y) < Math.abs(nearest.y - cell.y)
|
|
77
|
+
? candidate
|
|
78
|
+
: nearest;
|
|
79
|
+
}, null);
|
|
80
|
+
if (hit) {
|
|
81
|
+
const x = Math.min(cell.x, (hit.x1 ?? cell.x + 1) - 1);
|
|
82
|
+
drag.editor.place(offsetOf(hit, x));
|
|
83
|
+
if (drag.anchor !== drag.editor.cursor) drag.editor.anchor = drag.anchor;
|
|
84
|
+
drag.cell = cell;
|
|
85
|
+
revealEditor(ui);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (release) ui.nav.editDrag = null;
|
|
89
|
+
return true;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
hitAt,
|
|
94
|
+
offsetOf,
|
|
95
|
+
focusField,
|
|
96
|
+
beginEditDrag,
|
|
97
|
+
moveEditDrag,
|
|
98
|
+
};
|
|
@@ -107,6 +107,9 @@ const onFiles = (ui) => {
|
|
|
107
107
|
|
|
108
108
|
const onOpenFile = (ui) => {
|
|
109
109
|
if (ui.nav.pane === 'packages') return;
|
|
110
|
+
if (ui.nav.pane === 'branches' && ui.worktrees.focused()) {
|
|
111
|
+
return void ui.ops.hold(ui.worktrees.enterSelected());
|
|
112
|
+
}
|
|
110
113
|
if (ui.nav.pane === 'branches') {
|
|
111
114
|
return void ui.gitBranches.onCheckoutBranch();
|
|
112
115
|
}
|
package/lib/session/lifecycle.js
CHANGED
|
@@ -6,6 +6,9 @@ const path = require('node:path');
|
|
|
6
6
|
const { isAbort } = require('../common/process.js');
|
|
7
7
|
const { tagLoaded, fetchSnapshot, fetchExtras } = require('./load.js');
|
|
8
8
|
const { DiskWatcher, DEBOUNCE_MS } = require('../dashboard/watch.js');
|
|
9
|
+
const { watchedBy } = require('../dashboard/worktrees.js');
|
|
10
|
+
const { dirKey } = require('../git/linked.js');
|
|
11
|
+
const { resolveGitDirs } = require('../git/dir.js');
|
|
9
12
|
const { itemPath, REVIEW_DIR } = require('../common/files.js');
|
|
10
13
|
const { relativeAge } = require('../common/format.js');
|
|
11
14
|
const { MANIFEST, LOCKFILE, depFileMeta } = require('../deps/deps.js');
|
|
@@ -73,16 +76,28 @@ class Lifecycle {
|
|
|
73
76
|
constructor(ui) {
|
|
74
77
|
this.ui = ui;
|
|
75
78
|
this.diskWatcher = null;
|
|
79
|
+
this.treeWatcher = null;
|
|
76
80
|
this.watchRetry = null;
|
|
77
81
|
this.reviewStamp = '';
|
|
78
82
|
this.ignoreWatchUntil = 0;
|
|
79
|
-
this.
|
|
83
|
+
this.extras = new Map();
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
get root() {
|
|
83
87
|
return this.ui.top || this.ui.cwd;
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
// The reports of npm are kept for each worktree: showing another one does
|
|
91
|
+
// not ask npm again, and the reports of one are never taken for another
|
|
92
|
+
get cachedExtras() {
|
|
93
|
+
return this.extras.get(this.root) ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
set cachedExtras(entry) {
|
|
97
|
+
if (entry) this.extras.set(this.root, entry);
|
|
98
|
+
else this.extras.delete(this.root);
|
|
99
|
+
}
|
|
100
|
+
|
|
86
101
|
loadOptions(extra = {}) {
|
|
87
102
|
const ui = this.ui;
|
|
88
103
|
return {
|
|
@@ -194,7 +209,9 @@ class Lifecycle {
|
|
|
194
209
|
const here = ui.current();
|
|
195
210
|
ui.top = loaded.top ?? ui.cwd;
|
|
196
211
|
this.applyPrefs();
|
|
197
|
-
|
|
212
|
+
const seen = loaded.seenEpoch;
|
|
213
|
+
const fresh = seen === undefined || seen === ui.ops.writeEpoch;
|
|
214
|
+
if (fresh) collection.replace(loaded.items);
|
|
198
215
|
stampItemDates(ui.top, collection.items);
|
|
199
216
|
this.applySource(loaded);
|
|
200
217
|
if (!ui.review.store) ui.initReview();
|
|
@@ -242,7 +259,7 @@ class Lifecycle {
|
|
|
242
259
|
}
|
|
243
260
|
|
|
244
261
|
clearNpmExtras() {
|
|
245
|
-
this.
|
|
262
|
+
this.extras.clear();
|
|
246
263
|
}
|
|
247
264
|
|
|
248
265
|
npmExtras() {
|
|
@@ -279,7 +296,8 @@ class Lifecycle {
|
|
|
279
296
|
const next = outcome.value;
|
|
280
297
|
if (!this.isWanted(gen)) return;
|
|
281
298
|
if (next) {
|
|
282
|
-
|
|
299
|
+
const seenEpoch = loaded.seenEpoch;
|
|
300
|
+
this.applyLoaded(tagLoaded({ ...next, seenEpoch }, gen));
|
|
283
301
|
this.cacheNpmExtras(next);
|
|
284
302
|
} else {
|
|
285
303
|
ui.loader.pendingExtras = false;
|
|
@@ -316,6 +334,17 @@ class Lifecycle {
|
|
|
316
334
|
if (extra.afterLoad) extra.afterLoad();
|
|
317
335
|
}
|
|
318
336
|
|
|
337
|
+
// Remember the write epoch so a snapshot that started before stage or
|
|
338
|
+
// unstage does not paint that older index over the optimistic items.
|
|
339
|
+
async fetchSettled() {
|
|
340
|
+
const ui = this.ui;
|
|
341
|
+
const seenEpoch = ui.ops.writeEpoch;
|
|
342
|
+
const opts = this.snapshotOpts({ signal: ui.loader.signal });
|
|
343
|
+
const loaded = await fetchSnapshot(ui.repo, ui.cwd, ui.paths, opts);
|
|
344
|
+
if (!loaded) return loaded;
|
|
345
|
+
return { ...loaded, seenEpoch };
|
|
346
|
+
}
|
|
347
|
+
|
|
319
348
|
async runSnapshot(gen, kind, extra = {}) {
|
|
320
349
|
const ui = this.ui;
|
|
321
350
|
if (extra.quiet !== true) {
|
|
@@ -326,8 +355,7 @@ class Lifecycle {
|
|
|
326
355
|
let loaded = null;
|
|
327
356
|
const side = ui.reads.read();
|
|
328
357
|
try {
|
|
329
|
-
|
|
330
|
-
loaded = await fetchSnapshot(ui.repo, ui.cwd, ui.paths, opts);
|
|
358
|
+
loaded = await this.fetchSettled();
|
|
331
359
|
} catch (error) {
|
|
332
360
|
if (ui.loader.isCurrent(gen)) this.failOpen(error);
|
|
333
361
|
}
|
|
@@ -477,21 +505,46 @@ class Lifecycle {
|
|
|
477
505
|
stopWatch() {
|
|
478
506
|
this.clearWatchRetry();
|
|
479
507
|
this.reviewStamp = '';
|
|
508
|
+
this.stopTreeWatch();
|
|
480
509
|
if (!this.diskWatcher) return;
|
|
481
510
|
this.diskWatcher.close();
|
|
482
511
|
this.diskWatcher = null;
|
|
483
512
|
}
|
|
484
513
|
|
|
514
|
+
stopTreeWatch() {
|
|
515
|
+
if (!this.treeWatcher) return;
|
|
516
|
+
this.treeWatcher.close();
|
|
517
|
+
this.treeWatcher = null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// The other worktrees of the repository are watched for their tiles; the
|
|
521
|
+
// one that is shown is watched with everything else
|
|
522
|
+
watchTrees() {
|
|
523
|
+
this.stopTreeWatch();
|
|
524
|
+
const ui = this.ui;
|
|
525
|
+
const info = ui.treesInfo;
|
|
526
|
+
if (!info || ui.trees.rows.length < 2 || !ui.top) return;
|
|
527
|
+
const root = dirKey(info.root);
|
|
528
|
+
const paths = [];
|
|
529
|
+
for (const row of ui.trees.rows) {
|
|
530
|
+
const dirs = resolveGitDirs(row.key);
|
|
531
|
+
if (dirs) paths.push(dirKey(dirs.gitDir));
|
|
532
|
+
if (row.key !== dirKey(root)) paths.push(row.key);
|
|
533
|
+
}
|
|
534
|
+
const onPaths = (rels) => ui.trees.notePaths(rels);
|
|
535
|
+
this.treeWatcher = new DiskWatcher({ root, paths, onPaths });
|
|
536
|
+
}
|
|
537
|
+
|
|
485
538
|
startWatch() {
|
|
486
539
|
this.stopWatch();
|
|
487
540
|
const ui = this.ui;
|
|
488
541
|
if (ui.nav.pane === 'repos') {
|
|
489
|
-
|
|
490
|
-
|
|
542
|
+
if (!ui.workspaceRoot) return;
|
|
543
|
+
const root = dirKey(ui.workspaceRoot);
|
|
491
544
|
const info = ui.workspaceInfo;
|
|
492
545
|
const repos = info ? info.repos : [];
|
|
493
546
|
const paths = ['.'];
|
|
494
|
-
for (const repo of repos) paths.push(
|
|
547
|
+
for (const repo of repos) paths.push(...watchedBy(root, repo));
|
|
495
548
|
const matchReview = (rel) => rel.includes(`/${REVIEW_DIR}/`);
|
|
496
549
|
const onPaths = (rels) => ui.workspace.notePaths(rels);
|
|
497
550
|
const options = { root, paths, matchReview, onPaths };
|
|
@@ -505,10 +558,14 @@ class Lifecycle {
|
|
|
505
558
|
paths: this.ui.paths,
|
|
506
559
|
onChange: () => this.onDiskChange(),
|
|
507
560
|
onReview: () => this.applyReviewChange(),
|
|
508
|
-
onPaths: (paths) =>
|
|
561
|
+
onPaths: (paths) => {
|
|
562
|
+
this.ui.dashboard.noteDisk(paths);
|
|
563
|
+
this.ui.trees.notePaths(paths);
|
|
564
|
+
},
|
|
509
565
|
});
|
|
510
566
|
this.syncItemWatches();
|
|
511
567
|
this.noteReviewStamp();
|
|
568
|
+
this.watchTrees();
|
|
512
569
|
}
|
|
513
570
|
}
|
|
514
571
|
|
|
@@ -17,6 +17,7 @@ class Navigation {
|
|
|
17
17
|
this.index = 0;
|
|
18
18
|
this.fileCursor = 0;
|
|
19
19
|
this.branchCursor = 0;
|
|
20
|
+
this.treeFocus = false;
|
|
20
21
|
this.commitCursor = 0;
|
|
21
22
|
this.npmCursor = 0;
|
|
22
23
|
this.packagesCursor = 0;
|
|
@@ -28,9 +29,11 @@ class Navigation {
|
|
|
28
29
|
this.unitLine = 0;
|
|
29
30
|
this.selection = null;
|
|
30
31
|
this.mouseAnchor = null;
|
|
32
|
+
this.editDrag = null;
|
|
31
33
|
this.pendingClick = null;
|
|
32
34
|
this.scrollDrag = null;
|
|
33
35
|
this.lastClick = null;
|
|
36
|
+
this.contextMenu = null;
|
|
34
37
|
this.tasksOpen = false;
|
|
35
38
|
this.tasksFocus = 0;
|
|
36
39
|
this.find = null;
|
|
@@ -99,8 +102,10 @@ class Navigation {
|
|
|
99
102
|
clearSelection() {
|
|
100
103
|
this.selection = null;
|
|
101
104
|
this.mouseAnchor = null;
|
|
105
|
+
this.editDrag = null;
|
|
102
106
|
this.pendingClick = null;
|
|
103
107
|
this.scrollDrag = null;
|
|
108
|
+
this.contextMenu = null;
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
get listScroll() {
|
|
@@ -221,7 +221,8 @@ const whenLabel = (ms) => {
|
|
|
221
221
|
};
|
|
222
222
|
|
|
223
223
|
const elapsedText = (run, now) => {
|
|
224
|
-
const end =
|
|
224
|
+
const end =
|
|
225
|
+
run.status === 'running' ? now : run.endedAt || run.startedAt || 0;
|
|
225
226
|
const ms = Math.max(0, end - (run.startedAt || end));
|
|
226
227
|
const sec = Math.round(ms / 1000);
|
|
227
228
|
if (sec < 60) return `${sec}s`;
|
package/lib/session/npm/npm.js
CHANGED
|
@@ -30,6 +30,8 @@ const { historyName, historyMark, countText, elapsedText } = historyPart;
|
|
|
30
30
|
const { whenLabel, runLabel, logBase, applyExternal } = historyPart;
|
|
31
31
|
const { recordedItem, sameWindow, noteTests, filteredOutput } = historyPart;
|
|
32
32
|
const { size } = require('../../common/format.js');
|
|
33
|
+
const { TASKS_FILE } = require('../../common/files.js');
|
|
34
|
+
const { stripAnsi } = require('../../term/ansi.js');
|
|
33
35
|
|
|
34
36
|
const NAME_RE = /^[\w:.-]+$/;
|
|
35
37
|
|
|
@@ -54,11 +56,30 @@ class NpmController extends RunPanel {
|
|
|
54
56
|
this.reset();
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
// Runs belong to the worktree they started in and go on while another
|
|
60
|
+
// one is shown
|
|
61
|
+
get runs() {
|
|
62
|
+
let list = this.byTop.get(this.ui.top);
|
|
63
|
+
if (!list) {
|
|
64
|
+
list = [];
|
|
65
|
+
this.byTop.set(this.ui.top, list);
|
|
66
|
+
}
|
|
67
|
+
return list;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
set runs(list) {
|
|
71
|
+
this.byTop.set(this.ui.top, list);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
everyRun() {
|
|
75
|
+
return [...this.byTop.values()].flat();
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
reset() {
|
|
58
|
-
for (const run of this.
|
|
79
|
+
for (const run of this.byTop ? this.everyRun() : []) {
|
|
59
80
|
if (run.status === 'running' && run.child) run.child.kill();
|
|
60
81
|
}
|
|
61
|
-
this.
|
|
82
|
+
this.byTop = new Map();
|
|
62
83
|
this.nextId = 1;
|
|
63
84
|
this.viewId = 0;
|
|
64
85
|
this.focus = 'commands';
|
|
@@ -83,7 +104,28 @@ class NpmController extends RunPanel {
|
|
|
83
104
|
}
|
|
84
105
|
|
|
85
106
|
get running() {
|
|
86
|
-
return this.
|
|
107
|
+
return this.everyRun().some((run) => run.status === 'running');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The count of runs in work by the directory they started in
|
|
111
|
+
liveByRoot() {
|
|
112
|
+
const live = new Map();
|
|
113
|
+
for (const run of this.everyRun()) {
|
|
114
|
+
if (run.status !== 'running') continue;
|
|
115
|
+
live.set(run.top, (live.get(run.top) ?? 0) + 1);
|
|
116
|
+
}
|
|
117
|
+
return live;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Another worktree is shown: its commands and runs replace these
|
|
121
|
+
retarget() {
|
|
122
|
+
this.viewing = false;
|
|
123
|
+
this.verbose = false;
|
|
124
|
+
this.viewId = 0;
|
|
125
|
+
this.runCursor = 0;
|
|
126
|
+
this.commands = [];
|
|
127
|
+
this.dropName = '';
|
|
128
|
+
this.logBytes = 0;
|
|
87
129
|
}
|
|
88
130
|
|
|
89
131
|
view(active) {
|
|
@@ -103,6 +145,7 @@ class NpmController extends RunPanel {
|
|
|
103
145
|
npmDraftName: this.draftName,
|
|
104
146
|
npmDraftCommand: this.draftCommand,
|
|
105
147
|
npmRunning: this.viewedRunning(),
|
|
148
|
+
npmCanSolve: this.canSolve(),
|
|
106
149
|
npmLogs: active ? this.logLabel : '',
|
|
107
150
|
};
|
|
108
151
|
}
|
|
@@ -173,6 +216,39 @@ class NpmController extends RunPanel {
|
|
|
173
216
|
return Boolean(run && run.status === 'running');
|
|
174
217
|
}
|
|
175
218
|
|
|
219
|
+
canSolve() {
|
|
220
|
+
const run = this.viewedRun();
|
|
221
|
+
if (!this.viewing || !run || run.status !== 'exited') return false;
|
|
222
|
+
return run.exit !== 0 && run.exit !== '0' && run.exit !== '';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
solve() {
|
|
226
|
+
const ui = this.ui;
|
|
227
|
+
if (!this.canSolve() || !ui.review.store) return;
|
|
228
|
+
const run = this.viewedRun();
|
|
229
|
+
this.hydrate(run);
|
|
230
|
+
this.syncOutput(run);
|
|
231
|
+
const root = run.top ?? ui.top;
|
|
232
|
+
const output = stripAnsi(
|
|
233
|
+
run.raw
|
|
234
|
+
? filteredOutput(run.raw, root, run.exit).output
|
|
235
|
+
: reduceOutput(run.output, root, ''),
|
|
236
|
+
).trim();
|
|
237
|
+
const limit = 6000;
|
|
238
|
+
const shortened =
|
|
239
|
+
output.length > limit
|
|
240
|
+
? `${output.slice(0, limit)}\n… (truncated)`
|
|
241
|
+
: output;
|
|
242
|
+
const command = run.command || run.label || runLabel(run);
|
|
243
|
+
const text = `${command} failed (exit ${run.exit})\n\n${shortened}`;
|
|
244
|
+
const task = ui.review.store.addTask(TASKS_FILE, text, false, 'bugs');
|
|
245
|
+
this.closeView();
|
|
246
|
+
const tasks = ui.composer.tasks;
|
|
247
|
+
tasks.openTasksPage();
|
|
248
|
+
tasks.followRow({ task });
|
|
249
|
+
ui.flushReview(true);
|
|
250
|
+
}
|
|
251
|
+
|
|
176
252
|
historyRuns() {
|
|
177
253
|
const runs = this.runs.filter((run) => !launchedReslop(run));
|
|
178
254
|
return foldSteps(runs).slice(0, RUN_KEEP);
|
|
@@ -249,6 +325,7 @@ class NpmController extends RunPanel {
|
|
|
249
325
|
logName: item.logName,
|
|
250
326
|
rawName: item.rawName,
|
|
251
327
|
externalId: item.externalId || '',
|
|
328
|
+
top: this.ui.top,
|
|
252
329
|
result: item.result ?? null,
|
|
253
330
|
progress: item.progress ?? null,
|
|
254
331
|
saved: true,
|
|
@@ -508,8 +585,9 @@ class NpmController extends RunPanel {
|
|
|
508
585
|
if (!run.stale) return false;
|
|
509
586
|
run.stale = false;
|
|
510
587
|
run.shownAt = Date.now();
|
|
511
|
-
|
|
512
|
-
|
|
588
|
+
const top = run.top ?? this.ui.top;
|
|
589
|
+
noteTests(run, this.byTop.get(top) ?? this.runs, run.raw);
|
|
590
|
+
run.output = filteredOutput(run.raw, top, '').output;
|
|
513
591
|
return true;
|
|
514
592
|
}
|
|
515
593
|
|
|
@@ -521,25 +599,23 @@ class NpmController extends RunPanel {
|
|
|
521
599
|
|
|
522
600
|
async finishRun(run, entry, result) {
|
|
523
601
|
const ui = this.ui;
|
|
602
|
+
const top = run.top ?? ui.top;
|
|
524
603
|
const stopped = run.stopping === true;
|
|
525
604
|
const raw = stopped ? withNotice(run.raw, 'terminated') : result.text;
|
|
526
605
|
let slot = null;
|
|
527
606
|
try {
|
|
528
|
-
slot = nextLogFile(
|
|
607
|
+
slot = nextLogFile(top, entry.name);
|
|
529
608
|
} catch (error) {
|
|
530
609
|
ui.status = error.message;
|
|
531
610
|
}
|
|
532
611
|
const rawFile = slot ? slot.rawName : '';
|
|
533
612
|
const stoppedOutput = () => {
|
|
534
|
-
const output = withNotice(
|
|
535
|
-
reduceOutput(run.raw, ui.top, ''),
|
|
536
|
-
'terminated',
|
|
537
|
-
);
|
|
613
|
+
const output = withNotice(reduceOutput(run.raw, top, ''), 'terminated');
|
|
538
614
|
return { output, log: output };
|
|
539
615
|
};
|
|
540
616
|
const filtered = stopped
|
|
541
617
|
? stoppedOutput()
|
|
542
|
-
: filteredOutput(result.text,
|
|
618
|
+
: filteredOutput(result.text, top, result.status, rawFile);
|
|
543
619
|
run.raw = raw;
|
|
544
620
|
run.stale = false;
|
|
545
621
|
run.exit = stopped ? '' : result.status;
|
|
@@ -576,6 +652,7 @@ class NpmController extends RunPanel {
|
|
|
576
652
|
exit: '',
|
|
577
653
|
startedAt: Date.now(),
|
|
578
654
|
endedAt: 0,
|
|
655
|
+
top: ui.top,
|
|
579
656
|
});
|
|
580
657
|
this.nextId += 1;
|
|
581
658
|
this.runs.push(run);
|
|
@@ -659,7 +736,7 @@ class NpmController extends RunPanel {
|
|
|
659
736
|
}
|
|
660
737
|
|
|
661
738
|
stopAll() {
|
|
662
|
-
for (const run of this.
|
|
739
|
+
for (const run of this.everyRun()) this.stop(run);
|
|
663
740
|
}
|
|
664
741
|
|
|
665
742
|
toggleVerbose() {
|