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/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;
|
|
@@ -111,7 +128,6 @@ class BranchController {
|
|
|
111
128
|
const after = (extra) => this.refreshAfterWrite(extra);
|
|
112
129
|
const leave = () => {
|
|
113
130
|
ui.composer.closeCompose();
|
|
114
|
-
ui.showFiles();
|
|
115
131
|
};
|
|
116
132
|
const done = `created ${name}`;
|
|
117
133
|
ui.ops.runWrite(
|
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;
|
|
@@ -20,7 +20,13 @@ const unit = require('./unit.js');
|
|
|
20
20
|
const clipboard = require('../term/clipboard.js');
|
|
21
21
|
const { EDIT_OP, copyLine, cutLine } = require('../input/line-edit.js');
|
|
22
22
|
|
|
23
|
-
const COMPOSE_EDITS = [
|
|
23
|
+
const COMPOSE_EDITS = [
|
|
24
|
+
'backspace',
|
|
25
|
+
'ctrl-backspace',
|
|
26
|
+
'delete',
|
|
27
|
+
'ctrl-z',
|
|
28
|
+
'ctrl-y',
|
|
29
|
+
];
|
|
24
30
|
const COMPOSE_CLIP = ['ctrl-x', 'ctrl-v'];
|
|
25
31
|
const COMPOSE_LEAVE = ['next', 'prev', 'tasks'];
|
|
26
32
|
|
|
@@ -33,10 +39,18 @@ const KIND = {
|
|
|
33
39
|
branch: { enter: 'save', tab: 'template', escape: 'cancel', move: 'note' },
|
|
34
40
|
npm: { enter: 'save', tab: 'insert-tab', escape: 'cancel', move: 'note' },
|
|
35
41
|
package: { enter: 'save', tab: 'insert-tab', escape: 'cancel', move: 'note' },
|
|
42
|
+
worktree: { enter: 'save', tab: 'template', escape: 'cancel', move: 'note' },
|
|
36
43
|
};
|
|
37
44
|
|
|
38
|
-
const COMMAND_KINDS = ['commit', 'branch', 'npm', 'package'];
|
|
39
|
-
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
|
+
];
|
|
40
54
|
|
|
41
55
|
const itemNote = (item, text) => {
|
|
42
56
|
const hunk = item.hunk;
|
|
@@ -668,8 +682,8 @@ class Composer {
|
|
|
668
682
|
}
|
|
669
683
|
|
|
670
684
|
handleKey(key) {
|
|
671
|
-
if (key === 'enter') {
|
|
672
|
-
const result = this.enter();
|
|
685
|
+
if (key === 'enter' || isNewlineKey(key)) {
|
|
686
|
+
const result = isNewlineKey(key) ? this.insertNewline() : this.enter();
|
|
673
687
|
this.syncCodeScroll();
|
|
674
688
|
return result;
|
|
675
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,9 +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'),
|
|
120
|
+
agentInstall: (ui) => ui.agents.proposeInstall(),
|
|
121
|
+
agentStart: (ui) => ui.agents.startSelected(),
|
|
119
122
|
agentStop: (ui) => ui.agents.stop(),
|
|
120
123
|
agentRerun: (ui) => ui.agents.rerun(),
|
|
121
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(),
|
|
122
130
|
repoLeft: (ui) => ui.workspace.move(-1, 0),
|
|
123
131
|
repoRight: (ui) => ui.workspace.move(1, 0),
|
|
124
132
|
repoUp: (ui) => ui.workspace.move(0, -1),
|
|
@@ -126,7 +134,22 @@ const HANDLERS = {
|
|
|
126
134
|
...DASH_HANDLERS,
|
|
127
135
|
};
|
|
128
136
|
|
|
129
|
-
|
|
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;
|
|
130
153
|
if (CONFIRM_MODE.includes(ui.mode)) return;
|
|
131
154
|
if (ui.mode === 'import') stopImport(ui);
|
|
132
155
|
if (action === 'lines' && (ui.pane === 'diff' || ui.pane === 'unit')) {
|
|
@@ -158,6 +181,10 @@ const CONFIRMATIONS = {
|
|
|
158
181
|
accept: (ui) => ui.updater.accept(),
|
|
159
182
|
decline: (ui) => ui.updater.decline(),
|
|
160
183
|
},
|
|
184
|
+
confirmAgentInstall: {
|
|
185
|
+
confirm: (ui) => ui.agents.confirmInstall(),
|
|
186
|
+
cancel: (ui) => ui.agents.cancelInstall(),
|
|
187
|
+
},
|
|
161
188
|
confirmDrop: {
|
|
162
189
|
confirm: (ui) => {
|
|
163
190
|
if (ui.packages.dropName) return void ui.packages.confirmDrop();
|
|
@@ -172,6 +199,14 @@ const CONFIRMATIONS = {
|
|
|
172
199
|
ui.gitBranches.cancelDropBranch();
|
|
173
200
|
},
|
|
174
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
|
+
},
|
|
175
210
|
confirmPush: {
|
|
176
211
|
force: (ui) => ui.gitBranches.onForcePush(),
|
|
177
212
|
cancel: (ui) => ui.gitBranches.cancelPush(),
|
|
@@ -193,7 +228,10 @@ const confirmChoice = (ui, key) => {
|
|
|
193
228
|
|
|
194
229
|
const NPM_VIEW_KEYS = {
|
|
195
230
|
escape: (ui) => ui.npm.closeView(),
|
|
196
|
-
s: (ui) =>
|
|
231
|
+
s: (ui) => {
|
|
232
|
+
if (ui.npm.viewedRunning()) ui.npm.stop();
|
|
233
|
+
else ui.npm.solve();
|
|
234
|
+
},
|
|
197
235
|
r: (ui) => ui.npm.rerun(),
|
|
198
236
|
v: (ui) => ui.npm.toggleVerbose(),
|
|
199
237
|
e: () => {},
|
|
@@ -253,7 +291,9 @@ const planMenuKey = (ui, key) => {
|
|
|
253
291
|
};
|
|
254
292
|
|
|
255
293
|
const handleKey = (ui, key) => {
|
|
294
|
+
ui.nav.editDrag = null;
|
|
256
295
|
if (CONFIRM_MODE.includes(ui.mode)) return void confirmChoice(ui, key);
|
|
296
|
+
if (handleContextKey(ui, key)) return;
|
|
257
297
|
if (planMenuKey(ui, key)) return;
|
|
258
298
|
if (ui.pane === 'tasks' && (key === 'ctrl-up' || key === 'ctrl-down')) {
|
|
259
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/import.js
CHANGED
|
@@ -3,11 +3,22 @@
|
|
|
3
3
|
const { remoteKind, selectChangeSource } = require('../source/source.js');
|
|
4
4
|
const { lineKey, lineField } = require('../input/line-edit.js');
|
|
5
5
|
|
|
6
|
+
const changeRepositoryUrl = (change) => {
|
|
7
|
+
if (!change?.url || !change.repository) return '';
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(change.url);
|
|
10
|
+
return `${url.origin}/${change.repository}`;
|
|
11
|
+
} catch {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
6
16
|
const startImport = (ui) => {
|
|
7
17
|
if (ui.mode !== 'review' || ui.pane !== 'tasks') return;
|
|
8
18
|
ui.composer.tasks.closePlanMenu();
|
|
9
19
|
ui.mode = 'import';
|
|
10
|
-
ui.
|
|
20
|
+
const defaultUrl = ui.repositoryUrl || changeRepositoryUrl(ui.change);
|
|
21
|
+
ui.nav.import = lineField('url', defaultUrl);
|
|
11
22
|
ui.status = '';
|
|
12
23
|
};
|
|
13
24
|
|
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 {
|
|
@@ -169,6 +184,9 @@ class Lifecycle {
|
|
|
169
184
|
|
|
170
185
|
applySource(loaded) {
|
|
171
186
|
const ui = this.ui;
|
|
187
|
+
if (loaded.repositoryUrl !== undefined) {
|
|
188
|
+
ui.repositoryUrl = loaded.repositoryUrl || '';
|
|
189
|
+
}
|
|
172
190
|
if (loaded.rev) ui.rev = loaded.rev;
|
|
173
191
|
if (loaded.revShort) ui.revShort = loaded.revShort;
|
|
174
192
|
else if (ui.rev && !ui.revShort) ui.revShort = ui.rev.slice(0, 7);
|
|
@@ -191,7 +209,9 @@ class Lifecycle {
|
|
|
191
209
|
const here = ui.current();
|
|
192
210
|
ui.top = loaded.top ?? ui.cwd;
|
|
193
211
|
this.applyPrefs();
|
|
194
|
-
|
|
212
|
+
const seen = loaded.seenEpoch;
|
|
213
|
+
const fresh = seen === undefined || seen === ui.ops.writeEpoch;
|
|
214
|
+
if (fresh) collection.replace(loaded.items);
|
|
195
215
|
stampItemDates(ui.top, collection.items);
|
|
196
216
|
this.applySource(loaded);
|
|
197
217
|
if (!ui.review.store) ui.initReview();
|
|
@@ -239,7 +259,7 @@ class Lifecycle {
|
|
|
239
259
|
}
|
|
240
260
|
|
|
241
261
|
clearNpmExtras() {
|
|
242
|
-
this.
|
|
262
|
+
this.extras.clear();
|
|
243
263
|
}
|
|
244
264
|
|
|
245
265
|
npmExtras() {
|
|
@@ -276,7 +296,8 @@ class Lifecycle {
|
|
|
276
296
|
const next = outcome.value;
|
|
277
297
|
if (!this.isWanted(gen)) return;
|
|
278
298
|
if (next) {
|
|
279
|
-
|
|
299
|
+
const seenEpoch = loaded.seenEpoch;
|
|
300
|
+
this.applyLoaded(tagLoaded({ ...next, seenEpoch }, gen));
|
|
280
301
|
this.cacheNpmExtras(next);
|
|
281
302
|
} else {
|
|
282
303
|
ui.loader.pendingExtras = false;
|
|
@@ -313,6 +334,17 @@ class Lifecycle {
|
|
|
313
334
|
if (extra.afterLoad) extra.afterLoad();
|
|
314
335
|
}
|
|
315
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
|
+
|
|
316
348
|
async runSnapshot(gen, kind, extra = {}) {
|
|
317
349
|
const ui = this.ui;
|
|
318
350
|
if (extra.quiet !== true) {
|
|
@@ -323,8 +355,7 @@ class Lifecycle {
|
|
|
323
355
|
let loaded = null;
|
|
324
356
|
const side = ui.reads.read();
|
|
325
357
|
try {
|
|
326
|
-
|
|
327
|
-
loaded = await fetchSnapshot(ui.repo, ui.cwd, ui.paths, opts);
|
|
358
|
+
loaded = await this.fetchSettled();
|
|
328
359
|
} catch (error) {
|
|
329
360
|
if (ui.loader.isCurrent(gen)) this.failOpen(error);
|
|
330
361
|
}
|
|
@@ -474,21 +505,46 @@ class Lifecycle {
|
|
|
474
505
|
stopWatch() {
|
|
475
506
|
this.clearWatchRetry();
|
|
476
507
|
this.reviewStamp = '';
|
|
508
|
+
this.stopTreeWatch();
|
|
477
509
|
if (!this.diskWatcher) return;
|
|
478
510
|
this.diskWatcher.close();
|
|
479
511
|
this.diskWatcher = null;
|
|
480
512
|
}
|
|
481
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
|
+
|
|
482
538
|
startWatch() {
|
|
483
539
|
this.stopWatch();
|
|
484
540
|
const ui = this.ui;
|
|
485
541
|
if (ui.nav.pane === 'repos') {
|
|
486
|
-
|
|
487
|
-
|
|
542
|
+
if (!ui.workspaceRoot) return;
|
|
543
|
+
const root = dirKey(ui.workspaceRoot);
|
|
488
544
|
const info = ui.workspaceInfo;
|
|
489
545
|
const repos = info ? info.repos : [];
|
|
490
546
|
const paths = ['.'];
|
|
491
|
-
for (const repo of repos) paths.push(
|
|
547
|
+
for (const repo of repos) paths.push(...watchedBy(root, repo));
|
|
492
548
|
const matchReview = (rel) => rel.includes(`/${REVIEW_DIR}/`);
|
|
493
549
|
const onPaths = (rels) => ui.workspace.notePaths(rels);
|
|
494
550
|
const options = { root, paths, matchReview, onPaths };
|
|
@@ -502,10 +558,14 @@ class Lifecycle {
|
|
|
502
558
|
paths: this.ui.paths,
|
|
503
559
|
onChange: () => this.onDiskChange(),
|
|
504
560
|
onReview: () => this.applyReviewChange(),
|
|
505
|
-
onPaths: (paths) =>
|
|
561
|
+
onPaths: (paths) => {
|
|
562
|
+
this.ui.dashboard.noteDisk(paths);
|
|
563
|
+
this.ui.trees.notePaths(paths);
|
|
564
|
+
},
|
|
506
565
|
});
|
|
507
566
|
this.syncItemWatches();
|
|
508
567
|
this.noteReviewStamp();
|
|
568
|
+
this.watchTrees();
|
|
509
569
|
}
|
|
510
570
|
}
|
|
511
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() {
|
|
@@ -189,6 +189,7 @@ const recordedItem = (record) => {
|
|
|
189
189
|
return {
|
|
190
190
|
name,
|
|
191
191
|
kind: script ? 'script' : 'command',
|
|
192
|
+
script,
|
|
192
193
|
command,
|
|
193
194
|
status: externalStatus(record),
|
|
194
195
|
exit: externalExit(record),
|
|
@@ -220,7 +221,8 @@ const whenLabel = (ms) => {
|
|
|
220
221
|
};
|
|
221
222
|
|
|
222
223
|
const elapsedText = (run, now) => {
|
|
223
|
-
const end =
|
|
224
|
+
const end =
|
|
225
|
+
run.status === 'running' ? now : run.endedAt || run.startedAt || 0;
|
|
224
226
|
const ms = Math.max(0, end - (run.startedAt || end));
|
|
225
227
|
const sec = Math.round(ms / 1000);
|
|
226
228
|
if (sec < 60) return `${sec}s`;
|