reslop 0.2.6 → 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 +27 -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 +61 -66
  10. package/lib/{files.js → common/files.js} +51 -52
  11. package/lib/common/find.js +61 -0
  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 +117 -51
  18. package/lib/dashboard/npm.js +133 -32
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +41 -31
  21. package/lib/dashboard/workspace.js +151 -0
  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 -95
  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 +240 -71
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +36 -39
  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/dashboard/blocks.js +490 -0
  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 +22 -8
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +127 -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 +29 -13
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +89 -0
  66. package/lib/render/primitives.js +109 -16
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +214 -0
  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} +19 -61
  72. package/lib/render/tiles.js +203 -47
  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 +36 -44
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +122 -56
  100. package/lib/session/compose.js +197 -146
  101. package/lib/session/dashboard.js +183 -37
  102. package/lib/session/dispatch.js +120 -22
  103. package/lib/session/files-pane.js +60 -24
  104. package/lib/session/find.js +81 -0
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +96 -66
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +8 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/npm/packages.js +150 -0
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +216 -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} +166 -59
  120. package/lib/session/tasks.js +321 -66
  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 +72 -59
  126. package/lib/session/workspace.js +416 -0
  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 -505
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -437
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -628
  148. package/lib/render/dash-blocks.js +0 -245
  149. package/lib/render/present.js +0 -19
  150. package/lib/render/report.js +0 -43
  151. package/lib/review.js +0 -744
  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
package/lib/render/npm.js CHANGED
@@ -1,46 +1,22 @@
1
1
  'use strict';
2
2
 
3
- const ansi = require('../ansi.js');
4
- const npmCommands = require('../npm-commands.js');
5
- const wrap = require('../wrap.js');
6
- const chrome = require('./chrome.js');
3
+ const ansi = require('../term/ansi.js');
4
+ const { spinner } = require('./tiles.js');
7
5
  const primitives = require('./primitives.js');
8
-
9
- const { THEME, CODE_FG, paint, visibleWidth, truncateVisible } = ansi;
10
- const { trimVisible, seq, EL, RESET, fg } = ansi;
11
- const { clipAnsi, stripAnsi, foregroundOn } = ansi;
12
- const { TABLE_KEY, TABLE_VAL } = npmCommands;
13
- const { wrapPlain } = wrap;
14
- const { formatBusyStatus } = chrome;
6
+ const { scrollMark } = primitives;
7
+ const { placeHits } = primitives;
8
+ const { THEME, paint, visibleWidth } = ansi;
9
+ const { trimVisible, seq, EL } = ansi;
15
10
  const { fill, MARK_W, markPrefix, editSpan } = primitives;
16
11
  const { paneResult, paintCursorList, fieldWindow } = primitives;
12
+ const log = require('./log.js');
13
+ const { mixRgb, withLogScroller, paintNpmLog } = log;
17
14
 
18
15
  const EDGE = 2;
19
- const TAIL = 1;
20
- const LOG_PAD_Y = 1;
21
- const LOG_PAD_X = 2;
22
- const NPM_NAME_MAX = 24;
23
-
24
- const TABLE_INK = {
25
- actual: THEME.errorFg,
26
- expected: THEME.addLineFg,
27
- code: THEME.shaFg,
28
- type: CODE_FG.className,
29
- count: THEME.warnFg,
30
- omitted: THEME.warnFg,
31
- };
32
16
 
33
- const COUNT_INK = {
34
- failed: THEME.errorFg,
35
- errors: THEME.errorFg,
36
- passed: THEME.addLineFg,
37
- };
17
+ const TAIL = 1;
38
18
 
39
- const logViewRows = (bodyH) => {
40
- const height = Math.max(1, bodyH);
41
- if (height >= LOG_PAD_Y * 2 + 1) return height - LOG_PAD_Y * 2;
42
- return height;
43
- };
19
+ const NPM_NAME_MAX = 24;
44
20
 
