@xsft/panels 0.3.1 → 0.3.3
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 +1 -1
- package/lib/table.js +34 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Run the command in the folder of the product. Each judge starts in that folder,
|
|
|
31
31
|
|
|
32
32
|
On a terminal, `panels run` shows the score table, and each judge runs in its interactive harness in a pseudo-terminal inside the command. Each judge writes `judges/N/answer.json` and stays open. The table writes the report when each judge has an answer or an error, and the command exits from `report.json` when you quit. Without a terminal, the judges run in the background (`claude -p`, `codex exec`).
|
|
33
33
|
|
|
34
|
-
Keys in the table: `1` to `9` open the screen of that judge, and `q` quits and asks `y` while a judge still runs. In a judge, each key goes to the judge, and `ctrl-
|
|
34
|
+
Keys in the table: `1` to `9` open the screen of that judge, and `q` quits and asks `y` while a judge still runs. In a judge, each key goes to the judge, and `ctrl-q` returns to the table. A harness that has not seen the folder asks whether to trust it. The first judge starts alone; press `1`, answer, and the other judges start.
|
|
35
35
|
|
|
36
36
|
## Run folder
|
|
37
37
|
|
package/lib/table.js
CHANGED
|
@@ -8,28 +8,35 @@ import xterm from '@xterm/headless';
|
|
|
8
8
|
import { check, MINUTES_PER_CRITERION, parseAnswer, writeJudge } from './judge.js';
|
|
9
9
|
import { buildReport, renderMarkdown } from './report.js';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const LIVE_JUDGES = 10;
|
|
12
|
+
// A harness asks this in a folder it has not seen. Only the first judge starts until its screen no longer asks,
|
|
13
|
+
// so the engineer answers once and the harness saves the answer for the other judges.
|
|
14
|
+
const TRUST_PROMPT = /trust this folder/i;
|
|
15
|
+
const FIRST_JUDGE_HEAD_START = 3000;
|
|
13
16
|
const ORANGE = '\x1b[38;2;255;106;61m';
|
|
14
17
|
const GREY = '\x1b[90m';
|
|
15
18
|
const BOLD = '\x1b[1m';
|
|
16
19
|
const RESET = '\x1b[0m';
|
|
17
|
-
// ctrl-
|
|
18
|
-
const BACK = ['\
|
|
20
|
+
// ctrl-q returns from a judge to the table. A terminal in the kitty keyboard mode sends it as CSI 113;5u.
|
|
21
|
+
const BACK = ['\x11', '\x1b[113;5u'];
|
|
19
22
|
// Turns off each mode that the screen of a judge can turn on, so the table gets the keys and the screen as before.
|
|
20
23
|
const TABLE_MODES = '\x1b[?1049h\x1b[r\x1b[0m\x1b[4l\x1b[?6l\x1b[?7h\x1b[?1l\x1b>\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1004l\x1b[?2004l\x1b[<u\x1b[?25l\x1b[2J';
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
|
-
* The lines of the table. rows hold { rank, name, state, result }, and state is queued, running, done, or error.
|
|
26
|
+
* The lines of the table. rows hold { rank, name, state, result, trust }, and state is queued, running, done, or error.
|
|
27
|
+
* trust is true while the harness of a running judge asks whether to trust the folder.
|
|
24
28
|
* final is null until the report exists, then { status, path }. confirm is true while q waits for y.
|
|
25
29
|
*/
|
|
26
|
-
|
|
30
|
+
const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
31
|
+
|
|
32
|
+
export function render({ title, rows, final, confirm, width, frame = 0 }) {
|
|
27
33
|
const rule = `${GREY}${'─'.repeat(width)}${RESET}`;
|
|
28
34
|
const pad = Math.max(...rows.map((r) => r.name.length)) + 2;
|
|
29
35
|
const lines = [` ${BOLD}panels run${RESET} ${title}`, rule];
|
|
30
36
|
for (const r of rows) {
|
|
31
37
|
const head = `${String(r.rank).padStart(3)} ${r.name.padEnd(pad)}`;
|
|
32
|
-
if (r.state === 'running') lines.push(`${ORANGE}${head}
|
|
38
|
+
if (r.state === 'running' && r.trust) lines.push(`${ORANGE}${head}press ${r.rank} and answer the trust prompt${RESET}`);
|
|
39
|
+
else if (r.state === 'running') lines.push(`${ORANGE}${head}${SPIN[frame % SPIN.length]}${RESET}`);
|
|
33
40
|
else if (r.state === 'queued') lines.push(`${head}${GREY}queued${RESET}`);
|
|
34
41
|
else if (r.state === 'error') lines.push(`${head}error`);
|
|
35
42
|
else lines.push(`${head}${r.result.score} ${GREY}b${r.result.band}${RESET}`);
|
|
@@ -78,7 +85,14 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
|
|
|
78
85
|
}
|
|
79
86
|
if (judges[i] === undefined) return { state: 'queued' };
|
|
80
87
|
if (Date.now() - judges[i].started > MINUTES_PER_CRITERION * 60_000) return { state: 'error', error: `no answer.json after ${MINUTES_PER_CRITERION} minutes` };
|
|
81
|
-
return { state: 'running' };
|
|
88
|
+
return { state: 'running', trust: asksTrust(judges[i]) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function asksTrust(j) {
|
|
92
|
+
const buffer = j.term.buffer.active;
|
|
93
|
+
const lines = [];
|
|
94
|
+
for (let y = 0; y < buffer.length; y++) lines.push(buffer.getLine(y).translateToString(true));
|
|
95
|
+
return TRUST_PROMPT.test(lines.join(' '));
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
function start(rank) {
|
|
@@ -111,7 +125,7 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
|
|
|
111
125
|
function drawFooter() {
|
|
112
126
|
if (!ui.view) return;
|
|
113
127
|
const { cursorX, cursorY } = judges[ui.view - 1].term.buffer.active;
|
|
114
|
-
out.write(`\x1b[${out.rows};1H${RESET}${GREY} judge ${ui.view} ctrl-
|
|
128
|
+
out.write(`\x1b[${out.rows};1H${RESET}${GREY} judge ${ui.view} ctrl-q scores${RESET}\x1b[K\x1b[${cursorY + 1};${cursorX + 1}H`);
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
// Waits for a pause in the output, so the footer does not land inside an escape sequence that a chunk split.
|
|
@@ -141,8 +155,9 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
|
|
|
141
155
|
final = { status: report.status, path: relative(cwd, reportPath) };
|
|
142
156
|
}
|
|
143
157
|
|
|
158
|
+
let frame = 0;
|
|
144
159
|
function draw() {
|
|
145
|
-
const lines = render({ title: `${panel.owner}/${panel.name} v${panel.version}`, rows, final, confirm: ui.confirm, width: Math.min(out.columns, 100) });
|
|
160
|
+
const lines = render({ title: `${panel.owner}/${panel.name} v${panel.version}`, rows, final, confirm: ui.confirm, width: Math.min(out.columns, 100), frame });
|
|
146
161
|
out.write(`\x1b[H${lines.map((l) => `${l}\x1b[K`).join('\n')}\x1b[J`);
|
|
147
162
|
}
|
|
148
163
|
|
|
@@ -150,7 +165,10 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
|
|
|
150
165
|
if (!final) {
|
|
151
166
|
rows = criteria.map((c, i) => ({ ...c, ...state(c, i) }));
|
|
152
167
|
const live = rows.filter((r) => r.state === 'running').length;
|
|
153
|
-
|
|
168
|
+
const first = judges[0];
|
|
169
|
+
const hold = first && !first.exited && (Date.now() - first.started < FIRST_JUDGE_HEAD_START || rows[0].trust);
|
|
170
|
+
for (const r of rows.filter((x) => x.state === 'queued').slice(0, first ? LIVE_JUDGES - live : 1)) {
|
|
171
|
+
if (hold) break;
|
|
154
172
|
start(r.rank);
|
|
155
173
|
r.state = 'running';
|
|
156
174
|
}
|
|
@@ -161,6 +179,10 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
|
|
|
161
179
|
|
|
162
180
|
return new Promise((done) => {
|
|
163
181
|
const timer = setInterval(tick, 1000);
|
|
182
|
+
const spinner = setInterval(() => {
|
|
183
|
+
frame += 1;
|
|
184
|
+
if (!ui.view && !final) draw();
|
|
185
|
+
}, 100);
|
|
164
186
|
out.write('\x1b[?1049h\x1b[?25l');
|
|
165
187
|
process.stdin.setRawMode(true);
|
|
166
188
|
process.stdin.on('data', (data) => {
|
|
@@ -172,6 +194,7 @@ export function runTable({ panel, runDir, harness, cwd, target, brief, sources }
|
|
|
172
194
|
judges[from - 1].proc.write(key);
|
|
173
195
|
} else if (next.quit) {
|
|
174
196
|
clearInterval(timer);
|
|
197
|
+
clearInterval(spinner);
|
|
175
198
|
// The harness on PATH can be a shell script that runs the real binary as a child and waits for it, so
|
|
176
199
|
// SIGHUP goes to the process group of the judge. The pty makes each judge the leader of its own group.
|
|
177
200
|
for (const j of judges) if (j && !j.exited) process.kill(-j.proc.pid, 'SIGHUP');
|