reslop 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +30 -4
  3. package/lib/agents/acp.js +309 -0
  4. package/lib/agents/agents.js +274 -438
  5. package/lib/agents/allow.js +141 -52
  6. package/lib/agents/allow.json +16 -14
  7. package/lib/agents/models.js +250 -383
  8. package/lib/agents/registry.js +437 -0
  9. package/lib/agents/run.js +234 -0
  10. package/lib/agents/script.js +93 -0
  11. package/lib/agents/transcript.js +143 -0
  12. package/lib/cli.js +2 -1
  13. package/lib/common/process.js +26 -1
  14. package/lib/dashboard/git.js +11 -1
  15. package/lib/dashboard/reports.js +80 -0
  16. package/lib/dashboard/watch.js +43 -8
  17. package/lib/dashboard/workspace.js +19 -20
  18. package/lib/dashboard/worktrees.js +145 -0
  19. package/lib/git/dir.js +50 -0
  20. package/lib/git/linked.js +152 -0
  21. package/lib/git/local.js +179 -0
  22. package/lib/git/status.js +126 -0
  23. package/lib/git/worktree.js +113 -36
  24. package/lib/input/actions.js +83 -5
  25. package/lib/input/keys.js +30 -1
  26. package/lib/input/line-edit.js +5 -1
  27. package/lib/render/agents.js +84 -34
  28. package/lib/render/branches.js +55 -27
  29. package/lib/render/field.js +19 -5
  30. package/lib/render/footer.js +14 -6
  31. package/lib/render/frame.js +16 -2
  32. package/lib/render/menu.js +103 -6
  33. package/lib/render/name-edit.js +31 -0
  34. package/lib/render/repos.js +52 -8
  35. package/lib/render/status.js +22 -4
  36. package/lib/report/run.js +16 -23
  37. package/lib/review/agent-instructions.md +2 -2
  38. package/lib/runs/process.js +3 -3
  39. package/lib/runs/runs.js +27 -4
  40. package/lib/session/agents/agents.js +220 -205
  41. package/lib/session/agents/away.js +48 -0
  42. package/lib/session/agents/job.js +23 -86
  43. package/lib/session/agents/jobs.js +14 -27
  44. package/lib/session/agents/options.js +45 -101
  45. package/lib/session/agents/plans.js +53 -0
  46. package/lib/session/agents/view.js +34 -0
  47. package/lib/session/branches.js +18 -1
  48. package/lib/session/compose.js +13 -5
  49. package/lib/session/dispatch.js +43 -4
  50. package/lib/session/editor-pointer.js +98 -0
  51. package/lib/session/files-pane.js +3 -0
  52. package/lib/session/lifecycle.js +67 -10
  53. package/lib/session/navigation.js +5 -0
  54. package/lib/session/npm/history.js +2 -1
  55. package/lib/session/npm/npm.js +89 -12
  56. package/lib/session/ops.js +3 -0
  57. package/lib/session/pointer.js +304 -46
  58. package/lib/session/session.js +15 -2
  59. package/lib/session/terminal.js +2 -2
  60. package/lib/session/text-menu.js +50 -0
  61. package/lib/session/view.js +19 -2
  62. package/lib/session/workspace.js +179 -38
  63. package/lib/session/worktrees.js +280 -0
  64. package/lib/term/themes/dark.json +4 -0
  65. package/lib/term/themes/light.json +4 -0
  66. package/package.json +2 -2
