@umbra-privacy/ceremony 0.2.8 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +1307 -308
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,23 +4,38 @@
4
4
  import { render } from "ink";
5
5
 
6
6
  // src/components/App.tsx
7
- import { useEffect as useEffect5, useState as useState5 } from "react";
8
- import { Box as Box7, Text as Text7, useApp, useInput as useInput2 } from "ink";
7
+ import { useEffect as useEffect6, useState as useState6 } from "react";
8
+ import { Box as Box8, Text as Text8, useApp, useInput as useInput3 } from "ink";
9
9
 
10
10
  // src/cleanup.ts
11
11
  var pendingLeaveQueue = null;
12
+ var pendingClearSession = null;
12
13
  function setQueueCleanup(fn) {
13
14
  pendingLeaveQueue = fn;
14
15
  }
15
16
  function clearQueueCleanup() {
16
17
  pendingLeaveQueue = null;
17
18
  }
19
+ function setSessionCleanup(fn) {
20
+ pendingClearSession = fn;
21
+ }
22
+ function clearSessionCleanup() {
23
+ pendingClearSession = null;
24
+ }
18
25
  async function runQueueCleanup() {
19
26
  if (!pendingLeaveQueue) return;
20
27
  pendingLeaveQueue();
21
28
  pendingLeaveQueue = null;
22
29
  await new Promise((resolve) => setTimeout(resolve, 800));
23
30
  }
31
+ async function runSessionCleanup() {
32
+ if (!pendingClearSession) return;
33
+ try {
34
+ await pendingClearSession();
35
+ } catch {
36
+ }
37
+ pendingClearSession = null;
38
+ }
24
39
 
25
40
  // src/session.ts
26
41
  import { readFile as readFile2, unlink as unlink2, writeFile } from "fs/promises";
@@ -227,7 +242,16 @@ var STORE_FILE = process.env["CEREMONY_CONTRIBUTIONS_FILE"] ?? join2(homedir2(),
227
242
  async function load() {
228
243
  try {
229
244
  const raw = await readFile3(STORE_FILE, "utf8");
230
- return JSON.parse(raw);
245
+ const parsed = JSON.parse(raw);
246
+ const out = {};
247
+ for (const [trackId, value] of Object.entries(parsed)) {
248
+ if (Array.isArray(value)) {
249
+ out[trackId] = value;
250
+ } else if (value && typeof value === "object") {
251
+ out[trackId] = [value];
252
+ }
253
+ }
254
+ return out;
231
255
  } catch {
232
256
  return {};
233
257
  }
@@ -240,9 +264,21 @@ async function getContributions() {
240
264
  }
241
265
  async function recordContribution(trackId, contribution) {
242
266
  const store = await load();
243
- store[trackId] = contribution;
267
+ const existing = store[trackId] ?? [];
268
+ if (existing.some((c) => c.contributionId === contribution.contributionId)) {
269
+ return;
270
+ }
271
+ store[trackId] = [...existing, contribution];
244
272
  await save(store);
245
273
  }
274
+ function latestContribution(store, trackId) {
275
+ const list = store[trackId];
276
+ if (!list || list.length === 0) return null;
277
+ return list.reduce((a, b) => b.sequenceNumber > a.sequenceNumber ? b : a);
278
+ }
279
+ function roundCount(store, trackId) {
280
+ return store[trackId]?.length ?? 0;
281
+ }
246
282
 
247
283
  // src/components/Header.tsx
248
284
  import { Box, Text } from "ink";
@@ -498,37 +534,46 @@ function QueueView({ ceremonyId: ceremonyId2, trackId, token, onReady, onError }
498
534
  minute: "2-digit"
499
535
  }) : null;
500
536
  const fastPoll = status.queue_position <= 2;
537
+ const aheadCount = Math.max(0, status.queue_position - 1);
538
+ const peopleWord = aheadCount === 1 ? "person" : "people";
501
539
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
502
540
  /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
503
- /* @__PURE__ */ jsxs2(Text2, { children: [
504
- "Position",
541
+ aheadCount === 0 ? /* @__PURE__ */ jsxs2(Text2, { children: [
542
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "green", children: "You're next" }),
543
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " in line" })
544
+ ] }) : /* @__PURE__ */ jsxs2(Text2, { children: [
545
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: aheadCount }),
505
546
  " ",
506
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: status.queue_position }),
507
- status.queue_depth > 0 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
508
- " of ",
509
- status.queue_depth
547
+ peopleWord,
548
+ " ahead of you"
549
+ ] }),
550
+ status.queue_depth > 1 && /* @__PURE__ */ jsxs2(Fragment2, { children: [
551
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\xB7" }),
552
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
553
+ status.queue_depth,
554
+ " in queue"
510
555
  ] })
511
556
  ] }),
512
557
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\xB7" }),
513
558
  /* @__PURE__ */ jsxs2(Text2, { children: [
514
559
  "Estimated wait:",
515
560
  " ",
516
- /* @__PURE__ */ jsxs2(Text2, { bold: true, color: "cyan", children: [
517
- "~",
518
- waitMins,
519
- " min"
520
- ] })
561
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: aheadCount === 0 ? "any moment now" : `~${waitMins} min` })
521
562
  ] })
522
563
  ] }),
523
- status.status === "exporting" || status.status === "your_turn" || status.status === "ready_to_download" ? /* @__PURE__ */ jsx2(ExportingMessage, { status: status.status }) : status.active_since ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
564
+ status.status === "exporting" || status.status === "your_turn" || status.status === "ready_to_download" ? /* @__PURE__ */ jsx2(ExportingMessage, { status: status.status, slotExpiresAt: expiresAt }) : status.active_since ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
524
565
  "Another contributor is active",
525
566
  expiresAt ? ` \xB7 slot expires at ${expiresAt}` : ""
526
- ] }) }) : status.queue_position > 1 ? (
567
+ ] }) }) : aheadCount > 0 ? (
527
568
  // Slot is idle but people are ahead — they joined and left without releasing.
528
569
  // timeout_watchdog will clear each stale slot after contribution_timeout_secs.
529
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Slot is idle \u2014 waiting for positions ahead to respond or time out (up to ~5 min each)" })
570
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
571
+ "Slot is idle \u2014 waiting for ",
572
+ peopleWord,
573
+ " ahead to respond or time out (up to ~5 min each)"
574
+ ] })
530
575
  ) : (
531
- // Position 1, slot idle — advance_queue should fire shortly.
576
+ // At the front of the line, slot idle — advance_queue should fire shortly.
532
577
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Slot is idle \u2014 your turn is being prepared..." })
533
578
  ),
534
579
  pollErr ? /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
@@ -569,25 +614,38 @@ function TerminalSlotMessage({ status }) {
569
614
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: msg.detail })
570
615
  ] });
571
616
  }
572
- function ExportingMessage({ status }) {
617
+ function ExportingMessage({
618
+ status,
619
+ slotExpiresAt
620
+ }) {
573
621
  const exportingMsg = useCyclingMessage(EXPORTING_MESSAGES, 2500, status === "exporting");
574
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: status === "exporting" ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
575
- /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
576
- /* @__PURE__ */ jsx2(Spinner, { type: "dots" }),
577
- " ",
578
- exportingMsg,
579
- "\u2026"
580
- ] }),
581
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Your slot is active. The worker is preparing the challenge file just for you." })
582
- ] }) : /* @__PURE__ */ jsx2(Text2, { color: "green", children: "Challenge ready \u2014 loading contribution flow\u2026" }) });
622
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
623
+ status === "exporting" ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
624
+ /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
625
+ /* @__PURE__ */ jsx2(Spinner, { type: "dots" }),
626
+ " ",
627
+ exportingMsg,
628
+ "\u2026"
629
+ ] }),
630
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Your slot is active. The worker is preparing the challenge file just for you." })
631
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: "green", children: "Challenge ready \u2014 loading contribution flow\u2026" }),
632
+ slotExpiresAt && // Visible countdown is critical on power-18 circuits where the
633
+ // contributor compute window can run several minutes — without
634
+ // this they have no way to tell if they're about to be reaped.
635
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
636
+ "Your slot expires at ",
637
+ slotExpiresAt,
638
+ " (server time)."
639
+ ] })
640
+ ] });
583
641
  }
584
642
 
