@impeccable/reviews 0.1.1 → 0.1.2

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/dist/ui/look.js CHANGED
@@ -230,9 +230,15 @@ function runPage(scene, f, inner, st) {
230
230
  : t.status === 'skipped' ? '–'
231
231
  : '○';
232
232
  const ink = t.status === 'in_progress' || t.status === 'failed' ? st.full : t.status === 'skipped' ? st.faint : st.soft;
233
- // The task in progress carries the wait's timer, so a screen waiting on a host's build is never still.
234
- const detail = t.status === 'skipped' || t.status === 'failed' ? t.status : i === running && scene.wait ? scene.wait : undefined;
235
- return ink(`${mark} ${t.title}${detail ? ` · ${detail}` : ''}`);
233
+ const row = (detail) => `${mark} ${t.title}${detail ? ` · ${detail}` : ''}`;
234
+ if (t.status === 'skipped' || t.status === 'failed')
235
+ return ink(row(t.status));
236
+ if (i !== running || !scene.wait)
237
+ return ink(row());
238
+ // The task in progress says what the run waits for and the time so far, never how long it may wait, so a screen
239
+ // waiting on a host's build is never still; the time alone where the words would push it off the row.
240
+ const waiting = row(`waiting for ${scene.wait.host ? `${scene.wait.host}'s preview` : 'the preview'} · ${scene.wait.timer}`);
241
+ return ink(visibleWidth(waiting) <= inner ? waiting : row(scene.wait.timer));
236
242
  });
237
243
  const counted = scene.tasks.filter((t) => t.status !== 'skipped');
238
244
  const skipped = scene.tasks.length - counted.length;
