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
@@ -17,7 +17,8 @@ const fill = (width, fgRgb, bgRgb, color) =>
17
17
 
18
18
  const markPrefix = (selected) => ` ${selected ? FILE_MARK : ' '} `;
19
19
 
20
- const isListPane = (pane) => LIST_PANES.includes(pane) || pane === 'dashboard';
20
+ const isListPane = (pane) =>
21
+ LIST_PANES.includes(pane) || pane === 'dashboard' || pane === 'repos';
21
22
 
22
23
  const isTaskView = (view) => view.item?.origin === 'task';
23
24
 
@@ -111,11 +112,19 @@ const paintListWindow = (count, focus, pane, lineAt) => {
111
112
  return paneResult({ body, fileHits, editHits, cursor, listScroll: start });
112
113
  };
113
114
 
115
+ const insertedAt = (extra, count) => {
116
+ if (extra.at === 'start') return 0;
117
+ if (extra.at !== 'index') return count;
118
+ const index = extra.index ?? count;
119
+ return Math.max(0, Math.min(index, count));
120
+ };
121
+
114
122
  const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
115
123
  const lineAt = (index) => {
116
124
  const entry = items[index];
117
125
  if (!entry) return null;
118
- return listLine(paintRow(entry, !extra && index === cursor), index);
126
+ const selected = !extra && index === cursor;
127
+ return listLine(paintRow(entry, selected, index), index);
119
128
  };
120
129
  if (!extra) return paintListWindow(items.length, cursor, pane, lineAt);
121
130
  const edit = listLine(extra.row, -1, extra.cursor, extra.edits);
@@ -124,10 +133,8 @@ const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
124
133
  const replaced = (i) => (i === at ? edit : lineAt(i));
125
134
  return paintListWindow(items.length, at, pane, replaced);
126
135
  }
127
- const first = extra.at === 'start';
128
- const at = first ? 0 : items.length;
129
- const shift = first ? 1 : 0;
130
- const inserted = (i) => (i === at ? edit : lineAt(i - shift));
136
+ const at = insertedAt(extra, items.length);
137
+ const inserted = (i) => (i === at ? edit : lineAt(i < at ? i : i - 1));
131
138
  return paintListWindow(items.length + 1, at, pane, inserted);
132
139
  };