585
- // src/components/EntropyCollector.tsx
586
- import { useRef as useRef2, useState as useState3 } from "react";
643
+ // src/components/MouseEntropyCollector.tsx
644
+ import { useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
587
645
  import { Box as Box3, Text as Text3, useInput } from "ink";
588
646
 
589
647
  // src/entropy.ts
590
- import { createHash as createHash2, randomBytes } from "crypto";
648
+ import { createHash as createHash2, hkdfSync, randomBytes } from "crypto";
591
649
  function sha512(buf) {
592
650
  return createHash2("sha512").update(buf).digest();
593
651
  }
@@ -599,18 +657,405 @@ function buildEntropyFromKeystrokes(chars, timingsNs) {
599
657
  const osHash = sha512(randomBytes(64));
600
658
  return createHash2("sha512").update(keystrokeHash).update(osHash).digest("hex");
601
659
  }
660
+ var TRAIL_MIN_BYTES = 64;
661
+ function buildEntropyFromMouseTrail(strokes) {
662
+ const strokeBuf = Buffer.alloc(strokes.length * 12);
663
+ for (let i = 0; i < strokes.length; i++) {
664
+ const s = strokes[i];
665
+ strokeBuf.writeUInt16BE(s.x & 65535, i * 12);
666
+ strokeBuf.writeUInt16BE(s.y & 65535, i * 12 + 2);
667
+ strokeBuf.writeBigUInt64BE(s.dtNs, i * 12 + 4);
668
+ }
669
+ const padBytes = Math.max(0, TRAIL_MIN_BYTES - strokeBuf.length);
670
+ const trailInput = padBytes > 0 ? Buffer.concat([strokeBuf, randomBytes(padBytes)]) : strokeBuf;
671
+ const trailHash = sha512(trailInput);
672
+ const osHash = sha512(randomBytes(64));
673
+ return createHash2("sha512").update(trailHash).update(osHash).digest("hex");
674
+ }
675
+ function deriveCircuitEntropy(masterSeed, ceremonyId2, circuitName) {
676
+ const ikm = Buffer.from(masterSeed, "hex");
677
+ const salt = createHash2("sha256").update(ceremonyId2).digest();
678
+ const info = Buffer.from(`umbra-ceremony-${ceremonyId2}-circuit-${circuitName}`, "utf8");
679
+ const derived = Buffer.from(hkdfSync("sha512", ikm, salt, info, 64));
680
+ return derived.toString("hex");
681
+ }
602
682
 
603
- // src/components/EntropyCollector.tsx
683
+ // src/mouse.ts
684
+ var SGR_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
685
+ function classifyButton(buttonCode) {
686
+ if (buttonCode & 32) return "motion";
687
+ const base = buttonCode & 3;
688
+ if (base === 0) return "left";
689
+ if (base === 1) return "middle";
690
+ if (base === 2) return "right";
691
+ return "other";
692
+ }
693
+ function parseMouseChunk(chunk) {
694
+ const s = typeof chunk === "string" ? chunk : chunk.toString("binary");
695
+ const events = [];
696
+ SGR_RE.lastIndex = 0;
697
+ let m;
698
+ while ((m = SGR_RE.exec(s)) !== null) {
699
+ const buttonCode = parseInt(m[1], 10);
700
+ const col = parseInt(m[2], 10) - 1;
701
+ const row = parseInt(m[3], 10) - 1;
702
+ events.push({
703
+ button: classifyButton(buttonCode),
704
+ col,
705
+ row,
706
+ isRelease: m[4] === "m"
707
+ });
708
+ }
709
+ return events;
710
+ }
711
+ function enableMouseReporting() {
712
+ process.stdout.write("\x1B[?1003h\x1B[?1006h");
713
+ return () => {
714
+ process.stdout.write("\x1B[?1003l\x1B[?1006l");
715
+ };
716
+ }
717
+
718
+ // src/components/MouseEntropyCollector.tsx
604
719
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
720
+ var LEFT_OFFSET = 1;
721
+ var TOP_OFFSET = 2;
722
+ var MIN_W = 18;
723
+ var MIN_H = 12;
724
+ var MAX_W = 80;
725
+ var MAX_H = 32;
726
+ var DEFAULT_ASPECT = 2.2;
727
+ var ASPECT = (() => {
728
+ const raw = process.env["CEREMONY_CANVAS_ASPECT"];
729
+ if (!raw) return DEFAULT_ASPECT;
730
+ const parsed = Number.parseFloat(raw);
731
+ if (!Number.isFinite(parsed) || parsed < 0.5 || parsed > 6) return DEFAULT_ASPECT;
732
+ return parsed;
733
+ })();
734
+ var CHROME_ROWS = 6;
735
+ var CHROME_COLS = 2;
736
+ var MIN_ELAPSED_MS = 3e3;
737
+ var RENDER_INTERVAL_MS = 33;
738
+ function deriveCanvasSize(cols, rows) {
739
+ const availH = rows - CHROME_ROWS;
740
+ const availW = cols - CHROME_COLS;
741
+ let h = Math.min(MAX_H, availH);
742
+ let w = Math.round(h * ASPECT);
743
+ const wCap = Math.min(MAX_W, availW);
744
+ if (w > wCap) {
745
+ w = wCap;
746
+ h = Math.round(w / ASPECT);
747
+ }
748
+ if (w < MIN_W || h < MIN_H) return null;
749
+ return { w, h };
750
+ }
751
+ function readTerminalDims() {
752
+ return {
753
+ cols: process.stdout.columns ?? 80,
754
+ rows: process.stdout.rows ?? 24
755
+ };
756
+ }
757
+ function MouseEntropyCollector({ title, onComplete, onError }) {
758
+ const [dims, setDims] = useState3(() => {
759
+ const { cols, rows } = readTerminalDims();
760
+ return deriveCanvasSize(cols, rows);
761
+ });
762
+ const dimsRef = useRef2(dims);
763
+ useEffect3(() => {
764
+ dimsRef.current = dims;
765
+ }, [dims]);
766
+ const pixelsRef = useRef2(/* @__PURE__ */ new Set());
767
+ const strokesRef = useRef2([]);
768
+ const mouseDownRef = useRef2(false);
769
+ const lastPaintRef = useRef2(null);
770
+ const cursorRef = useRef2(null);
771
+ const lastStrokeTsRef = useRef2(process.hrtime.bigint());
772
+ const startTsRef = useRef2(process.hrtime.bigint());
773
+ const completedRef = useRef2(false);
774
+ const [pixelCount, setPixelCount] = useState3(0);
775
+ const [elapsedMs, setElapsedMs] = useState3(0);
776
+ const [done, setDone] = useState3(false);
777
+ const [nudge, setNudge] = useState3(null);
778
+ const [grid, setGrid] = useState3(
779
+ () => dims ? Array.from({ length: dims.h }, () => " ".repeat(dims.w)) : []
780
+ );
781
+ const [cursor, setCursor] = useState3(null);
782
+ const canvasW = dims?.w ?? 0;
783
+ const canvasH = dims?.h ?? 0;
784
+ const subH = canvasH * 2;
785
+ function idx(x, y) {
786
+ return y * canvasW + x;
787
+ }
788
+ function paintLine(pixels, x0, y0, x1, y1, onNew) {
789
+ let dx = Math.abs(x1 - x0);
790
+ let dy = -Math.abs(y1 - y0);
791
+ const sx = x0 < x1 ? 1 : -1;
792
+ const sy = y0 < y1 ? 1 : -1;
793
+ let err = dx + dy;
794
+ let x = x0;
795
+ let y = y0;
796
+ while (true) {
797
+ if (x >= 0 && x < canvasW && y >= 0 && y < subH) {
798
+ const id = idx(x, y);
799
+ if (!pixels.has(id)) {
800
+ pixels.add(id);
801
+ onNew(x, y);
802
+ }
803
+ }
804
+ if (x === x1 && y === y1) break;
805
+ const e2 = 2 * err;
806
+ if (e2 >= dy) {
807
+ if (x === x1) break;
808
+ err += dy;
809
+ x += sx;
810
+ }
811
+ if (e2 <= dx) {
812
+ if (y === y1) break;
813
+ err += dx;
814
+ y += sy;
815
+ }
816
+ }
817
+ }
818
+ useEffect3(() => {
819
+ const onResize = () => {
820
+ const { cols, rows } = readTerminalDims();
821
+ const next = deriveCanvasSize(cols, rows);
822
+ const cur = dimsRef.current;
823
+ if (next === null && cur === null) return;
824
+ if (next !== null && cur !== null && next.w === cur.w && next.h === cur.h) return;
825
+ pixelsRef.current.clear();
826
+ strokesRef.current = [];
827
+ lastPaintRef.current = null;
828
+ cursorRef.current = null;
829
+ startTsRef.current = process.hrtime.bigint();
830
+ lastStrokeTsRef.current = startTsRef.current;
831
+ setNudge(null);
832
+ setPixelCount(0);
833
+ setElapsedMs(0);
834
+ setCursor(null);
835
+ setGrid(next ? Array.from({ length: next.h }, () => " ".repeat(next.w)) : []);
836
+ setDims(next);
837
+ };
838
+ process.stdout.on("resize", onResize);
839
+ return () => {
840
+ process.stdout.off("resize", onResize);
841
+ };
842
+ }, []);
843
+ useEffect3(() => {
844
+ if (!dims) return;
845
+ const disableMouse = enableMouseReporting();
846
+ const onData = (chunk) => {
847
+ if (completedRef.current) return;
848
+ const events = parseMouseChunk(chunk);
849
+ for (const ev of events) handleMouseEvent(ev);
850
+ };
851
+ process.stdin.on("data", onData);
852
+ const renderTick = setInterval(() => {
853
+ if (completedRef.current) return;
854
+ const size = pixelsRef.current.size;
855
+ const elapsed = Number((process.hrtime.bigint() - startTsRef.current) / 1000000n);
856
+ if (size !== pixelCount) setPixelCount(size);
857
+ if (elapsed !== elapsedMs) setElapsedMs(elapsed);
858
+ const next = [];
859
+ for (let r = 0; r < canvasH; r++) {
860
+ let line = "";
861
+ for (let c = 0; c < canvasW; c++) {
862
+ const top = pixelsRef.current.has(idx(c, r * 2));
863
+ const bot = pixelsRef.current.has(idx(c, r * 2 + 1));
864
+ line += top && bot ? "\u2588" : top ? "\u2580" : bot ? "\u2584" : " ";
865
+ }
866
+ next.push(line);
867
+ }
868
+ setGrid(next);
869
+ setCursor(cursorRef.current);
870
+ }, RENDER_INTERVAL_MS);
871
+ return () => {
872
+ clearInterval(renderTick);
873
+ process.stdin.off("data", onData);
874
+ disableMouse();
875
+ };
876
+ }, [canvasW, canvasH]);
877
+ function handleMouseEvent(ev) {
878
+ if (completedRef.current) return;
879
+ const canvasCol = ev.col - LEFT_OFFSET;
880
+ const canvasRow = ev.row - TOP_OFFSET;
881
+ const inBounds = canvasCol >= 0 && canvasCol < canvasW && canvasRow >= 0 && canvasRow < canvasH;
882
+ if (inBounds) cursorRef.current = { row: canvasRow, col: canvasCol };
883
+ else cursorRef.current = null;
884
+ if (ev.button === "left" && !ev.isRelease) {
885
+ mouseDownRef.current = true;
886
+ lastPaintRef.current = null;
887
+ if (inBounds) paintAt(canvasCol, canvasRow);
888
+ return;
889
+ }
890
+ if (ev.isRelease) {
891
+ mouseDownRef.current = false;
892
+ lastPaintRef.current = null;
893
+ return;
894
+ }
895
+ if (ev.button === "motion" && mouseDownRef.current && inBounds) {
896
+ paintAt(canvasCol, canvasRow);
897
+ }
898
+ }
899
+ function paintAt(canvasCol, canvasRow) {
900
+ const x = canvasCol;
901
+ const yTop = canvasRow * 2;
902
+ const yBot = canvasRow * 2 + 1;
903
+ const now = process.hrtime.bigint();
904
+ const dt = now - lastStrokeTsRef.current;
905
+ lastStrokeTsRef.current = now;
906
+ if (lastPaintRef.current) {
907
+ const lp = lastPaintRef.current;
908
+ paintLine(pixelsRef.current, lp.x, lp.y, x, yTop, (px, py) => {
909
+ strokesRef.current.push({ x: px, y: py, dtNs: dt });
910
+ });
911
+ paintLine(pixelsRef.current, lp.x, lp.y + 1, x, yBot, () => {
912
+ });
913
+ } else {
914
+ if (!pixelsRef.current.has(idx(x, yTop))) {
915
+ pixelsRef.current.add(idx(x, yTop));
916
+ strokesRef.current.push({ x, y: yTop, dtNs: dt });
917
+ }
918
+ if (!pixelsRef.current.has(idx(x, yBot))) {
919
+ pixelsRef.current.add(idx(x, yBot));
920
+ }
921
+ }
922
+ lastPaintRef.current = { x, y: yTop };
923
+ }
924
+ useInput((input, key) => {
925
+ if (completedRef.current) return;
926
+ if (key.backspace || key.delete || input === "c" || input === "C") {
927
+ pixelsRef.current.clear();
928
+ strokesRef.current = [];
929
+ lastPaintRef.current = null;
930
+ cursorRef.current = null;
931
+ startTsRef.current = process.hrtime.bigint();
932
+ lastStrokeTsRef.current = startTsRef.current;
933
+ setNudge(null);
934
+ return;
935
+ }
936
+ if (key.return) {
937
+ const elapsed = Number((process.hrtime.bigint() - startTsRef.current) / 1000000n);
938
+ if (elapsed < MIN_ELAPSED_MS) {
939
+ const secLeft = Math.ceil((MIN_ELAPSED_MS - elapsed) / 1e3);
940
+ setNudge(`Wait ${secLeft}s before committing (anti-fat-finger).`);
941
+ return;
942
+ }
943
+ completedRef.current = true;
944
+ setDone(true);
945
+ try {
946
+ const entropy = buildEntropyFromMouseTrail(strokesRef.current);
947
+ setTimeout(() => onComplete(entropy), 250);
948
+ } catch (e) {
949
+ onError(e instanceof Error ? e : new Error(String(e)));
950
+ }
951
+ }
952
+ });
953
+ if (!dims) {
954
+ const { cols, rows } = readTerminalDims();
955
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
956
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: "\u26A0 Terminal too small for the mouse canvas" }),
957
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
958
+ "Need at least ",
959
+ MIN_W + CHROME_COLS,
960
+ " cols \xD7 ",
961
+ MIN_H + CHROME_ROWS,
962
+ " rows. Got ",
963
+ cols,
964
+ " \xD7 ",
965
+ rows,
966
+ "."
967
+ ] }),
968
+ /* @__PURE__ */ jsxs3(Text3, { children: [
969
+ "Resize this window larger, or press ",
970
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "\u232B Backspace" }),
971
+ " to go back and then ",
972
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "magenta", children: "K" }),
973
+ " on the mode-select modal to use keyboard entropy instead."
974
+ ] })
975
+ ] });
976
+ }
977
+ const secsElapsed = Math.floor(elapsedMs / 1e3);
978
+ const minSecs = Math.ceil(MIN_ELAPSED_MS / 1e3);
979
+ const timePct = Math.min(100, Math.round(elapsedMs / MIN_ELAPSED_MS * 100));
980
+ const filled = Math.round(timePct / 100 * 20);
981
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
982
+ const ready = elapsedMs >= MIN_ELAPSED_MS;
983
+ if (done) {
984
+ return /* @__PURE__ */ jsxs3(Box3, { gap: 2, children: [
985
+ /* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: "\u2713" }),
986
+ /* @__PURE__ */ jsx3(Text3, { children: "Entropy committed." })
987
+ ] });
988
+ }
989
+ const renderRow = (rowStr, rowIdx) => {
990
+ if (!cursor || cursor.row !== rowIdx) {
991
+ return /* @__PURE__ */ jsx3(Text3, { color: "white", children: rowStr });
992
+ }
993
+ const cIdx = cursor.col;
994
+ const before = rowStr.slice(0, cIdx);
995
+ const at = rowStr[cIdx] ?? " ";
996
+ const after = rowStr.slice(cIdx + 1);
997
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
998
+ /* @__PURE__ */ jsx3(Text3, { color: "white", children: before }),
999
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", inverse: true, children: at === " " ? "\xB7" : at }),
1000
+ /* @__PURE__ */ jsx3(Text3, { color: "white", children: after })
1001
+ ] });
1002
+ };
1003
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1004
+ /* @__PURE__ */ jsx3(Text3, { children: title ? /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: title.length > 70 ? title.slice(0, 67) + "\u2026" : title }) : /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "Draw to generate entropy \u2014 hold the LEFT mouse button and drag" }) }),
1005
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1006
+ "\u250C",
1007
+ "\u2500".repeat(canvasW),
1008
+ "\u2510"
1009
+ ] }),
1010
+ grid.map((row, i) => /* @__PURE__ */ jsxs3(Text3, { children: [
1011
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2502" }),
1012
+ renderRow(row, i),
1013
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2502" })
1014
+ ] }, i)),
1015
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1016
+ "\u2514",
1017
+ "\u2500".repeat(canvasW),
1018
+ "\u2518"
1019
+ ] }),
1020
+ /* @__PURE__ */ jsxs3(Box3, { gap: 2, children: [
1021
+ /* @__PURE__ */ jsxs3(Text3, { color: ready ? "green" : "cyan", children: [
1022
+ "[",
1023
+ bar,
1024
+ "]"
1025
+ ] }),
1026
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1027
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: ready ? "green" : "yellow", children: [
1028
+ secsElapsed,
1029
+ "s"
1030
+ ] }),
1031
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1032
+ " ",
1033
+ "/ ",
1034
+ minSecs,
1035
+ "s \xB7 ",
1036
+ pixelCount,
1037
+ " pixels drawn"
1038
+ ] })
1039
+ ] }),
1040
+ nudge && /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: nudge })
1041
+ ] }),
1042
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Enter = commit \xB7 c / \u232B = clear \xB7 Ctrl+C / Ctrl+Z = abort" })
1043
+ ] });
1044
+ }
1045
+
1046
+ // src/components/EntropyCollector.tsx
1047
+ import { useRef as useRef3, useState as useState4 } from "react";
1048
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
1049
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
605
1050
  var TARGET = 20;
