@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/lib/table.js +34 -11
  3. 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-]` returns to the table.
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
- // More parallel sessions on one account get 429 answers from the model api.
12
- const LIVE_JUDGES = 4;
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-] returns from a judge to the table. A terminal in the kitty keyboard mode sends it as CSI 93;5u.
18
- const BACK = ['\x1d', '\x1b[93;5u'];
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
- export function render({ title, rows, final, confirm, width }) {
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}…${RESET}`);
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-] scores${RESET}\x1b[K\x1b[${cursorY + 1};${cursorX + 1}H`);
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
- for (const r of rows.filter((x) => x.state === 'queued').slice(0, LIVE_JUDGES - live)) {
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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xsft/panels",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "description": "Run a panel of AI judges from cross.software/panels against a target.",
6
6
  "type": "module",