45
21
  const npmNameWidth = (commands, typed = '') => {
46
22
  let width = visibleWidth(typed);
@@ -50,11 +26,15 @@ const npmNameWidth = (commands, typed = '') => {
50
26
  return Math.min(NPM_NAME_MAX, width);
51
27
  };
52
28
 
29
+ const COMMAND_MIN = 8;
30
+
53
31
  const npmColumns = (width, nameCol) => {
54
32
  const rest = Math.max(1, width - EDGE - TAIL - MARK_W);
55
- const nameW = Math.min(nameCol, rest);
56
- const gap = rest > nameW + 2 ? ' ' : '';
57
- return { rest, nameW, gap, detailW: rest - nameW - gap.length };
33
+ const gap = rest > 3 ? ' ' : '';
34
+ const nameRoom = Math.max(1, rest - gap.length - COMMAND_MIN);
35
+ const nameW = Math.min(nameCol, nameRoom);
36
+ const detailW = Math.max(0, rest - nameW - gap.length);
37
+ return { rest, nameW, gap, detailW };
58
38
  };
59
39
 
60
40
  const paintNpmLine = (cols, name, detail, color, selected, bin) => {
@@ -65,8 +45,8 @@ const paintNpmLine = (cols, name, detail, color, selected, bin) => {
65
45
  const pad = ' '.repeat(Math.max(0, cols.rest - visibleWidth(mid + after)));
66
46
  const tail = ' '.repeat(TAIL);
67
47
  const fgRgb = selected ? THEME.chromeFg : THEME.mutedFg;
68
- const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
69
- const edge = fill(EDGE, fgRgb, THEME.ctxBg, color);
48
+ const bgRgb = selected ? THEME.checkBg : THEME.buttonBg;
49
+ const edge = fill(EDGE, fgRgb, bgRgb, color);
70
50
  if (!color) return `${prefix}${mid}${after}${pad}${tail}${edge}`;
71
51
  const nameFg = bin ? THEME.shaFg : THEME.buttonHotFg;
72
52
  let out = `${seq(fgRgb, bgRgb)}${EL}`;
@@ -88,9 +68,9 @@ const editingName = (view) => view.npmEditField !== 'command';
88
68
 
89
69
  const editWindow = (compose, live, draft, width) => {
90
70
  const value = `${(live ? compose.text : draft) ?? ''}`.replaceAll('\n', ' ');
91
- const win = live
92
- ? fieldWindow(value, compose.cursor, compose.scrollCol, width)
93
- : fieldWindow(value, 0, 0, width);
71
+ const cursor = live ? compose.cursor : 0;
72
+ const scrollCol = live ? compose.scrollCol : 0;
73
+ const win = fieldWindow(value, cursor, scrollCol, width);
94
74
  return { ...win, value };
95
75
  };
96
76
 
@@ -126,6 +106,9 @@ const paintNpmEdit = (view, width, color, nameCol) => {
126
106
  return { row, cursor, edits };
127
107
  };
128
108
 
109
+ const leftFill = (width, color) =>
110
+ fill(width, THEME.chromeFg, THEME.buttonBg, color);
111
+
129
112
  const paintNpmList = (view, width, color, bodyH, headerLines) => {
130
113
  const commands = view.npmCommands ?? [];
131
114
  const { compose } = view;
@@ -133,9 +116,17 @@ const paintNpmList = (view, width, color, bodyH, headerLines) => {
133
116
  const typed = editingName(view) ? compose?.text : view.npmDraftName;
134
117
  const nameW = npmNameWidth(commands, editing ? `${typed ?? ''}` : '');
135
118
  const cursor = view.npmCursor ?? 0;
136
- const pane = { width, color, bodyH, headerLines, offset: view.listScroll };
119
+ const pane = {
120
+ width,
121
+ color,
122
+ bodyH,
123
+ headerLines,
124
+ offset: view.listScroll,
125
+ blank: leftFill(width, color),
126
+ };
127
+ const onCommands = view.npmFocus !== 'runs';
137
128
  const paintRow = (entry, selected) =>
138
- paintNpmRow(entry, width, color, selected, nameW);
129
+ paintNpmRow(entry, width, color, onCommands && selected, nameW);
139
130
  let extra = null;
140
131
  if (editing) {
141
132
  const painted = paintNpmEdit(view, width, color, nameW);
@@ -145,194 +136,329 @@ const paintNpmList = (view, width, color, bodyH, headerLines) => {
145
136
  return paintCursorList(commands, cursor, pane, paintRow, extra);
146
137
  };
147
138
 
148
- const logStart = (view, count, rows) => {
149
- const maxStart = Math.max(0, count - rows);
150
- const start = view.npmFollow ? maxStart : view.npmScroll;
151
- return Math.max(0, Math.min(start ?? 0, maxStart));
139
+ const HISTORY_SHARE = 0.7;
140
+
141
+ const sessionSplit = (width) => {
142
+ const rightW = Math.max(1, Math.floor(width * HISTORY_SHARE));
143
+ const leftW = Math.max(0, width - rightW);
144
+ return { leftW, rightW };
152
145
  };
153
146
 
154
- const isTableLine = (line) => `${line ?? ''}`.startsWith(TABLE_KEY);
147
+ const rightFill = (width, color) =>
148
+ fill(width, THEME.chromeFg, THEME.ctxBg, color);
149
+
150
+ const HISTORY_HEADER = [
151
+ 'command',
152
+ 'done',
153
+ 'ok',
154
+ 'fail',
155
+ 'total',
156
+ 'duration',
157
+ 'time',
158
+ ];
159
+
160
+ const HISTORY_ALIGN = ['l', 'r', 'r', 'r', 'r', 'r', 'r'];
155
161
 
156
- const mixRgb = (from, to, part) =>
157
- from.map((value, i) => Math.round(value + (to[i] - value) * part));
162
+ const HISTORY_CAP = [0, 6, 6, 6, 8, 8, 14];
158
163
 
159
- const tableTone = (index) => {
160
- const lift = index % 2 === 0 ? 0.035 : 0.07;
161
- const valueBg = mixRgb(THEME.ctxBg, THEME.chromeFg, lift);
162
- const keyBg = mixRgb(THEME.ctxBg, THEME.chromeFg, lift + 0.025);
163
- const frame = mixRgb(THEME.ctxBg, THEME.chromeFg, lift * 0.4);
164
- return { valueBg, keyBg, frame };
164
+ const HISTORY_FLEX = 0;
165
+
166
+ const HISTORY_PREFIX = ' ';
167
+
168
+ const HISTORY_TAIL = ' ';
169
+
170
+ const SELECT_LIFT = 0.45;
171
+
172
+ const WHITE = [0xff, 0xff, 0xff];
173
+
174
+ const HISTORY_TONE = {
175
+ add: 'addLineFg',
176
+ error: 'errorFg',
177
+ warn: 'warnFg',
178
+ text: 'chromeFg',
179
+ caption: 'dashCaptionFg',
180
+ };
181
+
182
+ const historyTone = (name) => THEME[HISTORY_TONE[name] ?? 'mutedFg'];
183
+
184
+ const toneRgb = (name, selected) => {
185
+ const base = historyTone(name);
186
+ if (!selected || name === 'caption') return base;
187
+ return mixRgb(base, WHITE, SELECT_LIFT);
165
188
  };
166
189
 
167
- const tableIndex = (lines, index) => {
168
- let row = 0;
169
- while (index - row > 0 && isTableLine(lines[index - row - 1])) row += 1;
170
- return row;
190
+ const HISTORY_ICON = {
191
+ pass: { text: '✔', tone: 'add' },
192
+ stop: { text: '⊘', tone: 'muted' },
193
+ fail: { text: '✖', tone: 'error' },
171
194
  };
172
195
 
173
- const tableInk = (key, value) => {
174
- const name = key.trim();
175
- if (Object.hasOwn(COUNT_INK, name)) {
176
- return Number(value) > 0 ? COUNT_INK[name] : THEME.mutedFg;
196
+ const UNKNOWN_ICON = { text: '?', tone: 'muted' };
197
+
198
+ const historyIcon = (row, frame) => {
199
+ if (row.live || row.mark === 'run') {
200
+ const text = spinner(frame);
201
+ const tone = 'warn';
202
+ return { text, tone };
177
203
  }
178
- return Object.hasOwn(TABLE_INK, name) ? TABLE_INK[name] : THEME.chromeFg;
204
+ return HISTORY_ICON[row.mark] ?? UNKNOWN_ICON;
179
205
  };
180
206
 
181
- const tableParts = (line, width) => {
182
- const raw = line.slice(TABLE_KEY.length);
183
- const at = raw.indexOf(TABLE_VAL);
184
- const key = at < 0 ? raw : raw.slice(0, at);
185
- const rest = at < 0 ? '' : raw.slice(at + TABLE_VAL.length);
186
- const gap = rest.startsWith(' ') ? ' ' : '';
187
- const value = rest.slice(gap.length);
188
- const edge = Math.min(1, Math.floor(width / 2));
189
- const inner = Math.max(0, width - edge * 2);
190
- const keyW = Math.min(visibleWidth(key), inner);
191
- const gapW = Math.min(visibleWidth(gap), Math.max(0, inner - keyW));
192
- const valueW = Math.max(0, inner - keyW - gapW);
193
- return { key, gap, value, edge, keyW, gapW, valueW };
207
+ const commandLabel = (row, frame) => {
208
+ const icon = historyIcon(row, frame).text;
209
+ const name = `${row.name ?? ''}`;
210
+ if (!name) return icon;
211
+ return `${icon} ${name}`;
194
212
  };
195
213
 
196
- const wrapTableLine = (line, width) => {
197
- const part = tableParts(line, width);
198
- if (part.valueW < 1) return [line];
199
- const trimmed = part.value.trimEnd();
200
- const lead = trimmed.startsWith(' ') ? ' ' : '';
201
- const body = trimmed.slice(lead.length);
202
- const budget = Math.max(1, part.valueW - visibleWidth(lead));
203
- if (visibleWidth(body) <= budget) return [line];
204
- const blank = ' '.repeat(visibleWidth(part.key));
205
- return wrapPlain(body, budget).map((piece, index) => {
206
- const name = index === 0 ? part.key : blank;
207
- return `${TABLE_KEY}${name}${TABLE_VAL}${part.gap}${lead}${piece}`;
208
- });
214
+ const historyCells = (row, frame) => [
215
+ commandLabel(row, frame),
216
+ `${row.done ?? ''}`,
217
+ `${row.ok ?? ''}`,
218
+ `${row.fail ?? ''}`,
219
+ `${row.total ?? ''}`,
220
+ `${row.elapsed ?? ''}`,
221
+ `${row.when ?? ''}`,
222
+ ];
223
+
224
+ const columnWants = (rows, frame) => {
225
+ const want = HISTORY_HEADER.map((label) => visibleWidth(label));
226
+ for (const row of rows) {
227
+ if (row.empty) continue;
228
+ const cells = historyCells(row, frame);
229
+ for (let i = 0; i < cells.length; i++) {
230
+ const seen = visibleWidth(cells[i]);
231
+ const cap = HISTORY_CAP[i];
232
+ const width = i === HISTORY_FLEX || !cap ? seen : Math.min(cap, seen);
233
+ want[i] = Math.max(want[i], width);
234
+ }
235
+ }
236
+ return want;
237
+ };
238
+
239
+ const fitColumns = (want, budget) => {
240
+ const gaps = Math.max(0, want.length - 1);
241
+ let fixed = 0;
242
+ for (let i = 0; i < want.length; i++) {
243
+ if (i !== HISTORY_FLEX) fixed += want[i];
244
+ }
245
+ const room = budget - gaps - fixed;
246
+ if (room >= 1) {
247
+ const widths = want.slice();
248
+ widths[HISTORY_FLEX] = room;
249
+ return widths;
250
+ }
251
+ const widths = want.slice();
252
+ widths[HISTORY_FLEX] = 1;
253
+ let extra = 1 + fixed + gaps - budget;
254
+ for (let index = widths.length - 1; index >= 0 && extra > 0; index--) {
255
+ if (index === HISTORY_FLEX) continue;
256
+ const drop = Math.min(extra, widths[index]);
257
+ widths[index] -= drop;
258
+ extra -= drop;
259
+ }
260
+ return widths;
209
261
  };
210
262
 
211
- const wrapLogText = (line, inner) => {
212
- const mark = line.startsWith('│ ') ? '│ ' : '';
213
- const rest = line.slice(mark.length);
214
- const budget = Math.max(1, inner - visibleWidth(mark));
215
- if (visibleWidth(rest) <= budget) return [line];
216
- return wrapPlain(rest, budget).map((piece) => `${mark}${piece}`);
263
+ const historyWidths = (rows, budget, frame) =>
264
+ fitColumns(columnWants(rows, frame), budget);
265
+
266
+ const alignCell = (text, width, align) => {
267
+ const shown = trimVisible(`${text ?? ''}`, width);
268
+ const pad = ' '.repeat(Math.max(0, width - visibleWidth(shown)));
269
+ return align === 'r' ? `${pad}${shown}` : `${shown}${pad}`;
217
270
  };
218
271
 
219
- const expandLogLines = (text, width) => {
220
- const lines = `${text ?? ''}`.split('\n');
221
- if (lines.at(-1) === '') lines.pop();
222
- const inner = Math.max(1, width - LOG_PAD_X * 2);
223
- return lines.flatMap((line) => {
224
- if (isTableLine(line)) return wrapTableLine(line, width);
225
- return wrapLogText(line, inner);
226
- });
272
+ const cellTone = (row, index, statusTone) => {
273
+ if (index === 1) return 'text';
274
+ if (index === 2) return row.okN ? 'add' : 'muted';
275
+ if (index === 3) return row.failN ? 'error' : 'muted';
276
+ if (index === 4) return row.exitLabel ? statusTone : 'text';
277
+ if (index === 5 && row.live) return 'warn';
278
+ return 'muted';
227
279
  };
228
280
 
229
- const paintPlainTable = (line, width) => {
230
- const part = tableParts(line, width);
231
- const name = trimVisible(part.key, part.keyW);
232
- const namePad = ' '.repeat(Math.max(0, part.keyW - visibleWidth(name)));
233
- const shown = trimVisible(part.value, part.valueW);
234
- const pad = ' '.repeat(Math.max(0, part.valueW - visibleWidth(shown)));
235
- const edge = ' '.repeat(part.edge);
236
- const gap = part.gap.slice(0, part.gapW);
237
- return `${edge}${name}${namePad}${gap}${shown}${pad}${edge}`;
281
+ const paintPiece = (text, tone, bg, color, selected) => {
282
+ if (!color || text === '') return text;
283
+ return paint(text, toneRgb(tone, selected), bg, true);
238
284
  };
239
285
 
240
- const paintTableLine = (line, width, index) => {
241
- const part = tableParts(line, width);
242
- const shownKey = trimVisible(part.key, part.keyW);
243
- const shown = trimVisible(part.value, part.valueW);
244
- const pad = ' '.repeat(Math.max(0, part.valueW - visibleWidth(shown)));
245
- const tone = tableTone(index);
246
- const ink = THEME.ctxFg;
247
- const valueFg = tableInk(part.key, part.value);
248
- let out = fill(part.edge, ink, tone.frame, true);
249
- out += paint(shownKey, ink, tone.keyBg, true);
250
- out += paint(part.gap.slice(0, part.gapW), ink, tone.frame, true);
251
- out += paint(`${shown}${pad}`, valueFg, tone.valueBg, true);
252
- return out + fill(part.edge, ink, tone.frame, true);
286
+ const paintCommand = (row, frame, width, bg, color, selected) => {
287
+ const icon = historyIcon(row, frame);
288
+ if (width <= 0) return '';
289
+ if (width === 1) return paintPiece(icon.text, icon.tone, bg, color, selected);
290
+ const lead = `${icon.text} `;
291
+ const nameW = Math.max(0, width - visibleWidth(lead));
292
+ const name = trimVisible(`${row.name ?? ''}`, nameW);
293
+ const pad = ' '.repeat(Math.max(0, nameW - visibleWidth(name)));
294
+ const nameTone = selected ? icon.tone : 'text';
295
+ const mark = paintPiece(lead, icon.tone, bg, color, selected);
296
+ const label = paintPiece(`${name}${pad}`, nameTone, bg, color, selected);
297
+ return `${mark}${label}`;
253
298
  };
254
299
 
255
- const paintLogSource = (line) => {
256
- if (line !== stripAnsi(line)) return line;
257
- if (/^[✖✔△] /.test(line)) {
258
- const ink = line[0] === '✔' ? THEME.addLineFg : THEME.errorFg;
259
- const color = line[0] === '△' ? THEME.warnFg : ink;
260
- return paint(line, color, THEME.ctxBg, true, true);
300
+ const historyParts = (cells, widths, tones, bg, color, selected, command) => {
301
+ const parts = [];
302
+ for (let i = 0; i < cells.length; i++) {
303
+ if (!widths[i]) continue;
304
+ if (i === 0 && command) {
305
+ parts.push(command);
306
+ continue;
307
+ }
308
+ const shown = alignCell(cells[i], widths[i], HISTORY_ALIGN[i]);
309
+ parts.push(paintPiece(shown, tones[i], bg, color, selected));
261
310
  }
262
- if (line.startsWith('│')) {
263
- return `${fg(THEME.shaFg)}│${fg(THEME.ctxFg)}${line.slice(1)}`;
264
- }
265
- if (/^(Stack|Related failures|Stderr)$/.test(line)) {
266
- return paint(line, THEME.chromeFg, THEME.ctxBg, true, true);
267
- }
268
- if (!/^\s+(at|↳)\s/.test(line)) return line;
269
- const source = line.replace(
270
- /([^\s()|]+)(:\d+:\d+)(?=[\s)|]|$)/g,
271
- (match, file, position) => {
272
- const path = `${fg(THEME.shaFg)}${file}`;
273
- const point = `${fg(THEME.mutedFg)}${position}`;
274
- return `${path}${point}${fg(THEME.ctxFg)}`;
275
- },
276
- );
277
- const frame = /^(\s+at\s+)(.*?)(\s+\(.+\))$/.exec(source);
278
- if (!frame) return source;
279
- return (
280
- `${fg(THEME.mutedFg)}${frame[1]}${fg(CODE_FG.function)}${frame[2]}` +
281
- `${fg(THEME.ctxFg)}${frame[3]}`
282
- );
311
+ const gap = paintPiece(' ', 'muted', bg, color, selected);
312
+ return parts.join(gap);
283
313
  };
284
314
 
285
- const paintLogLine = (line, width, color, fgRgb = THEME.ctxFg) => {
286
- const inner = Math.max(1, width - LOG_PAD_X * 2);
287
- const lead = ' '.repeat(LOG_PAD_X);
288
- if (!color) {
289
- const body = truncateVisible(stripAnsi(line), inner);
290
- const pad = Math.max(0, width - LOG_PAD_X - visibleWidth(body));
291
- return `${lead}${body}${' '.repeat(pad)}`;
315
+ const historyLine = (row, widths, color, selected, frame) => {
316
+ const gaps = Math.max(0, widths.filter((n) => n > 0).length - 1);
317
+ const budget = widths.reduce((sum, n) => sum + n, 0) + gaps;
318
+ const bg = selected ? THEME.checkBg : THEME.ctxBg;
319
+ if (row.empty) {
320
+ const none = trimVisible('none', budget);
321
+ return paintPiece(none, 'muted', bg, color, selected);
292
322
  }
293
- const source = paintLogSource(`${line ?? ''}`);
294
- const body = foregroundOn(clipAnsi(source, inner), THEME.ctxBg);
295
- const pad = Math.max(0, width - LOG_PAD_X - visibleWidth(body));
296
- const base = seq(fgRgb, THEME.ctxBg);
297
- let out = `${base}${EL}${lead}${body}`;
298
- if (pad) out += `${RESET}${base}${' '.repeat(pad)}`;
299
- return `${out}${RESET}`;
323
+ const status = historyIcon(row, frame).tone;
324
+ const cells = historyCells(row, frame);
325
+ const tones = cells.map((_, index) => cellTone(row, index, status));
326
+ const command = paintCommand(row, frame, widths[0] ?? 0, bg, color, selected);
327
+ return historyParts(cells, widths, tones, bg, color, selected, command);
300
328
  };
301
329
 
302
- const paintLogRow = (view, lines, index, width, color) => {
303
- if (index < lines.length) {
304
- const line = lines[index];
305
- if (!isTableLine(line)) return paintLogLine(line, width, color);
306
- if (!color) return paintPlainTable(line, width);
307
- return paintTableLine(line, width, tableIndex(lines, index));
308
- }
309
- if (view.npmRunning === true && index === lines.length) {
310
- const label = formatBusyStatus('running', view.progressFrame ?? 0);
311
- return paintLogLine(label, width, color, THEME.chromeFg);
312
- }
313
- return fill(width, THEME.ctxFg, THEME.ctxBg, color);
330
+ const paintHistoryLine = (mid, width, color, bg) => {
331
+ const prefix = HISTORY_PREFIX;
332
+ const tail = HISTORY_TAIL;
333
+ const budget = Math.max(0, width - prefix.length - tail.length);
334
+ const pad = ' '.repeat(Math.max(0, budget - visibleWidth(mid)));
335
+ if (!color) return `${prefix}${mid}${pad}${tail}`;
336
+ const edge = paintPiece(prefix, 'muted', bg, true, false);
337
+ const rest = paintPiece(`${pad}${tail}`, 'muted', bg, true, false);
338
+ return `${edge}${mid}${rest}`;
339
+ };
340
+
341
+ const paintSessionRow = (row, width, color, selected, widths, frame) => {
342
+ const bg = selected ? THEME.checkBg : THEME.ctxBg;
343
+ const mid = historyLine(row, widths, color, selected, frame);
344
+ const line = paintHistoryLine(mid, width, color, bg);
345
+ return { row: line, hit: !row.empty };
346
+ };
347
+
348
+ const paintColumnHeader = (widths, width, color) => {
349
+ const bg = THEME.ctxBg;
350
+ const tones = HISTORY_HEADER.map(() => 'caption');
351
+ const mid = historyParts(HISTORY_HEADER, widths, tones, bg, color, false);
352
+ return paintHistoryLine(mid, width, color, bg);
314
353
  };
315
354
 
316
- const paintNpmLog = (view, width, color, bodyH) => {
317
- const lines = expandLogLines(view.npmOutput ?? '', width);
318
- const count = lines.length + (view.npmRunning === true ? 1 : 0);
319
- const rows = logViewRows(bodyH);
320
- const padY = bodyH - rows;
321
- const top = padY ? 1 : 0;
322
- const start = logStart(view, count, rows);
323
- const blank = fill(width, THEME.ctxFg, THEME.ctxBg, color);
355
+ const joinNpm = (left, right, width, color, bodyH) => {
356
+ const { leftW, rightW } = sessionSplit(width);
357
+ const blankL = leftFill(leftW, color);
358
+ const blankR = rightFill(rightW, color);
359
+ const count = Math.max(left.body.length, right.body.length, bodyH);
324
360
  const body = [];
325
- for (let i = 0; i < top; i++) body.push(blank);
326
- for (let i = 0; i < rows; i++) {
327
- body.push(paintLogRow(view, lines, start + i, width, color));
361
+ for (let i = 0; i < count; i++) {
362
+ const sideL = left.body[i] ?? blankL;
363
+ const sideR = right.body[i] ?? blankR;
364
+ body.push(`${sideL}${sideR}`);
328
365
  }
329
- for (let i = 0; i < padY - top; i++) body.push(blank);
330
- return paneResult({ body });
366
+ const fileHits = [
367
+ ...placeHits(left.fileHits, 0, leftW, 'commands'),
368
+ ...placeHits(right.fileHits, leftW, rightW, 'runs'),
369
+ ];
370
+ const { editHits, cursor, listScroll } = left;
371
+ return paneResult({ body, fileHits, editHits, cursor, listScroll });
372
+ };
373
+
374
+ const historyHead = (header, width, color, bodyH) => {
375
+ const blank = rightFill(width, color);
376
+ const lines = [blank, header || blank, blank];
377
+ return lines.slice(0, bodyH);
378
+ };
379
+
380
+ const historyBar = (lines, count, start, width, fullWidth, color, head) => {
381
+ const rows = lines.length;
382
+ const barred = lines.map((line, index) => {
383
+ const mark = scrollMark(index, count, rows, start);
384
+ return withLogScroller(line, mark, width, color);
385
+ });
386
+ const scrollBar = {
387
+ col: fullWidth,
388
+ row: head,
389
+ height: rows,
390
+ track: 0,
391
+ rows,
392
+ count,
393
+ start,
394
+ };
395
+ return { body: barred, scrollBar };
396
+ };
397
+
398
+ const paintNpmSessions = (view, width, color, bodyH, headLines, full) => {
399
+ const runs = view.npmRuns ?? [];
400
+ const frame = view.progressFrame ?? 0;
401
+ const listRoom = Math.max(0, bodyH - 3);
402
+ const overflow = runs.length > listRoom && listRoom > 0;
403
+ const inner = overflow ? Math.max(1, width - 1) : width;
404
+ const edge = HISTORY_PREFIX.length + HISTORY_TAIL.length;
405
+ const budget = Math.max(0, inner - edge);
406
+ const widths = historyWidths(runs, budget, frame);
407
+ const header = paintColumnHeader(widths, inner, color);
408
+ const pad = rightFill(width - inner, color);
409
+ const padded = overflow ? `${header}${pad}` : header;
410
+ const head = historyHead(padded, width, color, bodyH);
411
+ const listH = Math.max(0, bodyH - head.length);
412
+ const rows = runs.length ? runs : [{ empty: true }];
413
+ const cursor = view.npmRunCursor ?? 0;
414
+ const onRuns = view.npmFocus === 'runs';
415
+ const paintRun = (entry, selected) =>
416
+ paintSessionRow(entry, inner, color, onRuns && selected, widths, frame);
417
+ const painted = paintCursorList(
418
+ rows,
419
+ cursor,
420
+ {
421
+ width: inner,
422
+ color,
423
+ bodyH: listH,
424
+ headerLines: headLines + head.length,
425
+ offset: view.npmRunScroll ?? 0,
426
+ blank: rightFill(inner, color),
427
+ pad: 0,
428
+ },
429
+ paintRun,
430
+ );
431
+ const list = painted.body.slice();
432
+ while (list.length < listH) list.push(rightFill(inner, color));
433
+ let scrolled = { body: list, scrollBar: null };
434
+ if (overflow) {
435
+ const at = painted.listScroll;
436
+ const rows = runs.length;
437
+ const lead = head.length;
438
+ scrolled = historyBar(list, rows, at, width, full, color, lead);
439
+ }
440
+ const body = [...head, ...scrolled.body].slice(0, bodyH);
441
+ const { fileHits, listScroll } = painted;
442
+ const { scrollBar } = scrolled;
443
+ return { body, fileHits, listScroll, scrollBar };
331
444
  };
332
445
 
333
446
  const paintBodyNpm = (view, width, color, bodyH, headerLines) => {
334
447
  if (view.npmView) return paintNpmLog(view, width, color, bodyH);
335
- return paintNpmList(view, width, color, bodyH, headerLines);
448
+ const { leftW, rightW } = sessionSplit(width);
449
+ const left = paintNpmList(view, leftW, color, bodyH, headerLines);
450
+ const right = paintNpmSessions(
451
+ view,
452
+ rightW,
453
+ color,
454
+ bodyH,
455
+ headerLines,
456
+ width,
457
+ );
458
+ const joined = joinNpm(left, right, width, color, bodyH);
459
+ joined.runScroll = right.listScroll;
460
+ if (right.scrollBar) joined.scrollBar = right.scrollBar;
461
+ return joined;
336
462
  };
337
463
 
338
- module.exports = { paintBodyNpm, logViewRows, expandLogLines };
464
+ module.exports = { paintBodyNpm };