606
1051
  function EntropyCollector({ onComplete, onError }) {
607
- const [count, setCount] = useState3(0);
608
- const [done, setDone] = useState3(false);
609
- const charsRef = useRef2([]);
610
- const timingsRef = useRef2([]);
611
- const lastRef = useRef2(process.hrtime.bigint());
612
- const completedRef = useRef2(false);
613
- useInput((input) => {
1052
+ const [count, setCount] = useState4(0);
1053
+ const [done, setDone] = useState4(false);
1054
+ const charsRef = useRef3([]);
1055
+ const timingsRef = useRef3([]);
1056
+ const lastRef = useRef3(process.hrtime.bigint());
1057
+ const completedRef = useRef3(false);
1058
+ useInput2((input) => {
614
1059
  if (completedRef.current) return;
615
1060
  const now = process.hrtime.bigint();
616
1061
  timingsRef.current.push(now - lastRef.current);
@@ -634,32 +1079,32 @@ function EntropyCollector({ onComplete, onError }) {
634
1079
  const pct = Math.round(count / TARGET * 100);
635
1080
  const stars = "*".repeat(Math.min(count, 32));
636
1081
  if (done) {
637
- return /* @__PURE__ */ jsxs3(Box3, { gap: 2, children: [
638
- /* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: "\u2713" }),
639
- /* @__PURE__ */ jsxs3(Text3, { children: [
1082
+ return /* @__PURE__ */ jsxs4(Box4, { gap: 2, children: [
1083
+ /* @__PURE__ */ jsx4(Text4, { color: "green", bold: true, children: "\u2713" }),
1084
+ /* @__PURE__ */ jsxs4(Text4, { children: [
640
1085
  "Entropy collected \u2014 [",
641
1086
  bar,
642
1087
  "] 100%"
643
1088
  ] })
644
1089
  ] });
645
1090
  }
646
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
647
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Type anything to generate entropy:" }),
648
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Keystroke timing (nanosecond intervals) is the randomness source." }),
649
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
650
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " > " }),
651
- /* @__PURE__ */ jsx3(Text3, { color: "green", children: stars }),
652
- /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u2588" })
1091
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
1092
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Type anything to generate entropy:" }),
1093
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Keystroke timing (nanosecond intervals) is the randomness source." }),
1094
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
1095
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " > " }),
1096
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: stars }),
1097
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "\u2588" })
653
1098
  ] }),
654
- /* @__PURE__ */ jsxs3(Box3, { gap: 2, marginTop: 1, children: [
655
- /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
1099
+ /* @__PURE__ */ jsxs4(Box4, { gap: 2, marginTop: 1, children: [
1100
+ /* @__PURE__ */ jsxs4(Text4, { color: "cyan", children: [
656
1101
  "[",
657
1102
  bar,
658
1103
  "]"
659
1104
  ] }),
660
- /* @__PURE__ */ jsxs3(Text3, { children: [
661
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: count > 0 ? "green" : "yellow", children: count }),
662
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1105
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1106
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: count > 0 ? "green" : "yellow", children: count }),
1107
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
663
1108
  "/",
664
1109
  TARGET,
665
1110
  " keystrokes (",
@@ -668,13 +1113,14 @@ function EntropyCollector({ onComplete, onError }) {
668
1113
  ] })
669
1114
  ] })
670
1115
  ] }),
671
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Shown as * \u2014 your actual input is never revealed." })
1116
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Shown as * \u2014 your actual input is never revealed." }),
1117
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "To quit without contributing: Ctrl+C or Ctrl+Z (Q is part of the entropy alphabet here)." })
672
1118
  ] });
673
1119
  }
674
1120
 
675
1121
  // src/components/ContributeFlow.tsx
676
- import { useEffect as useEffect4, useState as useState4 } from "react";
677
- import { Box as Box4, Text as Text4 } from "ink";
1122
+ import { useEffect as useEffect5, useState as useState5 } from "react";
1123
+ import { Box as Box5, Text as Text5 } from "ink";
678
1124
  import Spinner2 from "ink-spinner";
679
1125
  import { tmpdir as tmpdir2 } from "os";
680
1126
  import { join as join4 } from "path";
@@ -701,7 +1147,7 @@ async function cleanupTemp(path) {
701
1147
  }
702
1148
 
703
1149
  // src/components/ContributeFlow.tsx
704
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1150
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
705
1151
  var STEP_LABELS = ["downloading", "computing", "uploading", "signalling", "verifying"];
706
1152
  var STEP_INDEX = {
707
1153
  downloading: 0,
@@ -712,8 +1158,8 @@ var STEP_INDEX = {
712
1158
  };
713
1159
  function ContributeFlow(props) {
714
1160
  const { ceremonyId: ceremonyId2, trackId, token, slotStatus, entropy, displayName: displayName2 } = props;
715
- const [step, setStep] = useState4({ name: "downloading", bytesReceived: 0, total: null });
716
- useEffect4(() => {
1161
+ const [step, setStep] = useState5({ name: "downloading", bytesReceived: 0, total: null });
1162
+ useEffect5(() => {
717
1163
  let cancelled = false;
718
1164
  const challengePath = join4(tmpdir2(), `ceremony-challenge-${Date.now()}.mpcparams`);
719
1165
  let responsePath = null;
@@ -796,112 +1242,138 @@ function ContributeFlow(props) {
796
1242
  const currentIdx = STEP_INDEX[step.name] ?? 0;
797
1243
  const computingMsg = useCyclingMessage(COMPUTING_MESSAGES, 2500, step.name === "computing");
798
1244
  const verifyingMsg = useCyclingMessage(VERIFYING_MESSAGES, 3e3, step.name === "verifying");
799
- return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", gap: 1, children: STEP_LABELS.map((label, i) => {
800
- const isDone = i < currentIdx;
801
- const isActive = i === currentIdx;
802
- const isPending = i > currentIdx;
803
- let indicator;
804
- if (isDone) indicator = /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u2713" });
805
- else if (isActive) indicator = /* @__PURE__ */ jsx4(Spinner2, { type: "dots" });
806
- else indicator = /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u25CB" });
807
- let detail = null;
808
- if (isActive && step.name === "downloading" && step.total) {
809
- const pct = Math.round(step.bytesReceived / step.total * 100);
810
- const filled = Math.round(pct / 5);
811
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
812
- detail = /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
813
- " ",
814
- "[",
815
- bar,
816
- "] ",
817
- pct,
818
- "%"
819
- ] });
820
- }
821
- if (isActive && step.name === "verifying") {
822
- const elapsed = step.attempt <= 10 ? step.attempt * 3 : 10 * 3 + (step.attempt - 10) * 10;
823
- const mins = Math.floor(elapsed / 60);
824
- const secs = elapsed % 60;
825
- const time = mins > 0 ? `${mins}m ${secs.toString().padStart(2, "0")}s` : `${secs}s`;
826
- detail = /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
827
- " ",
828
- verifyingMsg,
829
- "\u2026 (",
830
- time,
831
- " elapsed)"
832
- ] });
833
- }
834
- if (isActive && step.name === "computing") {
835
- detail = /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1245
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
1246
+ STEP_LABELS.map((label, i) => {
1247
+ const isDone = i < currentIdx;
1248
+ const isActive = i === currentIdx;
1249
+ const isPending = i > currentIdx;
1250
+ let indicator;
1251
+ if (isDone) indicator = /* @__PURE__ */ jsx5(Text5, { color: "green", children: "\u2713" });
1252
+ else if (isActive) indicator = /* @__PURE__ */ jsx5(Spinner2, { type: "dots" });
1253
+ else indicator = /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u25CB" });
1254
+ let detail = null;
1255
+ if (isActive && step.name === "downloading" && step.total) {
1256
+ const pct = Math.round(step.bytesReceived / step.total * 100);
1257
+ const filled = Math.round(pct / 5);
1258
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
1259
+ detail = /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1260
+ " ",
1261
+ "[",
1262
+ bar,
1263
+ "] ",
1264
+ pct,
1265
+ "%"
1266
+ ] });
1267
+ }
1268
+ if (isActive && step.name === "verifying") {
1269
+ const elapsed = step.attempt <= 10 ? step.attempt * 3 : 10 * 3 + (step.attempt - 10) * 10;
1270
+ const mins = Math.floor(elapsed / 60);
1271
+ const secs = elapsed % 60;
1272
+ const time = mins > 0 ? `${mins}m ${secs.toString().padStart(2, "0")}s` : `${secs}s`;
1273
+ detail = /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1274
+ " ",
1275
+ verifyingMsg,
1276
+ "\u2026 (",
1277
+ time,
1278
+ " elapsed)"
1279
+ ] });
1280
+ }
1281
+ if (isActive && step.name === "computing") {
1282
+ detail = /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1283
+ " ",
1284
+ computingMsg,
1285
+ "\u2026"
1286
+ ] });
1287
+ }
1288
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
1289
+ /* @__PURE__ */ jsxs5(Box5, { gap: 2, children: [
1290
+ indicator,
1291
+ /* @__PURE__ */ jsxs5(
1292
+ Text5,
1293
+ {
1294
+ color: isDone ? "green" : isActive ? "white" : void 0,
1295
+ bold: isActive,
1296
+ dimColor: isPending,
1297
+ children: [
1298
+ label.charAt(0).toUpperCase() + label.slice(1),
1299
+ " ",
1300
+ isActive && label === "computing" && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "(entropy stays local)" })
1301
+ ]
1302
+ }
1303
+ )
1304
+ ] }),
1305
+ detail
1306
+ ] }, label);
1307
+ }),
1308
+ slotStatus.slot_expires_at && step.name !== "verifying" && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1309
+ "Your slot expires at",
1310
+ " ",
1311
+ new Date(slotStatus.slot_expires_at).toLocaleTimeString([], {
1312
+ hour: "2-digit",
1313
+ minute: "2-digit"
1314
+ }),
1315
+ " ",
1316
+ "(server time)."
1317
+ ] }) }),
1318
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
1319
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
1320
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
1321
+ "Need to bail? Press",
836
1322
  " ",