@@ -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: { ...process.env, GIT_OPTIONAL_LOCKS: '0', ...extra.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 countUntrackedAdded = async (top) => {
187
- try {
188
- const result = await runGit(untrackedArgs([]), top);
189
- if (result.status !== 0) return 0;
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 toplevel = async (cwd, signal) => {
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,20 +287,19 @@ const shortRev = async (top, commit) => {
259
287
  return result.status === 0 ? result.stdout.trim() : commit.slice(0, 7);
260
288
  };
261
289
 
262
- const branchName = (result) => {
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 BRANCH_ARGS = ['rev-parse', '--abbrev-ref', 'HEAD'];
269
- const REMOTE_ARGS = ['config', '--get', 'remote.origin.url'];
270
-
271
- const currentBranch = async (top) => branchName(await runGit(BRANCH_ARGS, top));
272
-
273
- const repositoryUrl = (result) => {
274
- if (result.status !== 0) return '';
275
- let value = result.stdout.trim();
300
+ const repositoryUrl = (raw) => {
301
+ let value = `${raw ?? ''}`.trim();
302
+ if (!value) return '';
276
303
  const scp = /^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/.exec(value);
277
304
  if (scp && !value.includes('://')) value = `https://${scp[1]}/${scp[2]}`;
278
305
  else if (value.startsWith('git+')) value = value.slice(4);
@@ -303,12 +330,9 @@ const worktreeSnapshot = (top, outputs, untracked, branch, repoUrl) => {
303
330
 
304
331
  const snapshotCommit = async (top, commit, files) => {
305
332
  const parsed = dropReviewItems(itemsFromFiles(files, 'commit'));
306
- const names = [
307
- shortRev(top, commit),
308
- currentBranch(top),
309
- runGit(REMOTE_ARGS, top).then(repositoryUrl),
310
- ];
311
- const [revShort, branch, repoUrl] = await Promise.all(names);
333
+ const revShort = await shortRev(top, commit);
334
+ const branch = await currentBranch(top);
335
+ const repoUrl = repositoryUrl(remoteOrigin(top));
312
336
  return { top, parsed, rev: commit, revShort, branch, repositoryUrl: repoUrl };
313
337
  };
314
338
 
@@ -324,6 +348,38 @@ const worktreeCommands = (paths) => [
324
348
  untrackedArgs(paths),
325
349
  ];
326
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
+
327
383
  const readSnapshot = async (cwd, paths, options) => {
328
384
  const { commit: rev, signal } = options;
329
385
  const top = await toplevel(cwd, signal);
@@ -331,14 +387,11 @@ const readSnapshot = async (cwd, paths, options) => {
331
387
  const files = await runSteps(commitDiff(rev, paths), top, signal);
332
388
  return snapshotCommit(top, rev, files);
333
389
  }
334
- const commands = [...worktreeCommands(paths), BRANCH_ARGS, REMOTE_ARGS];
335
- const running = commands.map((args) => runGit(args, top, { signal }));
336
- const results = await Promise.all(running);
337
- const repoUrl = repositoryUrl(results.pop());
338
- const branch = branchName(results.pop());
339
- const outputs = results.map((result) => requireOk(result).stdout);
340
- const names = namesFromLs(outputs[2]);
341
- const untracked = await untrackedItems(top, names);
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];
342
395
  return worktreeSnapshot(top, outputs, untracked, branch, repoUrl);
343
396
  };
344
397
 
@@ -475,6 +528,29 @@ const stagePath = async (top, rel) => {
475
528
  if (rel) await gitOk(['add', '--', rel], top);
476
529
  };
477
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
+
478
554
  module.exports = {
479
555
  requireOk,
480
556
  gitOk,
@@ -492,6 +568,7 @@ module.exports = {
492
568
  currentBranch,
493
569
  fileText,
494
570
  countUntrackedAdded,
571
+ diffCounts,
495
572
  listScopeFiles,
496
573
  readSnapshot,
497
574
  editItem,
@@ -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,7 +70,8 @@ const AGENT_ACTIONS = {
69
70
  agentFast: ['a'],
70
71
  agentContext: ['t'],
71
72
  agentReview: ['p'],
72
- agentContinue: ['c'],
73
+ agentInstall: ['i'],
74
+ agentStart: ['s'],
73
75
  agentStop: ['s'],
74
76
  agentRerun: ['r'],
75
77
  };
@@ -80,7 +82,8 @@ const AGENT_LIST_ACTIONS = {
80
82
  agentFast: AGENT_ACTIONS.agentFast,
81
83
  agentContext: AGENT_ACTIONS.agentContext,
82
84
  agentReview: AGENT_ACTIONS.agentReview,
83
- agentContinue: AGENT_ACTIONS.agentContinue,
85
+ agentInstall: AGENT_ACTIONS.agentInstall,
86
+ agentStart: AGENT_ACTIONS.agentStart,
84
87
  };
85
88
 
86
89
  const DASH_TILES = [
@@ -102,6 +105,11 @@ for (const tile of DASH_TILES) {
102
105
  DASH_KEYS[tile.key] = tile.action;
103
106
  }
104
107
  const DASH_IDS = Object.keys(DASH_ACTIONS);
108
+ const WORKTREE_ACTIONS = {
109
+ worktreeNew: ['insert'],
110
+ worktreeDrop: ['delete'],
111
+ worktreeBranch: ['w'],
112
+ };
105
113
  const DASH_ALLOWED = [
106
114
  ...Object.keys(DASH_BLOCKS),
107
115
  'back',
@@ -117,17 +125,28 @@ const REPOS_ALLOWED = [
117
125
  'repoDown',
118
126
  'open',
119
127
  'back',
128
+ 'worktreeNew',
129
+ 'worktreeDrop',
120
130
  'quit',
121
131
  'theme',
122
132
  ];
123
133
  const DASH_HIDDEN = ACTION_IDS.filter((id) => id !== 'quit');
124
134
 
125
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',
126
142
  prev: '←',
127
143
  next: '→',
128
144
  drop: 'del',
145
+ worktreeDrop: 'del',
146
+ worktreeBranch: 'w',
129
147
  newBranch: 'ins',
130
148
  newCommit: 'ins',
149
+ worktreeNew: 'ins',
131
150
  feedback: 'q',
132
151
  check: 'space',
133
152
  taskUp: 'ctrl+up',
@@ -145,7 +164,13 @@ const ACTION_HOT = {
145
164
  };
146
165
 
147
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',
148
172
  ...DASH_BLOCKS,
173
+ open: 'enter open',
149
174
  revert: 'drop',
150
175
  check: 'space',
151
176
  taskUp: 'ctrl+up',
@@ -163,6 +188,9 @@ const ACTION_LABEL = {
163
188
  drop: 'delete',
164
189
  newBranch: 'insert',
165
190
  newCommit: 'insert',
191
+ worktreeNew: 'insert',
192
+ worktreeDrop: 'delete',
193
+ worktreeBranch: 'worktree',
166
194
  feedback: 'quote',
167
195
  npmEdit: 'edit',
168
196
  npmNew: 'insert',
@@ -173,6 +201,7 @@ const ACTION_LABEL = {
173
201
  packageWanted: 'wanted',
174
202
  packageLatest: 'latest',
175
203
  npmStop: 'stop',
204
+ npmSolve: 'solve',
176
205
  npmVerbose: 'verbose',
177
206
  npmRerun: 're-run',
178
207
  agentModel: 'model',
@@ -180,7 +209,8 @@ const ACTION_LABEL = {
180
209
  agentFast: 'fast',
181
210
  agentContext: 'context',
182
211
  agentReview: 'plan',
183
- agentContinue: 'continue',
212
+ agentInstall: 'install',
213
+ agentStart: 'start',
184
214
  agentStop: 'stop',
185
215
  agentRerun: 're-run',
186
216
  quit: 'q',
@@ -387,7 +417,12 @@ const keysForActions = (actions) => {
387
417
  const PANE_ONLY = ['branch', 'file', 'diff'];
388
418
  const REVIEW_DROP = { d: 'revert' };
389
419
  const DIFF_KEYS = { ...REVIEW_DROP, q: 'feedback', 'ctrl-l': 'lines' };
390
- const BRANCH_KEYS = keysForActions(BRANCH_ACTIONS);
420
+ const BRANCH_KEYS = {
421
+ ...keysForActions(BRANCH_ACTIONS),
422
+ w: 'worktreeBranch',
423
+ left: 'treeLeft',
424
+ right: 'treeRight',
425
+ };
391
426
  const COMMIT_KEYS = { ...keysForActions(COMMIT_ACTIONS), s: 'push' };
392
427
  const NPM_KEYS = {
393
428
  e: 'npmEdit',
@@ -416,6 +451,8 @@ const PANE_KEYS = {
416
451
  j: 'repoDown',
417
452
  p: 'repoLeft',
418
453
  n: 'repoRight',
454
+ insert: 'worktreeNew',
455
+ delete: 'worktreeDrop',
419
456
  },
420
457
  };
421
458
 
@@ -430,7 +467,8 @@ const KEY_TO_ACTION = {
430
467
  };
431
468
 
432
469
  const actionFromKey = (key, pane) => {
433
- if (LIST_PANES.includes(pane) && (key === 'left' || key === 'right')) {
470
+ const sideways = key === 'left' || key === 'right';
471
+ if (LIST_PANES.includes(pane) && sideways && pane !== 'branches') {
434
472
  return null;
435
473
  }
436
474
  const paneKeys = PANE_KEYS[pane];
@@ -446,6 +484,7 @@ const ACTION_KEYS = {
446
484
  ...NPM_ACTIONS,
447
485
  ...COMMIT_ACTIONS,
448
486
  ...BRANCH_ACTIONS,
487
+ ...WORKTREE_ACTIONS,
449
488
  ...ACTIONS,
450
489
  };
451
490
 
@@ -453,6 +492,17 @@ const actionKeys = (id) => ACTION_KEYS[id] ?? [];
453
492
 
454
493
  const actionLetter = (id) => ACTION_HOT[id] ?? actionKeys(id)[0];
455
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
+
456
506
  const buttonWord = (id) => {
457
507
  const word = ACTION_LABEL[id] ?? ACTION_HOT[id] ?? id;
458
508
  const mark = actionLetter(id);
@@ -489,6 +539,15 @@ const CONFIRM = {
489
539
  escape: 'decline',
490
540
  },
491
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
+ },
492
551
  confirmDrop: {
493
552
  choices: YES_NO,
494
553
  prefix: (view) => ` drop ${view.dropName ?? ''}?`,
@@ -498,6 +557,24 @@ const CONFIRM = {
498
557
  escape: 'cancel',
499
558
  },
500
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
+ },
501
578
  confirmPush: {
502
579
  choices: [{ letter: 'f', label: 'force push', lead: ' ' }],
503
580
  prefix: ' Need force-push?',
@@ -574,6 +651,7 @@ module.exports = {
574
651
  isFixupCommit,
575
652
  actionFromKey,
576
653
  actionLetter,
654
+ actionCaption,
577
655
  buttonWord,
578
656
  CONFIRM,
579
657
  CONFIRM_MODE,
package/lib/input/keys.js CHANGED
@@ -45,10 +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.
48
49
  const CSI_U = {
50
+ 10: 'enter',
51
+ 13: 'enter',
49
52
  127: 'backspace',
50
53
  };
51
54
 
55
+ const MODIFY_OTHER = '27';
56
+
52
57
  const CSI_MAX = 16;
53
58
 
54
59
  const LITERAL_KEY = {
@@ -80,6 +85,12 @@ const modifiedKey = (base, modText) => {
80
85
  const ctrl = (bits & 4) !== 0;
81
86
  const shift = (bits & 1) !== 0;
82
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
+ }
83
94
  if (!arrow && !end) return base;
84
95
  if (ctrl && shift && arrow) return `ctrl-shift-${base}`;
85
96
  if (ctrl && arrow) return `ctrl-${base}`;
@@ -87,8 +98,19 @@ const modifiedKey = (base, modText) => {
87
98
  return base;
88
99
  };
89
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
+
90
111
  const keyFromCsi = (paramsText, final) => {
91
112
  const params = paramsText ? paramsText.split(';') : [];
113
+ if (final === '~' && params[0] === MODIFY_OTHER) return otherModified(params);
92
114
  let base = CSI_KEYS[final];
93
115
  if (final === '~') base = CSI_TILDE[params[0]];
94
116
  if (final === 'u') base = CSI_U[params[0]];
@@ -115,6 +137,7 @@ const mouseKind = (button, press) => {
115
137
  const wheel = MOUSE_WHEEL[button];
116
138
  if (wheel) return wheel;
117
139
  if (!press) return 'release';
140
+ if ((button & 35) === 35) return 'move';
118
141
  return button & 32 ? 'drag' : 'press';
119
142
  };
120
143
 
@@ -197,4 +220,10 @@ const hitAction = (hits, x) => {
197
220
  return null;
198
221
  };
199
222
 
200
- module.exports = { ACTIONS, decodeChunk, hitAction, isTextKey };
223
+ module.exports = {
224
+ ACTIONS,
225
+ decodeChunk,
226
+ hitAction,
227
+ isTextKey,
228
+ isNewlineKey,
229
+ };
@@ -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) => {
@@ -57,6 +57,10 @@ const lineKey = (editor, key, ui, ends) => {
57
57
  copyLine(editor, ui);
58
58
  return 'caret';
59
59
  }
60
+ if (isNewlineKey(key)) {
61
+ editor.insert('\n');
62
+ return 'text';
63
+ }
60
64
  const op = (ends ? LINE_ENDS : LINE_OP)[key];
61
65
  if (!op && !isTextKey(key)) return null;
62
66
  const before = editor.text;