reslop 0.2.7 → 0.3.0

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 (158) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +123 -96
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +111 -48
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +15 -43
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +9 -14
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +13 -60
  72. package/lib/render/tiles.js +109 -48
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +46 -14
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +9 -0
  81. package/lib/review/format.js +426 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +958 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -39
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +181 -145
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +85 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +75 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +156 -63
  120. package/lib/session/tasks.js +320 -65
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +65 -65
  126. package/lib/session/workspace.js +39 -78
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +2 -2
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -1,12 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const ansi = require('../ansi.js');
4
- const files = require('../files.js');
5
- const wrap = require('../wrap.js');
3
+ const ansi = require('../term/ansi.js');
4
+ const { shortAge } = require('../common/format.js');
5
+ const { wrapMultiline, wrapDoc, cursorInWrap } = require('../term/wrap.js');
6
6
  const primitives = require('./primitives.js');
7
+ const { trimText } = require('../common/utilities.js');
7
8
 
8
- const { shortAge } = files;
9
- const { wrapMultiline, wrapDoc, cursorInWrap } = wrap;
10
9
  const { THEME, paint, visibleWidth, trimVisible, seq, EL } = ansi;
11
10
  const { MARK_W, markPrefix, padVisible } = primitives;
12
11
  const { editSpan, fieldWindow, paintBodyFill, listLine } = primitives;
@@ -55,10 +54,9 @@ const countLabel = (entry) => {
55
54
  const unstagedDel = entry.unstagedRemoved ?? 0;
56
55
  const any = stagedAdd || unstagedAdd || stagedDel || unstagedDel;
57
56
  if (!any) return NO_COUNT;
58
- return {
59
- add: `+${stagedAdd}/${unstagedAdd}`,
60
- del: `-${stagedDel}/${unstagedDel}`,
61
- };
57
+ const add = `+${stagedAdd}/${unstagedAdd}`;
58
+ const del = `-${stagedDel}/${unstagedDel}`;
59
+ return { add, del };
62
60
  }
63
61
  const added = entry.added ?? 0;
64
62
  const removed = entry.removed ?? 0;