837
- computingMsg,
838
- "\u2026"
839
- ] });
840
- }
841
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
842
- /* @__PURE__ */ jsxs4(Box4, { gap: 2, children: [
843
- indicator,
844
- /* @__PURE__ */ jsxs4(
845
- Text4,
846
- {
847
- color: isDone ? "green" : isActive ? "white" : void 0,
848
- bold: isActive,
849
- dimColor: isPending,
850
- children: [
851
- label.charAt(0).toUpperCase() + label.slice(1),
852
- " ",
853
- isActive && label === "computing" && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "(entropy stays local)" })
854
- ]
855
- }
856
- )
1323
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Q" }),
1324
+ ", ",
1325
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Ctrl+C" }),
1326
+ ", or ",
1327
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Ctrl+Z" }),
1328
+ " \u2014 your slot is released immediately so the next contributor can take over."
857
1329
  ] }),
858
- detail
859
- ] }, label);
860
- }) });
1330
+ step.name === "computing" && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "(During this step snarkjs is busy \u2014 Ctrl+C / Ctrl+Z respond faster than Q.)" })
1331
+ ] })
1332
+ ] });
861
1333
  }
862
1334
 
863
1335
  // src/components/Attestation.tsx
864
- import { Box as Box5, Text as Text5 } from "ink";
865
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1336
+ import { Box as Box6, Text as Text6 } from "ink";
1337
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
866
1338
  function Attestation({ contribution }) {
867
1339
  const hashShort = contribution.contributionHash ? `${contribution.contributionHash.slice(0, 16)}...${contribution.contributionHash.slice(-8)}` : "(pending verification)";
868
1340
  const verifiedAt = contribution.verifiedAt ? new Date(contribution.verifiedAt).toLocaleString() : null;
869
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
870
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "green", children: "\u2713 Contribution verified!" }),
871
- /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 0, children: [
872
- /* @__PURE__ */ jsxs5(Box5, { gap: 2, children: [
873
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Circuit" }),
874
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: contribution.circuitName })
1341
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
1342
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "green", children: "\u2713 Contribution verified!" }),
1343
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 0, children: [
1344
+ /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
1345
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Circuit" }),
1346
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: contribution.circuitName })
875
1347
  ] }),
876
- /* @__PURE__ */ jsxs5(Box5, { gap: 2, children: [
877
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Round " }),
878
- /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
1348
+ /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
1349
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Round " }),
1350
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
879
1351
  "#",
880
1352
  contribution.sequenceNumber
881
1353
  ] })
882
1354
  ] }),
883
- /* @__PURE__ */ jsxs5(Box5, { gap: 2, children: [
884
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Hash " }),
885
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: hashShort })
1355
+ /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
1356
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Hash " }),
1357
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: hashShort })
886
1358
  ] }),
887
- verifiedAt && /* @__PURE__ */ jsxs5(Box5, { gap: 2, children: [
888
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Time " }),
889
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: verifiedAt })
1359
+ verifiedAt && /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
1360
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Time " }),
1361
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: verifiedAt })
890
1362
  ] })
891
1363
  ] }),
892
- contribution.contributionHash && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
893
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Full hash (share this to prove participation):" }),
894
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", wrap: "wrap", children: contribution.contributionHash })
1364
+ contribution.contributionHash && /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
1365
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Full hash (share this to prove participation):" }),
1366
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", wrap: "wrap", children: contribution.contributionHash })
895
1367
  ] })
896
1368
  ] });
897
1369
  }
898
1370
 
899
1371
  // src/components/InfoModal.tsx
900
- import { Box as Box6, Text as Text6 } from "ink";
901
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1372
+ import { Box as Box7, Text as Text7 } from "ink";
1373
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
902
1374
  function InfoModal() {
903
- return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsxs6(
904
- Box6,
1375
+ return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsxs7(
1376
+ Box7,
905
1377
  {
906
1378
  flexDirection: "column",
907
1379
  borderStyle: "round",
@@ -911,17 +1383,17 @@ function InfoModal() {
911
1383
  width: 84,
912
1384
  gap: 1,
913
1385
  children: [
914
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: "Umbra Phase 2 Trusted-Setup Ceremony" }),
915
- /* @__PURE__ */ jsx6(Text6, { children: "Umbra uses Groth16 zero-knowledge proofs to give Solana users on-chain privacy. Every circuit needs a one-time multi-party ceremony to generate its proving key safely \u2014 that is Phase 2. Each contributor adds their own secret entropy and destroys it afterward. The setup stays secure as long as AT LEAST ONE contributor erased theirs, which is why your single contribution genuinely matters." }),
916
- /* @__PURE__ */ jsx6(Text6, { children: "What you will do: tap 20 keys to seed your entropy, download the latest challenge file, run snarkjs locally to combine your secret with the parameters, and upload the response. Your secret never leaves your machine. Erase it when you are done." }),
917
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Press B, Esc or ? to close" })
1386
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "Umbra Phase 2 Trusted-Setup Ceremony" }),
1387
+ /* @__PURE__ */ jsx7(Text7, { children: "Umbra uses Groth16 zero-knowledge proofs to give Solana users on-chain privacy. Every circuit needs a one-time multi-party ceremony to generate its proving key safely \u2014 that is Phase 2. Each contributor adds their own secret entropy and destroys it afterward. The setup stays secure as long as AT LEAST ONE contributor erased theirs, which is why your single contribution genuinely matters." }),
1388
+ /* @__PURE__ */ jsx7(Text7, { children: "What you will do: tap 20 keys to seed your entropy, download the latest challenge file, run snarkjs locally to combine your secret with the parameters, and upload the response. Your secret never leaves your machine. Erase it when you are done." }),
1389
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press B, Esc or ? to close" })
918
1390
  ]
919
1391
  }
920
1392
  ) });
921
1393
  }
922
1394
 
923
1395
  // src/components/App.tsx
924
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1396
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
925
1397
  var NAME_MAX_LEN = 100;
926
1398
  var NAME_VALID_RE = /^[\p{L}\p{N} _.\-]*$/u;
