nightralph 0.0.8 → 0.0.9

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.
package/README.md CHANGED
@@ -141,7 +141,8 @@ Flags:
141
141
  | `-X` | no | -- | Stop on merge conflict instead of re-spawning |
142
142
  | `--timeout` | no | `300` | Kill agent after N seconds |
143
143
 
144
- Notes:
144
+
145
+ **Notes**:
145
146
 
146
147
  * When `--spec <name>` is omitted, the CLI looks for
147
148
  `.scratch/<feature>/issues/` directories. If multiple features exist,
@@ -0,0 +1,298 @@
1
+ // src/display.ts
2
+ var ESC = "\x1B[";
3
+ var ansi = {
4
+ cursorUp(n) {
5
+ return `${ESC}${n}A`;
6
+ },
7
+ cursorTo(col) {
8
+ return `${ESC}${col}G`;
9
+ },
10
+ eraseLine() {
11
+ return `${ESC}2K`;
12
+ },
13
+ eraseLines(n) {
14
+ let out = "";
15
+ for (let i = 0; i < n; i++) {
16
+ out += ansi.eraseLine();
17
+ if (i < n - 1) out += ansi.cursorUp(1);
18
+ }
19
+ return out;
20
+ },
21
+ cursorHide() {
22
+ return `${ESC}?25l`;
23
+ },
24
+ cursorShow() {
25
+ return `${ESC}?25h`;
26
+ }
27
+ };
28
+ var color = {
29
+ green(text) {
30
+ return `${ESC}32m${text}${ESC}0m`;
31
+ },
32
+ yellow(text) {
33
+ return `${ESC}33m${text}${ESC}0m`;
34
+ },
35
+ red(text) {
36
+ return `${ESC}31m${text}${ESC}0m`;
37
+ },
38
+ dim(text) {
39
+ return `${ESC}2m${text}${ESC}0m`;
40
+ }
41
+ };
42
+ var FILLED_DOT = "\u25CF";
43
+ var EMPTY_DOT = "\u25CB";
44
+ function renderDot(status) {
45
+ switch (status) {
46
+ case "pending":
47
+ return color.dim(EMPTY_DOT);
48
+ case "in-progress":
49
+ return color.yellow(FILLED_DOT);
50
+ case "done":
51
+ return color.green(FILLED_DOT);
52
+ case "failed":
53
+ return color.red(FILLED_DOT);
54
+ }
55
+ }
56
+ var StatusDisplay = class {
57
+ wave = null;
58
+ messages = [];
59
+ renderedLines = 0;
60
+ config;
61
+ viewportStart = 0;
62
+ resizeListener = null;
63
+ signalHandler = null;
64
+ constructor(config) {
65
+ this.config = {
66
+ messageLines: config?.messageLines ?? 4,
67
+ stream: config?.stream ?? process.stdout
68
+ };
69
+ }
70
+ getAvailableRows() {
71
+ const termHeight = this.config.stream.rows ?? 24;
72
+ const overhead = 2 + this.config.messageLines;
73
+ const available = termHeight - overhead;
74
+ return Math.max(1, available);
75
+ }
76
+ startWave(waveNum, totalWaves, tickets2) {
77
+ this.wave = {
78
+ waveNum,
79
+ totalWaves,
80
+ tickets: tickets2.map((t) => ({
81
+ ...t,
82
+ status: "pending"
83
+ }))
84
+ };
85
+ this.messages = [];
86
+ this.viewportStart = 0;
87
+ this.config.stream.write(ansi.cursorHide());
88
+ if (this.config.stream.isTTY) {
89
+ if (this.resizeListener) {
90
+ this.config.stream.removeListener(
91
+ "resize",
92
+ this.resizeListener
93
+ );
94
+ }
95
+ this.resizeListener = () => this.render();
96
+ this.config.stream.on(
97
+ "resize",
98
+ this.resizeListener
99
+ );
100
+ }
101
+ if (this.signalHandler) {
102
+ process.removeListener("SIGINT", this.signalHandler);
103
+ process.removeListener("SIGTERM", this.signalHandler);
104
+ }
105
+ this.signalHandler = () => this.cleanup();
106
+ process.on("SIGINT", this.signalHandler);
107
+ process.on("SIGTERM", this.signalHandler);
108
+ this.render();
109
+ }
110
+ setTicketStatus(num, status) {
111
+ if (!this.wave) return;
112
+ const ticket = this.wave.tickets.find((t) => t.num === num);
113
+ if (ticket) {
114
+ ticket.status = status;
115
+ if (status === "in-progress") {
116
+ const ticketIndex = this.wave.tickets.indexOf(ticket);
117
+ const availableRows = this.getAvailableRows();
118
+ const totalTickets = this.wave.tickets.length;
119
+ if (ticketIndex < this.viewportStart) {
120
+ this.viewportStart = ticketIndex;
121
+ } else if (ticketIndex >= this.viewportStart + availableRows) {
122
+ this.viewportStart = ticketIndex - availableRows + 3;
123
+ }
124
+ this.viewportStart = Math.max(
125
+ 0,
126
+ Math.min(
127
+ this.viewportStart,
128
+ totalTickets - 1
129
+ )
130
+ );
131
+ }
132
+ this.render();
133
+ }
134
+ }
135
+ log(message) {
136
+ this.messages.push(message);
137
+ if (this.messages.length > this.config.messageLines) {
138
+ this.messages.shift();
139
+ }
140
+ this.render();
141
+ }
142
+ cleanup() {
143
+ if (this.resizeListener && this.config.stream.isTTY) {
144
+ this.config.stream.removeListener("resize", this.resizeListener);
145
+ }
146
+ if (this.signalHandler) {
147
+ process.removeListener("SIGINT", this.signalHandler);
148
+ process.removeListener("SIGTERM", this.signalHandler);
149
+ }
150
+ this.config.stream.write(ansi.cursorShow());
151
+ }
152
+ render() {
153
+ if (!this.wave) return;
154
+ let output = "";
155
+ if (this.renderedLines > 0) {
156
+ output += ansi.eraseLines(this.renderedLines + 1);
157
+ }
158
+ const completed = this.wave.tickets.filter(
159
+ (t) => t.status === "done"
160
+ ).length;
161
+ const total = this.wave.tickets.length;
162
+ const pct = total === 0 ? 0 : Math.round(completed / total * 100);
163
+ output += `Wave ${this.wave.waveNum}/${this.wave.totalWaves} -- ${pct}% complete
164
+ `;
165
+ const availableRows = this.getAvailableRows();
166
+ const tickets2 = this.wave.tickets;
167
+ let renderedTicketCount = 0;
168
+ if (tickets2.length <= availableRows) {
169
+ for (const ticket of tickets2) {
170
+ const dot = renderDot(ticket.status);
171
+ output += `${dot} ${ticket.filename}
172
+ `;
173
+ }
174
+ renderedTicketCount = tickets2.length;
175
+ } else {
176
+ this.viewportStart = Math.max(
177
+ 0,
178
+ Math.min(
179
+ this.viewportStart,
180
+ tickets2.length - 1
181
+ )
182
+ );
183
+ let hasMoreAbove = this.viewportStart > 0;
184
+ let tentativeCount = availableRows - (hasMoreAbove ? 1 : 0);
185
+ let hasMoreBelow = this.viewportStart + tentativeCount < tickets2.length;
186
+ let ticketsToShow = Math.max(
187
+ 0,
188
+ tentativeCount - (hasMoreBelow ? 1 : 0)
189
+ );
190
+ const maxStart = Math.max(
191
+ 0,
192
+ tickets2.length - Math.max(1, ticketsToShow)
193
+ );
194
+ if (this.viewportStart > maxStart) {
195
+ this.viewportStart = maxStart;
196
+ hasMoreAbove = this.viewportStart > 0;
197
+ tentativeCount = availableRows - (hasMoreAbove ? 1 : 0);
198
+ hasMoreBelow = this.viewportStart + tentativeCount < tickets2.length;
199
+ ticketsToShow = Math.max(
200
+ 0,
201
+ tentativeCount - (hasMoreBelow ? 1 : 0)
202
+ );
203
+ }
204
+ if (hasMoreAbove) {
205
+ const moreCount = this.viewportStart;
206
+ output += ` ... ${moreCount} more above
207
+ `;
208
+ }
209
+ const visibleTickets = tickets2.slice(
210
+ this.viewportStart,
211
+ this.viewportStart + ticketsToShow
212
+ );
213
+ for (const ticket of visibleTickets) {
214
+ const dot = renderDot(ticket.status);
215
+ output += `${dot} ${ticket.filename}
216
+ `;
217
+ }
218
+ if (hasMoreBelow) {
219
+ const moreCount = tickets2.length - (this.viewportStart + ticketsToShow);
220
+ output += ` ... ${moreCount} more below
221
+ `;
222
+ }
223
+ renderedTicketCount = (hasMoreAbove ? 1 : 0) + visibleTickets.length + (hasMoreBelow ? 1 : 0);
224
+ }
225
+ output += "-".repeat(50) + "\n";
226
+ const msgLines = Math.max(
227
+ 0,
228
+ this.config.messageLines - this.messages.length
229
+ );
230
+ for (const msg of this.messages) {
231
+ output += `${msg}
232
+ `;
233
+ }
234
+ for (let i = 0; i < msgLines; i++) {
235
+ output += "\n";
236
+ }
237
+ this.renderedLines = 2 + renderedTicketCount + this.config.messageLines;
238
+ this.config.stream.write(output);
239
+ }
240
+ };
241
+
242
+ // demo-display.ts
243
+ function sleep(ms) {
244
+ return new Promise((r) => setTimeout(r, ms));
245
+ }
246
+ var tickets = [
247
+ { num: 1, filename: "001-add-login-page.md" },
248
+ { num: 2, filename: "002-fix-header-alignment.md" },
249
+ { num: 3, filename: "003-refactor-auth-module.md" },
250
+ { num: 4, filename: "004-update-dependencies.md" },
251
+ { num: 5, filename: "005-add-dark-mode.md" },
252
+ { num: 6, filename: "006-improve-error-messages.md" },
253
+ { num: 7, filename: "007-add-search-feature.md" },
254
+ { num: 8, filename: "008-optimize-queries.md" }
255
+ ];
256
+ async function main() {
257
+ const display = new StatusDisplay();
258
+ const wave1 = tickets.slice(0, 4);
259
+ display.startWave(1, 3, wave1);
260
+ display.log("Starting wave 1...");
261
+ for (const t of wave1) {
262
+ await sleep(600);
263
+ display.setTicketStatus(t.num, "in-progress");
264
+ display.log(`Agent working on ${t.filename}`);
265
+ await sleep(1200);
266
+ const status = t.num === 3 ? "failed" : "done";
267
+ display.setTicketStatus(t.num, status);
268
+ display.log(
269
+ status === "done" ? `Completed ${t.filename}` : `Failed ${t.filename}`
270
+ );
271
+ }
272
+ await sleep(1e3);
273
+ const wave2 = tickets.slice(4, 7);
274
+ display.startWave(2, 3, wave2);
275
+ display.log("Starting wave 2...");
276
+ for (const t of wave2) {
277
+ await sleep(600);
278
+ display.setTicketStatus(t.num, "in-progress");
279
+ display.log(`Agent working on ${t.filename}`);
280
+ await sleep(1e3);
281
+ display.setTicketStatus(t.num, "done");
282
+ display.log(`Completed ${t.filename}`);
283
+ }
284
+ await sleep(1e3);
285
+ const wave3 = tickets.slice(7);
286
+ display.startWave(3, 3, wave3);
287
+ display.log("Starting wave 3...");
288
+ await sleep(600);
289
+ display.setTicketStatus(8, "in-progress");
290
+ display.log("Agent working on 008-optimize-queries.md");
291
+ await sleep(1500);
292
+ display.setTicketStatus(8, "done");
293
+ display.log("Completed 008-optimize-queries.md");
294
+ await sleep(800);
295
+ display.cleanup();
296
+ console.log("\nDemo complete.");
297
+ }
298
+ main();
@@ -0,0 +1,280 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+ const ESC = "\x1B[";
4
+ const ansi = {
5
+ cursorUp(n) {
6
+ return `${ESC}${n}A`;
7
+ },
8
+ cursorTo(col) {
9
+ return `${ESC}${col}G`;
10
+ },
11
+ eraseLine() {
12
+ return `${ESC}2K`;
13
+ },
14
+ eraseLines(n) {
15
+ let out = "";
16
+ for (let i = 0; i < n; i++) {
17
+ out += ansi.eraseLine();
18
+ if (i < n - 1) out += ansi.cursorUp(1);
19
+ }
20
+ return out;
21
+ },
22
+ cursorHide() {
23
+ return `${ESC}?25l`;
24
+ },
25
+ cursorShow() {
26
+ return `${ESC}?25h`;
27
+ }
28
+ };
29
+ const color = {
30
+ green(text) {
31
+ return `${ESC}32m${text}${ESC}0m`;
32
+ },
33
+ yellow(text) {
34
+ return `${ESC}33m${text}${ESC}0m`;
35
+ },
36
+ red(text) {
37
+ return `${ESC}31m${text}${ESC}0m`;
38
+ },
39
+ dim(text) {
40
+ return `${ESC}2m${text}${ESC}0m`;
41
+ }
42
+ };
43
+ const FILLED_DOT = "\u25CF";
44
+ const EMPTY_DOT = "\u25CB";
45
+ function renderDot(status) {
46
+ switch (status) {
47
+ case "pending":
48
+ return color.dim(EMPTY_DOT);
49
+ case "in-progress":
50
+ return color.yellow(FILLED_DOT);
51
+ case "done":
52
+ return color.green(FILLED_DOT);
53
+ case "failed":
54
+ return color.red(FILLED_DOT);
55
+ }
56
+ }
57
+ __name(renderDot, "renderDot");
58
+ class StatusDisplay {
59
+ static {
60
+ __name(this, "StatusDisplay");
61
+ }
62
+ wave = null;
63
+ messages = [];
64
+ renderedLines = 0;
65
+ config;
66
+ viewportStart = 0;
67
+ resizeListener = null;
68
+ signalHandler = null;
69
+ constructor(config) {
70
+ this.config = {
71
+ messageLines: config?.messageLines ?? 4,
72
+ stream: config?.stream ?? process.stdout
73
+ };
74
+ }
75
+ getAvailableRows() {
76
+ const termHeight = this.config.stream.rows ?? 24;
77
+ const overhead = 2 + this.config.messageLines;
78
+ const available = termHeight - overhead;
79
+ return Math.max(1, available);
80
+ }
81
+ startWave(waveNum, totalWaves, tickets) {
82
+ this.wave = {
83
+ waveNum,
84
+ totalWaves,
85
+ tickets: tickets.map((t) => ({
86
+ ...t,
87
+ status: "pending"
88
+ }))
89
+ };
90
+ this.messages = [];
91
+ this.viewportStart = 0;
92
+ this.config.stream.write(ansi.cursorHide());
93
+ if (this.config.stream.isTTY) {
94
+ if (this.resizeListener) {
95
+ this.config.stream.removeListener(
96
+ "resize",
97
+ this.resizeListener
98
+ );
99
+ }
100
+ this.resizeListener = () => this.render();
101
+ this.config.stream.on(
102
+ "resize",
103
+ this.resizeListener
104
+ );
105
+ }
106
+ if (this.signalHandler) {
107
+ process.removeListener("SIGINT", this.signalHandler);
108
+ process.removeListener("SIGTERM", this.signalHandler);
109
+ }
110
+ this.signalHandler = () => this.cleanup();
111
+ process.on("SIGINT", this.signalHandler);
112
+ process.on("SIGTERM", this.signalHandler);
113
+ this.render();
114
+ }
115
+ setTicketStatus(num, status) {
116
+ if (!this.wave) return;
117
+ const ticket = this.wave.tickets.find((t) => t.num === num);
118
+ if (ticket) {
119
+ ticket.status = status;
120
+ if (status === "in-progress") {
121
+ const ticketIndex = this.wave.tickets.indexOf(ticket);
122
+ const availableRows = this.getAvailableRows();
123
+ const totalTickets = this.wave.tickets.length;
124
+ if (ticketIndex < this.viewportStart) {
125
+ this.viewportStart = ticketIndex;
126
+ } else if (ticketIndex >= this.viewportStart + availableRows) {
127
+ this.viewportStart = ticketIndex - availableRows + 3;
128
+ }
129
+ this.viewportStart = Math.max(
130
+ 0,
131
+ Math.min(
132
+ this.viewportStart,
133
+ totalTickets - 1
134
+ )
135
+ );
136
+ }
137
+ this.render();
138
+ }
139
+ }
140
+ log(message) {
141
+ this.messages.push(message);
142
+ if (this.messages.length > this.config.messageLines) {
143
+ this.messages.shift();
144
+ }
145
+ this.render();
146
+ }
147
+ cleanup() {
148
+ if (this.resizeListener && this.config.stream.isTTY) {
149
+ this.config.stream.removeListener("resize", this.resizeListener);
150
+ }
151
+ if (this.signalHandler) {
152
+ process.removeListener("SIGINT", this.signalHandler);
153
+ process.removeListener("SIGTERM", this.signalHandler);
154
+ }
155
+ this.config.stream.write(ansi.cursorShow());
156
+ }
157
+ render() {
158
+ if (!this.wave) return;
159
+ let output = "";
160
+ if (this.renderedLines > 0) {
161
+ output += ansi.eraseLines(this.renderedLines + 1);
162
+ }
163
+ const completed = this.wave.tickets.filter(
164
+ (t) => t.status === "done"
165
+ ).length;
166
+ const total = this.wave.tickets.length;
167
+ const pct = total === 0 ? 0 : Math.round(completed / total * 100);
168
+ output += `Wave ${this.wave.waveNum}/${this.wave.totalWaves} -- ${pct}% complete
169
+ `;
170
+ const availableRows = this.getAvailableRows();
171
+ const tickets = this.wave.tickets;
172
+ let renderedTicketCount = 0;
173
+ if (tickets.length <= availableRows) {
174
+ for (const ticket of tickets) {
175
+ const dot = renderDot(ticket.status);
176
+ output += `${dot} ${ticket.filename}
177
+ `;
178
+ }
179
+ renderedTicketCount = tickets.length;
180
+ } else {
181
+ this.viewportStart = Math.max(
182
+ 0,
183
+ Math.min(
184
+ this.viewportStart,
185
+ tickets.length - 1
186
+ )
187
+ );
188
+ let hasMoreAbove = this.viewportStart > 0;
189
+ let tentativeCount = availableRows - (hasMoreAbove ? 1 : 0);
190
+ let hasMoreBelow = this.viewportStart + tentativeCount < tickets.length;
191
+ let ticketsToShow = Math.max(
192
+ 0,
193
+ tentativeCount - (hasMoreBelow ? 1 : 0)
194
+ );
195
+ const maxStart = Math.max(
196
+ 0,
197
+ tickets.length - Math.max(1, ticketsToShow)
198
+ );
199
+ if (this.viewportStart > maxStart) {
200
+ this.viewportStart = maxStart;
201
+ hasMoreAbove = this.viewportStart > 0;
202
+ tentativeCount = availableRows - (hasMoreAbove ? 1 : 0);
203
+ hasMoreBelow = this.viewportStart + tentativeCount < tickets.length;
204
+ ticketsToShow = Math.max(
205
+ 0,
206
+ tentativeCount - (hasMoreBelow ? 1 : 0)
207
+ );
208
+ }
209
+ if (hasMoreAbove) {
210
+ const moreCount = this.viewportStart;
211
+ output += ` ... ${moreCount} more above
212
+ `;
213
+ }
214
+ const visibleTickets = tickets.slice(
215
+ this.viewportStart,
216
+ this.viewportStart + ticketsToShow
217
+ );
218
+ for (const ticket of visibleTickets) {
219
+ const dot = renderDot(ticket.status);
220
+ output += `${dot} ${ticket.filename}
221
+ `;
222
+ }
223
+ if (hasMoreBelow) {
224
+ const moreCount = tickets.length - (this.viewportStart + ticketsToShow);
225
+ output += ` ... ${moreCount} more below
226
+ `;
227
+ }
228
+ renderedTicketCount = (hasMoreAbove ? 1 : 0) + visibleTickets.length + (hasMoreBelow ? 1 : 0);
229
+ }
230
+ output += "-".repeat(50) + "\n";
231
+ const msgLines = Math.max(
232
+ 0,
233
+ this.config.messageLines - this.messages.length
234
+ );
235
+ for (const msg of this.messages) {
236
+ output += `${msg}
237
+ `;
238
+ }
239
+ for (let i = 0; i < msgLines; i++) {
240
+ output += "\n";
241
+ }
242
+ this.renderedLines = 2 + renderedTicketCount + this.config.messageLines;
243
+ this.config.stream.write(output);
244
+ }
245
+ }
246
+ class PlainDisplay {
247
+ static {
248
+ __name(this, "PlainDisplay");
249
+ }
250
+ startWave(waveNum, _totalWaves, tickets) {
251
+ console.log(
252
+ `
253
+ Wave ${waveNum}: ` + tickets.map((t) => t.filename).join(", ")
254
+ );
255
+ }
256
+ setTicketStatus(_num, _status) {
257
+ }
258
+ log(message) {
259
+ console.log(message);
260
+ }
261
+ cleanup() {
262
+ }
263
+ }
264
+ function createDisplay(stream) {
265
+ const s = stream ?? process.stdout;
266
+ if (s.isTTY) {
267
+ return new StatusDisplay({ stream: s });
268
+ }
269
+ return new PlainDisplay();
270
+ }
271
+ __name(createDisplay, "createDisplay");
272
+ export {
273
+ PlainDisplay,
274
+ StatusDisplay,
275
+ ansi,
276
+ color,
277
+ createDisplay,
278
+ renderDot
279
+ };
280
+ //# sourceMappingURL=display.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/display.ts"],
4
+ "sourcesContent": ["export type TicketStatus =\n | 'pending'\n | 'in-progress'\n | 'done'\n | 'failed'\n\nexport type DisplayTicket = {\n readonly num:number\n readonly filename:string\n status:TicketStatus\n}\n\nexport type WaveState = {\n readonly waveNum:number\n readonly totalWaves:number\n readonly tickets:Array<DisplayTicket>\n}\n\nexport type DisplayConfig = {\n readonly messageLines:number\n readonly stream:NodeJS.WriteStream\n}\n\nexport type Display = {\n startWave(\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<{\n num:number\n filename:string\n }>\n ):void\n setTicketStatus(num:number, status:TicketStatus):void\n log(message:string):void\n cleanup():void\n}\n\nconst ESC = '\\x1b['\n\nexport const ansi = {\n cursorUp (n:number):string {\n return `${ESC}${n}A`\n },\n cursorTo (col:number):string {\n return `${ESC}${col}G`\n },\n eraseLine ():string {\n return `${ESC}2K`\n },\n eraseLines (n:number):string {\n let out = ''\n for (let i = 0; i < n; i++) {\n out += ansi.eraseLine()\n if (i < n - 1) out += ansi.cursorUp(1)\n }\n return out\n },\n cursorHide ():string {\n return `${ESC}?25l`\n },\n cursorShow ():string {\n return `${ESC}?25h`\n },\n} as const\n\nexport const color = {\n green (text:string):string {\n return `${ESC}32m${text}${ESC}0m`\n },\n yellow (text:string):string {\n return `${ESC}33m${text}${ESC}0m`\n },\n red (text:string):string {\n return `${ESC}31m${text}${ESC}0m`\n },\n dim (text:string):string {\n return `${ESC}2m${text}${ESC}0m`\n },\n} as const\n\nconst FILLED_DOT = '\u25CF'\nconst EMPTY_DOT = '\u25CB'\n\nexport function renderDot (status:TicketStatus):string {\n switch (status) {\n case 'pending':\n return color.dim(EMPTY_DOT)\n case 'in-progress':\n return color.yellow(FILLED_DOT)\n case 'done':\n return color.green(FILLED_DOT)\n case 'failed':\n return color.red(FILLED_DOT)\n }\n}\n\nexport class StatusDisplay implements Display {\n private wave:WaveState|null = null\n private messages:Array<string> = []\n private renderedLines:number = 0\n private config:DisplayConfig\n private viewportStart:number = 0\n private resizeListener:(() => void)|null = null\n private signalHandler:(() => void)|null = null\n\n constructor (config?:Readonly<Partial<DisplayConfig>>) {\n this.config = {\n messageLines:config?.messageLines ?? 4,\n stream:config?.stream ?? process.stdout,\n }\n }\n\n private getAvailableRows ():number {\n const termHeight = this.config.stream.rows ?? 24\n const overhead = 2 + this.config.messageLines\n const available = termHeight - overhead\n return Math.max(1, available)\n }\n\n startWave (\n waveNum:number,\n totalWaves:number,\n tickets:Array<Readonly<{ num:number; filename:string }>>,\n ):void {\n this.wave = {\n waveNum,\n totalWaves,\n tickets:tickets.map(t => ({\n ...t,\n status:'pending' as const,\n })),\n }\n this.messages = []\n this.viewportStart = 0\n this.config.stream.write(ansi.cursorHide())\n\n if (this.config.stream.isTTY) {\n if (this.resizeListener) {\n this.config.stream.removeListener(\n 'resize', this.resizeListener,\n )\n }\n this.resizeListener = () => this.render()\n this.config.stream.on(\n 'resize', this.resizeListener,\n )\n }\n\n // Register signal handler to restore cursor on interruption\n if (this.signalHandler) {\n process.removeListener('SIGINT', this.signalHandler)\n process.removeListener('SIGTERM', this.signalHandler)\n }\n this.signalHandler = () => this.cleanup()\n process.on('SIGINT', this.signalHandler)\n process.on('SIGTERM', this.signalHandler)\n\n this.render()\n }\n\n setTicketStatus (num:number, status:TicketStatus):void {\n if (!this.wave) return\n\n const ticket = this.wave.tickets.find(t => t.num === num)\n if (ticket) {\n ticket.status = status\n\n if (status === 'in-progress') {\n const ticketIndex =\n this.wave.tickets.indexOf(ticket)\n const availableRows =\n this.getAvailableRows()\n const totalTickets =\n this.wave.tickets.length\n\n if (ticketIndex < this.viewportStart) {\n this.viewportStart = ticketIndex\n } else if (\n ticketIndex >=\n this.viewportStart + availableRows\n ) {\n // offset by 3 to keep context visible below\n // the active ticket\n this.viewportStart =\n ticketIndex - availableRows + 3\n }\n this.viewportStart = Math.max(\n 0,\n Math.min(\n this.viewportStart,\n totalTickets - 1,\n ),\n )\n }\n\n this.render()\n }\n }\n\n log (message:string):void {\n this.messages.push(message)\n if (this.messages.length > this.config.messageLines) {\n this.messages.shift()\n }\n this.render()\n }\n\n cleanup ():void {\n // Remove resize listener if it exists\n if (this.resizeListener && this.config.stream.isTTY) {\n this.config.stream.removeListener('resize', this.resizeListener)\n }\n // Remove signal handlers\n if (this.signalHandler) {\n process.removeListener('SIGINT', this.signalHandler)\n process.removeListener('SIGTERM', this.signalHandler)\n }\n this.config.stream.write(ansi.cursorShow())\n }\n\n private render ():void {\n if (!this.wave) return\n\n let output = ''\n\n if (this.renderedLines > 0) {\n output +=\n ansi.eraseLines(this.renderedLines + 1)\n }\n\n const completed = this.wave.tickets.filter(\n t => t.status === 'done',\n ).length\n const total = this.wave.tickets.length\n const pct = total === 0 ? 0 : Math.round((completed / total) * 100)\n\n output += `Wave ${this.wave.waveNum}/` +\n `${this.wave.totalWaves} -- ` +\n `${pct}% complete\\n`\n\n // Pagination logic\n const availableRows = this.getAvailableRows()\n const tickets = this.wave.tickets\n let renderedTicketCount = 0\n\n if (tickets.length <= availableRows) {\n // All tickets fit, no pagination\n for (const ticket of tickets) {\n const dot = renderDot(ticket.status)\n output += `${dot} ${ticket.filename}\\n`\n }\n renderedTicketCount = tickets.length\n } else {\n // Need pagination -- iterative clamp\n this.viewportStart = Math.max(\n 0,\n Math.min(\n this.viewportStart,\n tickets.length - 1,\n ),\n )\n\n let hasMoreAbove = this.viewportStart > 0\n let tentativeCount =\n availableRows - (hasMoreAbove ? 1 : 0)\n let hasMoreBelow =\n this.viewportStart + tentativeCount <\n tickets.length\n let ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0),\n )\n\n const maxStart = Math.max(\n 0,\n tickets.length - Math.max(1, ticketsToShow),\n )\n if (this.viewportStart > maxStart) {\n this.viewportStart = maxStart\n hasMoreAbove = this.viewportStart > 0\n tentativeCount =\n availableRows - (hasMoreAbove ? 1 : 0)\n hasMoreBelow =\n this.viewportStart + tentativeCount <\n tickets.length\n ticketsToShow = Math.max(\n 0,\n tentativeCount -\n (hasMoreBelow ? 1 : 0),\n )\n }\n\n // Show \"more above\" indicator if needed\n if (hasMoreAbove) {\n const moreCount = this.viewportStart\n output += ` ... ${moreCount} more above\\n`\n }\n\n // Get visible tickets and render them\n const visibleTickets = tickets.slice(\n this.viewportStart,\n this.viewportStart + ticketsToShow,\n )\n\n for (const ticket of visibleTickets) {\n const dot = renderDot(ticket.status)\n output += `${dot} ${ticket.filename}\\n`\n }\n\n // Show \"more below\" indicator if needed\n if (hasMoreBelow) {\n const moreCount =\n tickets.length -\n (this.viewportStart + ticketsToShow)\n output += ` ... ${moreCount} more below\\n`\n }\n\n renderedTicketCount = (hasMoreAbove ? 1 : 0) +\n visibleTickets.length +\n (hasMoreBelow ? 1 : 0)\n }\n\n output += '-'.repeat(50) + '\\n'\n\n const msgLines = Math.max(\n 0,\n this.config.messageLines - this.messages.length,\n )\n for (const msg of this.messages) {\n output += `${msg}\\n`\n }\n for (let i = 0; i < msgLines; i++) {\n output += '\\n'\n }\n\n this.renderedLines = 2 + renderedTicketCount + this.config.messageLines\n this.config.stream.write(output)\n }\n}\n\nexport class PlainDisplay implements Display {\n startWave (\n waveNum:number,\n _totalWaves:number,\n tickets:ReadonlyArray<{\n num:number\n filename:string\n }>\n ):void {\n console.log(\n `\\nWave ${waveNum}: ` +\n tickets.map(t => t.filename).join(', ')\n )\n }\n\n setTicketStatus (\n _num:number,\n _status:TicketStatus\n ):void {\n // no-op: plain mode reports via log()\n }\n\n log (message:string):void {\n console.log(message)\n }\n\n cleanup ():void {\n // no-op\n }\n}\n\nexport function createDisplay (\n stream?:NodeJS.WriteStream\n):Display {\n const s = stream ?? process.stdout\n if (s.isTTY) {\n return new StatusDisplay({ stream:s })\n }\n return new PlainDisplay()\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  runWaves,
15
15
  runWavesIsolated
16
16
  } from "./orchestrator.js";
17
+ import { createDisplay } from "./display.js";
17
18
  import {
18
19
  findFeatureDirs,
19
20
  resolveIssuesDir,
@@ -178,6 +179,7 @@ async function runExecute(argv) {
178
179
  }
179
180
  return;
180
181
  }
182
+ const display = createDisplay();
181
183
  let result;
182
184
  if (isGitRepo()) {
183
185
  result = await runWavesIsolated({
@@ -188,7 +190,8 @@ async function runExecute(argv) {
188
190
  timeout,
189
191
  logsDir,
190
192
  featureName,
191
- conflictStrategy
193
+ conflictStrategy,
194
+ display
192
195
  });
193
196
  } else {
194
197
  result = await runWaves(
@@ -197,7 +200,8 @@ async function runExecute(argv) {
197
200
  argv.provider,
198
201
  argv.model ?? null,
199
202
  timeout,
200
- logsDir
203
+ logsDir,
204
+ display
201
205
  );
202
206
  }
203
207
  if (!result.allDone) {