@@ -69,17 +67,14 @@ const countLabel = (entry) => {
69
67
  const countWidths = (entries) =>
70
68
  measureColumns(entries, countLabel, COUNT_ALIGN);
71
69
 
72
- const refsText = (entry) => `${entry.refs ?? ''}`.trim();
70
+ const refsText = (entry) => trimText(entry.refs);
73
71
 
74
72
  const commitRowFields = (entry) => {
75
73
  const { add, del } = countLabel(entry);
76
- return {
77
- sha: entry.shortSha ?? '',
78
- refs: trimVisible(refsText(entry), REFS_MAX),
79
- add,
80
- del,
81
- date: shortAge(entry.date),
82
- };
74
+ const sha = entry.shortSha ?? '';
75
+ const refs = trimVisible(refsText(entry), REFS_MAX);
76
+ const date = shortAge(entry.date);
77
+ return { sha, refs, add, del, date };
83
78
  };
84
79
 
85
80
  const contentWidth = (width) => Math.max(1, width - COMMIT_TAIL);
@@ -107,27 +102,21 @@ const viewedCommit = (entry, rev, revShort) => {
107
102
 
108
103
  const commitTone = (selected, current) => {
109
104
  if (current) {
110
- return {
111
- current: true,
112
- fgRgb: THEME.headerFg,
113
- bgRgb: THEME.currentBg,
114
- nameFg: THEME.mutedFg,
115
- };
105
+ const fgRgb = THEME.headerFg;
106
+ const bgRgb = THEME.currentBg;
107
+ const nameFg = THEME.mutedFg;
108
+ return { current, fgRgb, bgRgb, nameFg };
116
109
  }
117
110
  if (selected) {
118
- return {
119
- current: false,
120
- fgRgb: THEME.chromeFg,
121
- bgRgb: THEME.buttonBg,
122
- nameFg: THEME.buttonHotFg,
123
- };
111
+ const fgRgb = THEME.chromeFg;
112
+ const bgRgb = THEME.buttonBg;
113
+ const nameFg = THEME.buttonHotFg;
114
+ return { current, fgRgb, bgRgb, nameFg };
124
115
  }
125
- return {
126
- current: false,
127
- fgRgb: THEME.mutedFg,
128
- bgRgb: THEME.ctxBg,
129
- nameFg: THEME.chromeFg,
130
- };
116
+ const fgRgb = THEME.mutedFg;
117
+ const bgRgb = THEME.ctxBg;
118
+ const nameFg = THEME.chromeFg;
119
+ return { current, fgRgb, bgRgb, nameFg };
131
120
  };
132
121
 
133
122
  const paintCommitRow = (entry, width, color, selected, cols, current) => {
@@ -159,13 +148,13 @@ const paintCommitEdit = (compose, width, color, entry, cols, index) => {
159
148
  const shown = { ...entry, body: win.text, subject: win.text };
160
149
  const textX = MARK_W + 1;
161
150
  const edit = editSpan(1, textX, textX + rest, 0, text);
162
- return {
163
- row: paintCommitRow(shown, width, color, true, cols),
164
- cursor: { x: textX + win.col, scroll: win.scroll },
165
- at: entry ? 'replace' : 'start',
166
- index,
167
- edits: [{ ...edit, scroll: win.scroll }],
168
- };
151
+ const row = paintCommitRow(shown, width, color, true, cols);
152
+ const x = textX + win.col;
153
+ const scroll = win.scroll;
154
+ const cursor = { x, scroll };
155
+ const at = entry ? 'replace' : 'start';
156
+ const edits = [{ ...edit, scroll }];
157
+ return { row, cursor, at, index, edits };
169
158
  };
170
159
 
171
160
  const editTarget = (compose, commits, cursor) => {
@@ -214,8 +203,11 @@ const paintPieces = (pieces, width, color, tone) => {
214
203
  return out + paint(tail, tone.fgRgb, tone.bgRgb, color);
215
204
  };
216
205
 
217
- const messageText = (entry) =>
218
- `${entry.body ?? ''}`.trimEnd() || `${entry.subject ?? ''}`.trimEnd();
206
+ const messageText = (entry) => {
207
+ const body = `${entry.body ?? ''}`.trimEnd();
208
+ const subject = `${entry.subject ?? ''}`.trimEnd();
209
+ return body || subject;
210
+ };
219
211
 
220
212
  const messageParts = (text, width) => {
221
213
  if (!text) return [{ text: '', kind: 'text' }];
@@ -231,8 +223,8 @@ const messageParts = (text, width) => {
231
223
  };
232
224
 
233
225
  const authorParts = (entry) => {
234
- const name = `${entry.author ?? ''}`.trim();
235
- const email = `${entry.email ?? ''}`.trim();
226
+ const name = trimText(entry.author);
227
+ const email = trimText(entry.email);
236
228
  if (name && email) {
237
229
  return [
238
230
  { text: name, kind: 'author' },
@@ -248,7 +240,7 @@ const partsWidth = (parts) =>
248
240
  parts.reduce((width, part) => width + visibleWidth(part.text), 0);
249
241
 
250
242
  const timeFields = (entry) => ({
251
- when: `${entry.when ?? ''}`.trim(),
243
+ when: trimText(entry.when),
252
244
  ago: shortAge(entry.date),
253
245
  });
254
246
 
@@ -363,21 +355,20 @@ const commitMessageWidth = (entry, screenWidth) => {
363
355
  return Math.max(1, cols.leftW);
364
356
  };
365
357
 
358
+ const PIECE_FG = {
359
+ add: () => THEME.addLineFg,
360
+ del: () => THEME.delLineFg,
361
+ hash: (tone) => (tone.current ? THEME.shaDarkFg : THEME.shaFg),
362
+ refs: (tone) => (tone.current ? THEME.shaDarkFg : THEME.warnFg),
363
+ author: (tone) => (tone.current ? tone.fgRgb : THEME.addLineFg),
364
+ email: (tone) => (tone.current ? tone.fgRgb : THEME.mutedFg),
365
+ text: (tone) => (tone.current ? THEME.mutedFg : tone.nameFg),
366
+ body: () => THEME.mutedFg,
367
+ };
368
+
366
369
  const pieceFg = (kind, tone) => {
367
- if (kind === 'add') return THEME.addLineFg;
368
- if (kind === 'del') return THEME.delLineFg;
369
- if (tone.current) {
370
- if (kind === 'hash') return THEME.shaDarkFg;
371
- if (kind === 'refs') return THEME.shaDarkFg;
372
- if (kind === 'text' || kind === 'body') return THEME.mutedFg;
373
- return tone.fgRgb;
374
- }
375
- if (kind === 'hash') return THEME.shaFg;
376
- if (kind === 'author') return THEME.addLineFg;
377
- if (kind === 'email') return THEME.mutedFg;
378
- if (kind === 'refs') return THEME.warnFg;
379
- if (kind === 'text') return tone.nameFg;
380
- if (kind === 'body') return THEME.mutedFg;
370
+ const pick = PIECE_FG[kind];
371
+ if (pick) return pick(tone);
381
372
  return tone.fgRgb;
382
373
  };
383
374
 
@@ -427,9 +418,8 @@ const commitLines = (entry, index, spec) => {
427
418
  const atCaret = caret && caret.row === messageRow;
428
419
  const cursor = atCaret ? { x: textX + caret.col } : null;
429
420
  const piece = doc[messageRow];
430
- const edits = piece
431
- ? [editSpan(1, textX, x1, piece.start, piece.text)]
432
- : [];
421
+ const edits = [];
422
+ if (piece) edits.push(editSpan(1, textX, x1, piece.start, piece.text));
433
423
  lines.push(listLine(painted, index, cursor, edits));
434
424
  }
435
425
  return lines;
@@ -1,18 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const tiles = require('./tiles.js');
4
- const { seg, spinner } = tiles;
5
- const table = require('./dash-table.js');
6
- const { ago, duration, isHot, waiting, cell, cellSegs, flexCell } = table;
7
- const { tableLines, stat, pairRows, titleAside, captionOver } = table;
3
+ const { ago, duration } = require('../../common/format.js');
4
+ const { seg, spinner, blinkTone, RUN_ICON } = require('../tiles.js');
5
+ const table = require('../table.js');
6
+ const { isHot, waiting, notice } = table;
7
+ const { cell, cellSegs, flexCell, tableLines } = table;
8
+ const { titleAside, captionOver } = table;
8
9
 
9
10
  const NAME_MAX = 18;
10
- const RUN_ICON = {
11
- passed: ['✔', 'add'],
12
- failed: ['✖', 'error'],
13
- aborted: ['⊘', 'muted'],
14
- };
15
-
16
11
  const blankCell = () => cell('', 'muted', 'r');
17
12
 
18
13
  const ageCell = (at, now) => cell(at ? ago(at, now) : '', 'muted', 'r');
@@ -37,8 +32,9 @@ const commitsBlock = (model, width, height, ctx, tile) => {
37
32
  const step = rebaseStep(data.rebase);
38
33
  badge.push(seg(`⟳ rebase ${step}`, 'warn', true));
39
34
  }
40
- const titleLine = titleAside(tile, [seg(`${total}`, 'muted')], width, badge);
41
- if (!recent.length) return { titleLine, lines: [[seg('none', 'muted')]] };
35
+ const count = [seg(`${total}`, 'key', true)];
36
+ const titleLine = titleAside(tile, count, width, badge);
37
+ if (!recent.length) return notice('no commits', titleLine);
42
38
  const hot = isHot(data.changedAt, ctx.now);
43
39
  const shown = recent.slice(0, Math.max(0, height));
44
40
  const rows = shown.map((entry, i) => commitRow(entry, ctx, hot && i === 0));
@@ -59,8 +55,6 @@ const branchOrder = (list) =>
59
55
  return right.at - left.at || left.name.localeCompare(right.name, 'en');
60
56
  });
61
57
 
62
- const blinkTone = (frame) => (Math.abs(frame) % 2 ? 'hot' : 'warn');
63
-
64
58
  const branchRow = (entry, data, ctx) => {
65
59
  const hot = isHot(data.hot.get(entry.name) ?? 0, ctx.now);
66
60
  const tone = hot ? blinkTone(ctx.frame) : 'text';
@@ -100,11 +94,9 @@ const branchesBlock = (model, width, height, ctx, tile) => {
100
94
  const data = model.branches;
101
95
  if (!data.ready) return waiting('reading git…');
102
96
  const badge = data.rebase ? [seg('⟳', 'warn', true)] : [];
103
- const aside = [seg(`${data.list.length}`, 'muted')];
97
+ const aside = [seg(`${data.list.length}`, 'key', true)];
104
98
  const titleLine = titleAside(tile, aside, width, badge);
105
- if (!data.list.length) {
106
- return { titleLine, lines: [[seg('no branches', 'muted')]] };
107
- }
99
+ if (!data.list.length) return notice('no branches', titleLine);
108
100
  const extras = branchExtras(data, ctx);
109
101
  const room = Math.max(1, height - extras.length);
110
102
  const shown = branchOrder(data.list).slice(0, room);
@@ -126,8 +118,7 @@ const countCell = (value, tone) => {
126
118
 
127
119
  const failTone = (value) => (value ? 'error' : 'muted');
128
120
 
129
- const testMetrics = (done, ok, fail, total) => [
130
- countCell(done, 'text'),
121
+ const testMetrics = (ok, fail, total) => [
131
122
  countCell(ok, ok ? 'add' : 'muted'),
132
123
  countCell(fail, failTone(fail)),
133
124
  countCell(total, 'text'),
@@ -138,12 +129,13 @@ const runMetrics = (run) => {
138
129
  if (result && typeof result.tests === 'number') {
139
130
  const fail = result.failed ?? 0;
140
131
  const ok = result.passed ?? Math.max(0, result.tests - fail);
141
- return testMetrics(result.tests, ok, fail, result.tests);
132
+ return testMetrics(ok, fail, result.tests);
142
133
  }
143
- const live = run.status === 'running' && run.source !== 'reslop';
144
- if (!live) return [blankCell(), blankCell(), blankCell(), blankCell()];
134
+ const counted = run.done > 0 || run.failed > 0 || run.expected > 0;
135
+ const live = run.status === 'running' && (run.source !== 'reslop' || counted);
136
+ if (!live) return [blankCell(), blankCell(), blankCell()];
145
137
  const total = run.expected > 0 ? run.expected : null;
146
- return testMetrics(run.done + run.failed, run.done, run.failed, total);
138
+ return testMetrics(run.done, run.failed, total);
147
139
  };
148
140
 
149
141
  const lintNote = (run) => {
@@ -157,7 +149,7 @@ const lintNote = (run) => {
157
149
  return note;
158
150
  };
159
151
 
160
- const durationCell = (run, ctx) => {
152
+ const spentCell = (run, ctx) => {
161
153
  const running = run.status === 'running';
162
154
  const end = running ? ctx.now : run.endedAt;
163
155
  if (!run.startedAt || !end) return blankCell();
@@ -168,16 +160,15 @@ const named = (icon, tone, name, nameTone) => ({
168
160
  segs: [seg(icon, tone, true), seg(' '), seg(name, nameTone)],
169
161
  align: 'l',
170
162
  flex: true,
171
- min: 1,
163
+ min: 6,
172
164
  });
173
165
 
174
166
  const runHeader = () => [
175
167
  cell(''),
176
- cell('done', 'muted', 'r'),
177
- cell('ok', 'muted', 'r'),
178
- cell('fail', 'muted', 'r'),
179
- cell('total', 'muted', 'r'),
180
- cell('duration', 'muted', 'r'),
168
+ cell('ok', 'caption', 'r'),
169
+ cell('fail', 'caption', 'r'),
170
+ cell('total', 'caption', 'r'),
171
+ cell('spent', 'caption', 'r'),
181
172
  ];
182
173
 
183
174
  const runRow = (run, ctx) => {
@@ -185,7 +176,7 @@ const runRow = (run, ctx) => {
185
176
  const tone = run.status === 'running' ? 'text' : 'muted';
186
177
  const label = named(icon[0], icon[1], run.name, tone);
187
178
  label.segs.push(...lintNote(run));
188
- return [label, ...runMetrics(run), durationCell(run, ctx)];
179
+ return [label, ...runMetrics(run), spentCell(run, ctx)];
189
180
  };
190
181
 
191
182
  const runsBlock = (model, width, height, ctx, tile) => {
@@ -194,13 +185,7 @@ const runsBlock = (model, width, height, ctx, tile) => {
194
185
  rows.push([named(spinner(ctx.frame), 'warn', model.busy, 'warn')]);
195
186
  }
196
187
  for (const run of model.runs) rows.push(runRow(run, ctx));
197
- if (!rows.length) {
198
- const items = [
199
- stat('status', 'idle', 'muted'),
200
- stat('usage', 'reslop t -- <command>', 'muted'),
201
- ];
202
- return { badge: [], lines: tableLines(pairRows(items, 1), width) };
203
- }
188
+ if (!rows.length) return notice(['idle', 'reslop t -- <command>']);
204
189
  const shown = rows.slice(0, Math.max(1, height));
205
190
  const lines = tableLines([runHeader(), ...shown], width);
206
191
  return {