927
1399
  function elideMiddle(s, maxLen) {
@@ -930,27 +1402,47 @@ function elideMiddle(s, maxLen) {
930
1402
  const keepTail = Math.floor((maxLen - 1) / 2);
931
1403
  return s.slice(0, keepHead) + "\u2026" + s.slice(s.length - keepTail);
932
1404
  }
1405
+ function orderTracksForAuto(tracks, contributed) {
1406
+ const openTracks = tracks.filter((t) => t.status === "open");
1407
+ const fresh = [];
1408
+ const alreadyDone = [];
1409
+ for (const t of openTracks) {
1410
+ if ((contributed[t.id]?.length ?? 0) > 0) alreadyDone.push(t);
1411
+ else fresh.push(t);
1412
+ }
1413
+ return [...fresh, ...alreadyDone];
1414
+ }
933
1415
  function copyToClipboardOSC52(value) {
934
1416
  const payload = Buffer.from(value, "utf8").toString("base64");
935
1417
  process.stdout.write(`\x1B]52;c;${payload}\x07`);
936
1418
  }
937
1419
  function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName }) {
938
1420
  const { exit } = useApp();
939
- const [activeCeremonyId, setActiveCeremonyId] = useState5(initialCeremonyId);
940
- const [displayName2, setDisplayName] = useState5(initialDisplayName ?? "anonymous");
941
- const [nameSet, setNameSet] = useState5(initialDisplayName !== void 0);
942
- const [screen, setScreen] = useState5(
1421
+ const [activeCeremonyId, setActiveCeremonyId] = useState6(initialCeremonyId);
1422
+ const [displayName2, setDisplayName] = useState6(initialDisplayName ?? "anonymous");
1423
+ const [nameSet, setNameSet] = useState6(initialDisplayName !== void 0);
1424
+ const [screen, setScreenRaw] = useState6(
943
1425
  initialDisplayName === void 0 ? { name: "name-input", value: "" } : initialCeremonyId ? { name: "loading" } : { name: "ceremony-picker", ceremonies: [], loading: true }
944
1426
  );
945
- const [ceremony, setCeremony] = useState5(null);
946
- const [session, setSession] = useState5(null);
947
- const [contributed, setContributed] = useState5({});
948
- const [selectedIdx, setSelectedIdx] = useState5(0);
949
- const [tab, setTab] = useState5(0);
950
- const [contribCursor, setContribCursor] = useState5(0);
951
- const [copyToast, setCopyToast] = useState5(null);
952
- const [showInfo, setShowInfo] = useState5(false);
953
- useEffect5(() => {
1427
+ const setScreen = (next) => {
1428
+ setScreenRaw((prev) => {
1429
+ const resolved = typeof next === "function" ? next(prev) : next;
1430
+ if (prev.name !== resolved.name) {
1431
+ process.stdout.write("\x1B[2J\x1B[H");
1432
+ }
1433
+ return resolved;
1434
+ });
1435
+ };
1436
+ const [ceremony, setCeremony] = useState6(null);
1437
+ const [session, setSession] = useState6(null);
1438
+ const [contributed, setContributed] = useState6({});
1439
+ const [selectedIdx, setSelectedIdx] = useState6(0);
1440
+ const [tab, setTab] = useState6(0);
1441
+ const [contribCursor, setContribCursor] = useState6(0);
1442
+ const [copyToast, setCopyToast] = useState6(null);
1443
+ const [showInfo, setShowInfo] = useState6(false);
1444
+ const [entropyMode, setEntropyMode] = useState6("mouse");
1445
+ useEffect6(() => {
954
1446
  if (!nameSet) return;
955
1447
  if (!initialCeremonyId) {
956
1448
  loadCeremonies();
@@ -998,7 +1490,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
998
1490
  return;
999
1491
  }
1000
1492
  setSelectedIdx(0);
1001
- setScreen({ name: "tracks", tracks });
1493
+ setScreen({ name: "mode-select", tracks });
1002
1494
  } catch (e) {
1003
1495
  if (e.code === "INVALID_SESSION" || e.status === 401) {
1004
1496
  try {
@@ -1015,7 +1507,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1015
1507
  return;
1016
1508
  }
1017
1509
  setSelectedIdx(0);
1018
- setScreen({ name: "tracks", tracks });
1510
+ setScreen({ name: "mode-select", tracks });
1019
1511
  } catch (e2) {
1020
1512
  setScreen({
1021
1513
  name: "error",
@@ -1073,9 +1565,25 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1073
1565
  setScreen({ name: "ceremony-picker", ceremonies: [], loading: true });
1074
1566
  loadCeremonies();
1075
1567
  }
1076
- useEffect5(() => {
1077
- if (screen.name === "queue" && session) {
1078
- const { trackId } = screen;
1568
+ useEffect6(() => {
1569
+ if (session) {
1570
+ setSessionCleanup(clearSession);
1571
+ } else {
1572
+ clearSessionCleanup();
1573
+ }
1574
+ }, [session]);
1575
+ useEffect6(() => {
1576
+ let trackId = null;
1577
+ if (screen.name === "queue" || screen.name === "entropy" || screen.name === "contribute") {
1578
+ trackId = screen.trackId;
1579
+ } else if (screen.name === "auto-running") {
1580
+ if (screen.subPhase.kind === "waiting" || screen.subPhase.kind === "contributing" || screen.subPhase.kind === "releasing") {
1581
+ trackId = screen.subPhase.trackId;
1582
+ } else if (screen.current) {
1583
+ trackId = screen.current.id;
1584
+ }
1585
+ }
1586
+ if (trackId && session) {
1079
1587
  setQueueCleanup(() => {
1080
1588
  api.leaveQueue(activeCeremonyId, trackId, session.session_token).catch(() => {
1081
1589
  });
@@ -1083,8 +1591,69 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1083
1591
  } else {
1084
1592
  clearQueueCleanup();
1085
1593
  }
1086
- }, [screen.name]);
1087
- useInput2((input, key) => {
1594
+ }, [screen.name, screen.name === "auto-running" ? screen.subPhase.kind : null]);
1595
+ useEffect6(() => {
1596
+ if (screen.name !== "auto-running") return;
1597
+ if (screen.current !== null) return;
1598
+ if (screen.subPhase.kind !== "queueing") return;
1599
+ if (!session) return;
1600
+ const currentScreen = screen;
1601
+ (async () => {
1602
+ let next = null;
1603
+ let nextRemaining = currentScreen.remaining;
1604
+ let nextRetryQueue = currentScreen.retryQueue;
1605
+ let nextRetryingPhase = currentScreen.retryingPhase;
1606
+ if (currentScreen.remaining.length > 0) {
1607
+ next = currentScreen.remaining[0];
1608
+ nextRemaining = currentScreen.remaining.slice(1);
1609
+ } else if (currentScreen.retryQueue.length > 0 && !currentScreen.retryingPhase) {
1610
+ nextRemaining = currentScreen.retryQueue.slice(1);
1611
+ next = currentScreen.retryQueue[0];
1612
+ nextRetryQueue = [];
1613
+ nextRetryingPhase = true;
1614
+ }
1615
+ if (!next) {
1616
+ setScreen({ name: "auto-summary", outcomes: currentScreen.done });
1617
+ return;
1618
+ }
1619
+ try {
1620
+ await api.joinQueue(activeCeremonyId, next.id, session.session_token);
1621
+ } catch (e) {
1622
+ const reason = e?.message ?? String(e);
1623
+ setScreen({
1624
+ ...currentScreen,
1625
+ remaining: nextRemaining,
1626
+ retryQueue: nextRetryQueue,
1627
+ retryingPhase: nextRetryingPhase,
1628
+ current: null,
1629
+ subPhase: { kind: "queueing" },
1630
+ done: [
1631
+ ...currentScreen.done,
1632
+ {
1633
+ trackId: next.id,
1634
+ circuitName: next.circuit_name,
1635
+ result: "failed",
1636
+ reason: `joinQueue: ${reason}`
1637
+ }
1638
+ ]
1639
+ });
1640
+ return;
1641
+ }
1642
+ setScreen({
1643
+ ...currentScreen,
1644
+ remaining: nextRemaining,
1645
+ retryQueue: nextRetryQueue,
1646
+ retryingPhase: nextRetryingPhase,
1647
+ current: next,
1648
+ subPhase: { kind: "waiting", trackId: next.id, circuitName: next.circuit_name }
1649
+ });
1650
+ })();
1651
+ }, [
1652
+ screen.name,
1653
+ screen.name === "auto-running" ? screen.subPhase.kind : null,
1654
+ screen.name === "auto-running" ? screen.current?.id ?? null : null
1655
+ ]);
1656
+ useInput3((input, key) => {
1088
1657
  const q = input.toLowerCase();
1089
1658
  if (showInfo) {
1090
1659
  if (key.escape || input === "?" || q === "b") {
@@ -1097,7 +1666,7 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1097
1666
  }
1098
1667
  return;
1099
1668
  }
1100
- if (input === "?" && screen.name !== "entropy") {
1669
+ if (input === "?" && screen.name !== "entropy" && screen.name !== "auto-entropy") {
1101
1670
  setShowInfo(true);
1102
1671
  return;
1103
1672
  }
@@ -1124,23 +1693,84 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1124
1693
  }
1125
1694
  return;
1126
1695
  }
1127
- if (q === "q" && screen.name !== "entropy") {
1128
- if (screen.name === "queue" && session) {
1696
+ if (q === "q" && screen.name !== "entropy" && screen.name !== "auto-entropy") {
1697
+ let trackId = null;
1698
+ if (screen.name === "queue" || screen.name === "contribute") {
1699
+ trackId = screen.trackId;
1700
+ } else if (screen.name === "auto-running") {
1701
+ if (screen.subPhase.kind === "waiting" || screen.subPhase.kind === "contributing" || screen.subPhase.kind === "releasing") {
1702
+ trackId = screen.subPhase.trackId;
1703
+ }
1704
+ }
1705
+ if (trackId && session) {
1129
1706
  clearQueueCleanup();
1130
- api.leaveQueue(activeCeremonyId, screen.trackId, session.session_token).catch(() => {
1707
+ clearSessionCleanup();
1708
+ api.leaveQueue(activeCeremonyId, trackId, session.session_token).catch(() => {
1709
+ });
1710
+ clearSession().catch(() => {
1131
1711
  });
1132
1712
  setTimeout(() => exit(), 500);
1713
+ } else if (session) {
1714
+ clearSessionCleanup();
1715
+ clearSession().catch(() => {
1716
+ });
1717
+ setTimeout(() => exit(), 100);
1133
1718
  } else {
1134
1719
  exit();
1135
1720
  }
1136
1721
  return;
1137
1722
  }
1138
1723
  if (key.backspace || key.delete) {
1139
- if (!initialCeremonyId && (screen.name === "tracks" || screen.name === "error")) {
1724
+ if (!initialCeremonyId && (screen.name === "tracks" || screen.name === "error" || screen.name === "mode-select" || screen.name === "auto-summary")) {
1140
1725
  goCeremonyPicker();
1141
1726
  return;
1142
1727
  }
1143
- if (screen.name !== "tracks" && screen.name !== "loading" && screen.name !== "joining" && screen.name !== "entropy" && screen.name !== "ceremony-picker") {
1728
+ if (screen.name === "auto-running" && (screen.subPhase.kind === "waiting" || screen.subPhase.kind === "contributing") && screen.current && session) {
1729
+ const skipped = screen.current;
1730
+ const skippedTrackId = screen.subPhase.trackId;
1731
+ const skippedCircuitName = screen.subPhase.circuitName;
1732
+ const sessionToken = session.session_token;
1733
+ setScreen({
1734
+ ...screen,
1735
+ subPhase: {
1736
+ kind: "releasing",
1737
+ trackId: skippedTrackId,
1738
+ circuitName: skippedCircuitName
1739
+ }
1740
+ });
1741
+ api.leaveQueue(activeCeremonyId, skippedTrackId, sessionToken).catch((err) => {
1742
+ const msg = err instanceof Error ? err.message : String(err);
1743
+ process.stderr.write(
1744
+ `[ceremony-tui] leaveQueue(${skippedTrackId}) failed during skip: ${msg}
1745
+ `
1746
+ );
1747
+ }).finally(() => {
1748
+ setScreen((prev) => {
1749
+ if (prev.name !== "auto-running" || prev.subPhase.kind !== "releasing" || prev.subPhase.trackId !== skippedTrackId) {
1750
+ return prev;
1751
+ }
1752
+ return {
1753
+ ...prev,
1754
+ current: null,
1755
+ subPhase: { kind: "queueing" },
1756
+ done: [
1757
+ ...prev.done,
1758
+ {
1759
+ trackId: skipped.id,
1760
+ circuitName: skipped.circuit_name,
1761
+ result: "failed",
1762
+ reason: "user skipped"
1763
+ }
1764
+ ]
1765
+ // Don't queue skipped circuits for retry — the user
1766
+ // actively chose to skip, so a silent retry would
1767
+ // defeat that intent.
1768
+ };
1769
+ });
1770
+ });
1771
+ return;
1772
+ }
1773
+ if (screen.name !== "tracks" && screen.name !== "loading" && screen.name !== "joining" && screen.name !== "entropy" && screen.name !== "auto-entropy" && screen.name !== "ceremony-picker") {
1144
1774
  goHome();
1145
1775
  }
1146
1776
  return;
@@ -1165,6 +1795,22 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1165
1795
  }
1166
1796
  return;
1167
1797
  }
1798
+ if (screen.name === "mode-select") {
1799
+ if (q === "k") {
1800
+ setEntropyMode((m) => m === "mouse" ? "keyboard" : "mouse");
1801
+ return;
1802
+ }
1803
+ if (key.return) {
1804
+ setScreen({ name: "auto-entropy", tracks: screen.tracks });
1805
+ return;
1806
+ }
1807
+ if (q === "n" || q === "m") {
1808
+ setSelectedIdx(0);
1809
+ setScreen({ name: "tracks", tracks: screen.tracks });
1810
+ return;
1811
+ }
1812
+ return;
1813
+ }
1168
1814
  if (screen.name === "tracks") {
1169
1815
  const { tracks } = screen;
1170
1816
  if (key.tab) {
@@ -1183,13 +1829,15 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1183
1829
  }
1184
1830
  if (key.return) {
1185
1831
  const t = tracks[selectedIdx];
1186
- if (t && t.status === "open" && !contributed[t.id]) joinTrack(t);
1832
+ if (t && t.status === "open") joinTrack(t);
1187
1833
  return;
1188
1834
  }
1189
1835
  } else if (tab === 1) {
1190
- const myContribs = Object.values(contributed).filter(
1191
- (c) => c.ceremonyId === activeCeremonyId
1192
- );
1836
+ const myContribs = Object.values(contributed).flat().filter((c) => c.ceremonyId === activeCeremonyId).sort((a, b) => {
1837
+ const ta = a.verifiedAt ? Date.parse(a.verifiedAt) : 0;
1838
+ const tb = b.verifiedAt ? Date.parse(b.verifiedAt) : 0;
1839
+ return tb - ta;
1840
+ });
1193
1841
  if (key.upArrow) {
1194
1842
  setContribCursor((i) => Math.max(0, i - 1));
1195
1843
  return;
@@ -1221,52 +1869,55 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1221
1869
  }
1222
1870
  });
1223
1871
  if (showInfo) {
1224
- return /* @__PURE__ */ jsx7(InfoModal, {});
1872
+ return /* @__PURE__ */ jsx8(InfoModal, {});
1225
1873
  }
1226
1874
  if (screen.name === "name-input") {
1227
1875
  const { value } = screen;
1228
1876
  const canSubmit = value.trim().length > 0;
1229
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1230
- /* @__PURE__ */ jsx7(Header, { ceremony: null }),
1231
- /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginTop: 1, gap: 1, children: [
1232
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Who are you contributing as?" }),
1233
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Your display name appears next to your contributions in the public transcript. Pick anything \u2014 your real name, a handle, or hit Tab for a random anonymous name." }),
1234
- /* @__PURE__ */ jsxs7(Box7, { children: [
1235
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: " > " }),
1236
- /* @__PURE__ */ jsx7(Text7, { children: value }),
1237
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", inverse: true, children: " " })
1877
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1878
+ /* @__PURE__ */ jsx8(Header, { ceremony: null }),
1879
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, gap: 1, children: [
1880
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Who are you contributing as?" }),
1881
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Your display name appears next to your contributions in the public transcript. Pick anything \u2014 your real name, a handle, or hit Tab for a random anonymous name." }),
1882
+ /* @__PURE__ */ jsxs8(Box8, { children: [
1883
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: " > " }),
1884
+ /* @__PURE__ */ jsx8(Text8, { children: value }),
1885
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", inverse: true, children: " " })
1238
1886
  ] }),
1239
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: canSubmit ? "Enter to continue \xB7 Tab rerolls random name \xB7 \u232B backspace \xB7 Esc to quit" : "Type a name, or press Tab to suggest a random anonymous one \xB7 Esc to quit" })
1887
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: canSubmit ? "Enter to continue \xB7 Tab rerolls random name \xB7 \u232B backspace \xB7 Esc to quit" : "Type a name, or press Tab to suggest a random anonymous one \xB7 Esc to quit" })
1240
1888
  ] })
1241
1889
  ] });
1242
1890
  }
1243
1891
  if (screen.name === "ceremony-picker") {
1244
1892
  const { ceremonies, loading } = screen;
1245
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1246
- /* @__PURE__ */ jsx7(Header, { ceremony: null }),
1247
- loading ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Loading ceremonies..." }) : ceremonies.length === 0 ? /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
1248
- /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "No ceremonies found." }),
1249
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "The server may not have any active ceremonies yet." }),
1250
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Q to quit" })
1251
- ] }) : /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1252
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Select a ceremony:" }),
1253
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(60) }),
1893
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1894
+ /* @__PURE__ */ jsx8(Header, { ceremony: null }),
1895
+ loading ? /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1896
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Loading ceremonies..." }),
1897
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q / Ctrl+C / Ctrl+Z to quit" }) })
1898
+ ] }) : ceremonies.length === 0 ? /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
1899
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "No ceremonies found." }),
1900
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "The server may not have any active ceremonies yet." }),
1901
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q to quit" })
1902
+ ] }) : /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1903
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Select a ceremony:" }),
1904
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + "\u2500".repeat(60) }),
1254
1905
  ceremonies.map((c, i) => {
1255
1906
  const isSelected = i === selectedIdx;
1256
1907
  const isOpen = c.status === "open";
1257
1908
  const statusColor = c.status === "open" ? "green" : c.status === "completed" ? "cyan" : "yellow";
1258
1909
  const statusLabel = ceremonyStatusLabel(c.status);
1259
- return /* @__PURE__ */ jsxs7(Box7, { gap: 2, children: [
1260
- /* @__PURE__ */ jsxs7(Text7, { color: isSelected ? "cyan" : isOpen ? void 0 : "gray", children: [
1910
+ return /* @__PURE__ */ jsxs8(Box8, { gap: 2, children: [
1911
+ /* @__PURE__ */ jsxs8(Text8, { color: isSelected ? "cyan" : isOpen ? void 0 : "gray", children: [
1261
1912
  isSelected ? "\u25B6 " : " ",
1262
1913
  c.name.padEnd(30)
1263
1914
  ] }),
1264
- /* @__PURE__ */ jsxs7(Text7, { color: statusColor, children: [
1915
+ /* @__PURE__ */ jsxs8(Text8, { color: statusColor, children: [
1265
1916
  "[",
1266
1917
  statusLabel,
1267
1918
  "]"
1268
1919
  ] }),
1269
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1920
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
1270
1921
  c.track_count,
1271
1922
  " track",
1272
1923
  c.track_count !== 1 ? "s" : "",
@@ -1277,65 +1928,379 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1277
1928
  ] })
1278
1929
  ] }, c.id);
1279
1930
  }),
1280
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(60) }),
1931
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + "\u2500".repeat(60) }),
1281
1932
  (() => {
1282
1933
  const c = ceremonies[selectedIdx];
1283
1934
  if (!c) return null;
1935
+ const navHint = /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2191/\u2193 select \xB7 Enter join \xB7 Q / Ctrl+C / Ctrl+Z to quit" });
1284
1936
  if (c.status === "initialized") {
1285
- return /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
1286
- " ",
1287
- "This ceremony is not yet open for contributions. The Umbra team is still preparing the circuits \u2014 please check back shortly."
1937
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1938
+ /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
1939
+ " ",
1940
+ "This ceremony is not yet open for contributions. The Umbra team is still preparing the circuits \u2014 please check back shortly."
1941
+ ] }),
1942
+ navHint
1288
1943
  ] });