133
140
 
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('../ansi.js');
4
+ const { THEME } = ansi;
5
+ const primitives = require('./primitives.js');
6
+ const { paintBodyFill, paneResult } = primitives;
7
+ const tiles = require('./tiles.js');
8
+ const { seg, spinner, equalGrid, pageWindow, placeEqualTiles } = tiles;
9
+ const { paintTile, composeBands, tileHits, pageFill } = tiles;
10
+ const { segmentsWidth, clipSegments } = tiles;
11
+ const table = require('./dash-table.js');
12
+ const { size, isHot } = table;
13
+
14
+ const REPO_MIN_H = 6;
15
+ const MARGIN_MIN_H = 12;
16
+ const EXT_LIMIT = 3;
17
+ const DIM_BACKS = {
18
+ head: THEME.dashDimHeadBg,
19
+ tile: THEME.dashDimTileBg,
20
+ row: THEME.dashDimRowBg,
21
+ };
22
+ const RUN_ICON = {
23
+ passed: ['✔', 'add'],
24
+ failed: ['✖', 'error'],
25
+ aborted: ['⊘', 'muted'],
26
+ };
27
+
28
+ const extName = (key) => {
29
+ const text = `${key ?? ''}`;
30
+ if (!text || text === '(none)') return 'none';
31
+ return text.startsWith('.') ? text.slice(1) : text;
32
+ };
33
+
34
+ const blinkTone = (frame) => (Math.abs(frame) % 2 ? 'hot' : 'warn');
35
+
36
+ const spin = (frame) => seg(spinner(frame), 'warn', true);
37
+
38
+ const rowPending = (row) =>
39
+ !row.filesReady ||
40
+ !row.commitsReady ||
41
+ !row.diffReady ||
42
+ !row.tasksReady ||
43
+ !row.runsReady ||
44
+ !row.npmReady;
45
+
46
+ const fitAside = (left, right, width) => {
47
+ const rightW = segmentsWidth(right);
48
+ const room = Math.max(0, width - rightW - 1);
49
+ const clipped = clipSegments(left, room);
50
+ const gap = Math.max(1, width - segmentsWidth(clipped) - rightW);
51
+ return [...clipped, seg(' '.repeat(gap)), ...right];
52
+ };
53
+
54
+ const titleLine = (row, ctx, selected, width) => {
55
+ const hot = isHot(row.activityAt, ctx.now);
56
+ const tone = hot ? blinkTone(ctx.frame) : 'text';
57
+ const left = [seg(row.name, tone, selected || hot)];
58
+ if (hot) left.push(seg(' ●', tone, true));
59
+ const right = [];
60
+ if (rowPending(row)) right.push(spin(ctx.frame), seg(' '));
61
+ const label = row.filesReady ? size(row.bytes) : '…';
62
+ const sizeTone = row.filesReady ? 'text' : 'muted';
63
+ right.push(seg(label, sizeTone, row.filesReady));
64
+ return fitAside(left, right, width);
65
+ };
66
+
67
+ const extLine = (row, ctx) => {
68
+ if (!row.filesReady) return [spin(ctx.frame)];
69
+ const exts = row.exts.slice(0, EXT_LIMIT);
70
+ if (!exts.length) return [seg('no files', 'muted')];
71
+ const segs = [];
72
+ for (let i = 0; i < exts.length; i++) {
73
+ if (i) segs.push(seg(' '));
74
+ segs.push(seg(extName(exts[i].key), 'sha'));
75
+ segs.push(seg(` ${size(exts[i].bytes)}`, 'text'));
76
+ }
77
+ return segs;
78
+ };
79
+
80
+ const countValue = (ready, text, frame) =>
81
+ ready ? seg(text, 'text', true) : spin(frame);
82
+
83
+ const countLine = (row, ctx) => [
84
+ seg('tasks: ', 'muted'),
85
+ countValue(row.tasksReady, `${row.tasksDone}/${row.tasks}`, ctx.frame),
86
+ seg(' commits: ', 'muted'),
87
+ countValue(row.commitsReady, `${row.commits}`, ctx.frame),
88
+ ];
89
+
90
+ const resultNote = (run) => {
91
+ const result = run.result;
92
+ if (!result) return [];
93
+ if (typeof result.tests === 'number') {
94
+ const note = [seg(` ${result.tests}`, 'muted')];
95
+ if (result.failed) note.push(seg(` ${result.failed} fail`, 'error'));
96
+ return note;
97
+ }
98
+ if (typeof result.errors === 'number') {
99
+ return [seg(` ${result.errors} err`, result.errors ? 'error' : 'muted')];
100
+ }
101
+ return [];
102
+ };
103
+
104
+ const runLine = (row, ctx) => {
105
+ if (!row.runsReady) return [seg('run: ', 'muted'), spin(ctx.frame)];
106
+ const run = row.run;
107
+ if (!run) return [seg('run: ', 'muted'), seg('idle', 'muted')];
108
+ if (run.status === 'running') {
109
+ const seen = run.done + run.failed;
110
+ const total = run.expected > 0 ? `/${run.expected}` : '';
111
+ const fail = run.failed ? ` ${run.failed} fail` : '';
112
+ return [
113
+ seg(`${spinner(ctx.frame)} `, 'warn', true),
114
+ seg(run.name || 'run', 'text'),
115
+ seg(` ${seen}${total}${fail}`, 'warn'),
116
+ ];
117
+ }
118
+ const icon = RUN_ICON[run.status] ?? ['?', 'muted'];
119
+ return [
120
+ seg(`${icon[0]} `, icon[1], true),
121
+ seg(run.name || 'run', 'text'),
122
+ seg(` ${run.status}`, icon[1]),
123
+ ...resultNote(run),
124
+ ];
125
+ };
126
+
127
+ const npmLine = (row, ctx) => {
128
+ if (!row.npmReady) return [seg('deps: ', 'muted'), spin(ctx.frame)];
129
+ const npm = row.npm;
130
+ if (!npm || !npm.hasManifest) return [seg('no package.json', 'muted')];
131
+ const modules = npm.modules || { count: 0, bytes: 0 };
132
+ const segs = [
133
+ seg('deps: ', 'muted'),
134
+ seg(`${npm.deps}`, 'text'),
135
+ seg(' dev: ', 'muted'),
136
+ seg(`${npm.dev}`, 'text'),
137
+ seg(' all: ', 'muted'),
138
+ seg(`${modules.count}`, 'text'),
139
+ seg(' (', 'muted'),
140
+ seg(size(modules.bytes), 'text'),
141
+ seg(')', 'muted'),
142
+ ];
143
+ if (npm.audit) {
144
+ segs.push(
145
+ seg(' '),
146
+ seg('🚨 ', 'error'),
147
+ seg(`${npm.audit}`, 'error', true),
148
+ );
149
+ }
150
+ if (npm.outdated) {
151
+ segs.push(
152
+ seg(' '),
153
+ seg('⚠️ ', 'warn'),
154
+ seg(`${npm.outdated}`, 'warn', true),
155
+ );
156
+ }
157
+ return segs;
158
+ };
159
+
160
+ const diffLine = (row, ctx) => {
161
+ if (!row.diffReady) return [seg('diff: ', 'muted'), spin(ctx.frame)];
162
+ const diff = row.diff;
163
+ const added = diff.stagedAdded + diff.unstagedAdded;
164
+ const removed = diff.stagedRemoved + diff.unstagedRemoved;
165
+ return [
166
+ seg('diff: ', 'muted'),
167
+ seg(`+${diff.stagedAdded}/${added}`, 'add'),
168
+ seg(' '),
169
+ seg(`-${diff.stagedRemoved}/${removed}`, 'del'),
170
+ ];
171
+ };
172
+
173
+ const repoContent = (row, ctx, selected, width) => ({
174
+ titleLine: titleLine(row, ctx, selected, width),
175
+ lines: [
176
+ extLine(row, ctx),
177
+ countLine(row, ctx),
178
+ runLine(row, ctx),
179
+ diffLine(row, ctx),
180
+ npmLine(row, ctx),
181
+ ],
182
+ });
183
+
184
+ const paintRepos = (model, bands, color) => {
185
+ const painted = new Map();
186
+ const ctx = { now: model.now, frame: model.frame };
187
+ const selected = model.tiles[model.cursor];
188
+ const selectedId = selected ? selected.id : '';
189
+ for (const band of bands) {
190
+ for (const place of band.tiles) {
191
+ const row = model.tiles.find((tile) => tile.id === place.id);
192
+ if (!row) continue;
193
+ const tile = { id: row.id, key: '', title: row.name };
194
+ const inner = Math.max(0, place.w - 2);
195
+ const picked = row.id === selectedId;
196
+ const content = repoContent(row, ctx, picked, inner);
197
+ const backs = picked ? null : DIM_BACKS;
198
+ const rows = paintTile(tile, content, place.w, band.h, color, backs);
199
+ painted.set(place.id, rows);
200
+ }
201
+ }
202
+ return painted;
203
+ };
204
+
205
+ const paintBodyRepos = (view, width, color, bodyH, headerLines) => {
206
+ const model = view.repos;
207
+ if (!model || !model.tiles.length) {
208
+ const blank = paintBodyFill(width, color, 'files');
209
+ return paneResult({ body: new Array(bodyH).fill(blank) });
210
+ }
211
+ const margin = bodyH >= MARGIN_MIN_H ? 1 : 0;
212
+ const height = Math.max(1, bodyH - margin * 2);
213
+ const grid = equalGrid(width, height, model.tiles.length, 34, REPO_MIN_H);
214
+ const page = pageWindow(model.tiles.length, model.cursor, grid.cap);
215
+ const shown = model.tiles.slice(page.start, page.end);
216
+ const bands = placeEqualTiles(shown, grid);
217
+ model.cols = grid.cols;
218
+ const painted = paintRepos(model, bands, color);
219
+ const rows = composeBands(bands, painted, width, height, color);
220
+ const pad = new Array(margin).fill(pageFill(width, color));
221
+ const body = [...pad, ...rows, ...pad];
222
+ const fileHits = tileHits(bands, headerLines + margin);
223
+ return paneResult({ body, fileHits });
224
+ };
225
+
226
+ module.exports = { REPO_MIN_H, extName, paintBodyRepos };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('../ansi.js');
4
+ const { THEME, paint } = ansi;
5
+
6
+ const paintPathText = (text, fg, bg, color, bold, spans) => {
7
+ if (!spans.length) return paint(text, fg, bg, color, bold);
8
+ let out = '';
9
+ let at = 0;
10
+ for (const span of spans) {
11
+ if (span.start > at) {
12
+ out += paint(text.slice(at, span.start), fg, bg, color, bold);
13
+ }
14
+ const on = span.on === true;
15
+ const markFg = on ? THEME.searchOnFg : THEME.searchFg;
16
+ const markBg = on ? THEME.searchOnBg : THEME.searchBg;
17
+ const slice = text.slice(span.start, span.end);
18
+ out += paint(slice, markFg, markBg, color, true);
19
+ at = span.end;
20
+ }
21
+ if (at < text.length) out += paint(text.slice(at), fg, bg, color, bold);
22
+ return out;
23
+ };
24
+
25
+ module.exports = { paintPathText };
@@ -19,6 +19,7 @@ const seg = (text, tone = 'text', bold = false) => ({
19
19
  text: `${text}`,
20
20
  tone,
21
21
  bold,
22
+ bg: null,
22
23
  });
