reslop 0.2.6 → 0.2.7

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 (42) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/lib/ansi.js +42 -2
  3. package/lib/api.js +1 -1
  4. package/lib/cli.js +21 -1
  5. package/lib/dashboard/model.js +28 -3
  6. package/lib/dashboard/npm.js +116 -19
  7. package/lib/files.js +13 -7
  8. package/lib/find.js +61 -0
  9. package/lib/git-deps.js +1 -0
  10. package/lib/git-worktree.js +22 -4
  11. package/lib/keys.js +26 -1
  12. package/lib/render/chrome.js +257 -79
  13. package/lib/render/dash-blocks.js +297 -46
  14. package/lib/render/dash-table.js +6 -1
  15. package/lib/render/files.js +23 -4
  16. package/lib/render/frame.js +4 -0
  17. package/lib/render/notes.js +13 -2
  18. package/lib/render/packages.js +93 -0
  19. package/lib/render/primitives.js +13 -6
  20. package/lib/render/repos.js +226 -0
  21. package/lib/render/search.js +25 -0
  22. package/lib/render/tiles.js +107 -12
  23. package/lib/review.js +5 -4
  24. package/lib/session/actions.js +136 -30
  25. package/lib/session/branches.js +3 -5
  26. package/lib/session/commit.js +31 -4
  27. package/lib/session/compose.js +19 -4
  28. package/lib/session/dashboard.js +160 -15
  29. package/lib/session/dispatch.js +38 -4
  30. package/lib/session/files-pane.js +17 -2
  31. package/lib/session/find.js +94 -0
  32. package/lib/session/lifecycle.js +27 -0
  33. package/lib/session/navigation.js +2 -0
  34. package/lib/session/packages.js +143 -0
  35. package/lib/session/pointer.js +14 -0
  36. package/lib/session/tasks.js +3 -3
  37. package/lib/session/view.js +15 -2
  38. package/lib/session/watch.js +5 -0
  39. package/lib/session/workspace.js +455 -0
  40. package/lib/session.js +14 -0
  41. package/lib/workspace.js +166 -0
  42. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased][unreleased]
4
4
 
5
+ ## [0.2.7][] - 2026-10-03
6
+
7
+ - Add tiling view for multi-repository environment
8
+ - Improve navigation and hotkeys
9
+ - Add npm and dependencies screen
10
+ - Rework tasks: regroup into feature, bug, debt, research, and security
11
+ - Show tasks progress
12
+ - Search file paths in the files and diff screens
13
+
5
14
  ## [0.2.6][] - 2026-10-02
6
15
 
7
16
  - Add a live dashboard as the start screen
@@ -163,7 +172,8 @@
163
172
  - Feedback and todos that produce a repair plan for an agent
164
173
  - Intra-line highlighting in unified, mixed, and side-by-side layouts
165
174
 