1289
1944
  }
1290
1945
  if (c.status === "finalizing") {
1291
- return /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
1292
- " ",
1293
- "Contributions are closed. The ceremony is computing the final verification key \u2014 no further contributions can be added."
1946
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1947
+ /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
1948
+ " ",
1949
+ "Contributions are closed. The ceremony is computing the final verification key \u2014 no further contributions can be added."
1950
+ ] }),
1951
+ navHint
1294
1952
  ] });
1295
1953
  }
1296
1954
  if (c.status === "completed") {
1297
- return /* @__PURE__ */ jsxs7(Text7, { color: "cyan", children: [
1298
- " ",
1299
- "Ceremony complete. The verification keys are finalised \u2014 thank you to everyone who contributed."
1955
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1956
+ /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
1957
+ " ",
1958
+ "Ceremony complete. The verification keys are finalised \u2014 thank you to everyone who contributed."
1959
+ ] }),
1960
+ navHint
1300
1961
  ] });
1301
1962
  }
1302
- return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2191/\u2193 select \xB7 Enter join \xB7 Q quit" });
1963
+ return navHint;
1303
1964
  })()
1304
1965
  ] })
1305
1966
  ] });
1306
1967
  }
1307
1968
  if (screen.name === "loading") {
1308
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1309
- /* @__PURE__ */ jsx7(Header, { ceremony }),
1310
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Loading tracks..." })
1969
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1970
+ /* @__PURE__ */ jsx8(Header, { ceremony }),
1971
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Loading tracks..." }),
1972
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q / Ctrl+C / Ctrl+Z to quit" }) })
1973
+ ] });
1974
+ }
1975
+ if (screen.name === "mode-select") {
1976
+ const { tracks } = screen;
1977
+ const openCount = tracks.filter((t) => t.status === "open").length;
1978
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1979
+ /* @__PURE__ */ jsx8(Header, { ceremony }),
1980
+ /* @__PURE__ */ jsxs8(
1981
+ Box8,
1982
+ {
1983
+ flexDirection: "column",
1984
+ borderStyle: "round",
1985
+ borderColor: "cyan",
1986
+ paddingX: 2,
1987
+ paddingY: 0,
1988
+ children: [
1989
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: "Automate?" }),
1990
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
1991
+ openCount,
1992
+ " circuit",
1993
+ openCount === 1 ? "" : "s",
1994
+ " open \xB7 auto runs the whole loop"
1995
+ ] }),
1996
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
1997
+ /* @__PURE__ */ jsxs8(Text8, { children: [
1998
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "green", children: "Enter" }),
1999
+ " ",
2000
+ "auto \xB7 entropy once, every circuit in order"
2001
+ ] }),
2002
+ /* @__PURE__ */ jsxs8(Text8, { children: [
2003
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "yellow", children: "N" }),
2004
+ " ",
2005
+ "manual \xB7 pick circuits one at a time"
2006
+ ] }),
2007
+ /* @__PURE__ */ jsxs8(Text8, { children: [
2008
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "magenta", children: "K" }),
2009
+ " ",
2010
+ "input:",
2011
+ " ",
2012
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: entropyMode === "mouse" ? "cyan" : "yellow", children: entropyMode }),
2013
+ " ",
2014
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "(press K to toggle)" })
2015
+ ] })
2016
+ ] })
2017
+ ]
2018
+ }
2019
+ ),
2020
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u232B back \xB7 Q quit" }) })
2021
+ ] });
2022
+ }
2023
+ if (screen.name === "auto-entropy") {
2024
+ const { tracks } = screen;
2025
+ const onComplete = (masterSeed) => {
2026
+ const ordered = orderTracksForAuto(tracks, contributed);
2027
+ setScreen({
2028
+ name: "auto-running",
2029
+ tracks,
2030
+ masterSeed,
2031
+ remaining: ordered,
2032
+ retryQueue: [],
2033
+ retryingPhase: false,
2034
+ done: [],
2035
+ current: null,
2036
+ subPhase: { kind: "queueing" }
2037
+ });
2038
+ };
2039
+ const onError = (e) => setScreen({ name: "error", message: e.message, recoverable: false });
2040
+ if (entropyMode === "mouse") {
2041
+ return /* @__PURE__ */ jsx8(
2042
+ MouseEntropyCollector,
2043
+ {
2044
+ title: `Auto-mode entropy \xB7 one drawing seeds every circuit (HKDF per circuit)`,
2045
+ onComplete,
2046
+ onError
2047
+ }
2048
+ );
2049
+ }
2050
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2051
+ /* @__PURE__ */ jsx8(Header, { ceremony, subtitle: "Auto-mode entropy \xB7 seeds every circuit (HKDF)" }),
2052
+ /* @__PURE__ */ jsx8(EntropyCollector, { onComplete, onError })
2053
+ ] });
2054
+ }
2055
+ if (screen.name === "auto-running") {
2056
+ const totalCount = screen.done.length + screen.remaining.length + screen.retryQueue.length + (screen.current ? 1 : 0);
2057
+ const verifiedCount = screen.done.filter((d) => d.result === "verified").length;
2058
+ const failedCount = screen.done.filter((d) => d.result === "failed").length;
2059
+ const currentLabel = screen.current ? `current: ${screen.current.circuit_name}` : "advancing to next circuit\u2026";
2060
+ const progressStrip = /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, children: [
2061
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2500".repeat(60) }),
2062
+ /* @__PURE__ */ jsxs8(Text8, { children: [
2063
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "cyan", children: "Auto-mode" }),
2064
+ " \xB7 ",
2065
+ /* @__PURE__ */ jsx8(Text8, { children: verifiedCount }),
2066
+ "/",
2067
+ /* @__PURE__ */ jsx8(Text8, { children: totalCount }),
2068
+ " verified",
2069
+ failedCount > 0 && /* @__PURE__ */ jsxs8(Fragment3, { children: [
2070
+ " \xB7 ",
2071
+ /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
2072
+ failedCount,
2073
+ " failed"
2074
+ ] }),
2075
+ screen.retryingPhase ? " (retrying)" : ""
2076
+ ] }),
2077
+ screen.retryQueue.length > 0 && !screen.retryingPhase && /* @__PURE__ */ jsxs8(Fragment3, { children: [
2078
+ " \xB7 ",
2079
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2080
+ screen.retryQueue.length,
2081
+ " queued for retry"
2082
+ ] })
2083
+ ] })
2084
+ ] }),
2085
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: currentLabel }),
2086
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2500".repeat(60) })
2087
+ ] });
2088
+ if (screen.subPhase.kind === "releasing") {
2089
+ const sub = screen.subPhase;
2090
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2091
+ /* @__PURE__ */ jsx8(
2092
+ Header,
2093
+ {
2094
+ ceremony,
2095
+ subtitle: `Auto-mode \xB7 releasing slot for ${sub.circuitName}`
2096
+ }
2097
+ ),
2098
+ progressStrip,
2099
+ /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
2100
+ "\u23F3 Releasing slot for ",
2101
+ sub.circuitName,
2102
+ "\u2026"
2103
+ ] }),
2104
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Waiting for the server to confirm the slot is free before joining the next circuit's queue." }),
2105
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q / Ctrl+C / Ctrl+Z abort run" })
2106
+ ] });
2107
+ }
2108
+ if (screen.subPhase.kind === "queueing") {
2109
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2110
+ /* @__PURE__ */ jsx8(Header, { ceremony, subtitle: "Auto-mode \xB7 joining next queue" }),
2111
+ progressStrip,
2112
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Picking the next circuit\u2026" }),
2113
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q / Ctrl+C / Ctrl+Z to abort (releases any active slot)" })
2114
+ ] });
2115
+ }
2116
+ if (screen.subPhase.kind === "waiting") {
2117
+ const sub = screen.subPhase;
2118
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2119
+ /* @__PURE__ */ jsx8(
2120
+ Header,
2121
+ {
2122
+ ceremony,
2123
+ subtitle: `Auto-mode \xB7 waiting for slot \xB7 ${sub.circuitName}`
2124
+ }
2125
+ ),
2126
+ progressStrip,
2127
+ /* @__PURE__ */ jsx8(
2128
+ QueueView,
2129
+ {
2130
+ ceremonyId: activeCeremonyId,
2131
+ trackId: sub.trackId,
2132
+ token: session.session_token,
2133
+ onReady: (status) => {
2134
+ const entropy = deriveCircuitEntropy(
2135
+ screen.masterSeed,
2136
+ activeCeremonyId,
2137
+ sub.circuitName
2138
+ );
2139
+ setScreen({
2140
+ ...screen,
2141
+ subPhase: {
2142
+ kind: "contributing",
2143
+ trackId: sub.trackId,
2144
+ circuitName: sub.circuitName,
2145
+ slotStatus: status,
2146
+ entropy
2147
+ }
2148
+ });
2149
+ },
2150
+ onError: (e) => {
2151
+ const failedTrack = screen.current;
2152
+ const newDone = {
2153
+ trackId: failedTrack.id,
2154
+ circuitName: failedTrack.circuit_name,
2155
+ result: "failed",
2156
+ reason: e.message
2157
+ };
2158
+ setScreen({
2159
+ ...screen,
2160
+ current: null,
2161
+ subPhase: { kind: "queueing" },
2162
+ done: [...screen.done, newDone],
2163
+ retryQueue: screen.retryingPhase ? screen.retryQueue : [...screen.retryQueue, failedTrack]
2164
+ });
2165
+ }
2166
+ }
2167
+ ),
2168
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u232B Backspace skip this circuit \xB7 Q / Ctrl+C / Ctrl+Z abort run" })
2169
+ ] });
2170
+ }
2171
+ if (screen.subPhase.kind === "contributing") {
2172
+ const sub = screen.subPhase;
2173
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2174
+ /* @__PURE__ */ jsx8(
2175
+ Header,
2176
+ {
2177
+ ceremony,
2178
+ subtitle: `Auto-mode \xB7 contributing \xB7 ${sub.circuitName}`
2179
+ }
2180
+ ),
2181
+ progressStrip,
2182
+ /* @__PURE__ */ jsx8(
2183
+ ContributeFlow,
2184
+ {
2185
+ ceremonyId: activeCeremonyId,
2186
+ trackId: sub.trackId,
2187
+ token: session.session_token,
2188
+ slotStatus: sub.slotStatus,
2189
+ entropy: sub.entropy,
2190
+ displayName: displayName2,
2191
+ onComplete: async (contributionId, receipt) => {
2192
+ const local = {
2193
+ contributionId,
2194
+ sequenceNumber: receipt?.sequence_number ?? 0,
2195
+ contributionHash: receipt?.contribution_hash ?? "",
2196
+ circuitName: sub.circuitName,
2197
+ ceremonyId: activeCeremonyId,
2198
+ verifiedAt: receipt?.verified_at ?? (/* @__PURE__ */ new Date()).toISOString()
2199
+ };
2200
+ await recordContribution(sub.trackId, local);
2201
+ setContributed((prev) => {
2202
+ const existing = prev[sub.trackId] ?? [];
2203
+ if (existing.some((c) => c.contributionId === local.contributionId)) return prev;
2204
+ return { ...prev, [sub.trackId]: [...existing, local] };
2205
+ });
2206
+ const successTrack = screen.current;
2207
+ setScreen({
2208
+ ...screen,
2209
+ current: null,
2210
+ subPhase: { kind: "queueing" },
2211
+ done: [
2212
+ ...screen.done,
2213
+ {
2214
+ trackId: successTrack.id,
2215
+ circuitName: successTrack.circuit_name,
2216
+ result: "verified",
2217
+ hash: local.contributionHash
2218
+ }
2219
+ ]
2220
+ });
2221
+ },
2222
+ onError: (e) => {
2223
+ const failedTrack = screen.current;
2224
+ setScreen({
2225
+ ...screen,
2226
+ current: null,
2227
+ subPhase: { kind: "queueing" },
2228
+ done: [
2229
+ ...screen.done,
2230
+ {
2231
+ trackId: failedTrack.id,
2232
+ circuitName: failedTrack.circuit_name,
2233
+ result: "failed",
2234
+ reason: e.message
2235
+ }
2236
+ ],
2237
+ retryQueue: screen.retryingPhase ? screen.retryQueue : [...screen.retryQueue, failedTrack]
2238
+ });
2239
+ }
2240
+ }
2241
+ )
2242
+ ] });
2243
+ }
2244
+ return null;
2245
+ }
2246
+ if (screen.name === "auto-summary") {
2247
+ const verified = screen.outcomes.filter((o) => o.result === "verified");
2248
+ const failed = screen.outcomes.filter((o) => o.result === "failed");
2249
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2250
+ /* @__PURE__ */ jsx8(Header, { ceremony, subtitle: "Auto-mode \xB7 complete" }),
2251
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
2252
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: failed.length === 0 ? "green" : "yellow", children: failed.length === 0 ? `\u2713 Auto-mode complete \u2014 ${verified.length} circuits verified` : `\u26A0 Auto-mode complete with ${failed.length} failure${failed.length === 1 ? "" : "s"}` }),
2253
+ verified.length > 0 && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2254
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Verified contributions:" }),
2255
+ verified.map((o, i) => /* @__PURE__ */ jsxs8(Box8, { paddingLeft: 2, children: [
2256
+ /* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2713 " }),
2257
+ /* @__PURE__ */ jsx8(Text8, { children: elideMiddle(o.circuitName, 42).padEnd(44) }),
2258
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: o.hash ? o.hash.slice(0, 16) + "\u2026" : "" })
2259
+ ] }, i))
2260
+ ] }),
2261
+ failed.length > 0 && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2262
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Failed (retry already attempted):" }),
2263
+ failed.map((o, i) => /* @__PURE__ */ jsxs8(Box8, { paddingLeft: 2, flexDirection: "column", children: [
2264
+ /* @__PURE__ */ jsxs8(Box8, { children: [
2265
+ /* @__PURE__ */ jsx8(Text8, { color: "red", children: "\u2717 " }),
2266
+ /* @__PURE__ */ jsx8(Text8, { children: elideMiddle(o.circuitName, 42) })
2267
+ ] }),
2268
+ /* @__PURE__ */ jsx8(Box8, { paddingLeft: 4, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: o.reason ?? "(no reason recorded)" }) })
2269
+ ] }, i)),
2270
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u232B Backspace / B \u2014 back to track list (you can retry failed circuits manually)" })
2271
+ ] }),
2272
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q / Ctrl+C / Ctrl+Z \u2014 quit" })
2273
+ ] })
1311
2274
  ] });
