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/git/worktree.js
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
|
-
const { runProc, isAbort } = require('../common/process.js');
|
|
6
|
+
const { runProc, isAbort, abortError } = require('../common/process.js');
|
|
7
7
|
const { oneLine } = require('../common/utilities.js');
|
|
8
8
|
const diff = require('../diff/diff.js');
|
|
9
9
|
const { parseDiff, synthesizeNewFile, itemsFromFiles } = diff;
|
|
10
10
|
const files = require('../common/files.js');
|
|
11
11
|
const { groupByPath } = files;
|
|
12
12
|
const { itemPath, isReadOnlyOrigin, lineDelta, REVIEW_DIR } = files;
|
|
13
|
+
const local = require('./local.js');
|
|
14
|
+
const { worktreeRoot, branchOf, remoteOrigin } = local;
|
|
15
|
+
const { parseStatus } = require('./status.js');
|
|
13
16
|
const { replaceBlockAdds } = require('../diff/patch.js');
|
|
14
17
|
|
|
15
18
|
const GIT_CONFIG = [
|
|
@@ -20,10 +23,30 @@ const GIT_CONFIG = [
|
|
|
20
23
|
];
|
|
21
24
|
const DIFF_OPTS = ['--no-color', '--no-ext-diff', '--no-renames', '-U3'];
|
|
22
25
|
const BINARY_SIZE = 1000000;
|
|
26
|
+
const STATUS_ARGS = [
|
|
27
|
+
'-c',
|
|
28
|
+
'core.quotepath=false',
|
|
29
|
+
'-c',
|
|
30
|
+
'color.ui=false',
|
|
31
|
+
'-c',
|
|
32
|
+
'diff.renames=false',
|
|
33
|
+
'status',
|
|
34
|
+
'-vv',
|
|
35
|
+
'--no-renames',
|
|
36
|
+
'--no-ahead-behind',
|
|
37
|
+
'-uall',
|
|
38
|
+
];
|
|
39
|
+
const STATUS_ENV = { LC_MESSAGES: 'C', LANGUAGE: 'C' };
|
|
23
40
|
|
|
24
41
|
const gitOptions = (cwd, extra) => ({
|
|
25
42
|
cwd,
|
|
26
|
-
env: {
|
|
43
|
+
env: {
|
|
44
|
+
...process.env,
|
|
45
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
46
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
47
|
+
GCM_INTERACTIVE: 'Never',
|
|
48
|
+
...extra.env,
|
|
49
|
+
},
|
|
27
50
|
input: extra.input,
|
|
28
51
|
signal: extra.signal,
|
|
29
52
|
});
|
|
@@ -183,17 +206,12 @@ const untrackedItems = async (top, names) => {
|
|
|
183
206
|
return itemsFromFiles(files, 'untracked');
|
|
184
207
|
};
|
|
185
208
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const items = untrackedItemsFrom(top, namesFromLs(result.stdout));
|
|
191
|
-
let added = 0;
|
|
192
|
-
for (const item of items) added += lineDelta(item).added;
|
|
193
|
-
return added;
|
|
194
|
-
} catch {
|
|
195
|
-
return 0;
|
|
209
|
+
const addedOf = (names, top) => {
|
|
210
|
+
let added = 0;
|
|
211
|
+
for (const item of untrackedItemsFrom(top, names)) {
|
|
212
|
+
added += lineDelta(item).added;
|
|
196
213
|
}
|
|
214
|
+
return added;
|
|
197
215
|
};
|
|
198
216
|
|
|
199
217
|
const gitText = async (top, spec) => {
|
|
@@ -240,13 +258,23 @@ const readDepSides = async (top, rev, origin, rel) => {
|
|
|
240
258
|
return { oldText, newText };
|
|
241
259
|
};
|
|
242
260
|
|
|
243
|
-
const
|
|
261
|
+
const toplevelGit = async (cwd, signal) => {
|
|
244
262
|
const args = ['rev-parse', '--show-toplevel'];
|
|
245
263
|
const result = await runGit(args, cwd, { signal });
|
|
246
264
|
if (result.status !== 0) throw new Error('not a git repository');
|
|
247
265
|
return result.stdout.trim();
|
|
248
266
|
};
|
|
249
267
|
|
|
268
|
+
const toplevel = async (cwd, signal) => {
|
|
269
|
+
if (signal && signal.aborted) throw abortError();
|
|
270
|
+
if (process.env.GIT_DIR || process.env.GIT_WORK_TREE) {
|
|
271
|
+
return toplevelGit(cwd, signal);
|
|
272
|
+
}
|
|
273
|
+
const found = worktreeRoot(cwd);
|
|
274
|
+
if (found) return found;
|
|
275
|
+
return toplevelGit(cwd, signal);
|
|
276
|
+
};
|
|
277
|
+
|
|
250
278
|
const resolveRev = async (cwd, spec) => {
|
|
251
279
|
if (!spec) return null;
|
|
252
280
|
const args = ['rev-parse', '--verify', `${spec}^{commit}`];
|
|
@@ -259,29 +287,53 @@ const shortRev = async (top, commit) => {
|
|
|
259
287
|
return result.status === 0 ? result.stdout.trim() : commit.slice(0, 7);
|
|
260
288
|
};
|
|
261
289
|
|
|
262
|
-
const
|
|
290
|
+
const currentBranch = async (top) => {
|
|
291
|
+
const root = worktreeRoot(top);
|
|
292
|
+
if (root) return branchOf(root);
|
|
293
|
+
const args = ['rev-parse', '--abbrev-ref', 'HEAD'];
|
|
294
|
+
const result = await runGit(args, top);
|
|
263
295
|
if (result.status !== 0) return '';
|
|
264
296
|
const name = result.stdout.trim();
|
|
265
297
|
return name === 'HEAD' ? '' : name;
|
|
266
298
|
};
|
|
267
299
|
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
300
|
+
const repositoryUrl = (raw) => {
|
|
301
|
+
let value = `${raw ?? ''}`.trim();
|
|
302
|
+
if (!value) return '';
|
|
303
|
+
const scp = /^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/.exec(value);
|
|
304
|
+
if (scp && !value.includes('://')) value = `https://${scp[1]}/${scp[2]}`;
|
|
305
|
+
else if (value.startsWith('git+')) value = value.slice(4);
|
|
306
|
+
let url;
|
|
307
|
+
try {
|
|
308
|
+
url = new URL(value);
|
|
309
|
+
} catch {
|
|
310
|
+
return '';
|
|
311
|
+
}
|
|
312
|
+
if (!['http:', 'https:', 'git:', 'ssh:'].includes(url.protocol)) return '';
|
|
313
|
+
url.protocol = 'https:';
|
|
314
|
+
url.username = '';
|
|
315
|
+
url.password = '';
|
|
316
|
+
if (url.port === '22') url.port = '';
|
|
317
|
+
url.search = '';
|
|
318
|
+
url.hash = '';
|
|
319
|
+
url.pathname = url.pathname.replace(/\.git\/?$/i, '').replace(/\/$/, '');
|
|
320
|
+
return url.toString().replace(/\/$/, '');
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const worktreeSnapshot = (top, outputs, untracked, branch, repoUrl) => {
|
|
273
324
|
const staged = itemsFromFiles(parseDiff(outputs[0]), 'staged');
|
|
274
325
|
const unstaged = itemsFromFiles(parseDiff(outputs[1]), 'unstaged');
|
|
275
326
|
const grouped = groupByPath([...staged, ...unstaged, ...untracked]);
|
|
276
327
|
const parsed = dropReviewItems([...grouped.values()].flat());
|
|
277
|
-
return { top, parsed, branch };
|
|
328
|
+
return { top, parsed, branch, repositoryUrl: repoUrl };
|
|
278
329
|
};
|
|
279
330
|
|
|
280
331
|
const snapshotCommit = async (top, commit, files) => {
|
|
281
332
|
const parsed = dropReviewItems(itemsFromFiles(files, 'commit'));
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
|
|
333
|
+
const revShort = await shortRev(top, commit);
|
|
334
|
+
const branch = await currentBranch(top);
|
|
335
|
+
const repoUrl = repositoryUrl(remoteOrigin(top));
|
|
336
|
+
return { top, parsed, rev: commit, revShort, branch, repositoryUrl: repoUrl };
|
|
285
337
|
};
|
|
286
338
|
|
|
287
339
|
function* commitDiff(commit, paths) {
|
|
@@ -296,6 +348,38 @@ const worktreeCommands = (paths) => [
|
|
|
296
348
|
untrackedArgs(paths),
|
|
297
349
|
];
|
|
298
350
|
|
|
351
|
+
const gitView = async (top, paths, signal) => {
|
|
352
|
+
const commands = worktreeCommands(paths);
|
|
353
|
+
const running = commands.map((args) => runGit(args, top, { signal }));
|
|
354
|
+
const results = await Promise.all(running);
|
|
355
|
+
const outputs = results.map((result) => requireOk(result).stdout);
|
|
356
|
+
return {
|
|
357
|
+
staged: outputs[0],
|
|
358
|
+
work: outputs[1],
|
|
359
|
+
untracked: namesFromLs(outputs[2]),
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const worktreeView = async (top, paths, signal) => {
|
|
364
|
+
// status -vv prints every diff and ignores the pathspec
|
|
365
|
+
if (paths.length) return gitView(top, paths, signal);
|
|
366
|
+
const extra = { signal, env: STATUS_ENV };
|
|
367
|
+
const result = await runGit(STATUS_ARGS, top, extra);
|
|
368
|
+
if (result.status !== 0) return gitView(top, paths, signal);
|
|
369
|
+
const parsed = parseStatus(result.stdout);
|
|
370
|
+
if (!parsed) return gitView(top, paths, signal);
|
|
371
|
+
return parsed;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const countUntrackedAdded = async (top) => {
|
|
375
|
+
try {
|
|
376
|
+
const view = await worktreeView(top, []);
|
|
377
|
+
return addedOf(view.untracked, top);
|
|
378
|
+
} catch {
|
|
379
|
+
return 0;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
299
383
|
const readSnapshot = async (cwd, paths, options) => {
|
|
300
384
|
const { commit: rev, signal } = options;
|
|
301
385
|
const top = await toplevel(cwd, signal);
|
|
@@ -303,14 +387,12 @@ const readSnapshot = async (cwd, paths, options) => {
|
|
|
303
387
|
const files = await runSteps(commitDiff(rev, paths), top, signal);
|
|
304
388
|
return snapshotCommit(top, rev, files);
|
|
305
389
|
}
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
const outputs =
|
|
311
|
-
|
|
312
|
-
const untracked = await untrackedItems(top, names);
|
|
313
|
-
return worktreeSnapshot(top, outputs, untracked, branch);
|
|
390
|
+
const view = await worktreeView(top, paths, signal);
|
|
391
|
+
const untracked = await untrackedItems(top, view.untracked);
|
|
392
|
+
const branch = await currentBranch(top);
|
|
393
|
+
const repoUrl = repositoryUrl(remoteOrigin(top));
|
|
394
|
+
const outputs = [view.staged, view.work];
|
|
395
|
+
return worktreeSnapshot(top, outputs, untracked, branch, repoUrl);
|
|
314
396
|
};
|
|
315
397
|
|
|
316
398
|
const applyPatch = async (cwd, patch, flags) => {
|
|
@@ -446,6 +528,29 @@ const stagePath = async (top, rel) => {
|
|
|
446
528
|
if (rel) await gitOk(['add', '--', rel], top);
|
|
447
529
|
};
|
|
448
530
|
|
|
531
|
+
const sumDelta = (items) => {
|
|
532
|
+
let added = 0;
|
|
533
|
+
let removed = 0;
|
|
534
|
+
for (const item of items) {
|
|
535
|
+
const delta = lineDelta(item);
|
|
536
|
+
added += delta.added;
|
|
537
|
+
removed += delta.removed;
|
|
538
|
+
}
|
|
539
|
+
return { added, removed };
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const diffCounts = async (top) => {
|
|
543
|
+
const view = await worktreeView(top, []);
|
|
544
|
+
const staged = sumDelta(itemsFromFiles(parseDiff(view.staged), 'staged'));
|
|
545
|
+
const open = sumDelta(itemsFromFiles(parseDiff(view.work), 'unstaged'));
|
|
546
|
+
return {
|
|
547
|
+
stagedAdded: staged.added,
|
|
548
|
+
unstagedAdded: open.added + addedOf(view.untracked, top),
|
|
549
|
+
stagedRemoved: staged.removed,
|
|
550
|
+
unstagedRemoved: open.removed,
|
|
551
|
+
};
|
|
552
|
+
};
|
|
553
|
+
|
|
449
554
|
module.exports = {
|
|
450
555
|
requireOk,
|
|
451
556
|
gitOk,
|
|
@@ -463,6 +568,7 @@ module.exports = {
|
|
|
463
568
|
currentBranch,
|
|
464
569
|
fileText,
|
|
465
570
|
countUntrackedAdded,
|
|
571
|
+
diffCounts,
|
|
466
572
|
listScopeFiles,
|
|
467
573
|
readSnapshot,
|
|
468
574
|
editItem,
|
package/lib/input/actions.js
CHANGED
|
@@ -59,6 +59,7 @@ const NPM_ACTIONS = {
|
|
|
59
59
|
npmDrop: ['delete'],
|
|
60
60
|
npmLogs: ['l'],
|
|
61
61
|
npmStop: ['s'],
|
|
62
|
+
npmSolve: ['s'],
|
|
62
63
|
npmVerbose: ['v'],
|
|
63
64
|
npmRerun: ['r'],
|
|
64
65
|
};
|
|
@@ -69,6 +70,8 @@ const AGENT_ACTIONS = {
|
|
|
69
70
|
agentFast: ['a'],
|
|
70
71
|
agentContext: ['t'],
|
|
71
72
|
agentReview: ['p'],
|
|
73
|
+
agentInstall: ['i'],
|
|
74
|
+
agentStart: ['s'],
|
|
72
75
|
agentStop: ['s'],
|
|
73
76
|
agentRerun: ['r'],
|
|
74
77
|
};
|
|
@@ -79,6 +82,8 @@ const AGENT_LIST_ACTIONS = {
|
|
|
79
82
|
agentFast: AGENT_ACTIONS.agentFast,
|
|
80
83
|
agentContext: AGENT_ACTIONS.agentContext,
|
|
81
84
|
agentReview: AGENT_ACTIONS.agentReview,
|
|
85
|
+
agentInstall: AGENT_ACTIONS.agentInstall,
|
|
86
|
+
agentStart: AGENT_ACTIONS.agentStart,
|
|
82
87
|
};
|
|
83
88
|
|
|
84
89
|
const DASH_TILES = [
|
|
@@ -100,6 +105,11 @@ for (const tile of DASH_TILES) {
|
|
|
100
105
|
DASH_KEYS[tile.key] = tile.action;
|
|
101
106
|
}
|
|
102
107
|
const DASH_IDS = Object.keys(DASH_ACTIONS);
|
|
108
|
+
const WORKTREE_ACTIONS = {
|
|
109
|
+
worktreeNew: ['insert'],
|
|
110
|
+
worktreeDrop: ['delete'],
|
|
111
|
+
worktreeBranch: ['w'],
|
|
112
|
+
};
|
|
103
113
|
const DASH_ALLOWED = [
|
|
104
114
|
...Object.keys(DASH_BLOCKS),
|
|
105
115
|
'back',
|
|
@@ -115,17 +125,28 @@ const REPOS_ALLOWED = [
|
|
|
115
125
|
'repoDown',
|
|
116
126
|
'open',
|
|
117
127
|
'back',
|
|
128
|
+
'worktreeNew',
|
|
129
|
+
'worktreeDrop',
|
|
118
130
|
'quit',
|
|
119
131
|
'theme',
|
|
120
132
|
];
|
|
121
133
|
const DASH_HIDDEN = ACTION_IDS.filter((id) => id !== 'quit');
|
|
122
134
|
|
|
123
135
|
const ACTION_HOT = {
|
|
136
|
+
textCut: 'ctrl+x',
|
|
137
|
+
textCopy: 'ctrl+c',
|
|
138
|
+
textPaste: 'ctrl+v',
|
|
139
|
+
textDelete: 'del',
|
|
140
|
+
textSelectAll: 'ctrl+a',
|
|
141
|
+
open: 'enter',
|
|
124
142
|
prev: '←',
|
|
125
143
|
next: '→',
|
|
126
144
|
drop: 'del',
|
|
145
|
+
worktreeDrop: 'del',
|
|
146
|
+
worktreeBranch: 'w',
|
|
127
147
|
newBranch: 'ins',
|
|
128
148
|
newCommit: 'ins',
|
|
149
|
+
worktreeNew: 'ins',
|
|
129
150
|
feedback: 'q',
|
|
130
151
|
check: 'space',
|
|
131
152
|
taskUp: 'ctrl+up',
|
|
@@ -143,7 +164,13 @@ const ACTION_HOT = {
|
|
|
143
164
|
};
|
|
144
165
|
|
|
145
166
|
const ACTION_LABEL = {
|
|
167
|
+
textCut: 'ctrl+x cut',
|
|
168
|
+
textCopy: 'ctrl+c copy',
|
|
169
|
+
textPaste: 'ctrl+v paste',
|
|
170
|
+
textDelete: 'delete',
|
|
171
|
+
textSelectAll: 'ctrl+a select all',
|
|
146
172
|
...DASH_BLOCKS,
|
|
173
|
+
open: 'enter open',
|
|
147
174
|
revert: 'drop',
|
|
148
175
|
check: 'space',
|
|
149
176
|
taskUp: 'ctrl+up',
|
|
@@ -161,6 +188,9 @@ const ACTION_LABEL = {
|
|
|
161
188
|
drop: 'delete',
|
|
162
189
|
newBranch: 'insert',
|
|
163
190
|
newCommit: 'insert',
|
|
191
|
+
worktreeNew: 'insert',
|
|
192
|
+
worktreeDrop: 'delete',
|
|
193
|
+
worktreeBranch: 'worktree',
|
|
164
194
|
feedback: 'quote',
|
|
165
195
|
npmEdit: 'edit',
|
|
166
196
|
npmNew: 'insert',
|
|
@@ -171,6 +201,7 @@ const ACTION_LABEL = {
|
|
|
171
201
|
packageWanted: 'wanted',
|
|
172
202
|
packageLatest: 'latest',
|
|
173
203
|
npmStop: 'stop',
|
|
204
|
+
npmSolve: 'solve',
|
|
174
205
|
npmVerbose: 'verbose',
|
|
175
206
|
npmRerun: 're-run',
|
|
176
207
|
agentModel: 'model',
|
|
@@ -178,6 +209,8 @@ const ACTION_LABEL = {
|
|
|
178
209
|
agentFast: 'fast',
|
|
179
210
|
agentContext: 'context',
|
|
180
211
|
agentReview: 'plan',
|
|
212
|
+
agentInstall: 'install',
|
|
213
|
+
agentStart: 'start',
|
|
181
214
|
agentStop: 'stop',
|
|
182
215
|
agentRerun: 're-run',
|
|
183
216
|
quit: 'q',
|
|
@@ -384,7 +417,12 @@ const keysForActions = (actions) => {
|
|
|
384
417
|
const PANE_ONLY = ['branch', 'file', 'diff'];
|
|
385
418
|
const REVIEW_DROP = { d: 'revert' };
|
|
386
419
|
const DIFF_KEYS = { ...REVIEW_DROP, q: 'feedback', 'ctrl-l': 'lines' };
|
|
387
|
-
const BRANCH_KEYS =
|
|
420
|
+
const BRANCH_KEYS = {
|
|
421
|
+
...keysForActions(BRANCH_ACTIONS),
|
|
422
|
+
w: 'worktreeBranch',
|
|
423
|
+
left: 'treeLeft',
|
|
424
|
+
right: 'treeRight',
|
|
425
|
+
};
|
|
388
426
|
const COMMIT_KEYS = { ...keysForActions(COMMIT_ACTIONS), s: 'push' };
|
|
389
427
|
const NPM_KEYS = {
|
|
390
428
|
e: 'npmEdit',
|
|
@@ -413,6 +451,8 @@ const PANE_KEYS = {
|
|
|
413
451
|
j: 'repoDown',
|
|
414
452
|
p: 'repoLeft',
|
|
415
453
|
n: 'repoRight',
|
|
454
|
+
insert: 'worktreeNew',
|
|
455
|
+
delete: 'worktreeDrop',
|
|
416
456
|
},
|
|
417
457
|
};
|
|
418
458
|
|
|
@@ -427,7 +467,8 @@ const KEY_TO_ACTION = {
|
|
|
427
467
|
};
|
|
428
468
|
|
|
429
469
|
const actionFromKey = (key, pane) => {
|
|
430
|
-
|
|
470
|
+
const sideways = key === 'left' || key === 'right';
|
|
471
|
+
if (LIST_PANES.includes(pane) && sideways && pane !== 'branches') {
|
|
431
472
|
return null;
|
|
432
473
|
}
|
|
433
474
|
const paneKeys = PANE_KEYS[pane];
|
|
@@ -443,6 +484,7 @@ const ACTION_KEYS = {
|
|
|
443
484
|
...NPM_ACTIONS,
|
|
444
485
|
...COMMIT_ACTIONS,
|
|
445
486
|
...BRANCH_ACTIONS,
|
|
487
|
+
...WORKTREE_ACTIONS,
|
|
446
488
|
...ACTIONS,
|
|
447
489
|
};
|
|
448
490
|
|
|
@@ -450,6 +492,17 @@ const actionKeys = (id) => ACTION_KEYS[id] ?? [];
|
|
|
450
492
|
|
|
451
493
|
const actionLetter = (id) => ACTION_HOT[id] ?? actionKeys(id)[0];
|
|
452
494
|
|
|
495
|
+
const ACTION_CAPTION = {
|
|
496
|
+
open: 'open',
|
|
497
|
+
check: 'check',
|
|
498
|
+
textCut: 'cut',
|
|
499
|
+
textCopy: 'copy',
|
|
500
|
+
textPaste: 'paste',
|
|
501
|
+
textSelectAll: 'select all',
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const actionCaption = (id) => ACTION_CAPTION[id] ?? ACTION_LABEL[id] ?? id;
|
|
505
|
+
|
|
453
506
|
const buttonWord = (id) => {
|
|
454
507
|
const word = ACTION_LABEL[id] ?? ACTION_HOT[id] ?? id;
|
|
455
508
|
const mark = actionLetter(id);
|
|
@@ -486,6 +539,15 @@ const CONFIRM = {
|
|
|
486
539
|
escape: 'decline',
|
|
487
540
|
},
|
|
488
541
|
},
|
|
542
|
+
confirmAgentInstall: {
|
|
543
|
+
choices: YES_NO,
|
|
544
|
+
prefix: (view) => ` install ${view.agentInstallPackage ?? ''}?`,
|
|
545
|
+
keys: {
|
|
546
|
+
y: 'confirm',
|
|
547
|
+
n: 'cancel',
|
|
548
|
+
escape: 'cancel',
|
|
549
|
+
},
|
|
550
|
+
},
|
|
489
551
|
confirmDrop: {
|
|
490
552
|
choices: YES_NO,
|
|
491
553
|
prefix: (view) => ` drop ${view.dropName ?? ''}?`,
|
|
@@ -495,6 +557,24 @@ const CONFIRM = {
|
|
|
495
557
|
escape: 'cancel',
|
|
496
558
|
},
|
|
497
559
|
},
|
|
560
|
+
confirmWorktree: {
|
|
561
|
+
choices: YES_NO,
|
|
562
|
+
prefix: (view) => ` remove ${view.dropName ?? ''}?`,
|
|
563
|
+
keys: {
|
|
564
|
+
y: 'confirm',
|
|
565
|
+
n: 'cancel',
|
|
566
|
+
escape: 'cancel',
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
confirmCreateWorktree: {
|
|
570
|
+
choices: YES_NO,
|
|
571
|
+
prefix: (view) => ` create ${view.worktreeTarget ?? ''}?`,
|
|
572
|
+
keys: {
|
|
573
|
+
y: 'confirm',
|
|
574
|
+
n: 'cancel',
|
|
575
|
+
escape: 'cancel',
|
|
576
|
+
},
|
|
577
|
+
},
|
|
498
578
|
confirmPush: {
|
|
499
579
|
choices: [{ letter: 'f', label: 'force push', lead: ' ' }],
|
|
500
580
|
prefix: ' Need force-push?',
|
|
@@ -571,6 +651,7 @@ module.exports = {
|
|
|
571
651
|
isFixupCommit,
|
|
572
652
|
actionFromKey,
|
|
573
653
|
actionLetter,
|
|
654
|
+
actionCaption,
|
|
574
655
|
buttonWord,
|
|
575
656
|
CONFIRM,
|
|
576
657
|
CONFIRM_MODE,
|
package/lib/input/editor.js
CHANGED
|
@@ -139,6 +139,15 @@ class Editor {
|
|
|
139
139
|
this.edit(next, this.cursor - size);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
backspaceWord() {
|
|
143
|
+
this.wantCol = null;
|
|
144
|
+
if (this.removeSpan()) return;
|
|
145
|
+
const { parts, i } = graphemeIndex(this.text, this.cursor);
|
|
146
|
+
const start = offsetAt(parts, wordIndex(parts, i, -1));
|
|
147
|
+
if (start === this.cursor) return;
|
|
148
|
+
this.edit(this.text.slice(0, start) + this.text.slice(this.cursor), start);
|
|
149
|
+
}
|
|
150
|
+
|
|
142
151
|
delete() {
|
|
143
152
|
this.wantCol = null;
|
|
144
153
|
if (this.removeSpan()) return;
|
package/lib/input/keys.js
CHANGED
|
@@ -45,6 +45,15 @@ const CSI_TILDE = {
|
|
|
45
45
|
24: 'f12',
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
// Kitty CSI-u and xterm modifyOtherKeys. 13 is Enter, 10 is line feed.
|
|
49
|
+
const CSI_U = {
|
|
50
|
+
10: 'enter',
|
|
51
|
+
13: 'enter',
|
|
52
|
+
127: 'backspace',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const MODIFY_OTHER = '27';
|
|
56
|
+
|
|
48
57
|
const CSI_MAX = 16;
|
|
49
58
|
|
|
50
59
|
const LITERAL_KEY = {
|
|
@@ -71,20 +80,40 @@ const keyAt = (key, end) => ({ event: { type: 'key', key }, end });
|
|
|
71
80
|
const modifiedKey = (base, modText) => {
|
|
72
81
|
const arrow = ARROW_KEYS.includes(base);
|
|
73
82
|
const end = EDGE_KEYS.includes(base);
|
|
74
|
-
if (!arrow && !end) return base;
|
|
75
83
|
const mod = parseInt(modText, 10);
|
|
76
84
|
const bits = Number.isFinite(mod) ? mod - 1 : 0;
|
|
77
85
|
const ctrl = (bits & 4) !== 0;
|
|
78
86
|
const shift = (bits & 1) !== 0;
|
|
87
|
+
if (base === 'backspace' && ctrl) return 'ctrl-backspace';
|
|
88
|
+
if (base === 'enter') {
|
|
89
|
+
if (ctrl && shift) return 'ctrl-shift-enter';
|
|
90
|
+
if (ctrl) return 'ctrl-enter';
|
|
91
|
+
if (shift) return 'shift-enter';
|
|
92
|
+
return 'enter';
|
|
93
|
+
}
|
|
94
|
+
if (!arrow && !end) return base;
|
|
79
95
|
if (ctrl && shift && arrow) return `ctrl-shift-${base}`;
|
|
80
96
|
if (ctrl && arrow) return `ctrl-${base}`;
|
|
81
97
|
if (shift) return `shift-${base}`;
|
|
82
98
|
return base;
|
|
83
99
|
};
|
|
84
100
|
|
|
101
|
+
const NEWLINE_KEYS = ['ctrl-enter', 'shift-enter', 'ctrl-shift-enter'];
|
|
102
|
+
|
|
103
|
+
const isNewlineKey = (key) => NEWLINE_KEYS.includes(key);
|
|
104
|
+
|
|
105
|
+
const otherModified = (params) => {
|
|
106
|
+
const base = CSI_U[params[2]];
|
|
107
|
+
if (!base) return null;
|
|
108
|
+
return modifiedKey(base, params[1] ?? '1');
|
|
109
|
+
};
|
|
110
|
+
|
|
85
111
|
const keyFromCsi = (paramsText, final) => {
|
|
86
112
|
const params = paramsText ? paramsText.split(';') : [];
|
|
87
|
-
|
|
113
|
+
if (final === '~' && params[0] === MODIFY_OTHER) return otherModified(params);
|
|
114
|
+
let base = CSI_KEYS[final];
|
|
115
|
+
if (final === '~') base = CSI_TILDE[params[0]];
|
|
116
|
+
if (final === 'u') base = CSI_U[params[0]];
|
|
88
117
|
if (!base) return null;
|
|
89
118
|
if (params.length < 2) return base;
|
|
90
119
|
return modifiedKey(base, params[1]);
|
|
@@ -108,6 +137,7 @@ const mouseKind = (button, press) => {
|
|
|
108
137
|
const wheel = MOUSE_WHEEL[button];
|
|
109
138
|
if (wheel) return wheel;
|
|
110
139
|
if (!press) return 'release';
|
|
140
|
+
if ((button & 35) === 35) return 'move';
|
|
111
141
|
return button & 32 ? 'drag' : 'press';
|
|
112
142
|
};
|
|
113
143
|
|
|
@@ -190,4 +220,10 @@ const hitAction = (hits, x) => {
|
|
|
190
220
|
return null;
|
|
191
221
|
};
|
|
192
222
|
|
|
193
|
-
module.exports = {
|
|
223
|
+
module.exports = {
|
|
224
|
+
ACTIONS,
|
|
225
|
+
decodeChunk,
|
|
226
|
+
hitAction,
|
|
227
|
+
isTextKey,
|
|
228
|
+
isNewlineKey,
|
|
229
|
+
};
|
package/lib/input/line-edit.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { Editor } = require('./editor.js');
|
|
4
|
-
const { isTextKey } = require('./keys.js');
|
|
4
|
+
const { isTextKey, isNewlineKey } = require('./keys.js');
|
|
5
5
|
const clipboard = require('../term/clipboard.js');
|
|
6
6
|
|
|
7
7
|
const copyLine = (editor, ui) => {
|
|
@@ -33,6 +33,7 @@ const EDIT_OP = {
|
|
|
33
33
|
'ctrl-shift-left': (editor) => editor.moveWord(-1, true),
|
|
34
34
|
'ctrl-shift-right': (editor) => editor.moveWord(1, true),
|
|
35
35
|
backspace: (editor) => editor.backspace(),
|
|
36
|
+
'ctrl-backspace': (editor) => editor.backspaceWord(),
|
|
36
37
|
delete: (editor) => editor.delete(),
|
|
37
38
|
};
|
|
38
39
|
|
|
@@ -56,6 +57,10 @@ const lineKey = (editor, key, ui, ends) => {
|
|
|
56
57
|
copyLine(editor, ui);
|
|
57
58
|
return 'caret';
|
|
58
59
|
}
|
|
60
|
+
if (isNewlineKey(key)) {
|
|
61
|
+
editor.insert('\n');
|
|
62
|
+
return 'text';
|
|
63
|
+
}
|
|
59
64
|
const op = (ends ? LINE_ENDS : LINE_OP)[key];
|
|
60
65
|
if (!op && !isTextKey(key)) return null;
|
|
61
66
|
const before = editor.text;
|
|
@@ -64,8 +69,8 @@ const lineKey = (editor, key, ui, ends) => {
|
|
|
64
69
|
return editor.text === before ? 'caret' : 'text';
|
|
65
70
|
};
|
|
66
71
|
|
|
67
|
-
const lineField = (name) => {
|
|
68
|
-
const editor = new Editor(
|
|
72
|
+
const lineField = (name, text = '') => {
|
|
73
|
+
const editor = new Editor(text);
|
|
69
74
|
return {
|
|
70
75
|
editor,
|
|
71
76
|
get [name]() {
|