@@ -277,8 +283,10 @@ function pageOf(f, inner) {
277
283
  switch (scene.kind) {
278
284
  case 'wait':
279
285
  return { heading: START.heading, text: said(START.text, inner, st), rule: divider(inner, st), action: [center(st.faint('One moment…'), inner)] };
280
- case 'intro':
281
- return { heading: START.heading, text: said(START.text, inner, st), rule: divider(inner, st), action: [center(st.full("Let's start with login"), inner)], ways: [['Continue', 'Enter']] };
286
+ case 'intro': {
287
+ const note = scene.note ? ['', ...block(hang(scene.note, Math.min(TEXT_WIDTH, inner), '! ').map(st.soft), inner)] : [];
288
+ return { heading: START.heading, text: said(START.text, inner, st), rule: divider(inner, st), action: [center(st.full("Let's start with login"), inner), ...note], ways: [['Continue', 'Enter']] };
289
+ }
282
290
  case 'signin': {
283
291
  // P still takes a pasted code on a remote shell; --help says so, the screen does not.
284
292
  const under = scene.note ?? (scene.copied ? 'Copied to clipboard!' : '');
@@ -24,7 +24,7 @@ export class RunState {
24
24
  tasks = [];
25
25
  previous;
26
26
  current;
27
- /** A bounded wait under way: the current line carries its timer, counting up to the limit. */
27
+ /** A bounded wait under way: the current line carries its timer, counting up. */
28
28
  waiting;
29
29
  /** When something last changed on screen: a status, the task list, an answered question. */
30
30
  changedAt;
@@ -51,12 +51,12 @@ export class RunState {
51
51
  this.touch();
52
52
  }
53
53
  /**
54
- * The wait's timer, "1:05 of 5:00", while one is under way: the task in progress carries it on the tasks screen.
55
- * `live` false gives the wait's start, "0:00 of 5:00", the same on every tick: a drawing that changes only when
56
- * a wait begins or ends, so the frames record the wait once rather than once a second.
54
+ * The wait under way, which the task in progress carries on the tasks screen: the host whose preview it is and the
55
+ * time so far, "1:05". `live` false gives the wait's start, "0:00", the same on every tick: a drawing that changes
56
+ * only when a wait begins or ends, so the frames record the wait once rather than once a second.
57
57
  */
58
58
  timer(live = true) {
59
- return this.waiting ? `${timer(live ? this.now() - this.waiting.since : 0)} of ${timer(this.waiting.limitMs)}` : undefined;
59
+ return this.waiting && { host: this.waiting.host, timer: timer(live ? this.now() - this.waiting.since : 0) };
60
60
  }
61
61
  setTasks(tasks) {
62
62
  this.tasks = tasks.map((t) => ({ ...t }));
@@ -70,14 +70,14 @@ export class RunState {
70
70
  * The band's two lines as they read now: after a minute with nothing new the
71
71
  * last line moves up, dimmed, under "Still working…" and the time since.
72
72
  * `quiet` is false while the person is being asked. During a bounded wait
73
- * the current line carries the timer, "1:05 of 5:00", so the screen never
73
+ * the current line carries the timer, "1:05", so the screen never
74
74
  * looks stalled; `live` false holds the timer at the wait's start, for a
75
75
  * drawing that changes only when the words do.
76
76
  */
77
77
  band(quiet = true, live = true) {
78
- if (this.waiting && this.current) {
79
- return { previous: this.previous, current: `${this.current} · ${this.timer(live)}` };
80
- }
78
+ const wait = this.timer(live);
79
+ if (wait && this.current)
80
+ return { previous: this.previous, current: `${this.current} · ${wait.timer}` };
81
81
  const silence = this.now() - this.changedAt;
82
82
  if (quiet && silence >= SILENCE_MS)
83
83
  return { previous: this.current, current: `Still working… ${elapsed(silence)}` };
@@ -1,13 +1,17 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { format } from 'node:util';
3
3
  import { CURSOR_MARKER, getCapabilities, Input, KeybindingsManager, matchesKey, ProcessTerminal, setKeybindings, TUI_KEYBINDINGS, TuiAltScreen } from '@earendil-works/pi-tui';
4
- import { readCheckout } from '../preflight.js';
5
4
  import { copyText } from '../login.js';
6
5
  import { workStarted } from '../stages.js';
7
6
  import { mountUi } from './bridge.js';
8
7
  import { createFrameWriter, frameState } from './frames.js';
9
8
  import { drawFrame, OUTCOME, PRODUCT, palette } from './look.js';
10
9
  import { RunState } from './run-state.js';
10
+ /** What the command does before a screen ends it at once (Ctrl+C before a run, or a signal): report the stop (main.ts), briefly. */
11
+ let beforeExit = async () => { };
12
+ export function onScreenExit(fn) {
13
+ beforeExit = fn;
14
+ }
11
15
  /** How often what moves on screen moves: the loader, the spinner. */
12
16
  const TICK_MS = 80;
13
17
  /** The run screen's first line, before the agent has said anything. */
@@ -44,7 +48,7 @@ class Wizard {
44
48
  onExit = () => this.stop();
45
49
  onSignal = (signal) => {
46
50
  this.stop();
47
- process.kill(process.pid, signal);
51
+ void beforeExit('terminated').finally(() => process.kill(process.pid, signal));
48
52
  };
49
53
  constructor() {
50
54
  // The alternate screen's own scrolling and search keys stay off: the wizard draws every row, and a typed answer keeps Home and End.
@@ -228,7 +232,8 @@ class Wizard {
228
232
  const run = this.views.find((v) => v.kind === 'run');
229
233
  if (!run) {
230
234
  this.stop();
231
- process.exit(130);
235
+ void beforeExit('ctrl_c').finally(() => process.exit(130));
236
+ return;
232
237
  }
233
238
  this.stopAsked = 0;
234
239
  for (const question of this.questions.splice(0))
@@ -330,7 +335,7 @@ class Wizard {
330
335
  if (!view)
331
336
  return { kind: 'wait' };
332
337
  if (view.kind === 'intro')
333
- return { kind: 'intro' };
338
+ return { kind: 'intro', note: view.note };
334
339
  if (view.kind === 'signin')
335
340
  return { kind: 'signin', link: view.link, copied: view.copied, again: view.again, note: view.pasting ? undefined : view.note };
336
341
  if (view.kind === 'pull')
@@ -401,30 +406,24 @@ function host() {
401
406
  wizard ??= new Wizard();
402
407
  return wizard;
403
408
  }
404
- function repository() {
405
- try {
406
- const checkout = readCheckout(process.cwd());
407
- return `${checkout.owner}/${checkout.repo}`;
408
- }
409
- catch {
410
- return undefined;
411
- }
412
- }
413
409
  /** Writes every screen into `dir` as it changes (--frames), for the eval. */
414
410
  export function recordFrames(dir) {
415
411
  frames = createFrameWriter(dir);
416
412
  }
417
- /** The first screen: what the command does, and the way on. Resolves true to go on, false to cancel. */
418
- export function showIntro() {
413
+ /** The first screen: what the command does, a warning to read before anything runs (`note`), and the way on; without a terminal, a line naming `repository`, the owner/name being set up. Resolves true to go on, false to cancel. */
414
+ export function showIntro(repository, note) {
419
415
  const screen = host();
420
416
  if (!screen) {
421
- console.log(`${PRODUCT}: setting up ${repository() ?? 'this repository'}.`);
417
+ console.log(`${PRODUCT}: setting up ${repository}.`);
418
+ if (note)
419
+ console.log(`! ${note}`);
422
420
  console.log();
423
421
  return Promise.resolve(true);
424
422
  }
425
423
  return new Promise((resolve) => {
426
424
  const view = {
427
425
  kind: 'intro',
426
+ note,
428
427
  done: (ok) => {
429
428
  screen.remove(view);
430
429
  if (ok)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impeccable/reviews",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Set up a repository for Impeccable for PRs from your terminal.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "private": false,
@@ -24,6 +24,7 @@
24
24
  "eval": "npm run build && node dist/eval/main.js"
25
25
  },
26
26
  "dependencies": {
27
+ "@anthropic-ai/sandbox-runtime": "0.0.78",
27
28
  "@earendil-works/pi-coding-agent": "~0.87.1",
28
29
  "@earendil-works/pi-tui": "~0.87.1"
29
30
  },