1312
2275
  }
1313
2276
  if (screen.name === "error") {
1314
2277
  const backHint = !initialCeremonyId ? "\u232B Backspace \u2014 back to ceremony list \xB7 Q to quit" : screen.recoverable ? "\u232B Backspace / B to go back \xB7 Q to quit" : "Q to quit";
1315
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1316
- /* @__PURE__ */ jsx7(Header, { ceremony }),
1317
- /* @__PURE__ */ jsxs7(Text7, { color: "red", bold: true, children: [
2278
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2279
+ /* @__PURE__ */ jsx8(Header, { ceremony }),
2280
+ /* @__PURE__ */ jsxs8(Text8, { color: "red", bold: true, children: [
1318
2281
  "\u2717 ",
1319
2282
  screen.message
1320
2283
  ] }),
1321
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: backHint })
2284
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: backHint })
1322
2285
  ] });
1323
2286
  }
1324
2287
  if (screen.name === "tracks") {
1325
2288
  const { tracks } = screen;
1326
2289
  const openTracks = tracks.filter((t) => t.status === "open");
1327
- const myContributions = Object.values(contributed).filter(
1328
- (c) => c.ceremonyId === activeCeremonyId
1329
- );
1330
- const TabBar = () => /* @__PURE__ */ jsxs7(Box7, { gap: 1, marginBottom: 1, children: [
1331
- /* @__PURE__ */ jsx7(Text7, { bold: tab === 0, color: tab === 0 ? "cyan" : void 0, dimColor: tab !== 0, children: tab === 0 ? "[ Dashboard ]" : " Dashboard " }),
1332
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "|" }),
1333
- /* @__PURE__ */ jsx7(Text7, { bold: tab === 1, color: tab === 1 ? "cyan" : void 0, dimColor: tab !== 1, children: tab === 1 ? `[ My Contributions (${myContributions.length}) ]` : ` My Contributions (${myContributions.length}) ` }),
1334
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " Tab to switch" })
2290
+ const myContributions = Object.values(contributed).flat().filter((c) => c.ceremonyId === activeCeremonyId).sort((a, b) => {
2291
+ const ta = a.verifiedAt ? Date.parse(a.verifiedAt) : 0;
2292
+ const tb = b.verifiedAt ? Date.parse(b.verifiedAt) : 0;
2293
+ return tb - ta;
2294
+ });
2295
+ const TabBar = () => /* @__PURE__ */ jsxs8(Box8, { gap: 1, marginBottom: 1, children: [
2296
+ /* @__PURE__ */ jsx8(Text8, { bold: tab === 0, color: tab === 0 ? "cyan" : void 0, dimColor: tab !== 0, children: tab === 0 ? "[ Dashboard ]" : " Dashboard " }),
2297
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "|" }),
2298
+ /* @__PURE__ */ jsx8(Text8, { bold: tab === 1, color: tab === 1 ? "cyan" : void 0, dimColor: tab !== 1, children: tab === 1 ? `[ My Contributions (${myContributions.length}) ]` : ` My Contributions (${myContributions.length}) ` }),
2299
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " Tab to switch" })
1335
2300
  ] });
1336
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1337
- /* @__PURE__ */ jsx7(Header, { ceremony }),
1338
- /* @__PURE__ */ jsx7(TabBar, {}),
2301
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2302
+ /* @__PURE__ */ jsx8(Header, { ceremony }),
2303
+ /* @__PURE__ */ jsx8(TabBar, {}),
1339
2304
  tab === 0 ? (
1340
2305
  // ── Dashboard tab ────────────────────────────────────────────────
1341
2306
  // CIRCUIT column is sized to fit the longest mainnet name
@@ -1343,76 +2308,82 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1343
2308
  // longer than that are middle-elided so the disambiguating suffix
1344
2309
  // (-n1/-n2/-n4) stays visible — previously a hard slice(0,24)
1345
2310
  // showed every claim variant as `claim-deposit-into-confi..`.
1346
- /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1347
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2311
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2312
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
1348
2313
  " CIRCUIT".padEnd(46),
1349
2314
  "TOTAL".padEnd(8),
1350
2315
  "QUEUE".padEnd(8),
1351
2316
  "STATUS".padEnd(14),
1352
2317
  "MY CONTRIBUTIONS"
1353
2318
  ] }),
1354
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(90) }),
2319
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + "\u2500".repeat(90) }),
1355
2320
  tracks.map((t, i) => {
1356
2321
  const isSelected = i === selectedIdx;
1357
2322
  const canContribute = t.status === "open";
1358
2323
  const nameDisplay = elideMiddle(t.circuit_name, 42);
1359
2324
  const statusColor = t.status === "open" ? "green" : t.status === "finalized" ? "cyan" : "yellow";
1360
- const myContrib = contributed[t.id];
1361
- return /* @__PURE__ */ jsxs7(Box7, { children: [
1362
- /* @__PURE__ */ jsxs7(Text7, { color: isSelected ? "cyan" : canContribute ? void 0 : "gray", children: [
2325
+ const myRounds = roundCount(contributed, t.id);
2326
+ return /* @__PURE__ */ jsxs8(Box8, { children: [
2327
+ /* @__PURE__ */ jsxs8(Text8, { color: isSelected ? "cyan" : canContribute ? void 0 : "gray", children: [
1363
2328
  isSelected ? "\u25B6 " : " ",
1364
2329
  nameDisplay.padEnd(44),
1365
2330
  String(t.contribution_count).padEnd(8),
1366
2331
  String(t.queue_depth).padEnd(8)
1367
2332
  ] }),
1368
- /* @__PURE__ */ jsx7(Text7, { color: statusColor, children: trackStatusLabel(t.status).padEnd(14) }),
1369
- myContrib ? isSelected ? /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
1370
- "\u2713 contributed (round #",
1371
- myContrib.sequenceNumber,
1372
- ") \u2014 already done"
1373
- ] }) : /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
1374
- "\u2713 contributed (round #",
1375
- myContrib.sequenceNumber,
2333
+ /* @__PURE__ */ jsx8(Text8, { color: statusColor, children: trackStatusLabel(t.status).padEnd(14) }),
2334
+ myRounds > 0 ? canContribute && isSelected ? /* @__PURE__ */ jsxs8(Text8, { color: "green", children: [
2335
+ "\u2713 contributed (",
2336
+ myRounds,
2337
+ " round",
2338
+ myRounds === 1 ? "" : "s",
2339
+ ") \u2014",
2340
+ " ",
2341
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "Enter to add more" })
2342
+ ] }) : /* @__PURE__ */ jsxs8(Text8, { color: "green", children: [
2343
+ "\u2713 contributed (",
2344
+ myRounds,
2345
+ " round",
2346
+ myRounds === 1 ? "" : "s",
1376
2347
  ")"
1377
- ] }) : canContribute ? isSelected ? /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "\u2190 Enter to contribute" }) : /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "not contributed" }) : /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2014" })
2348
+ ] }) : canContribute ? isSelected ? /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "\u2190 Enter to contribute" }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "not contributed" }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2014" })
1378
2349
  ] }, t.id);
1379
2350
  }),
1380
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(70) }),
1381
- openTracks.length === 0 ? /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: "No circuits are open for contributions right now \u2014 the ceremony may be closing or already complete." }) : /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2351
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + "\u2500".repeat(70) }),
2352
+ openTracks.length === 0 ? /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: "No circuits are open for contributions right now \u2014 the ceremony may be closing or already complete." }) : /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
1382
2353
  "\u2191/\u2193 select \xB7 Enter contribute \xB7 R refresh \xB7 Q quit",