166
- [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.6...HEAD
175
+ [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.7...HEAD
176
+ [0.2.7]: https://github.com/tshemsedinov/reslop/compare/v0.2.6...v0.2.7
167
177
  [0.2.6]: https://github.com/tshemsedinov/reslop/compare/v0.2.5...v0.2.6
168
178
  [0.2.5]: https://github.com/tshemsedinov/reslop/compare/v0.2.4...v0.2.5
169
179
  [0.2.4]: https://github.com/tshemsedinov/reslop/compare/v0.2.3...v0.2.4
package/lib/ansi.js CHANGED
@@ -75,6 +75,10 @@ const DARK_UI = {
75
75
  warnFg: [0xe8, 0xc8, 0x4a],
76
76
  mutedFg: [0x7a, 0x7a, 0x8a],
77
77
  shaFg: [0x56, 0x9c, 0xd6],
78
+ pkgDepFg: [0xaf, 0xe0, 0xb9],
79
+ pkgDevFg: [0x9a, 0xc4, 0xe6],
80
+ pkgFg: [0xc8, 0xc8, 0xd2],
81
+ pkgChainFg: [0x55, 0x55, 0x62],
78
82
  shaDarkFg: [0x15, 0x4a, 0x8c],
79
83
  stagedDelLineFg: [0xa0, 0xa0, 0xa8],
80
84
  stagedDelLineBg: [0x20, 0x20, 0x26],
@@ -88,10 +92,26 @@ const DARK_UI = {
88
92
  dashTileBg: [0x26, 0x26, 0x2a],
89
93
  dashRowBg: [0x1c, 0x1c, 0x1f],
90
94
  dashHeadBg: [0x70, 0x70, 0x7c],
95
+ dashDimHeadBg: [0x30, 0x30, 0x38],
96
+ dashDimTileBg: [0x12, 0x12, 0x14],
97
+ dashDimRowBg: [0x0c, 0x0c, 0x0e],
91
98
  dashHeadMuted: [0xf2, 0xf2, 0xfa],
92
99
  dashDelFg: [0xff, 0xd2, 0xd2],
93
- dashTrendFg: [0x4a, 0x62, 0x70],
100
+ heatTrack: [0x3a, 0x3c, 0x48],
101
+ heat1: [0x1c, 0x3a, 0x32],
102
+ heat2: [0x2a, 0x5a, 0x38],
103
+ heat3: [0x8a, 0x6a, 0x18],
104
+ heat4: [0xa0, 0x3a, 0x28],
105
+ heatFg: [0xf4, 0xf4, 0xf8],
106
+ heatBlink: [0xf2, 0xe2, 0x8a],
107
+ heatBlinkFg: [0x1a, 0x16, 0x08],
94
108
  dashKeyFg: [0xff, 0xff, 0xff],
109
+ taskHeadFg: [0xff, 0xff, 0xff],
110
+ taskHeadBg: [0x3c, 0x3c, 0x48],
111
+ searchFg: [0x1a, 0x16, 0x08],
112
+ searchBg: [0x8a, 0x6a, 0x18],
113
+ searchOnFg: [0x1a, 0x16, 0x08],
114
+ searchOnBg: [0xf2, 0xe2, 0x8a],
95
115
  };
96
116
 
97
117
  const DARK_CODE = {
@@ -175,6 +195,10 @@ const LIGHT_UI = {
175
195
  warnFg: [0x9a, 0x67, 0x00],
176
196
  mutedFg: [0x6e, 0x77, 0x81],
177
197
  shaFg: [0x05, 0x50, 0xae],
198
+ pkgDepFg: [0x3c, 0xa8, 0x5a],
199
+ pkgDevFg: [0x3d, 0x7e, 0xc8],
200
+ pkgFg: [0x9a, 0xa2, 0xac],
201
+ pkgChainFg: [0x3a, 0x42, 0x4c],
178
202
  shaDarkFg: [0x0b, 0x3d, 0x70],
179
203
  stagedDelLineFg: [0x57, 0x60, 0x6a],
180
204
  stagedDelLineBg: [0xf3, 0xf4, 0xf6],
@@ -188,10 +212,26 @@ const LIGHT_UI = {
188
212
  dashTileBg: [0xe4, 0xe7, 0xeb],
189
213
  dashRowBg: [0xd9, 0xdc, 0xe1],
190
214
  dashHeadBg: [0xe8, 0xeb, 0xef],
215
+ dashDimHeadBg: [0xb0, 0xb6, 0xbe],
216
+ dashDimTileBg: [0xc4, 0xc8, 0xce],
217
+ dashDimRowBg: [0xb0, 0xb4, 0xba],
191
218
  dashHeadMuted: [0x3e, 0x46, 0x52],
192
219
  dashDelFg: [0xd0, 0x3a, 0x44],
193
- dashTrendFg: [0x8a, 0x98, 0xa6],
220
+ heatTrack: [0xc5, 0xca, 0xd3],
221
+ heat1: [0xc8, 0xe6, 0xd4],
222
+ heat2: [0x7d, 0xc8, 0x96],
223
+ heat3: [0xe6, 0xb4, 0x3c],
224
+ heat4: [0xe0, 0x78, 0x68],
225
+ heatFg: [0x1f, 0x23, 0x28],
226
+ heatBlink: [0xff, 0xf3, 0xb0],
227
+ heatBlinkFg: [0x1a, 0x16, 0x08],
194
228
  dashKeyFg: [0x00, 0x00, 0x00],
229
+ taskHeadFg: [0xff, 0xff, 0xff],
230
+ taskHeadBg: [0x6e, 0x77, 0x81],
231
+ searchFg: [0x1a, 0x16, 0x08],
232
+ searchBg: [0xf0, 0xc8, 0x6a],
233
+ searchOnFg: [0x1a, 0x16, 0x08],
234
+ searchOnBg: [0xff, 0xe1, 0x4a],
195
235
  };
196
236
 
197
237
  const LIGHT_CODE = {
package/lib/api.js CHANGED
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
-
2
+ // a
3
3
  const ansi = require('./ansi.js');
4
4
  const diff = require('./diff/diff.js');
5
5
  const git = require('./git.js');
package/lib/cli.js CHANGED
@@ -21,6 +21,8 @@ const ansi = require('./ansi.js');
21
21
  const { setTheme } = ansi;
22
22
  const reportRun = require('./report-run.js');
23
23
  const { runCapture } = reportRun;
24
+ const workspace = require('./workspace.js');
25
+ const { workspaceFromArgs } = workspace;
24
26
 
25
27
  const HEX_REV = /^[0-9a-f]{4,40}$/i;
26
28
  const HEAD_REV = /^HEAD([~^].*)?$/i;
@@ -85,6 +87,20 @@ const resolveScope = async (cwd, paths, repo) => {
85
87
 
86
88
  const localOptions = async (deps, args, selected, cwd) => {
87
89
  const rawRepo = deps.repo ?? createGitRepo();
90
+ const found = workspaceFromArgs(cwd, selected.paths);
91
+ if (found) {
92
+ const capabilities = capabilitiesFor('local', { readOnly: args.readOnly });
93
+ return {
94
+ cwd: found.root,
95
+ repo: attachCapabilities(rawRepo, capabilities),
96
+ paths: [],
97
+ rev: null,
98
+ capabilities,
99
+ startPane: 'repos',
100
+ workspace: found,
101
+ repoName: path.basename(found.root),
102
+ };
103
+ }
88
104
  const scope = await resolveScope(cwd, selected.paths, rawRepo);
89
105
  const kind = scope.rev ? 'commit' : 'local';
90
106
  const capabilities = capabilitiesFor(kind, { readOnly: args.readOnly });
@@ -143,7 +159,7 @@ const loadSession = async (proc, deps, args) => {
143
159
  audit: true,
144
160
  ...extra,
145
161
  });
146
- session.ensureRepo();
162
+ if (session.pane !== 'repos') session.ensureRepo();
147
163
  return session;
148
164
  };
149
165
 
@@ -176,6 +192,10 @@ const run = async (proc, deps = {}) => {
176
192
  }
177
193
  const tty = proc.stdin.isTTY === true && stdout.isTTY === true;
178
194
  if (tty) return runUi(session, proc);
195
+ if (session.pane === 'repos') {
196
+ stderr.write('reslop: interactive terminal required\n');
197
+ return 1;
198
+ }
179
199
  try {
180
200
  await session.loadReady();
181
201
  } catch (error) {
@@ -5,6 +5,7 @@ const { folderOf, extOf } = tree;
5
5
  const files = require('../files.js');
6
6
  const { TASK_KINDS, taskKind } = files;
7
7
 
8
+ const MINUTE_MS = 60_000;
8
9
  const RUNS_SHOWN = 6;
9
10
  const CHAIN_GAP_MS = 2000;
10
11
  const NPM_NAMED = /^npm (?:run|exec)(?:\s+-s)?\s+(\S+)/;
@@ -237,12 +238,25 @@ const versionFields = (pkg, outdatedMap) => {
237
238
  return { current, wanted: hit.wanted, latest: hit.latest };
238
239
  };
239
240
 
241
+ const auditFields = (pkg, auditMap) => {
242
+ const finding = auditMap ? auditMap.get(pkg.name) : null;
243
+ return {
244
+ audit: Boolean(finding),
245
+ fix: finding && finding.fix ? finding.fix : '',
246
+ range: finding && finding.range ? finding.range : '',
247
+ severity: finding && finding.severity ? finding.severity : '',
248
+ title: finding && finding.title ? finding.title : '',
249
+ };
250
+ };
251
+
240
252
  const modulesModel = (modules, auditMap, outdatedMap) => {
241
253
  const packages = modules.packages.map((pkg) => ({
242
254
  name: pkg.name,
243
255
  bytes: pkg.bytes,
244
256
  dev: pkg.dev,
245
- audit: Boolean(auditMap && auditMap.get(pkg.name)),
257
+ transitive: Boolean(pkg.transitive),
258
+ chain: pkg.chain || '',
259
+ ...auditFields(pkg, auditMap),
246
260
  ...versionFields(pkg, outdatedMap),
247
261
  }));
248
262
  return { ...modules, packages };
@@ -266,6 +280,8 @@ const npmModel = (input) => {
266
280
  };
267
281
  };
268
282
 
283
+ const minuteOf = (at) => Math.floor((at || 0) / MINUTE_MS);
284
+
269
285
  const filesModel = (input) => {
270
286
  const index = input.index;
271
287
  const ready = Boolean(index && index.ready);
@@ -288,7 +304,7 @@ const diffsModel = (input) => {
288
304
  dirs: groups.dirs,
289
305
  exts: groups.exts,
290
306
  files: input.entries.length,
291
- samples: input.samples,
307
+ activity: input.activity ?? { minutes: [], live: false },
292
308
  delta: input.diffDelta,
293
309
  };
294
310
  };
@@ -329,4 +345,13 @@ const buildModel = (input) => ({
329
345
  tasks: tasksModel(input),
330
346
  });
331
347
 
332
- module.exports = { groupChanges, mergeRuns, scriptName, runName, buildModel };
348
+ module.exports = {
349
+ MINUTE_MS,
350
+ minuteOf,
351
+ groupChanges,
352
+ mergeRuns,
353
+ scriptName,
354
+ runName,
355
+ npmModel,
356
+ buildModel,
357
+ };
@@ -60,7 +60,28 @@ const rankedPackages = (totals) => {
60
60
  );
61
61
  };
62
62
 
63
- const devOnlyNames = (pkg, lock) => {
63
+ const DIRECT_SECTIONS = [
64
+ 'dependencies',
65
+ 'devDependencies',
66
+ 'optionalDependencies',
67
+ 'peerDependencies',
68
+ ];
69
+ const CHILD_SECTIONS = [
70
+ 'dependencies',
71
+ 'optionalDependencies',
72
+ 'peerDependencies',
73
+ ];
74
+ const CHAIN_MARK = ' 🢒 ';
75
+
76
+ const directNames = (pkg) => {
77
+ const names = new Set();
78
+ for (const section of DIRECT_SECTIONS) {
79
+ for (const name of declaredNames(pkg, section)) names.add(name);
80
+ }
81
+ return names;
82
+ };
83
+
84
+ const devNames = (pkg) => {
64
85
  const keep = new Set([
65
86
  ...declaredNames(pkg, 'dependencies'),
66
87
  ...declaredNames(pkg, 'optionalDependencies'),
@@ -69,16 +90,84 @@ const devOnlyNames = (pkg, lock) => {
69
90
  for (const name of declaredNames(pkg, 'devDependencies')) {
70
91
  if (!keep.has(name)) names.add(name);
71
92
  }
93
+ return names;
94
+ };
95
+
96
+ const entryName = (key) => {
97
+ const at = key.lastIndexOf(MODULE_PREFIX);
98
+ if (at < 0) return '';
99
+ const rest = key.slice(at + MODULE_PREFIX.length);
100
+ if (rest.startsWith('@')) {
101
+ const parts = rest.split('/');
102
+ if (!parts[1]) return '';
103
+ return `${parts[0]}/${parts[1]}`;
104
+ }
105
+ return rest.split('/')[0] || '';
106
+ };
107
+
108
+ const childNames = (entry) => {
109
+ const names = [];
110
+ if (!entry || typeof entry !== 'object') return names;
111
+ for (const section of CHILD_SECTIONS) {
112
+ const map = entry[section];
113
+ if (!map || typeof map !== 'object') continue;
114
+ for (const name of Object.keys(map)) {
115
+ if (!names.includes(name)) names.push(name);
116
+ }
117
+ }
118
+ return names;
119
+ };
120
+
121
+ const dependencyChildren = (lock) => {
122
+ const children = new Map();
72
123
  const packages = lockPackages(lock);
73
- if (!packages) return names;
124
+ if (!packages) return children;
74
125
  for (const key of Object.keys(packages)) {
75
- const entry = packages[key];
76
- if (!entry || !entry.dev || !key.startsWith(MODULE_PREFIX)) continue;
77
- const name = key.slice(MODULE_PREFIX.length);
78
- if (!name || name.includes('/node_modules/') || keep.has(name)) continue;
79
- names.add(name);
126
+ if (!key) continue;
127
+ const name = entryName(key);
128
+ if (!name) continue;
129
+ const list = children.get(name) ?? [];
130
+ for (const child of childNames(packages[key])) {
131
+ if (!list.includes(child)) list.push(child);
132
+ }
133
+ if (list.length) children.set(name, list);
80
134
  }
81
- return names;
135
+ return children;
136
+ };
137
+
138
+ const parentChain = (name, declared, children) => {
139
+ if (declared.has(name)) return '';
140
+ const prev = new Map();
141
+ const queue = [];
142
+ const roots = [...declared].sort();
143
+ for (const root of roots) {
144
+ if (prev.has(root)) continue;
145
+ prev.set(root, '');
146
+ queue.push(root);
147
+ }
148
+ let head = 0;
149
+ while (head < queue.length) {
150
+ const at = queue[head];
151
+ head += 1;
152
+ const kids = children.get(at);
153
+ if (!kids) continue;
154
+ for (const kid of kids) {
155
+ if (prev.has(kid)) continue;
156
+ prev.set(kid, at);
157
+ if (kid !== name) {
158
+ queue.push(kid);
159
+ continue;
160
+ }
161
+ const path = [];
162
+ let cursor = at;
163
+ while (cursor) {
164
+ path.push(cursor);
165
+ cursor = prev.get(cursor) || '';
166
+ }
167
+ return path.join(CHAIN_MARK);
168
+ }
169
+ }
170
+ return '';
82
171
  };
83
172
 
84
173
  const lockVersion = (lock, name) => {
@@ -89,16 +178,24 @@ const lockVersion = (lock, name) => {
89
178
  };
90
179
 
91
180
  const topPackages = (ranked, pkg, lock) => {
92
- const devNames = devOnlyNames(pkg, lock);
93
- const rows = ranked.map((item) => ({
94
- name: item.name,
95
- bytes: item.bytes,
96
- dev: devNames.has(item.name),
97
- version: lockVersion(lock, item.name),
98
- }));
99
- const runtime = rows.filter((row) => !row.dev);
100
- const devs = rows.filter((row) => row.dev);
101
- return [...runtime, ...devs].slice(0, TOP_PACKAGES);
181
+ const declared = directNames(pkg);
182
+ const devs = devNames(pkg);
183
+ const children = dependencyChildren(lock);
184
+ const rows = ranked.map((item) => {
185
+ const direct = declared.has(item.name);
186
+ return {
187
+ name: item.name,
188
+ bytes: item.bytes,
189
+ dev: direct && devs.has(item.name),
190
+ transitive: !direct,
191
+ chain: parentChain(item.name, declared, children),
192
+ version: lockVersion(lock, item.name),
193
+ };
194
+ });
195
+ const runtime = rows.filter((row) => !row.dev && !row.transitive);
196
+ const development = rows.filter((row) => row.dev);
197
+ const nested = rows.filter((row) => row.transitive);
198
+ return [...runtime, ...development, ...nested];
102
199
  };
103
200
 
104
201
  const measureModules = async (root) => {
@@ -190,4 +287,4 @@ const readNpmSummary = async (top, previous) => {
190
287
  };
191
288
  };
192
289
 
193
- module.exports = { readNpmSummary };
290
+ module.exports = { TOP_PACKAGES, readNpmSummary };
package/lib/files.js CHANGED
@@ -6,19 +6,24 @@ const { listFilePath } = manifest;
6
6
  const REVIEW_DIR = '.review';
7
7
  const ORIGIN_ORDER = ['staged', 'unstaged', 'untracked', 'commit', 'pr'];
8
8
  const TASKS_FILE = 'TODOs';
9
- const REPO_TASKS_LABEL =
10
- 'Project Tasks: Backlog, Issues, Bug Reports, Feature Requests';
9
+ const REPO_TASKS_LABEL = [
10
+ 'Project Tasks: Feature requests, Bug reports,',
11
+ 'Technical debt, Research, Security',
12
+ ].join(' ');
11
13
  const TASK_KINDS = [
12
- { id: 'backlog', title: 'Backlog' },
13
- { id: 'issues', title: 'Issues' },
14
- { id: 'bugs', title: 'Bug Reports' },
15
- { id: 'features', title: 'Feature Requests' },
14
+ { id: 'features', title: 'Feature requests' },
15
+ { id: 'bugs', title: 'Bug reports' },
16
+ { id: 'debt', title: 'Technical debt' },
17
+ { id: 'research', title: 'Research' },
18
+ { id: 'security', title: 'Security' },
16
19
  ];
17
20
  const TASK_KIND_IDS = new Set(TASK_KINDS.map((kind) => kind.id));
21
+ const TASK_KIND_ALIAS = { backlog: 'debt', issues: 'debt' };
18
22
 
19
23
  const taskKind = (todo) => {
20
24
  const kind = todo && todo.kind;
21
- return TASK_KIND_IDS.has(kind) ? kind : 'backlog';
25
+ if (TASK_KIND_IDS.has(kind)) return kind;
26
+ return TASK_KIND_ALIAS[kind] ?? 'debt';
22
27
  };
23
28
 
24
29
  const taskTitle = (id) => {
@@ -236,6 +241,7 @@ module.exports = {
236
241
  isTotalEntry,
237
242
  makeTaskItem,
238
243
  fileStatus,
244
+ lineDelta,
239
245
  fileEntries,
240
246
  fileTotals,
241
247
  relativeAge,
package/lib/find.js ADDED
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const matchesIn = (text, query) => {
4
+ const spans = [];
5
+ if (!query || !text) return spans;
6
+ const hay = text.toLowerCase();
7
+ const needle = query.toLowerCase();
8
+ const step = needle.length;
9
+ let from = 0;
10
+ while (from <= hay.length - step) {
11
+ const at = hay.indexOf(needle, from);
12
+ if (at < 0) break;
13
+ spans.push({ start: at, end: at + step });
14
+ from = at + step;
15
+ }
16
+ return spans;
17
+ };
18
+
19
+ const fileMatches = (entries, query) => {
20
+ const hits = [];
21
+ if (!query) return hits;
22
+ for (let index = 0; index < entries.length; index++) {
23
+ const entry = entries[index];
24
+ const path = entry && entry.path ? entry.path : '';
25
+ if (!path) continue;
26
+ for (const span of matchesIn(path, query)) {
27
+ hits.push({ index, path, start: span.start, end: span.end });
28
+ }
29
+ }
30
+ return hits;
31
+ };
32
+
33
+ const activeHit = (entries, find) => {
34
+ if (!find || !find.query) return null;
35
+ const hits = fileMatches(entries, find.query);
36
+ if (!hits.length) return null;
37
+ const count = hits.length;
38
+ let at = find.at % count;
39
+ if (at < 0) at += count;
40
+ return hits[at];
41
+ };
42
+
43
+ const keptPrefix = (shown, full) => {
44
+ const mark = shown.endsWith('…') && shown !== full ? 1 : 0;
45
+ const keep = shown.length - mark;
46
+ if (!full.startsWith(shown.slice(0, keep))) return -1;
47
+ return keep;
48
+ };
49
+
50
+ const spansFor = (shown, full, query, hit) => {
51
+ const spans = matchesIn(shown, query);
52
+ if (!hit || hit.path !== full) return spans;
53
+ const keep = keptPrefix(shown, full);
54
+ if (keep < 0 || hit.start >= keep) return spans;
55
+ return spans.map((span) => ({
56
+ ...span,
57
+ on: span.start === hit.start,
58
+ }));
59
+ };
60
+
61
+ module.exports = { matchesIn, fileMatches, activeHit, spansFor };
package/lib/git-deps.js CHANGED
@@ -346,6 +346,7 @@ const revertFile = (top, rel, items) => {
346
346
  };
347
347
 
348
348
  module.exports = {
349
+ collectReportAsync,
349
350
  finishLoad,
350
351
  loadExtras,
351
352
  captureFile,
@@ -8,7 +8,7 @@ const { runProc, runProcSync, isAbort, oneLine } = utilities;
8
8
  const diff = require('./diff/diff.js');
9
9
  const { parseDiff, synthesizeNewFile, itemsFromFiles } = diff;
10
10
  const files = require('./files.js');
11
- const { itemPath, isReadOnlyOrigin, REVIEW_DIR } = files;
11
+ const { itemPath, isReadOnlyOrigin, lineDelta, REVIEW_DIR } = files;
12
12
  const edit = require('./diff/edit.js');
13
13
  const { replaceBlockAdds } = edit;
14
14
 
@@ -119,6 +119,8 @@ const untrackedArgs = (paths) => [
119
119
 
120
120
  const isReviewFile = (rel) => rel.split(/[/\\]/)[0] === REVIEW_DIR;
121
121
 
122
+ const isDirEntry = (rel) => rel.endsWith('/');
123
+
122
124
  const dropReviewItems = (items) =>
123
125
  items.filter((item) => !isReviewFile(itemPath(item)));
124
126
 
@@ -132,7 +134,9 @@ const listScopeFiles = (top, paths = [], options = {}) => {
132
134
  ? ['ls-tree', '-r', '--name-only', '-z', rev, ...pathArgs(paths)]
133
135
  : ['ls-files', '-co', '--exclude-standard', '-z', ...pathArgs(paths)];
134
136
  const result = requireOk(runGit(args, top), 'ls-files failed');
135
- const names = namesFromLs(result.stdout).filter((rel) => !isReviewFile(rel));
137
+ const names = namesFromLs(result.stdout).filter(
138
+ (rel) => !isReviewFile(rel) && !isDirEntry(rel),
139
+ );
136
140
  names.sort((left, right) => left.localeCompare(right, 'en'));
137
141
  return names;
138
142
  };
@@ -150,12 +154,12 @@ const binaryNewFile = (rel) => ({
150
154
  const untrackedItemsFrom = (top, names) => {
151
155
  const files = [];
152
156
  for (const rel of names) {
153
- if (isReviewFile(rel)) continue;
157
+ if (isReviewFile(rel) || isDirEntry(rel)) continue;
154
158
  let buf;
155
159
  try {
156
160
  buf = fs.readFileSync(path.join(top, rel));
157
161
  } catch (error) {
158
- if (error.code === 'ENOENT') continue;
162
+ if (error.code === 'ENOENT' || error.code === 'EISDIR') continue;
159
163
  throw error;
160
164
  }
161
165
  if (isBinaryBuffer(buf)) files.push(binaryNewFile(rel));
@@ -164,6 +168,19 @@ const untrackedItemsFrom = (top, names) => {
164
168
  return itemsFromFiles(files, 'untracked');
165
169
  };
166
170
 
171
+ const countUntrackedAdded = async (top) => {
172
+ try {
173
+ const result = await runGitAsync(untrackedArgs([]), top);
174
+ if (result.status !== 0) return 0;
175
+ const items = untrackedItemsFrom(top, namesFromLs(result.stdout));
176
+ let added = 0;
177
+ for (const item of items) added += lineDelta(item).added;
178
+ return added;
179
+ } catch {
180
+ return 0;
181
+ }
182
+ };
183
+
167
184
  const groupByPath = (items) => {
168
185
  const byPath = new Map();
169
186
  for (const item of items) {
@@ -424,6 +441,7 @@ module.exports = {
424
441
  resolveRev,
425
442
  currentBranch,
426
443
  fileText,
444
+ countUntrackedAdded,
427
445
  listScopeFiles,
428
446
  readSnapshot,
429
447
  readSnapshotAsync,
package/lib/keys.js CHANGED
@@ -8,6 +8,7 @@ const { DIFF_DISABLED, FILES_GIT_DISABLED, FILES_HIDDEN } = actions;
8
8
  const { TASKS_DISABLED, BRANCHES_DISABLED } = actions;
9
9
  const { COMMITS_DISABLED, UNIT_DISABLED, NPM_DISABLED } = actions;
10
10
  const { diffGitDisabled, branchGitDisabled, commitGitDisabled } = actions;
11
+ const { entryHasDiff, lineHasDiff, withoutMoves } = actions;
11
12
  const { actionLetter, buttonWord } = actions;
12
13
  const { CONFIRM, confirmPrefix, promptFromChoices } = actions;
13
14
 
@@ -20,14 +21,35 @@ const CSI_KEYS = {
20
21
  F: 'end',
21
22
  };
22
23
 
24
+ const SS3_KEYS = {
25
+ ...CSI_KEYS,
26
+ P: 'f1',
27
+ Q: 'f2',
28
+ R: 'f3',
29
+ S: 'f4',
30
+ };
31
+
23
32
  const CSI_TILDE = {
24
33
  1: 'home',
34
+ 2: 'insert',
25
35
  3: 'delete',
26
36
  4: 'end',
27
37
  5: 'pageUp',
28
38
  6: 'pageDown',
29
39
  7: 'home',
30
40
  8: 'end',
41
+ 11: 'f1',
42
+ 12: 'f2',
43
+ 13: 'f3',
44
+ 14: 'f4',
45
+ 15: 'f5',
46
+ 17: 'f6',
47
+ 18: 'f7',
48
+ 19: 'f8',
49
+ 20: 'f9',
50
+ 21: 'f10',
51
+ 23: 'f11',
52
+ 24: 'f12',
31
53
  };
32
54
 
33
55
  const CSI_MAX = 16;
@@ -121,7 +143,7 @@ const parseEsc = (text, start) => {
121
143
  if (rest.startsWith('\x1b[')) return parseCsi(rest, start);
122
144
  if (rest.startsWith('\x1bO')) {
123
145
  if (rest.length < 3) return NEED_MORE;
124
- const key = CSI_KEYS[rest[2]];
146
+ const key = SS3_KEYS[rest[2]];
125
147
  return key ? keyAt(key, start + 3) : keyAt('escape', start + 1);
126
148
  }
127
149
  return keyAt('escape', start + 1);
@@ -184,6 +206,9 @@ module.exports = {
184
206
  NPM_DISABLED,
185
207
  UNIT_DISABLED,
186
208
  diffGitDisabled,
209
+ entryHasDiff,
210
+ lineHasDiff,
211
+ withoutMoves,
187
212
  branchGitDisabled,
188
213
  commitGitDisabled,
189
214
  decodeChunk,