23
24
 
24
25
  const toneColors = () => ({
@@ -30,7 +31,12 @@ const toneColors = () => ({
30
31
  warn: THEME.warnFg,
31
32
  error: THEME.errorFg,
32
33
  sha: THEME.shaFg,
33
- trend: THEME.dashTrendFg,
34
+ dep: THEME.pkgDepFg,
35
+ dev: THEME.pkgDevFg,
36
+ pkg: THEME.pkgFg,
37
+ chain: THEME.pkgChainFg,
38
+ heat: THEME.heatFg,
39
+ blink: THEME.heatBlinkFg,
34
40
  key: THEME.dashKeyFg,
35
41
  });
36
42
 
@@ -78,7 +84,8 @@ const paintSegments = (segs, width, bg, color, fill = ' ', colors = null) => {
78
84
  }
79
85
  let out = '';
80
86
  for (const item of clipped) {
81
- out += paint(item.text, ink[item.tone], bg, color, item.bold);
87
+ const pieceBg = item.bg ?? bg;
88
+ out += paint(item.text, ink[item.tone], pieceBg, color, item.bold);
82
89
  }
83
90
  return out + paint(fill.repeat(pad), ink.muted, bg, color);
84
91
  };
@@ -173,9 +180,15 @@ const paintFramed = (line, width, bg, color, fill = ' ', colors = null) => {
173
180
  return edge + paintSegments(line, inner, bg, color, fill, colors) + edge;
174
181
  };
175
182
 
176
- const paintHead = (tile, content, width, color) => {
183
+ const paintFlushRight = (line, width, bg, color) => {
184
+ const edge = paintSegments([seg(' ')], 1, bg, color);
185
+ const inner = Math.max(0, width - 1);
186
+ return edge + paintSegments(line, inner, bg, color);
187
+ };
188
+
189
+ const paintHead = (tile, content, width, color, headBg) => {
177
190
  const fill = color ? ' ' : RULE;
178
- const bg = THEME.dashHeadBg;
191
+ const bg = headBg;
179
192
  const colors = headColors();
180
193
  if (content.titleLine) {
181
194
  return paintFramed(content.titleLine, width, bg, color, fill, colors);
@@ -184,22 +197,28 @@ const paintHead = (tile, content, width, color) => {
184
197
  return paintSegments(segs, width, bg, color, fill, colors);
185
198
  };
186
199
 
187
- const rowBackground = (index, filled) => {
188
- if (filled && index % 2 === 1) return THEME.dashRowBg;
189
- return THEME.dashTileBg;
200
+ const rowBackground = (index, filled, backs) => {
201
+ if (filled && index % 2 === 1) return backs.row;
202
+ return backs.tile;
190
203
  };
191
204
 
192
- const paintTile = (tile, content, width, height, color) => {
205
+ const paintTile = (tile, content, width, height, color, backs = null) => {
206
+ const shade = backs ?? {
207
+ head: THEME.dashHeadBg,
208
+ tile: THEME.dashTileBg,
209
+ row: THEME.dashRowBg,
210
+ };
193
211
  const { lines, footer } = content;
194
- const rows = [paintHead(tile, content, width, color)];
212
+ const rows = [paintHead(tile, content, width, color, shade.head)];
195
213
  const bodyRoom = footer ? height - 2 : height - 1;
196
214
  for (let i = 0; i < bodyRoom; i++) {
197
- const bg = rowBackground(i, i < lines.length);
215
+ const bg = rowBackground(i, i < lines.length, shade);
198
216
  rows.push(paintFramed(lines[i] ?? [], width, bg, color));
199
217
  }
200
218
  if (footer && height > 1) {
201
- const bg = rowBackground(bodyRoom, true);
202
- rows.push(paintFramed(footer, width, bg, color));
219
+ const bg = rowBackground(bodyRoom, true, shade);
220
+ const paintRow = content.footerFlush ? paintFlushRight : paintFramed;
221
+ rows.push(paintRow(footer, width, bg, color));
203
222
  }
204
223
  return rows;
205
224
  };
@@ -229,6 +248,78 @@ const composeBands = (bands, painted, width, height, color) => {
229
248
  return rows.slice(0, height);
230
249
  };
231
250
 
251
+ const gridSize = (width, height, cols, rows) => ({
252
+ tileW: Math.floor((width - (cols - 1) * GAP_X) / cols),
253
+ tileH: Math.floor((height - (rows - 1) * GAP_Y) / rows),
254
+ });
255
+
256
+ const equalGrid = (width, height, count, minW = MIN_W, minH = MIN_H) => {
257
+ const safe = Math.max(1, count);
258
+ const colCap = Math.max(
259
+ 1,
260
+ Math.min(safe, Math.floor((width + GAP_X) / (minW + GAP_X)) || 1),
261
+ );
262
+ const rowCap = Math.max(
263
+ 1,
264
+ Math.min(safe, Math.floor((height + GAP_Y) / (minH + GAP_Y)) || 1),
265
+ );
266
+ let best = null;
267
+ for (let cols = 1; cols <= colCap; cols++) {
268
+ for (let rows = 1; rows <= rowCap; rows++) {
269
+ const size = gridSize(width, height, cols, rows);
270
+ if (size.tileW < minW || size.tileH < minH) continue;
271
+ const cap = cols * rows;
272
+ const shown = Math.min(safe, cap);
273
+ const score = shown * 1000000 + size.tileW * size.tileH;
274
+ if (!best || score > best.score) {
275
+ best = { cols, rows, cap, ...size, score };
276
+ }
277
+ }
278
+ }
279
+ if (best) return best;
280
+ return {
281
+ cols: 1,
282
+ rows: 1,
283
+ cap: 1,
284
+ tileW: Math.max(1, width),
285
+ tileH: Math.max(1, height),
286
+ score: 0,
287
+ };
288
+ };
289
+
290
+ const pageWindow = (count, cursor, cap) => {
291
+ const room = Math.max(1, cap);
292
+ if (count <= room) return { start: 0, end: count };
293
+ const at = Math.min(Math.max(0, cursor), count - 1);
294
+ let start = Math.floor(at / room) * room;
295
+ if (start + room > count) start = count - room;
296
+ return { start, end: start + room };
297
+ };
298
+
299
+ const placeEqualTiles = (tiles, grid) => {
300
+ const bands = [];
301
+ const cols = Math.max(1, grid.cols);
302
+ const rows = Math.ceil(tiles.length / cols);
303
+ for (let row = 0; row < rows; row++) {
304
+ const slice = tiles.slice(row * cols, (row + 1) * cols);
305
+ if (!slice.length) break;
306
+ const placed = [];
307
+ for (let i = 0; i < slice.length; i++) {
308
+ placed.push({
309
+ id: slice[i].id,
310
+ x: i * (grid.tileW + GAP_X),
311
+ w: grid.tileW,
312
+ });
313
+ }
314
+ bands.push({
315
+ y: row * (grid.tileH + GAP_Y),
316
+ h: grid.tileH,
317
+ tiles: placed,
318
+ });
319
+ }
320
+ return bands;
321
+ };
322
+
232
323
  const tileHits = (bands, headerLines) => {
233
324
  const hits = [];
234
325
  for (const band of bands) {
@@ -253,6 +344,10 @@ module.exports = {
253
344
  spinner,
254
345
  titleSegments,
255
346
  layoutTiles,
347
+ equalGrid,
348
+ pageWindow,
349
+ placeEqualTiles,
350
+ paintSegments,
256
351
  paintTile,
257
352
  composeBands,
258
353
  tileHits,
package/lib/review.js CHANGED
@@ -75,7 +75,7 @@ const setCode = (store, key, note) => {
75
75
  putNote(store, store.code, key, note, Boolean(note.clear));
76
76
  };
77
77
 
78
- const addTask = (store, file, text = '', done = false, kind = 'backlog') => {
78
+ const addTask = (store, file, text = '', done = false, kind = 'features') => {
79
79
  const id = store.nextTaskId;
80
80
  store.nextTaskId += 1;
81
81
  const todo = { id, file, text, done, kind: taskKind({ kind }) };
@@ -202,11 +202,12 @@ const SECTION_LINE = {
202
202
  const BACKLOG_HEAD = 'Backlog';
203
203
  const LEGACY_BACKLOG = 'TODOs';
204
204
  const TASK_TITLES = new Map([
205
- [BACKLOG_HEAD, 'backlog'],
206
- [LEGACY_BACKLOG, 'backlog'],
207
- ['Issues', 'issues'],
205
+ [BACKLOG_HEAD, 'debt'],
206
+ [LEGACY_BACKLOG, 'debt'],
207
+ ['Issues', 'debt'],
208
208
  ['Bug Reports', 'bugs'],
209
209
  ['Feature Requests', 'features'],
210
+ ...TASK_KINDS.map((kind) => [kind.title, kind.id]),
210
211
  ]);
211
212
 
212
213
  const AGENT_INSTRUCTIONS = [