1383
2354
  !initialCeremonyId ? " \xB7 \u232B back to ceremony list" : ""
1384
2355
  ] })
1385
2356
  ] })
1386
2357
  ) : (
1387
2358
  // ── My Contributions tab ─────────────────────────────────────────
1388
- /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: myContributions.length === 0 ? /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
1389
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "No contributions yet." }),
1390
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Switch to Dashboard tab and press Enter on a circuit to contribute." })
1391
- ] }) : /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1392
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2359
+ /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: myContributions.length === 0 ? /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
2360
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "No contributions yet." }),
2361
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Switch to Dashboard tab and press Enter on a circuit to contribute." })
2362
+ ] }) : /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2363
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
1393
2364
  " CIRCUIT".padEnd(44),
1394
2365
  "ROUND".padEnd(7),
1395
2366
  "VERIFIED AT".padEnd(22)
1396
2367
  ] }),
1397
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(90) }),
2368
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + "\u2500".repeat(90) }),
1398
2369
  myContributions.map((c, i) => {
1399
2370
  const isSel = i === contribCursor;
1400
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1401
- /* @__PURE__ */ jsxs7(Box7, { children: [
1402
- /* @__PURE__ */ jsx7(Text7, { color: isSel ? "cyan" : "green", children: isSel ? "\u25B6 " : " " }),
1403
- /* @__PURE__ */ jsx7(Text7, { bold: isSel, children: elideMiddle(c.circuitName, 40).padEnd(42) }),
1404
- /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: ("#" + c.sequenceNumber).padEnd(7) }),
1405
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: c.verifiedAt ? new Date(c.verifiedAt).toLocaleString() : "\u2014" })
2371
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2372
+ /* @__PURE__ */ jsxs8(Box8, { children: [
2373
+ /* @__PURE__ */ jsx8(Text8, { color: isSel ? "cyan" : "green", children: isSel ? "\u25B6 " : " " }),
2374
+ /* @__PURE__ */ jsx8(Text8, { bold: isSel, children: elideMiddle(c.circuitName, 40).padEnd(42) }),
2375
+ /* @__PURE__ */ jsx8(Text8, { color: "yellow", children: ("#" + c.sequenceNumber).padEnd(7) }),
2376
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: c.verifiedAt ? new Date(c.verifiedAt).toLocaleString() : "\u2014" })
1406
2377
  ] }),
1407
- /* @__PURE__ */ jsx7(Box7, { paddingLeft: 4, children: /* @__PURE__ */ jsxs7(Text7, { color: isSel ? "cyan" : "gray", dimColor: !isSel, children: [
2378
+ /* @__PURE__ */ jsx8(Box8, { paddingLeft: 4, children: /* @__PURE__ */ jsxs8(Text8, { color: isSel ? "cyan" : "gray", dimColor: !isSel, children: [
1408
2379
  "hash:",
1409
2380
  " ",
1410
2381
  c.contributionHash ? c.contributionHash : "(pending \u2014 verify still in flight)"
1411
2382
  ] }) })
1412
2383
  ] }, i);
1413
2384
  }),
1414
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " " + "\u2500".repeat(90) }),
1415
- copyToast ? /* @__PURE__ */ jsx7(Text7, { color: "green", children: copyToast }) : /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2385
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " " + "\u2500".repeat(90) }),
2386
+ copyToast ? /* @__PURE__ */ jsx8(Text8, { color: "green", children: copyToast }) : /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
1416
2387
  "Total: ",
1417
2388
  myContributions.length,
1418
2389
  " contribution",
@@ -1425,24 +2396,28 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1425
2396
  ] });
1426
2397
  }
1427
2398
  if (screen.name === "joining") {
1428
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1429
- /* @__PURE__ */ jsx7(Header, { ceremony }),
1430
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Joining queue..." })
2399
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2400
+ /* @__PURE__ */ jsx8(Header, { ceremony }),
2401
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Joining queue..." }),
2402
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Q / Ctrl+C / Ctrl+Z to quit" }) })
1431
2403
  ] });
1432
2404
  }
1433
2405
  if (screen.name === "queue") {
1434
2406
  const { trackId, circuitName } = screen;
1435
- const priorContrib = contributed[trackId];
1436
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1437
- /* @__PURE__ */ jsx7(Header, { ceremony, subtitle: `Circuit: ${circuitName}` }),
1438
- priorContrib && /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, paddingX: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
1439
- "\u26A0 You already contributed to this circuit (round #",
1440
- priorContrib.sequenceNumber,
1441
- ").",
1442
- " ",
1443
- "Contributing again is allowed and adds more entropy."
2407
+ const priorRounds = roundCount(contributed, trackId);
2408
+ const lastContrib = latestContribution(contributed, trackId);
2409
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2410
+ /* @__PURE__ */ jsx8(Header, { ceremony, subtitle: `Circuit: ${circuitName}` }),
2411
+ priorRounds > 0 && lastContrib && /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, paddingX: 1, children: /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
2412
+ "\u26A0 You've contributed to this circuit ",
2413
+ priorRounds,
2414
+ " time",
2415
+ priorRounds === 1 ? "" : "s",
2416
+ " (latest: round #",
2417
+ lastContrib.sequenceNumber,
2418
+ "). Each additional round adds more entropy and strengthens the ceremony \u2014 go ahead."
1444
2419
  ] }) }),
1445
- /* @__PURE__ */ jsx7(
2420
+ /* @__PURE__ */ jsx8(
1446
2421
  QueueView,
1447
2422
  {
1448
2423
  ceremonyId: activeCeremonyId,
@@ -1452,27 +2427,39 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1452
2427
  onError: (e) => setScreen({ name: "error", message: e.message, recoverable: true })
1453
2428
  }
1454
2429
  ),
1455
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u232B Backspace to go back \xB7 Q to quit" })
2430
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u232B Backspace to go back \xB7 Q to quit" })
1456
2431
  ] });
1457
2432
  }
1458
2433
  if (screen.name === "entropy") {
1459
2434
  const { trackId, circuitName, slotStatus } = screen;
1460
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1461
- /* @__PURE__ */ jsx7(Header, { ceremony, subtitle: `Circuit: ${circuitName} \xB7 Your turn!` }),
1462
- /* @__PURE__ */ jsx7(
1463
- EntropyCollector,
2435
+ const onComplete = (entropy) => setScreen({ name: "contribute", trackId, circuitName, slotStatus, entropy });
2436
+ const onError = (e) => setScreen({ name: "error", message: e.message, recoverable: false });
2437
+ if (entropyMode === "mouse") {
2438
+ return /* @__PURE__ */ jsx8(
2439
+ MouseEntropyCollector,
1464
2440
  {
1465
- onComplete: (entropy) => setScreen({ name: "contribute", trackId, circuitName, slotStatus, entropy }),
1466
- onError: (e) => setScreen({ name: "error", message: e.message, recoverable: false })
2441
+ title: `Your turn \xB7 ${circuitName} \u2014 draw entropy for this contribution`,
2442
+ onComplete,
2443
+ onError
1467
2444
  }
1468
- )
2445
+ );
2446
+ }
2447
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2448
+ /* @__PURE__ */ jsx8(
2449
+ Header,
2450
+ {
2451
+ ceremony,
2452
+ subtitle: `Circuit: ${circuitName} \xB7 Type to collect entropy`
2453
+ }
2454
+ ),
2455
+ /* @__PURE__ */ jsx8(EntropyCollector, { onComplete, onError })
1469
2456
  ] });
1470
2457
  }
1471
2458
  if (screen.name === "contribute") {
1472
2459
  const { trackId, circuitName, slotStatus, entropy } = screen;
1473
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1474
- /* @__PURE__ */ jsx7(Header, { ceremony, subtitle: `Circuit: ${circuitName} \xB7 Contributing` }),
1475
- /* @__PURE__ */ jsx7(
2460
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2461
+ /* @__PURE__ */ jsx8(Header, { ceremony, subtitle: `Circuit: ${circuitName} \xB7 Contributing` }),
2462
+ /* @__PURE__ */ jsx8(
1476
2463
  ContributeFlow,
1477
2464
  {
1478
2465
  ceremonyId: activeCeremonyId,
@@ -1491,7 +2478,13 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1491
2478
  verifiedAt: receipt?.verified_at ?? (/* @__PURE__ */ new Date()).toISOString()
1492
2479
  };
1493
2480
  await recordContribution(trackId, local);
1494
- setContributed((prev) => ({ ...prev, [trackId]: local }));
2481
+ setContributed((prev) => {
2482
+ const existing = prev[trackId] ?? [];
2483
+ if (existing.some((c) => c.contributionId === local.contributionId)) {
2484
+ return prev;
2485
+ }
2486
+ return { ...prev, [trackId]: [...existing, local] };
2487
+ });
1495
2488
  setScreen({ name: "done", contribution: local });
1496
2489
  },
1497
2490
  onError: (e) => setScreen({ name: "error", message: e.message, recoverable: false })
@@ -1501,41 +2494,47 @@ function App({ ceremonyId: initialCeremonyId, displayName: initialDisplayName })
1501
2494
  }
1502
2495
  if (screen.name === "done") {
1503
2496
  const { contribution } = screen;
1504
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1505
- /* @__PURE__ */ jsx7(Header, { ceremony }),
1506
- /* @__PURE__ */ jsx7(Attestation, { contribution }),
1507
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u232B Backspace / B = contribute to another circuit \xB7 Q to quit" }) })
2497
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
2498
+ /* @__PURE__ */ jsx8(Header, { ceremony }),
2499
+ /* @__PURE__ */ jsx8(Attestation, { contribution }),
2500
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u232B Backspace / B = contribute to another circuit \xB7 Q to quit" }) })
1508
2501
  ] });
1509
2502
  }
1510
2503
  return null;
1511
2504
  }
1512
2505
 
1513
2506
  // src/index.tsx
1514
- import { jsx as jsx8 } from "react/jsx-runtime";
2507
+ import { jsx as jsx9 } from "react/jsx-runtime";
1515
2508
  var ceremonyId = process.env["CEREMONY_ID"] ?? "";
1516
2509
  var displayName = process.env["CONTRIBUTOR_NAME"];
1517
2510
  process.stdout.write("\x1B[?1049h\x1B[H");
1518
2511
  function restoreScreen() {
1519
2512
  process.stdout.write("\x1B[?1049l");
1520
2513
  }
1521
- async function gracefulExit(code = 0) {
2514
+ var exiting = false;
2515
+ async function gracefulExit(code = 0, deliberate = false) {
2516
+ if (exiting) {
2517
+ process.exit(code);
2518
+ }
2519
+ exiting = true;
1522
2520
  restoreScreen();
1523
2521
  await runQueueCleanup();
2522
+ if (deliberate) {
2523
+ await runSessionCleanup();
2524
+ }
1524
2525
  process.exit(code);
1525
2526
  }
1526
- process.on("SIGINT", () => {
1527
- gracefulExit(0).catch(() => {
1528
- restoreScreen();
1529
- process.exit(0);
1530
- });
1531
- });
1532
- process.on("SIGTERM", () => {
1533
- gracefulExit(0).catch(() => {
1534
- restoreScreen();
1535
- process.exit(0);
2527
+ globalThis.__ceremonyGracefulExit = gracefulExit;
2528
+ var DELIBERATE_SIGNALS = /* @__PURE__ */ new Set(["SIGINT", "SIGTSTP"]);
2529
+ for (const signal of ["SIGINT", "SIGTERM", "SIGTSTP", "SIGHUP"]) {
2530
+ process.on(signal, () => {
2531
+ gracefulExit(0, DELIBERATE_SIGNALS.has(signal)).catch(() => {
2532
+ restoreScreen();
2533
+ process.exit(0);
2534
+ });
1536
2535
  });
1537
- });
1538
- var { waitUntilExit } = render(/* @__PURE__ */ jsx8(App, { ceremonyId, displayName }), {
2536
+ }
2537
+ var { waitUntilExit } = render(/* @__PURE__ */ jsx9(App, { ceremonyId, displayName }), {
1539
2538
  exitOnCtrlC: false
1540
2539
  });
1541
2540
  await waitUntilExit();