recappi 0.1.54 → 0.1.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -59,18 +59,18 @@ function applyRecordingEventToTelemetry(telemetry, event) {
59
59
  }
60
60
  if (event.type === "audio.level") {
61
61
  const level = levelFromRmsDb(event.rmsDb);
62
- const levelDb3 = typeof event.rmsDb === "number" && Number.isFinite(event.rmsDb) ? event.rmsDb : void 0;
62
+ const levelDb4 = typeof event.rmsDb === "number" && Number.isFinite(event.rmsDb) ? event.rmsDb : void 0;
63
63
  if (event.input === "microphone") {
64
64
  return {
65
65
  ...telemetry,
66
66
  level: { ...telemetry.level, mic: level },
67
- levelDb: { ...telemetry.levelDb, ...levelDb3 != null ? { mic: levelDb3 } : {} }
67
+ levelDb: { ...telemetry.levelDb, ...levelDb4 != null ? { mic: levelDb4 } : {} }
68
68
  };
69
69
  }
70
70
  return {
71
71
  ...telemetry,
72
72
  level: { ...telemetry.level, system: level },
73
- levelDb: { ...telemetry.levelDb, ...levelDb3 != null ? { system: levelDb3 } : {} }
73
+ levelDb: { ...telemetry.levelDb, ...levelDb4 != null ? { system: levelDb4 } : {} }
74
74
  };
75
75
  }
76
76
  if (event.type === "error") {
@@ -199,8 +199,8 @@ function jobDetail(item) {
199
199
  if (item.status === "running") {
200
200
  const fraction = transcribeFraction(item);
201
201
  if (fraction != null) {
202
- const pct = Math.round(fraction * 100);
203
- return `${progressBar(fraction)} ${String(pct).padStart(3)}% ${formatClockMs(
202
+ const pct2 = Math.round(fraction * 100);
203
+ return `${progressBar(fraction)} ${String(pct2).padStart(3)}% ${formatClockMs(
204
204
  item.processedDurationMs
205
205
  )} / ${formatClockMs(item.recording?.durationMs)}`;
206
206
  }
@@ -591,704 +591,436 @@ var init_LiveCaptionsScreen = __esm({
591
591
  }
592
592
  });
593
593
 
594
- // src/tui/RecordingHeroScreen.tsx
595
- import { useEffect as useEffect2, useRef, useState as useState3 } from "react";
596
- import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
597
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
598
- function waveRowsFor(terminalRows) {
599
- return terminalRows >= 30 ? 5 : 3;
594
+ // src/tui/AccountView.tsx
595
+ import { Box as Box3, Text as Text3 } from "ink";
596
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
597
+ function AccountView({
598
+ status,
599
+ nowMs = Date.now()
600
+ }) {
601
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, children: [
602
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "\u2039 Account" }),
603
+ status === "loading" || status === void 0 ? /* @__PURE__ */ jsx5(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Loading account\u2026" }) }) : status === "error" ? /* @__PURE__ */ jsx5(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text3, { color: "red", children: "Couldn't load account status" }) }) : !status.loggedIn ? /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
604
+ /* @__PURE__ */ jsx5(Text3, { color: "yellow", children: "Not signed in" }),
605
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: `origin ${status.origin}` }),
606
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Run `recappi auth login` to sign in." })
607
+ ] }) : /* @__PURE__ */ jsx5(AccountBody, { status, nowMs }),
608
+ /* @__PURE__ */ jsx5(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "r refresh \xB7 esc back \xB7 q quit" }) })
609
+ ] });
600
610
  }
601
- function litCount(level, rows) {
602
- const amp = Math.max(0, Math.min(1, level));
603
- if (amp <= 0.028) return 0;
604
- return Math.max(1, Math.min(rows, Math.ceil(Math.pow(amp, 0.58) * rows)));
611
+ function AccountBody({
612
+ status,
613
+ nowMs
614
+ }) {
615
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
616
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
617
+ /* @__PURE__ */ jsxs3(Text3, { children: [
618
+ /* @__PURE__ */ jsx5(Text3, { color: "green", children: "\u25CF " }),
619
+ /* @__PURE__ */ jsx5(Text3, { bold: true, children: status.email ?? status.userId ?? "Signed in" })
620
+ ] }),
621
+ status.email && status.userId ? /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: status.userId }) : null,
622
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: `origin ${status.origin}` })
623
+ ] }),
624
+ status.billing ? /* @__PURE__ */ jsx5(Usage, { billing: status.billing, nowMs }) : null,
625
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
626
+ /* @__PURE__ */ jsx5(Text3, { bold: true, dimColor: true, children: "LOCAL STORE" }),
627
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, wrap: "truncate-middle", children: status.localStore.path }),
628
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
629
+ `${status.localStore.accountScopedArtifacts} artifact${status.localStore.accountScopedArtifacts === 1 ? "" : "s"} for this account`,
630
+ status.localStore.unattributedArtifacts > 0 ? ` \xB7 ${status.localStore.unattributedArtifacts} unattributed` : ""
631
+ ] })
632
+ ] })
633
+ ] });
605
634
  }
606
- function litCounts(samples, width, rows) {
607
- if (width <= 0) return [];
608
- const tail = samples.slice(-width);
609
- return [...Array(Math.max(0, width - tail.length)).fill(0), ...tail].map((v) => litCount(v, rows));
635
+ function Usage({
636
+ billing,
637
+ nowMs
638
+ }) {
639
+ const minutesCap = billing.minutesCap;
640
+ const minutesUsed = billing.minutesUsed;
641
+ const storageCap = billing.storageCapBytes;
642
+ return /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
643
+ /* @__PURE__ */ jsx5(Text3, { bold: true, dimColor: true, children: "USAGE" }),
644
+ /* @__PURE__ */ jsxs3(Text3, { children: [
645
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Plan " }),
646
+ /* @__PURE__ */ jsx5(Text3, { bold: true, children: billing.tier })
647
+ ] }),
648
+ /* @__PURE__ */ jsxs3(Text3, { children: [
649
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Minutes " }),
650
+ minutesCap != null ? /* @__PURE__ */ jsx5(Text3, { color: billing.isOverMinutes ? "red" : "cyan", children: `${progressBar(minutesUsed / Math.max(1, minutesCap), 12)} ` }) : null,
651
+ /* @__PURE__ */ jsx5(Text3, { color: billing.isOverMinutes ? "red" : void 0, children: `${Math.round(minutesUsed)}` }),
652
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: ` / ${minutesCap != null ? Math.round(minutesCap) : "\u221E"} min` }),
653
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: ` (batch ${Math.round(billing.batchMinutesUsed)} \xB7 live ${Math.round(billing.realtimeMinutesUsed)})` })
654
+ ] }),
655
+ /* @__PURE__ */ jsxs3(Text3, { children: [
656
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Storage " }),
657
+ storageCap != null ? /* @__PURE__ */ jsx5(Text3, { color: billing.isOverStorage ? "red" : "cyan", children: `${progressBar(billing.storageBytes / Math.max(1, storageCap), 12)} ` }) : null,
658
+ /* @__PURE__ */ jsx5(Text3, { color: billing.isOverStorage ? "red" : void 0, children: formatBytes2(billing.storageBytes) }),
659
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: ` / ${storageCap != null ? formatBytes2(storageCap) : "\u221E"}` })
660
+ ] }),
661
+ billing.isOverMinutes || billing.isOverStorage ? /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
662
+ billing.isOverMinutes ? "Over minutes limit. " : "",
663
+ billing.isOverStorage ? "Over storage limit." : ""
664
+ ] }) : null,
665
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: `Period ${periodText(billing, nowMs)}` })
666
+ ] });
610
667
  }
611
- function levelDb(level) {
612
- if (level <= 0.03) return "silent";
613
- return `${Math.round(level * 60 - 60)} dB`;
668
+ function periodText(billing, nowMs) {
669
+ const remainingMs = epochToMs(billing.periodEnd) - nowMs;
670
+ if (!Number.isFinite(remainingMs) || remainingMs <= 0) return "\u2014";
671
+ const days = Math.floor(remainingMs / 864e5);
672
+ if (days >= 1) return `${days}d left`;
673
+ return `${formatClockMs(remainingMs)} left`;
614
674
  }
615
- function MeterRow({
675
+ function epochToMs(value) {
676
+ return value > 1e12 ? value : value * 1e3;
677
+ }
678
+ var init_AccountView = __esm({
679
+ "src/tui/AccountView.tsx"() {
680
+ "use strict";
681
+ init_format();
682
+ }
683
+ });
684
+
685
+ // src/tui/chrome.tsx
686
+ import { Box as Box4, Text as Text4 } from "ink";
687
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
688
+ function Header({ active }) {
689
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
690
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "green", children: [
691
+ "\u25CF Recappi",
692
+ " "
693
+ ] }),
694
+ /* @__PURE__ */ jsx6(Tab, { num: "1", label: "Overview", active: active === "overview" }),
695
+ /* @__PURE__ */ jsx6(Tab, { num: "2", label: "Jobs", active: active === "jobs" }),
696
+ /* @__PURE__ */ jsx6(Tab, { num: "3", label: "Account", active: active === "account" })
697
+ ] });
698
+ }
699
+ function Tab({
700
+ num,
616
701
  label,
617
- samples,
618
- level,
619
- paused,
620
- width,
621
- rows
702
+ active
622
703
  }) {
623
- const silent = level <= 0.03;
624
- const cols = litCounts(samples, width, rows);
625
- const litColor = paused ? "gray" : "cyan";
626
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
627
- /* @__PURE__ */ jsxs2(Box2, { width: width + 9, children: [
628
- /* @__PURE__ */ jsx3(Box2, { width: 9, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: label }) }),
629
- /* @__PURE__ */ jsx3(Box2, { flexGrow: 1, justifyContent: "flex-end", children: !paused && silent ? /* @__PURE__ */ jsx3(Text2, { color: "yellow", children: "silent" }) : /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: paused ? "paused" : levelDb(level) }) })
630
- ] }),
631
- Array.from({ length: rows }, (_, r) => {
632
- const fromBottom = rows - r;
633
- return /* @__PURE__ */ jsx3(Text2, { children: cols.map(
634
- (c, i) => c >= fromBottom ? /* @__PURE__ */ jsx3(Text2, { color: litColor, children: c === fromBottom ? "\u2022" : "\u25CF" }, i) : /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "\xB7" }, i)
635
- ) }, r);
636
- })
704
+ if (active) {
705
+ return /* @__PURE__ */ jsx6(Text4, { bold: true, inverse: true, color: "cyan", children: ` ${num} ${label} ` });
706
+ }
707
+ return /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
708
+ " ",
709
+ /* @__PURE__ */ jsx6(Text4, { color: "cyan", children: num }),
710
+ ` ${label} `
637
711
  ] });
638
712
  }
639
- function ProgressBar({ fraction, width = 12 }) {
640
- const f = Math.max(0, Math.min(1, fraction));
641
- const filled = Math.round(f * width);
642
- return /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
643
- "\u2593".repeat(filled),
644
- /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "\u2591".repeat(Math.max(0, width - filled)) })
713
+ function Footer({ keys }) {
714
+ const segments = keys.split(" \xB7 ");
715
+ return /* @__PURE__ */ jsx6(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text4, { children: segments.map((segment, i) => {
716
+ const space = segment.indexOf(" ");
717
+ const key = space === -1 ? segment : segment.slice(0, space);
718
+ const desc = space === -1 ? "" : segment.slice(space);
719
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
720
+ i > 0 ? /* @__PURE__ */ jsx6(Text4, { dimColor: true, children: " \xB7 " }) : null,
721
+ /* @__PURE__ */ jsx6(Text4, { color: "cyan", children: key }),
722
+ /* @__PURE__ */ jsx6(Text4, { dimColor: true, children: desc })
723
+ ] }, `${segment}-${i}`);
724
+ }) }) });
725
+ }
726
+ var init_chrome = __esm({
727
+ "src/tui/chrome.tsx"() {
728
+ "use strict";
729
+ }
730
+ });
731
+
732
+ // src/tui/JobRow.tsx
733
+ import { Box as Box5, Text as Text5 } from "ink";
734
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
735
+ function JobRow({
736
+ item,
737
+ selected,
738
+ spinnerFrame
739
+ }) {
740
+ const style = statusStyle(item.status);
741
+ const glyph = statusGlyph(item.status, spinnerFrame);
742
+ const title = item.recording?.title ?? item.recordingId;
743
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
744
+ /* @__PURE__ */ jsx7(Box5, { width: 3, children: /* @__PURE__ */ jsx7(Text5, { color: "cyan", children: selected ? "\u25B8" : "" }) }),
745
+ /* @__PURE__ */ jsx7(Box5, { width: 2, children: /* @__PURE__ */ jsx7(Text5, { color: style.color, children: glyph }) }),
746
+ /* @__PURE__ */ jsx7(Box5, { width: 13, children: /* @__PURE__ */ jsx7(Text5, { color: style.color, children: style.label }) }),
747
+ /* @__PURE__ */ jsx7(Box5, { width: 26, children: /* @__PURE__ */ jsx7(Text5, { bold: selected, wrap: "truncate-end", children: title }) }),
748
+ /* @__PURE__ */ jsx7(Text5, { dimColor: !selected, children: jobDetail(item) })
645
749
  ] });
646
750
  }
647
- function stoppedPhase(artifact) {
648
- if (!artifact) return null;
649
- if (artifact.uploadStatus === "uploading") {
650
- return { label: "Uploading to Recappi Cloud", fraction: artifact.uploadProgress };
751
+ var init_JobRow = __esm({
752
+ "src/tui/JobRow.tsx"() {
753
+ "use strict";
754
+ init_format();
651
755
  }
652
- if (artifact.uploadStatus === "queued") return { label: "Queued to upload" };
653
- if (artifact.transcriptionStatus === "processing") {
654
- return { label: "Transcribing", fraction: artifact.transcriptionProgress };
756
+ });
757
+
758
+ // src/tui/JobsView.tsx
759
+ import { Box as Box6, Text as Text6 } from "ink";
760
+ import { jsx as jsx8 } from "react/jsx-runtime";
761
+ function JobsView({
762
+ items,
763
+ selectedIndex,
764
+ spinnerFrame
765
+ }) {
766
+ if (items.length === 0) {
767
+ return /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: "No transcription jobs yet \u2014 run: recappi upload <file> --transcribe" }) });
655
768
  }
656
- return null;
769
+ return /* @__PURE__ */ jsx8(Box6, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx8(
770
+ JobRow,
771
+ {
772
+ item,
773
+ selected: index === selectedIndex,
774
+ spinnerFrame
775
+ },
776
+ item.jobId
777
+ )) });
657
778
  }
658
- function RecordingHeroScreen({
659
- telemetry,
660
- artifact,
661
- captions,
662
- canTranscribe = false,
663
- canPause = false,
664
- now = () => Date.now()
665
- }) {
666
- const size = useTerminalSize();
667
- const [tick, setTick] = useState3(() => now());
668
- const [waveSys, setWaveSys] = useState3([]);
669
- const [waveMic, setWaveMic] = useState3([]);
670
- const [captionMode, setCaptionMode] = useState3("both");
671
- const lastAppendRef = useRef(0);
672
- useInput2((input) => {
673
- if (input === "c") {
674
- setCaptionMode((m) => m === "both" ? "source" : m === "source" ? "translation" : "both");
675
- }
676
- });
677
- useEffect2(() => {
678
- if (telemetry.level == null) return;
679
- const t = now();
680
- if (t - lastAppendRef.current < WAVE_THROTTLE_MS) return;
681
- lastAppendRef.current = t;
682
- setWaveSys((w) => [...w.slice(-512), telemetry.level.system ?? 0]);
683
- setWaveMic((w) => [...w.slice(-512), telemetry.level.mic ?? 0]);
684
- }, [telemetry.level]);
685
- useEffect2(() => {
686
- const id = setInterval(() => setTick(now()), 1e3);
687
- return () => clearInterval(id);
688
- }, []);
689
- const elapsed = telemetry.startedAtMs != null ? formatClockMs(Math.max(0, tick - telemetry.startedAtMs)) : "00:00";
690
- const innerWidth = Math.max(10, size.columns - 4);
691
- if (telemetry.status === "stopped") {
692
- const handoff = stoppedHandoffCopy(artifact, canTranscribe);
693
- const phase = stoppedPhase(artifact);
694
- const meta3 = [
695
- telemetry.durationMs != null ? formatClockMs(telemetry.durationMs) : null,
696
- formatBytes2(telemetry.sizeBytes) || null
697
- ].filter(Boolean).join(" \xB7 ");
698
- const saved = artifact?.uploadStatus === "uploaded" ? "\u2713 Saved to Recappi Cloud" : "\u2713 Saved to your Mac";
699
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
700
- /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "recappi \xB7 Recording" }),
701
- /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
702
- /* @__PURE__ */ jsx3(Text2, { color: "green", children: saved }),
703
- meta3 ? /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: meta3 }) : null,
704
- telemetry.savedPath ? /* @__PURE__ */ jsx3(Text2, { dimColor: true, wrap: "truncate-middle", children: telemetry.savedPath }) : null
705
- ] }),
706
- phase ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
707
- /* @__PURE__ */ jsx3(Text2, { color: "cyan", children: `\u25D0 ${phase.label}` }),
708
- phase.fraction != null ? /* @__PURE__ */ jsxs2(Fragment, { children: [
709
- /* @__PURE__ */ jsx3(Text2, { children: " " }),
710
- /* @__PURE__ */ jsx3(ProgressBar, { fraction: phase.fraction }),
711
- /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: ` ${Math.round(phase.fraction * 100)}%` })
712
- ] }) : /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "\u2026" })
713
- ] }) : null,
714
- /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
715
- /* @__PURE__ */ jsx3(Text2, { color: handoff.tone === "red" ? "red" : handoff.tone === "green" ? "green" : void 0, dimColor: handoff.tone === "dim", children: handoff.text }),
716
- artifact?.error ? /* @__PURE__ */ jsx3(Text2, { color: "red", wrap: "truncate-end", children: artifact.error }) : null
717
- ] })
718
- ] });
719
- }
720
- if (telemetry.status === "error") {
721
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
722
- /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "recappi \xB7 Recording" }),
723
- /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { color: "red", children: telemetry.error ? `Recording error: ${telemetry.error}` : "Recording error" }) }),
724
- /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "esc back" }) })
725
- ] });
779
+ var init_JobsView = __esm({
780
+ "src/tui/JobsView.tsx"() {
781
+ "use strict";
782
+ init_JobRow();
726
783
  }
727
- const paused = telemetry.status === "paused";
728
- const starting = telemetry.status === "starting" || telemetry.status === "stopping";
729
- const badge = paused ? "\u23F8 PAUSED" : starting ? "\u2026" : "\u23FA REC";
730
- const meterW = Math.max(10, Math.min(72, innerWidth - 20));
731
- const sizeStr = telemetry.sizeBytes ? formatBytes2(telemetry.sizeBytes) : "";
732
- const context = [telemetry.sourceLabel, telemetry.micEnabled ? "Microphone" : null, sizeStr || null].filter(Boolean).join(" \xB7 ");
733
- const waveRows = waveRowsFor(size.rows);
734
- const meterBlockRows = (telemetry.micEnabled ? 2 : 1) * (waveRows + 1) + (telemetry.micEnabled ? 1 : 0);
735
- const fixedRows = 8 + meterBlockRows;
736
- const captionRows = Math.max(2, size.rows - fixedRows);
737
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
738
- /* @__PURE__ */ jsxs2(Text2, { children: [
739
- /* @__PURE__ */ jsx3(Text2, { bold: true, color: "green", children: "recappi" }),
740
- /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: " \xB7 Recording" })
741
- ] }),
742
- /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, paddingX: 1, flexDirection: "column", children: [
743
- /* @__PURE__ */ jsxs2(Text2, { children: [
744
- /* @__PURE__ */ jsx3(Text2, { bold: true, color: paused ? "yellow" : "red", children: badge }),
745
- /* @__PURE__ */ jsx3(Text2, { children: " " }),
746
- /* @__PURE__ */ jsx3(Text2, { bold: true, children: elapsed })
747
- ] }),
748
- /* @__PURE__ */ jsx3(Box2, { marginTop: 1, flexDirection: "column", children: telemetry.level == null ? (
749
- // No level telemetry yet — honest activity, not a flat meter that
750
- // reads as silence (the elapsed timer above proves it's live).
751
- /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: paused ? "Paused" : `Capturing audio${".".repeat(Math.floor(tick / 1e3) % 3 + 1)}` })
752
- ) : /* @__PURE__ */ jsxs2(Fragment, { children: [
753
- /* @__PURE__ */ jsx3(MeterRow, { label: "System", samples: waveSys, level: telemetry.level.system ?? 0, paused, width: meterW, rows: waveRows }),
754
- telemetry.micEnabled ? /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(MeterRow, { label: "Mic", samples: waveMic, level: telemetry.level.mic ?? 0, paused, width: meterW, rows: waveRows }) }) : null
755
- ] }) }),
756
- /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: context }) }),
757
- captions ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
758
- /* @__PURE__ */ jsx3(Text2, { bold: true, dimColor: true, children: "LIVE CAPTIONS" }),
759
- /* @__PURE__ */ jsx3(HeroCaptions, { state: captions, maxRows: captionRows, width: innerWidth, mode: captionMode })
760
- ] }) : null
761
- ] }),
762
- /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
763
- "q stop & save",
764
- canPause ? ` \xB7 p ${paused ? "resume" : "pause"}` : "",
765
- captions ? " \xB7 c captions" : ""
766
- ] }) })
767
- ] });
784
+ });
785
+
786
+ // src/tui/RecordingRow.tsx
787
+ import { Box as Box7, Text as Text7 } from "ink";
788
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
789
+ function recordingTitle2(item) {
790
+ const named = (item.title || item.summaryTitle || "").trim();
791
+ if (named && !UUID_RE.test(named)) return named;
792
+ return "Untitled";
768
793
  }
769
- function wrappedRows(text, width) {
770
- return Math.max(1, Math.ceil(displayWidth(text) / Math.max(1, width)));
794
+ function recordingProcessingState(item, jobStatus, spinnerFrame) {
795
+ if (item.status === "uploading") return { glyph: "\u2191", color: "cyan" };
796
+ if (item.status === "failed" || jobStatus === "failed") return { glyph: "\u2717", color: "red" };
797
+ if (jobStatus === "running") return { glyph: spinnerChar(spinnerFrame), color: "cyan" };
798
+ if (jobStatus === "queued") return { glyph: "\u25CB", color: "yellow" };
799
+ if (item.status === "aborted") return { glyph: "\u2022", color: "gray" };
800
+ if (item.activeTranscriptId) return { glyph: "\u2713", color: "green" };
801
+ return { glyph: "\xB7", color: "gray" };
771
802
  }
772
- function captionColumn(items, maxRows, width, dim) {
773
- const budget = Math.max(1, maxRows);
774
- const chosen = [];
775
- let used = 0;
776
- for (let i = items.length - 1; i >= 0; i--) {
777
- const h = wrappedRows(items[i].text, width);
778
- if (used + h > budget && chosen.length > 0) break;
779
- chosen.unshift(items[i]);
780
- used += h;
781
- }
782
- return chosen.map((it) => /* @__PURE__ */ jsx3(Text2, { dimColor: dim, wrap: "wrap", children: it.text }, it.key));
803
+ function recordingLayout(columns) {
804
+ const usable = Math.max(20, columns - 2);
805
+ const showWhen = usable >= 54;
806
+ const title = Math.max(
807
+ 10,
808
+ usable - MARKER_W - GLYPH_W - LENGTH_W - (showWhen ? WHEN_W : 0) - DL_W
809
+ );
810
+ return { title, showWhen };
783
811
  }
784
- function HeroCaptions({
785
- state,
786
- maxRows,
787
- width,
788
- mode
812
+ function RecordingRow({
813
+ item,
814
+ selected,
815
+ nowMs,
816
+ columns,
817
+ jobStatus,
818
+ spinnerFrame = 0,
819
+ downloaded = false
789
820
  }) {
790
- const hasPartial = Boolean(state.partial && state.partial.length > 0);
791
- const captionError = state.status === "error" ? `Captions unavailable: ${state.error ?? "Live captions unavailable."}` : null;
792
- if (state.lines.length === 0 && !hasPartial) {
793
- return /* @__PURE__ */ jsx3(Text2, { color: captionError ? "yellow" : void 0, dimColor: !captionError, children: captionError ?? (state.status === "live" ? "Listening for speech\u2026" : liveCaptionStatusLabel(state.status)) });
794
- }
795
- const sourceItems = state.lines.map((l) => ({
796
- key: `${l.id}-s`,
797
- text: `${l.speaker ? `${l.speaker}: ` : ""}${trimLead(l.text)}`
798
- }));
799
- if (hasPartial) sourceItems.push({ key: "sp", text: trimLead(state.partial) });
800
- const translationItems = state.lines.filter((l) => l.translation).map((l) => ({ key: `${l.id}-t`, text: trimLead(l.translation) }));
801
- if (state.translationPartial) translationItems.push({ key: "tp", text: trimLead(state.translationPartial) });
802
- const errLine = captionError ? /* @__PURE__ */ jsx3(Text2, { color: "yellow", wrap: "wrap", children: captionError }) : null;
803
- const hasTranslation = translationItems.length > 0;
804
- if (mode === "source" || !hasTranslation) {
805
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
806
- captionColumn(sourceItems, maxRows, width, false),
807
- errLine
808
- ] });
809
- }
810
- if (mode === "translation") {
811
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
812
- captionColumn(translationItems, maxRows, width, false),
813
- errLine
814
- ] });
815
- }
816
- const gap = 2;
817
- const colW = Math.max(12, Math.floor((width - gap) / 2));
818
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
819
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", children: [
820
- /* @__PURE__ */ jsxs2(Box2, { width: colW, flexDirection: "column", marginRight: gap, children: [
821
- /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "ORIGINAL" }),
822
- captionColumn(sourceItems, Math.max(1, maxRows - 1), colW, false)
823
- ] }),
824
- /* @__PURE__ */ jsxs2(Box2, { width: colW, flexDirection: "column", children: [
825
- /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "TRANSLATION" }),
826
- captionColumn(translationItems, Math.max(1, maxRows - 1), colW, false)
827
- ] })
828
- ] }),
829
- errLine
821
+ const { title, showWhen } = recordingLayout(columns);
822
+ const { glyph, color } = recordingProcessingState(item, jobStatus, spinnerFrame);
823
+ const duration3 = item.durationMs ? formatClockMs(item.durationMs) : "\u2014";
824
+ return /* @__PURE__ */ jsxs6(Box7, { children: [
825
+ /* @__PURE__ */ jsx9(Box7, { width: MARKER_W, children: /* @__PURE__ */ jsx9(Text7, { color: "cyan", children: selected ? "\u25B8" : "" }) }),
826
+ /* @__PURE__ */ jsx9(Box7, { width: GLYPH_W, children: /* @__PURE__ */ jsx9(Text7, { color, children: glyph }) }),
827
+ /* @__PURE__ */ jsx9(Box7, { width: title, children: /* @__PURE__ */ jsx9(Text7, { bold: selected, wrap: "truncate-end", children: recordingTitle2(item) }) }),
828
+ /* @__PURE__ */ jsx9(Box7, { width: LENGTH_W, justifyContent: "flex-end", children: /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: duration3 }) }),
829
+ showWhen ? /* @__PURE__ */ jsx9(Box7, { width: WHEN_W, justifyContent: "flex-end", children: /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: formatAge(item.createdAt, nowMs) }) }) : null,
830
+ /* @__PURE__ */ jsx9(Box7, { width: DL_W, justifyContent: "flex-end", children: /* @__PURE__ */ jsx9(Text7, { color: "green", children: downloaded ? "\u2913" : "" }) })
830
831
  ] });
831
832
  }
832
- function stoppedHandoffCopy(artifact, canTranscribe) {
833
- if (artifact?.uploadStatus === "uploading" || artifact?.transcriptionStatus === "processing") {
834
- return { text: "esc run in background", tone: "dim" };
835
- }
836
- if (artifact?.transcriptionStatus === "queued") {
837
- return { text: "Transcription queued \xB7 \u23CE open recording \xB7 n not now", tone: "green" };
838
- }
839
- if (artifact?.transcriptionStatus === "ready") {
840
- return { text: "Transcription ready \xB7 \u23CE open recording \xB7 n not now", tone: "green" };
841
- }
842
- if (artifact?.uploadStatus === "failed" || artifact?.transcriptionStatus === "failed") {
843
- return { text: "Transcription failed \xB7 \u23CE retry \xB7 n not now", tone: "red" };
833
+ var UUID_RE, MARKER_W, GLYPH_W, LENGTH_W, WHEN_W, DL_W;
834
+ var init_RecordingRow = __esm({
835
+ "src/tui/RecordingRow.tsx"() {
836
+ "use strict";
837
+ init_format();
838
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
839
+ MARKER_W = 3;
840
+ GLYPH_W = 2;
841
+ LENGTH_W = 8;
842
+ WHEN_W = 9;
843
+ DL_W = 3;
844
844
  }
845
- if (!canTranscribe || !artifact?.audioPath) {
846
- return { text: "Saved locally \xB7 n back", tone: "dim" };
845
+ });
846
+
847
+ // src/tui/RecordingsView.tsx
848
+ import React5 from "react";
849
+ import { Box as Box8, Text as Text8 } from "ink";
850
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
851
+ function RecordingsView({
852
+ items,
853
+ selectedIndex,
854
+ nowMs,
855
+ columns,
856
+ jobStatusByRecording,
857
+ downloadedRecordingIds,
858
+ spinnerFrame = 0
859
+ }) {
860
+ if (items.length === 0) {
861
+ return /* @__PURE__ */ jsx10(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "No recordings yet \u2014 run: recappi upload <file>" }) });
847
862
  }
848
- return { text: "Starting transcription\u2026", tone: "normal" };
863
+ return /* @__PURE__ */ jsx10(Box8, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => {
864
+ const bucket = dateBucket(item.createdAt, nowMs);
865
+ const showHeader = index === 0 || bucket !== dateBucket(items[index - 1].createdAt, nowMs);
866
+ return /* @__PURE__ */ jsxs7(React5.Fragment, { children: [
867
+ showHeader ? /* @__PURE__ */ jsx10(Box8, { marginTop: index === 0 ? 0 : 1, children: /* @__PURE__ */ jsx10(Text8, { bold: true, dimColor: true, children: bucket }) }) : null,
868
+ /* @__PURE__ */ jsx10(
869
+ RecordingRow,
870
+ {
871
+ item,
872
+ selected: index === selectedIndex,
873
+ nowMs,
874
+ columns,
875
+ jobStatus: jobStatusByRecording?.get(item.recordingId),
876
+ downloaded: downloadedRecordingIds?.has(item.recordingId) ?? false,
877
+ spinnerFrame
878
+ }
879
+ )
880
+ ] }, item.recordingId);
881
+ }) });
849
882
  }
850
- var WAVE_THROTTLE_MS, trimLead;
851
- var init_RecordingHeroScreen = __esm({
852
- "src/tui/RecordingHeroScreen.tsx"() {
883
+ var init_RecordingsView = __esm({
884
+ "src/tui/RecordingsView.tsx"() {
853
885
  "use strict";
886
+ init_RecordingRow();
854
887
  init_format();
855
- init_liveCaptions();
856
- init_terminal();
857
- WAVE_THROTTLE_MS = 220;
858
- trimLead = (s) => s.replace(/^\s+/, "");
859
888
  }
860
889
  });
861
890
 
862
- // src/tui/AccountView.tsx
863
- import { Box as Box3, Text as Text3 } from "ink";
864
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
865
- function AccountView({
866
- status,
867
- nowMs = Date.now()
868
- }) {
869
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, children: [
870
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "\u2039 Account" }),
871
- status === "loading" || status === void 0 ? /* @__PURE__ */ jsx5(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Loading account\u2026" }) }) : status === "error" ? /* @__PURE__ */ jsx5(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text3, { color: "red", children: "Couldn't load account status" }) }) : !status.loggedIn ? /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
872
- /* @__PURE__ */ jsx5(Text3, { color: "yellow", children: "Not signed in" }),
873
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: `origin ${status.origin}` }),
874
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Run `recappi auth login` to sign in." })
875
- ] }) : /* @__PURE__ */ jsx5(AccountBody, { status, nowMs }),
876
- /* @__PURE__ */ jsx5(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "r refresh \xB7 esc back \xB7 q quit" }) })
877
- ] });
891
+ // src/tui/RecordingPeek.tsx
892
+ import { Box as Box9, Text as Text9 } from "ink";
893
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
894
+ function RecordingPeek({
895
+ item,
896
+ summary,
897
+ nowMs,
898
+ width
899
+ }) {
900
+ return /* @__PURE__ */ jsx11(Box9, { width, borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: !item ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "No selection" }) : /* @__PURE__ */ jsx11(PeekBody, { item, summary, nowMs }) });
878
901
  }
879
- function AccountBody({
880
- status,
902
+ function PeekBody({
903
+ item,
904
+ summary,
881
905
  nowMs
882
906
  }) {
883
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
884
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
885
- /* @__PURE__ */ jsxs3(Text3, { children: [
886
- /* @__PURE__ */ jsx5(Text3, { color: "green", children: "\u25CF " }),
887
- /* @__PURE__ */ jsx5(Text3, { bold: true, children: status.email ?? status.userId ?? "Signed in" })
888
- ] }),
889
- status.email && status.userId ? /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: status.userId }) : null,
890
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: `origin ${status.origin}` })
907
+ const style = recordingStatusStyle(item.status);
908
+ const meta3 = [
909
+ item.durationMs ? formatClockMs(item.durationMs) : null,
910
+ formatBytes2(item.sizeBytes) || null,
911
+ formatAge(item.createdAt, nowMs)
912
+ ].filter(Boolean).join(" \xB7 ");
913
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
914
+ /* @__PURE__ */ jsx11(Text9, { bold: true, wrap: "truncate-end", children: recordingTitle2(item) }),
915
+ /* @__PURE__ */ jsxs8(Box9, { children: [
916
+ /* @__PURE__ */ jsx11(Text9, { color: style.color, children: `${style.glyph} ${style.label}` }),
917
+ meta3 ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` ${meta3}` }) : null
891
918
  ] }),
892
- status.billing ? /* @__PURE__ */ jsx5(Usage, { billing: status.billing, nowMs }) : null,
893
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
894
- /* @__PURE__ */ jsx5(Text3, { bold: true, dimColor: true, children: "LOCAL STORE" }),
895
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, wrap: "truncate-middle", children: status.localStore.path }),
896
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
897
- `${status.localStore.accountScopedArtifacts} artifact${status.localStore.accountScopedArtifacts === 1 ? "" : "s"} for this account`,
898
- status.localStore.unattributedArtifacts > 0 ? ` \xB7 ${status.localStore.unattributedArtifacts} unattributed` : ""
899
- ] })
900
- ] })
919
+ /* @__PURE__ */ jsx11(Box9, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx11(SummarySection, { item, summary }) }),
920
+ /* @__PURE__ */ jsx11(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "\u23CE open \xB7 t transcript" }) })
901
921
  ] });
902
922
  }
903
- function Usage({
904
- billing,
905
- nowMs
923
+ function SummarySection({
924
+ item,
925
+ summary
906
926
  }) {
907
- const minutesCap = billing.minutesCap;
908
- const minutesUsed = billing.minutesUsed;
909
- const storageCap = billing.storageCapBytes;
910
- return /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
911
- /* @__PURE__ */ jsx5(Text3, { bold: true, dimColor: true, children: "USAGE" }),
912
- /* @__PURE__ */ jsxs3(Text3, { children: [
913
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Plan " }),
914
- /* @__PURE__ */ jsx5(Text3, { bold: true, children: billing.tier })
915
- ] }),
916
- /* @__PURE__ */ jsxs3(Text3, { children: [
917
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Minutes " }),
918
- minutesCap != null ? /* @__PURE__ */ jsx5(Text3, { color: billing.isOverMinutes ? "red" : "cyan", children: `${progressBar(minutesUsed / Math.max(1, minutesCap), 12)} ` }) : null,
919
- /* @__PURE__ */ jsx5(Text3, { color: billing.isOverMinutes ? "red" : void 0, children: `${Math.round(minutesUsed)}` }),
920
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: ` / ${minutesCap != null ? Math.round(minutesCap) : "\u221E"} min` }),
921
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: ` (batch ${Math.round(billing.batchMinutesUsed)} \xB7 live ${Math.round(billing.realtimeMinutesUsed)})` })
922
- ] }),
923
- /* @__PURE__ */ jsxs3(Text3, { children: [
924
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Storage " }),
925
- storageCap != null ? /* @__PURE__ */ jsx5(Text3, { color: billing.isOverStorage ? "red" : "cyan", children: `${progressBar(billing.storageBytes / Math.max(1, storageCap), 12)} ` }) : null,
926
- /* @__PURE__ */ jsx5(Text3, { color: billing.isOverStorage ? "red" : void 0, children: formatBytes2(billing.storageBytes) }),
927
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: ` / ${storageCap != null ? formatBytes2(storageCap) : "\u221E"}` })
928
- ] }),
929
- billing.isOverMinutes || billing.isOverStorage ? /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
930
- billing.isOverMinutes ? "Over minutes limit. " : "",
931
- billing.isOverStorage ? "Over storage limit." : ""
932
- ] }) : null,
933
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: `Period ${periodText(billing, nowMs)}` })
927
+ if (!item.activeTranscriptId) return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "No transcript yet" });
928
+ if (summary === "loading" || summary === void 0) return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "Loading summary\u2026" });
929
+ if (summary === "error") return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "(summary unavailable)" });
930
+ if (summary.status !== "succeeded" || !summary.tldr) {
931
+ return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: `Summary ${summary.status}` });
932
+ }
933
+ const points = (summary.keyPoints ?? []).slice(0, 3);
934
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
935
+ /* @__PURE__ */ jsx11(Text9, { bold: true, dimColor: true, children: "SUMMARY" }),
936
+ /* @__PURE__ */ jsx11(Text9, { children: summary.tldr }),
937
+ points.length > 0 ? /* @__PURE__ */ jsx11(Box9, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx11(Text9, { dimColor: true, wrap: "truncate-end", children: `\u2022 ${point}` }, i)) }) : null
934
938
  ] });
935
939
  }
936
- function periodText(billing, nowMs) {
937
- const remainingMs = epochToMs(billing.periodEnd) - nowMs;
938
- if (!Number.isFinite(remainingMs) || remainingMs <= 0) return "\u2014";
939
- const days = Math.floor(remainingMs / 864e5);
940
- if (days >= 1) return `${days}d left`;
941
- return `${formatClockMs(remainingMs)} left`;
942
- }
943
- function epochToMs(value) {
944
- return value > 1e12 ? value : value * 1e3;
945
- }
946
- var init_AccountView = __esm({
947
- "src/tui/AccountView.tsx"() {
940
+ var init_RecordingPeek = __esm({
941
+ "src/tui/RecordingPeek.tsx"() {
948
942
  "use strict";
949
943
  init_format();
944
+ init_RecordingRow();
950
945
  }
951
946
  });
952
947
 
953
- // src/tui/chrome.tsx
954
- import { Box as Box4, Text as Text4 } from "ink";
955
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
956
- function Header({ active }) {
957
- return /* @__PURE__ */ jsxs4(Box4, { children: [
958
- /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "green", children: [
959
- "\u25CF Recappi",
960
- " "
961
- ] }),
962
- /* @__PURE__ */ jsx6(Tab, { num: "1", label: "Overview", active: active === "overview" }),
963
- /* @__PURE__ */ jsx6(Tab, { num: "2", label: "Jobs", active: active === "jobs" }),
964
- /* @__PURE__ */ jsx6(Tab, { num: "3", label: "Account", active: active === "account" })
965
- ] });
966
- }
967
- function Tab({
968
- num,
969
- label,
970
- active
948
+ // src/tui/OverviewView.tsx
949
+ import { Box as Box10, Text as Text10 } from "ink";
950
+ import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
951
+ function OverviewView({
952
+ recordings,
953
+ jobs,
954
+ stats,
955
+ selectedIndex,
956
+ spinnerFrame,
957
+ nowMs,
958
+ columns,
959
+ jobStatusByRecording,
960
+ downloadedRecordingIds,
961
+ peekItem,
962
+ peekSummary,
963
+ showPeek = false,
964
+ peekWidth = 0
971
965
  }) {
972
- if (active) {
973
- return /* @__PURE__ */ jsx6(Text4, { bold: true, inverse: true, color: "cyan", children: ` ${num} ${label} ` });
974
- }
975
- return /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
976
- " ",
977
- /* @__PURE__ */ jsx6(Text4, { color: "cyan", children: num }),
978
- ` ${label} `
966
+ const jobCounts = countJobs(jobs);
967
+ const running = stats?.jobs.running ?? jobCounts.running;
968
+ const queued = stats?.jobs.queued ?? jobCounts.queued;
969
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", children: [
970
+ /* @__PURE__ */ jsxs9(Box10, { children: [
971
+ /* @__PURE__ */ jsx12(Text10, { bold: true, children: stats?.recordings.total ?? recordings.length }),
972
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " recordings" }),
973
+ stats?.recordings.ready != null ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
974
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
975
+ /* @__PURE__ */ jsx12(Text10, { color: "green", children: `${stats.recordings.ready} ready` })
976
+ ] }) : null,
977
+ running > 0 ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
978
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
979
+ /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: `${running} transcribing` })
980
+ ] }) : null,
981
+ queued > 0 ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
982
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
983
+ /* @__PURE__ */ jsx12(Text10, { color: "yellow", children: `${queued} queued` })
984
+ ] }) : null,
985
+ stats?.recordings.totalDurationMs != null ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` \xB7 ${formatClockMs(stats.recordings.totalDurationMs)} transcribed` }) : null
986
+ ] }),
987
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", alignItems: "flex-start", children: [
988
+ /* @__PURE__ */ jsx12(Box10, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx12(
989
+ RecordingsView,
990
+ {
991
+ items: recordings,
992
+ selectedIndex,
993
+ nowMs,
994
+ columns: showPeek ? Math.max(20, columns - peekWidth - 1) : columns,
995
+ jobStatusByRecording,
996
+ downloadedRecordingIds,
997
+ spinnerFrame
998
+ }
999
+ ) }),
1000
+ showPeek ? /* @__PURE__ */ jsx12(Box10, { marginLeft: 1, marginTop: 1, children: /* @__PURE__ */ jsx12(RecordingPeek, { item: peekItem, summary: peekSummary, nowMs, width: peekWidth }) }) : null
1001
+ ] })
979
1002
  ] });
980
1003
  }
981
- function Footer({ keys }) {
982
- const segments = keys.split(" \xB7 ");
983
- return /* @__PURE__ */ jsx6(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text4, { children: segments.map((segment, i) => {
984
- const space = segment.indexOf(" ");
985
- const key = space === -1 ? segment : segment.slice(0, space);
986
- const desc = space === -1 ? "" : segment.slice(space);
987
- return /* @__PURE__ */ jsxs4(Text4, { children: [
988
- i > 0 ? /* @__PURE__ */ jsx6(Text4, { dimColor: true, children: " \xB7 " }) : null,
989
- /* @__PURE__ */ jsx6(Text4, { color: "cyan", children: key }),
990
- /* @__PURE__ */ jsx6(Text4, { dimColor: true, children: desc })
991
- ] }, `${segment}-${i}`);
992
- }) }) });
993
- }
994
- var init_chrome = __esm({
995
- "src/tui/chrome.tsx"() {
1004
+ var init_OverviewView = __esm({
1005
+ "src/tui/OverviewView.tsx"() {
996
1006
  "use strict";
1007
+ init_RecordingsView();
1008
+ init_RecordingPeek();
1009
+ init_format();
997
1010
  }
998
1011
  });
999
1012
 
1000
- // src/tui/JobRow.tsx
1001
- import { Box as Box5, Text as Text5 } from "ink";
1002
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1003
- function JobRow({
1013
+ // src/tui/JobDetailView.tsx
1014
+ import { Box as Box11, Text as Text11 } from "ink";
1015
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1016
+ function JobDetailView({
1004
1017
  item,
1005
- selected,
1006
- spinnerFrame
1018
+ origin,
1019
+ spinnerFrame,
1020
+ nowMs
1007
1021
  }) {
1008
1022
  const style = statusStyle(item.status);
1009
- const glyph = statusGlyph(item.status, spinnerFrame);
1010
- const title = item.recording?.title ?? item.recordingId;
1011
- return /* @__PURE__ */ jsxs5(Box5, { children: [
1012
- /* @__PURE__ */ jsx7(Box5, { width: 3, children: /* @__PURE__ */ jsx7(Text5, { color: "cyan", children: selected ? "\u25B8" : "" }) }),
1013
- /* @__PURE__ */ jsx7(Box5, { width: 2, children: /* @__PURE__ */ jsx7(Text5, { color: style.color, children: glyph }) }),
1014
- /* @__PURE__ */ jsx7(Box5, { width: 13, children: /* @__PURE__ */ jsx7(Text5, { color: style.color, children: style.label }) }),
1015
- /* @__PURE__ */ jsx7(Box5, { width: 26, children: /* @__PURE__ */ jsx7(Text5, { bold: selected, wrap: "truncate-end", children: title }) }),
1016
- /* @__PURE__ */ jsx7(Text5, { dimColor: !selected, children: jobDetail(item) })
1017
- ] });
1018
- }
1019
- var init_JobRow = __esm({
1020
- "src/tui/JobRow.tsx"() {
1021
- "use strict";
1022
- init_format();
1023
- }
1024
- });
1025
-
1026
- // src/tui/JobsView.tsx
1027
- import { Box as Box6, Text as Text6 } from "ink";
1028
- import { jsx as jsx8 } from "react/jsx-runtime";
1029
- function JobsView({
1030
- items,
1031
- selectedIndex,
1032
- spinnerFrame
1033
- }) {
1034
- if (items.length === 0) {
1035
- return /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: "No transcription jobs yet \u2014 run: recappi upload <file> --transcribe" }) });
1036
- }
1037
- return /* @__PURE__ */ jsx8(Box6, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx8(
1038
- JobRow,
1039
- {
1040
- item,
1041
- selected: index === selectedIndex,
1042
- spinnerFrame
1043
- },
1044
- item.jobId
1045
- )) });
1046
- }
1047
- var init_JobsView = __esm({
1048
- "src/tui/JobsView.tsx"() {
1049
- "use strict";
1050
- init_JobRow();
1051
- }
1052
- });
1053
-
1054
- // src/tui/RecordingRow.tsx
1055
- import { Box as Box7, Text as Text7 } from "ink";
1056
- import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1057
- function recordingTitle2(item) {
1058
- const named = (item.title || item.summaryTitle || "").trim();
1059
- if (named && !UUID_RE.test(named)) return named;
1060
- return "Untitled";
1061
- }
1062
- function recordingProcessingState(item, jobStatus, spinnerFrame) {
1063
- if (item.status === "uploading") return { glyph: "\u2191", color: "cyan" };
1064
- if (item.status === "failed" || jobStatus === "failed") return { glyph: "\u2717", color: "red" };
1065
- if (jobStatus === "running") return { glyph: spinnerChar(spinnerFrame), color: "cyan" };
1066
- if (jobStatus === "queued") return { glyph: "\u25CB", color: "yellow" };
1067
- if (item.status === "aborted") return { glyph: "\u2022", color: "gray" };
1068
- if (item.activeTranscriptId) return { glyph: "\u2713", color: "green" };
1069
- return { glyph: "\xB7", color: "gray" };
1070
- }
1071
- function recordingLayout(columns) {
1072
- const usable = Math.max(20, columns - 2);
1073
- const showWhen = usable >= 54;
1074
- const title = Math.max(
1075
- 10,
1076
- usable - MARKER_W - GLYPH_W - LENGTH_W - (showWhen ? WHEN_W : 0) - DL_W
1077
- );
1078
- return { title, showWhen };
1079
- }
1080
- function RecordingRow({
1081
- item,
1082
- selected,
1083
- nowMs,
1084
- columns,
1085
- jobStatus,
1086
- spinnerFrame = 0,
1087
- downloaded = false
1088
- }) {
1089
- const { title, showWhen } = recordingLayout(columns);
1090
- const { glyph, color } = recordingProcessingState(item, jobStatus, spinnerFrame);
1091
- const duration3 = item.durationMs ? formatClockMs(item.durationMs) : "\u2014";
1092
- return /* @__PURE__ */ jsxs6(Box7, { children: [
1093
- /* @__PURE__ */ jsx9(Box7, { width: MARKER_W, children: /* @__PURE__ */ jsx9(Text7, { color: "cyan", children: selected ? "\u25B8" : "" }) }),
1094
- /* @__PURE__ */ jsx9(Box7, { width: GLYPH_W, children: /* @__PURE__ */ jsx9(Text7, { color, children: glyph }) }),
1095
- /* @__PURE__ */ jsx9(Box7, { width: title, children: /* @__PURE__ */ jsx9(Text7, { bold: selected, wrap: "truncate-end", children: recordingTitle2(item) }) }),
1096
- /* @__PURE__ */ jsx9(Box7, { width: LENGTH_W, justifyContent: "flex-end", children: /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: duration3 }) }),
1097
- showWhen ? /* @__PURE__ */ jsx9(Box7, { width: WHEN_W, justifyContent: "flex-end", children: /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: formatAge(item.createdAt, nowMs) }) }) : null,
1098
- /* @__PURE__ */ jsx9(Box7, { width: DL_W, justifyContent: "flex-end", children: /* @__PURE__ */ jsx9(Text7, { color: "green", children: downloaded ? "\u2913" : "" }) })
1099
- ] });
1100
- }
1101
- var UUID_RE, MARKER_W, GLYPH_W, LENGTH_W, WHEN_W, DL_W;
1102
- var init_RecordingRow = __esm({
1103
- "src/tui/RecordingRow.tsx"() {
1104
- "use strict";
1105
- init_format();
1106
- UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1107
- MARKER_W = 3;
1108
- GLYPH_W = 2;
1109
- LENGTH_W = 8;
1110
- WHEN_W = 9;
1111
- DL_W = 3;
1112
- }
1113
- });
1114
-
1115
- // src/tui/RecordingsView.tsx
1116
- import React5 from "react";
1117
- import { Box as Box8, Text as Text8 } from "ink";
1118
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1119
- function RecordingsView({
1120
- items,
1121
- selectedIndex,
1122
- nowMs,
1123
- columns,
1124
- jobStatusByRecording,
1125
- downloadedRecordingIds,
1126
- spinnerFrame = 0
1127
- }) {
1128
- if (items.length === 0) {
1129
- return /* @__PURE__ */ jsx10(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "No recordings yet \u2014 run: recappi upload <file>" }) });
1130
- }
1131
- return /* @__PURE__ */ jsx10(Box8, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => {
1132
- const bucket = dateBucket(item.createdAt, nowMs);
1133
- const showHeader = index === 0 || bucket !== dateBucket(items[index - 1].createdAt, nowMs);
1134
- return /* @__PURE__ */ jsxs7(React5.Fragment, { children: [
1135
- showHeader ? /* @__PURE__ */ jsx10(Box8, { marginTop: index === 0 ? 0 : 1, children: /* @__PURE__ */ jsx10(Text8, { bold: true, dimColor: true, children: bucket }) }) : null,
1136
- /* @__PURE__ */ jsx10(
1137
- RecordingRow,
1138
- {
1139
- item,
1140
- selected: index === selectedIndex,
1141
- nowMs,
1142
- columns,
1143
- jobStatus: jobStatusByRecording?.get(item.recordingId),
1144
- downloaded: downloadedRecordingIds?.has(item.recordingId) ?? false,
1145
- spinnerFrame
1146
- }
1147
- )
1148
- ] }, item.recordingId);
1149
- }) });
1150
- }
1151
- var init_RecordingsView = __esm({
1152
- "src/tui/RecordingsView.tsx"() {
1153
- "use strict";
1154
- init_RecordingRow();
1155
- init_format();
1156
- }
1157
- });
1158
-
1159
- // src/tui/RecordingPeek.tsx
1160
- import { Box as Box9, Text as Text9 } from "ink";
1161
- import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1162
- function RecordingPeek({
1163
- item,
1164
- summary,
1165
- nowMs,
1166
- width
1167
- }) {
1168
- return /* @__PURE__ */ jsx11(Box9, { width, borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: !item ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "No selection" }) : /* @__PURE__ */ jsx11(PeekBody, { item, summary, nowMs }) });
1169
- }
1170
- function PeekBody({
1171
- item,
1172
- summary,
1173
- nowMs
1174
- }) {
1175
- const style = recordingStatusStyle(item.status);
1176
- const meta3 = [
1177
- item.durationMs ? formatClockMs(item.durationMs) : null,
1178
- formatBytes2(item.sizeBytes) || null,
1179
- formatAge(item.createdAt, nowMs)
1180
- ].filter(Boolean).join(" \xB7 ");
1181
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
1182
- /* @__PURE__ */ jsx11(Text9, { bold: true, wrap: "truncate-end", children: recordingTitle2(item) }),
1183
- /* @__PURE__ */ jsxs8(Box9, { children: [
1184
- /* @__PURE__ */ jsx11(Text9, { color: style.color, children: `${style.glyph} ${style.label}` }),
1185
- meta3 ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` ${meta3}` }) : null
1186
- ] }),
1187
- /* @__PURE__ */ jsx11(Box9, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx11(SummarySection, { item, summary }) }),
1188
- /* @__PURE__ */ jsx11(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "\u23CE open \xB7 t transcript" }) })
1189
- ] });
1190
- }
1191
- function SummarySection({
1192
- item,
1193
- summary
1194
- }) {
1195
- if (!item.activeTranscriptId) return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "No transcript yet" });
1196
- if (summary === "loading" || summary === void 0) return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "Loading summary\u2026" });
1197
- if (summary === "error") return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "(summary unavailable)" });
1198
- if (summary.status !== "succeeded" || !summary.tldr) {
1199
- return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: `Summary ${summary.status}` });
1200
- }
1201
- const points = (summary.keyPoints ?? []).slice(0, 3);
1202
- return /* @__PURE__ */ jsxs8(Fragment3, { children: [
1203
- /* @__PURE__ */ jsx11(Text9, { bold: true, dimColor: true, children: "SUMMARY" }),
1204
- /* @__PURE__ */ jsx11(Text9, { children: summary.tldr }),
1205
- points.length > 0 ? /* @__PURE__ */ jsx11(Box9, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx11(Text9, { dimColor: true, wrap: "truncate-end", children: `\u2022 ${point}` }, i)) }) : null
1206
- ] });
1207
- }
1208
- var init_RecordingPeek = __esm({
1209
- "src/tui/RecordingPeek.tsx"() {
1210
- "use strict";
1211
- init_format();
1212
- init_RecordingRow();
1213
- }
1214
- });
1215
-
1216
- // src/tui/OverviewView.tsx
1217
- import { Box as Box10, Text as Text10 } from "ink";
1218
- import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1219
- function OverviewView({
1220
- recordings,
1221
- jobs,
1222
- stats,
1223
- selectedIndex,
1224
- spinnerFrame,
1225
- nowMs,
1226
- columns,
1227
- jobStatusByRecording,
1228
- downloadedRecordingIds,
1229
- peekItem,
1230
- peekSummary,
1231
- showPeek = false,
1232
- peekWidth = 0
1233
- }) {
1234
- const jobCounts = countJobs(jobs);
1235
- const running = stats?.jobs.running ?? jobCounts.running;
1236
- const queued = stats?.jobs.queued ?? jobCounts.queued;
1237
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", children: [
1238
- /* @__PURE__ */ jsxs9(Box10, { children: [
1239
- /* @__PURE__ */ jsx12(Text10, { bold: true, children: stats?.recordings.total ?? recordings.length }),
1240
- /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " recordings" }),
1241
- stats?.recordings.ready != null ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
1242
- /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
1243
- /* @__PURE__ */ jsx12(Text10, { color: "green", children: `${stats.recordings.ready} ready` })
1244
- ] }) : null,
1245
- running > 0 ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
1246
- /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
1247
- /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: `${running} transcribing` })
1248
- ] }) : null,
1249
- queued > 0 ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
1250
- /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
1251
- /* @__PURE__ */ jsx12(Text10, { color: "yellow", children: `${queued} queued` })
1252
- ] }) : null,
1253
- stats?.recordings.totalDurationMs != null ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` \xB7 ${formatClockMs(stats.recordings.totalDurationMs)} transcribed` }) : null
1254
- ] }),
1255
- /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", alignItems: "flex-start", children: [
1256
- /* @__PURE__ */ jsx12(Box10, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx12(
1257
- RecordingsView,
1258
- {
1259
- items: recordings,
1260
- selectedIndex,
1261
- nowMs,
1262
- columns: showPeek ? Math.max(20, columns - peekWidth - 1) : columns,
1263
- jobStatusByRecording,
1264
- downloadedRecordingIds,
1265
- spinnerFrame
1266
- }
1267
- ) }),
1268
- showPeek ? /* @__PURE__ */ jsx12(Box10, { marginLeft: 1, marginTop: 1, children: /* @__PURE__ */ jsx12(RecordingPeek, { item: peekItem, summary: peekSummary, nowMs, width: peekWidth }) }) : null
1269
- ] })
1270
- ] });
1271
- }
1272
- var init_OverviewView = __esm({
1273
- "src/tui/OverviewView.tsx"() {
1274
- "use strict";
1275
- init_RecordingsView();
1276
- init_RecordingPeek();
1277
- init_format();
1278
- }
1279
- });
1280
-
1281
- // src/tui/JobDetailView.tsx
1282
- import { Box as Box11, Text as Text11 } from "ink";
1283
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1284
- function JobDetailView({
1285
- item,
1286
- origin,
1287
- spinnerFrame,
1288
- nowMs
1289
- }) {
1290
- const style = statusStyle(item.status);
1291
- const links = resolveJobLinks(item, origin);
1023
+ const links = resolveJobLinks(item, origin);
1292
1024
  const title = item.recording?.title ?? item.recordingId;
1293
1025
  return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
1294
1026
  /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
@@ -1374,9 +1106,9 @@ function StatusLine({
1374
1106
  const fraction = transcribeFraction(item);
1375
1107
  const elapsed = item.startedAt ? ` \xB7 ${formatClockMs(nowMs - item.startedAt)} elapsed` : "";
1376
1108
  if (fraction != null) {
1377
- const pct = Math.round(fraction * 100);
1109
+ const pct2 = Math.round(fraction * 100);
1378
1110
  return /* @__PURE__ */ jsxs10(Text11, { children: [
1379
- `${progressBar(fraction)} ${pct}% ${formatClockMs(item.processedDurationMs)} / ${formatClockMs(
1111
+ `${progressBar(fraction)} ${pct2}% ${formatClockMs(item.processedDurationMs)} / ${formatClockMs(
1380
1112
  item.recording?.durationMs
1381
1113
  )}`,
1382
1114
  /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: elapsed })
@@ -1898,14 +1630,201 @@ var init_RecordSetupView = __esm({
1898
1630
  }
1899
1631
  });
1900
1632
 
1901
- // src/tui/AppShell.tsx
1902
- import { useCallback, useEffect as useEffect4, useRef as useRef3, useState as useState7 } from "react";
1903
- import { Box as Box16, Text as Text16, useApp, useInput as useInput7 } from "ink";
1904
- import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
1905
- function recordErrorCopy(code, message) {
1906
- switch (code) {
1907
- case "record.helper_unavailable":
1908
- return {
1633
+ // src/tui/RecordFrame.tsx
1634
+ import { useState as useState7 } from "react";
1635
+ import { Box as Box16, Text as Text16, useInput as useInput7 } from "ink";
1636
+ import { jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
1637
+ function levelDb3(level) {
1638
+ if (level <= 0.03) return "silent";
1639
+ return `${Math.round(level * 60 - 60)} dB`;
1640
+ }
1641
+ function CompactMeter({ label, level }) {
1642
+ const width = 12;
1643
+ const filled = Math.max(0, Math.min(width, Math.round(Math.max(0, Math.min(1, level)) * width)));
1644
+ const silent = level <= 0.03;
1645
+ return /* @__PURE__ */ jsxs15(Text16, { children: [
1646
+ /* @__PURE__ */ jsxs15(Text16, { dimColor: true, children: [
1647
+ label,
1648
+ " "
1649
+ ] }),
1650
+ /* @__PURE__ */ jsx18(Text16, { color: silent ? "yellow" : "cyan", children: "\u25CF".repeat(filled) }),
1651
+ /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: "\xB7".repeat(width - filled) }),
1652
+ /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: ` ${levelDb3(level)}` })
1653
+ ] });
1654
+ }
1655
+ function CaptionColumn({
1656
+ lines,
1657
+ width,
1658
+ rows,
1659
+ dim,
1660
+ scrollBack = 0
1661
+ }) {
1662
+ const end = lines.length - Math.min(scrollBack, Math.max(0, lines.length - 1));
1663
+ const chosen = [];
1664
+ let used = 0;
1665
+ for (let i = end - 1; i >= 0; i--) {
1666
+ const h = wrappedRows2(lines[i], width);
1667
+ if (used + h > rows && chosen.length > 0) break;
1668
+ chosen.unshift(lines[i]);
1669
+ used += h;
1670
+ }
1671
+ return /* @__PURE__ */ jsx18(Box16, { width, flexDirection: "column", children: chosen.length === 0 ? /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: "Listening for speech\u2026" }) : chosen.map((l, i) => /* @__PURE__ */ jsx18(Text16, { dimColor: dim, wrap: "wrap", children: l }, i)) });
1672
+ }
1673
+ function outcomeLine(telemetry, artifact) {
1674
+ if (telemetry.status === "recording" || telemetry.status === "starting") {
1675
+ return "Recording\u2026 stop to auto-save + transcribe + summarize";
1676
+ }
1677
+ const up = artifact?.uploadStatus;
1678
+ const tr = artifact?.transcriptionStatus;
1679
+ if (up === "uploading") return `Uploading to Recappi Cloud\u2026 ${pct(artifact?.uploadProgress)}`;
1680
+ if (tr === "processing") return `Transcribing\u2026 ${pct(artifact?.transcriptionProgress)}`;
1681
+ if (tr === "ready") return "Transcript ready \xB7 \u23CE open \xB7 T re-transcribe";
1682
+ if (up === "failed" || tr === "failed") {
1683
+ return artifact?.error ? `Cloud handoff failed \xB7 ${artifact.error}` : "Cloud handoff failed \xB7 T retry";
1684
+ }
1685
+ return "Saved \xB7 \u23CE open";
1686
+ }
1687
+ function pct(f) {
1688
+ return f == null ? "" : `${Math.round(Math.max(0, Math.min(1, f)) * 100)}%`;
1689
+ }
1690
+ function RecordFrame({
1691
+ telemetry,
1692
+ captions,
1693
+ artifact,
1694
+ recordings = [],
1695
+ selectedIndex = 0,
1696
+ title = "New recording",
1697
+ recordingId,
1698
+ jobId,
1699
+ nowMs = Date.now(),
1700
+ spinnerFrame = 0
1701
+ }) {
1702
+ const size = useTerminalSize();
1703
+ const [captionMode, setCaptionMode] = useState7("both");
1704
+ const [scrollBack, setScrollBack] = useState7(0);
1705
+ const PAGE = 8;
1706
+ useInput7((input, key) => {
1707
+ if (input === "c") {
1708
+ setCaptionMode((m) => m === "both" ? "source" : m === "source" ? "translation" : "both");
1709
+ } else if (key.upArrow || input === "k") setScrollBack((s) => s + 1);
1710
+ else if (key.downArrow || input === "j") setScrollBack((s) => Math.max(0, s - 1));
1711
+ else if (key.pageUp || input === "b") setScrollBack((s) => s + PAGE);
1712
+ else if (key.pageDown || input === " ") setScrollBack((s) => Math.max(0, s - PAGE));
1713
+ else if (input === "g") setScrollBack(Number.MAX_SAFE_INTEGER);
1714
+ else if (input === "G") setScrollBack(0);
1715
+ });
1716
+ const elapsed = telemetry.startedAtMs != null ? formatClockMs(Math.max(0, nowMs - telemetry.startedAtMs)) : "00:00";
1717
+ const recording = telemetry.status === "recording" || telemetry.status === "starting";
1718
+ const stateLabel = recording ? "\u23FA REC" : telemetry.status === "paused" ? "\u23F8 PAUSED" : telemetry.status === "stopped" ? "\u25A0 STOPPED" : "\u2026";
1719
+ const ids = [recordingId, jobId].filter(Boolean).join(" \xB7 ");
1720
+ const innerWidth = Math.max(20, size.columns - 2);
1721
+ const listWidth = Math.min(20, Math.max(14, Math.floor(innerWidth * 0.22)));
1722
+ const rightWidth = Math.max(20, innerWidth - listWidth - 3);
1723
+ const captionRows = Math.max(3, size.rows - 10);
1724
+ const sourceLines = captions ? [
1725
+ ...captions.lines.map((l) => `${l.speaker ? `${l.speaker}: ` : ""}${trimLead2(l.text)}`),
1726
+ ...captions.partial ? [trimLead2(captions.partial)] : []
1727
+ ] : [];
1728
+ const translationLines = captions ? [
1729
+ ...captions.lines.filter((l) => l.translation).map((l) => trimLead2(l.translation)),
1730
+ ...captions.translationPartial ? [trimLead2(captions.translationPartial)] : []
1731
+ ] : [];
1732
+ const status = telemetry.sizeBytes ? formatBytes2(telemetry.sizeBytes) : "";
1733
+ const sourceLine = [telemetry.sourceLabel, telemetry.micEnabled ? "Microphone" : null, status || null].filter(Boolean).join(" \xB7 ");
1734
+ return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", paddingX: 1, height: size.rows, children: [
1735
+ /* @__PURE__ */ jsxs15(Box16, { justifyContent: "space-between", children: [
1736
+ /* @__PURE__ */ jsxs15(Text16, { children: [
1737
+ /* @__PURE__ */ jsx18(Text16, { bold: true, color: "green", children: "recappi" }),
1738
+ /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: " \xB7 Recording" })
1739
+ ] }),
1740
+ /* @__PURE__ */ jsxs15(Text16, { children: [
1741
+ /* @__PURE__ */ jsx18(Text16, { bold: true, color: recording ? "red" : "gray", children: stateLabel }),
1742
+ /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: ` ${elapsed}${ids ? ` \xB7 ${ids}` : ""}` })
1743
+ ] })
1744
+ ] }),
1745
+ /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: "\u2500".repeat(innerWidth) }),
1746
+ /* @__PURE__ */ jsxs15(Box16, { flexGrow: 1, children: [
1747
+ /* @__PURE__ */ jsxs15(Box16, { width: listWidth, flexDirection: "column", children: [
1748
+ /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: `RECORDINGS \xB7 ${recordings.length}` }),
1749
+ /* @__PURE__ */ jsx18(Box16, { marginTop: 1, flexDirection: "column", children: recordings.slice(0, Math.max(1, size.rows - 8)).map((rec, i) => {
1750
+ const st = recordingProcessingState(rec, void 0, spinnerFrame);
1751
+ const sel = i === selectedIndex;
1752
+ return /* @__PURE__ */ jsxs15(Box16, { children: [
1753
+ /* @__PURE__ */ jsx18(Box16, { width: 2, children: /* @__PURE__ */ jsx18(Text16, { color: "cyan", children: sel ? "\u25B8" : "" }) }),
1754
+ /* @__PURE__ */ jsx18(Box16, { width: 2, children: /* @__PURE__ */ jsx18(Text16, { color: st.color, children: st.glyph }) }),
1755
+ /* @__PURE__ */ jsx18(Box16, { width: listWidth - 4, children: /* @__PURE__ */ jsx18(Text16, { bold: sel, wrap: "truncate-end", children: recordingTitle2(rec) }) })
1756
+ ] }, rec.recordingId);
1757
+ }) })
1758
+ ] }),
1759
+ /* @__PURE__ */ jsx18(Box16, { width: 3, flexDirection: "column", alignItems: "center", children: Array.from({ length: Math.max(1, size.rows - 6) }, (_, i) => /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: "\u2502" }, i)) }),
1760
+ /* @__PURE__ */ jsxs15(Box16, { width: rightWidth, flexDirection: "column", children: [
1761
+ /* @__PURE__ */ jsx18(Text16, { bold: true, wrap: "truncate-end", children: title }),
1762
+ /* @__PURE__ */ jsx18(Text16, { dimColor: true, wrap: "truncate-end", children: sourceLine }),
1763
+ telemetry.level ? /* @__PURE__ */ jsxs15(Box16, { children: [
1764
+ /* @__PURE__ */ jsx18(CompactMeter, { label: "System", level: telemetry.level.system ?? 0 }),
1765
+ telemetry.micEnabled ? /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: " " }) : null,
1766
+ telemetry.micEnabled ? /* @__PURE__ */ jsx18(CompactMeter, { label: "Mic", level: telemetry.level.mic ?? 0 }) : null
1767
+ ] }) : /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: "Capturing audio\u2026" }),
1768
+ /* @__PURE__ */ jsxs15(Box16, { marginTop: 1, flexDirection: "column", flexGrow: 1, children: [
1769
+ /* @__PURE__ */ jsxs15(Box16, { children: [
1770
+ captionMode !== "translation" ? /* @__PURE__ */ jsx18(Box16, { width: captionMode === "both" ? Math.floor((rightWidth - 3) / 2) : rightWidth, children: /* @__PURE__ */ jsx18(Text16, { bold: true, dimColor: true, children: "ORIGINAL" }) }) : null,
1771
+ captionMode === "both" ? /* @__PURE__ */ jsx18(Box16, { width: 3 }) : null,
1772
+ captionMode !== "source" ? /* @__PURE__ */ jsx18(Text16, { bold: true, dimColor: true, children: "TRANSLATION" }) : null,
1773
+ scrollBack > 0 ? /* @__PURE__ */ jsx18(Text16, { color: "yellow", children: " \u23F8 scrolled \xB7 G live" }) : null
1774
+ ] }),
1775
+ /* @__PURE__ */ jsxs15(Box16, { children: [
1776
+ captionMode !== "translation" ? /* @__PURE__ */ jsx18(
1777
+ CaptionColumn,
1778
+ {
1779
+ lines: sourceLines,
1780
+ width: captionMode === "both" ? Math.floor((rightWidth - 3) / 2) : rightWidth,
1781
+ rows: captionRows,
1782
+ scrollBack
1783
+ }
1784
+ ) : null,
1785
+ captionMode === "both" ? /* @__PURE__ */ jsx18(Box16, { width: 3, flexDirection: "column", children: Array.from({ length: Math.min(captionRows, 12) }, (_, i) => /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: "\u2502" }, i)) }) : null,
1786
+ captionMode !== "source" ? /* @__PURE__ */ jsx18(
1787
+ CaptionColumn,
1788
+ {
1789
+ lines: translationLines,
1790
+ width: captionMode === "both" ? Math.floor((rightWidth - 3) / 2) : rightWidth,
1791
+ rows: captionRows,
1792
+ dim: true,
1793
+ scrollBack
1794
+ }
1795
+ ) : null
1796
+ ] })
1797
+ ] }),
1798
+ /* @__PURE__ */ jsxs15(Box16, { marginTop: 1, children: [
1799
+ /* @__PURE__ */ jsx18(Text16, { bold: true, dimColor: true, children: "OUTCOME " }),
1800
+ /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: outcomeLine(telemetry, artifact) })
1801
+ ] })
1802
+ ] })
1803
+ ] }),
1804
+ /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: "\u2500".repeat(innerWidth) }),
1805
+ /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: recording ? `q stop & save \xB7 c captions (${captionMode}) \xB7 \u2191\u2193 scroll \xB7 G live` : `\u23CE open \xB7 T re-transcribe \xB7 c captions (${captionMode}) \xB7 \u2191\u2193 scroll \xB7 1 overview 2 jobs 3 account` })
1806
+ ] });
1807
+ }
1808
+ var trimLead2, wrappedRows2;
1809
+ var init_RecordFrame = __esm({
1810
+ "src/tui/RecordFrame.tsx"() {
1811
+ "use strict";
1812
+ init_format();
1813
+ init_RecordingRow();
1814
+ init_terminal();
1815
+ trimLead2 = (s) => s.replace(/^\s+/, "");
1816
+ wrappedRows2 = (text, width) => Math.max(1, Math.ceil(displayWidth(text) / Math.max(1, width)));
1817
+ }
1818
+ });
1819
+
1820
+ // src/tui/AppShell.tsx
1821
+ import { useCallback, useEffect as useEffect4, useRef as useRef3, useState as useState8 } from "react";
1822
+ import { Box as Box17, Text as Text17, useApp, useInput as useInput8 } from "ink";
1823
+ import { Fragment as Fragment6, jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
1824
+ function recordErrorCopy(code, message) {
1825
+ switch (code) {
1826
+ case "record.helper_unavailable":
1827
+ return {
1909
1828
  title: "This CLI install is missing its local recorder.",
1910
1829
  detail: "Run npm install -g recappi@latest, or use npx -y recappi@latest.",
1911
1830
  tone: "yellow"
@@ -2019,6 +1938,11 @@ function transcriptionStatusFromJob(status) {
2019
1938
  return "not_started";
2020
1939
  }
2021
1940
  }
1941
+ function recordFrameTitle(recordings, recordingId) {
1942
+ if (!recordingId) return "New recording";
1943
+ const recording = recordings.find((item) => item.recordingId === recordingId);
1944
+ return recording ? recordingTitle2(recording) : "New recording";
1945
+ }
2022
1946
  function permissionItemsFromRecordError(data) {
2023
1947
  const sidecarError = isRecord8(data) ? data : void 0;
2024
1948
  const sidecarData = isRecord8(sidecarError?.data) ? sidecarError.data : void 0;
@@ -2059,6 +1983,7 @@ function AppShell({
2059
1983
  startLiveRecord,
2060
1984
  startRecordSetupPreview,
2061
1985
  transcribeRecordingArtifact,
1986
+ onRetranscribe,
2062
1987
  initialView = "overview",
2063
1988
  openUrl: openUrl2,
2064
1989
  copyText: copyText2,
@@ -2068,34 +1993,34 @@ function AppShell({
2068
1993
  }) {
2069
1994
  const { exit } = useApp();
2070
1995
  const size = useTerminalSize();
2071
- const [jobs, setJobs] = useState7([]);
2072
- const [recordings, setRecordings] = useState7([]);
2073
- const [recordingsNextCursor, setRecordingsNextCursor] = useState7(null);
2074
- const [recordingsTotalCount, setRecordingsTotalCount] = useState7(void 0);
2075
- const [stats, setStats] = useState7(void 0);
2076
- const [accountStatus, setAccountStatus] = useState7("loading");
2077
- const [origin, setOrigin] = useState7("");
2078
- const [stack, setStack] = useState7([{ kind: initialView }]);
2079
- const [selected, setSelected] = useState7(0);
2080
- const [spinnerFrame, setSpinnerFrame] = useState7(0);
2081
- const [loadingMoreRecordings, setLoadingMoreRecordings] = useState7(false);
2082
- const [loadError, setLoadError] = useState7(void 0);
2083
- const [notice, setNotice] = useState7(void 0);
2084
- const [summaryCache, setSummaryCache] = useState7(() => /* @__PURE__ */ new Map());
2085
- const [transcriptCache, setTranscriptCache] = useState7(
1996
+ const [jobs, setJobs] = useState8([]);
1997
+ const [recordings, setRecordings] = useState8([]);
1998
+ const [recordingsNextCursor, setRecordingsNextCursor] = useState8(null);
1999
+ const [recordingsTotalCount, setRecordingsTotalCount] = useState8(void 0);
2000
+ const [stats, setStats] = useState8(void 0);
2001
+ const [accountStatus, setAccountStatus] = useState8("loading");
2002
+ const [origin, setOrigin] = useState8("");
2003
+ const [stack, setStack] = useState8([{ kind: initialView }]);
2004
+ const [selected, setSelected] = useState8(0);
2005
+ const [spinnerFrame, setSpinnerFrame] = useState8(0);
2006
+ const [loadingMoreRecordings, setLoadingMoreRecordings] = useState8(false);
2007
+ const [loadError, setLoadError] = useState8(void 0);
2008
+ const [notice, setNotice] = useState8(void 0);
2009
+ const [summaryCache, setSummaryCache] = useState8(() => /* @__PURE__ */ new Map());
2010
+ const [transcriptCache, setTranscriptCache] = useState8(
2086
2011
  () => /* @__PURE__ */ new Map()
2087
2012
  );
2088
- const [audioCache, setAudioCache] = useState7(() => /* @__PURE__ */ new Map());
2089
- const [downloadedIds, setDownloadedIds] = useState7(() => /* @__PURE__ */ new Set());
2090
- const [liveRecord, setLiveRecord] = useState7(void 0);
2091
- const [recordSetupInputs, setRecordSetupInputs] = useState7({
2013
+ const [audioCache, setAudioCache] = useState8(() => /* @__PURE__ */ new Map());
2014
+ const [downloadedIds, setDownloadedIds] = useState8(() => /* @__PURE__ */ new Set());
2015
+ const [liveRecord, setLiveRecord] = useState8(void 0);
2016
+ const [recordSetupInputs, setRecordSetupInputs] = useState8({
2092
2017
  sources: DEFAULT_RECORDING_SOURCES,
2093
2018
  microphones: []
2094
2019
  });
2095
- const [recordSetupSelection, setRecordSetupSelection] = useState7(
2020
+ const [recordSetupSelection, setRecordSetupSelection] = useState8(
2096
2021
  DEFAULT_RECORDING_SELECTION
2097
2022
  );
2098
- const [recordSetupLevels, setRecordSetupLevels] = useState7({
2023
+ const [recordSetupLevels, setRecordSetupLevels] = useState8({
2099
2024
  bySourceId: {},
2100
2025
  byMicrophoneId: {}
2101
2026
  });
@@ -2399,6 +2324,65 @@ function AppShell({
2399
2324
  setNotice("Transcription failed. Press enter to retry.");
2400
2325
  }
2401
2326
  }, [liveRecord, refresh, transcribeRecordingArtifact]);
2327
+ const retranscribeStoppedRecording = useCallback(async () => {
2328
+ const current = liveRecord;
2329
+ if (current?.kind !== "stopped") return;
2330
+ const artifact = current.artifact;
2331
+ if (!artifact?.recordingId) {
2332
+ setNotice("This recording is not in Recappi Cloud yet.");
2333
+ return;
2334
+ }
2335
+ if (!onRetranscribe) {
2336
+ setNotice("Re-transcribe is not available in this CLI session.");
2337
+ return;
2338
+ }
2339
+ setLiveRecord({
2340
+ ...current,
2341
+ artifact: {
2342
+ ...artifact,
2343
+ uploadStatus: "uploaded",
2344
+ uploadProgress: 1,
2345
+ transcriptionStatus: "queued",
2346
+ transcriptionProgress: void 0,
2347
+ error: void 0
2348
+ }
2349
+ });
2350
+ try {
2351
+ const data = await onRetranscribe(artifact.recordingId);
2352
+ setLiveRecord((latest) => {
2353
+ const base = latest?.kind === "stopped" && latest.artifact?.recordingId === artifact.recordingId ? latest : current;
2354
+ return {
2355
+ ...base,
2356
+ artifact: {
2357
+ ...artifact,
2358
+ ...base.artifact ?? {},
2359
+ recordingId: data.recordingId,
2360
+ jobId: data.jobId,
2361
+ ...data.transcriptId ? { transcriptId: data.transcriptId } : {},
2362
+ uploadStatus: "uploaded",
2363
+ uploadProgress: 1,
2364
+ transcriptionStatus: transcriptionStatusFromJob(data.status),
2365
+ ...data.status === "succeeded" ? { transcriptionProgress: 1 } : {}
2366
+ }
2367
+ };
2368
+ });
2369
+ setNotice(
2370
+ data.status === "succeeded" ? "Re-transcription ready." : data.status === "running" ? "Re-transcription running." : data.status === "failed" ? "Re-transcription failed. Press T to retry." : "Re-transcription queued."
2371
+ );
2372
+ await refresh({ resetRecordings: true });
2373
+ } catch {
2374
+ setLiveRecord({
2375
+ ...current,
2376
+ artifact: {
2377
+ ...artifact,
2378
+ uploadStatus: "uploaded",
2379
+ transcriptionStatus: "failed",
2380
+ error: "Could not start re-transcription. Please try again."
2381
+ }
2382
+ });
2383
+ setNotice("Re-transcription failed. Press T to retry.");
2384
+ }
2385
+ }, [liveRecord, onRetranscribe, refresh]);
2402
2386
  useEffect4(() => {
2403
2387
  if (liveRecord?.kind !== "stopped") return;
2404
2388
  const artifact = liveRecord.artifact;
@@ -2556,7 +2540,12 @@ function AppShell({
2556
2540
  setNotice(void 0);
2557
2541
  };
2558
2542
  const back = () => setStack((st) => st.length > 1 ? st.slice(0, -1) : st);
2559
- useInput7((input, key) => {
2543
+ const hasCurrentRecord = liveRecord?.kind === "starting" || liveRecord?.kind === "live" || liveRecord?.kind === "stopping" || liveRecord?.kind === "stopped";
2544
+ const openCurrentRecord = () => {
2545
+ setStack((st) => st[st.length - 1]?.kind === "record" ? st : [...st, { kind: "record" }]);
2546
+ setNotice(void 0);
2547
+ };
2548
+ useInput8((input, key) => {
2560
2549
  setNotice(void 0);
2561
2550
  if (screen.kind === "recordSetup") {
2562
2551
  if (input === "q" || key.leftArrow) back();
@@ -2575,11 +2564,24 @@ function AppShell({
2575
2564
  void transcribeStoppedRecording();
2576
2565
  return;
2577
2566
  }
2567
+ if (liveRecord?.kind === "stopped" && input === "T") {
2568
+ void retranscribeStoppedRecording();
2569
+ return;
2570
+ }
2578
2571
  if (liveRecord?.kind === "stopped" && input === "n") {
2579
2572
  void stopLiveRecord();
2580
2573
  return;
2581
2574
  }
2582
- if (input === "q" || key.escape || key.leftArrow) void stopLiveRecord();
2575
+ if (liveRecord?.kind === "stopped" && input === "1") return goTab("overview");
2576
+ if (liveRecord?.kind === "stopped" && input === "2") return goTab("jobs");
2577
+ if (liveRecord?.kind === "stopped" && input === "3") return goTab("account");
2578
+ if (input === "q") {
2579
+ void stopLiveRecord();
2580
+ return;
2581
+ }
2582
+ if ((liveRecord?.kind === "stopped" || liveRecord?.kind === "error") && (key.escape || key.leftArrow)) {
2583
+ void stopLiveRecord();
2584
+ }
2583
2585
  return;
2584
2586
  }
2585
2587
  if (input === "q") return exit();
@@ -2588,6 +2590,10 @@ function AppShell({
2588
2590
  if (input === "2") return goTab("jobs");
2589
2591
  if (input === "3") return goTab("account");
2590
2592
  if (input === "n") {
2593
+ if (hasCurrentRecord) {
2594
+ openCurrentRecord();
2595
+ return;
2596
+ }
2591
2597
  setStack((st) => [...st, { kind: "recordSetup" }]);
2592
2598
  if (fetchRecordSetup) {
2593
2599
  fetchRecordSetup().then((model) => {
@@ -2651,18 +2657,18 @@ function AppShell({
2651
2657
  }
2652
2658
  });
2653
2659
  if (screen.kind === "transcript") {
2654
- return /* @__PURE__ */ jsx18(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
2660
+ return /* @__PURE__ */ jsx19(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
2655
2661
  }
2656
2662
  if (screen.kind === "jobDetail") {
2657
2663
  const job = jobs.find((j) => j.jobId === screen.jobId);
2658
- if (!job) return /* @__PURE__ */ jsx18(Missing, { label: "Job" });
2659
- return /* @__PURE__ */ jsx18(Detail, { notice, children: /* @__PURE__ */ jsx18(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
2664
+ if (!job) return /* @__PURE__ */ jsx19(Missing, { label: "Job" });
2665
+ return /* @__PURE__ */ jsx19(Detail, { notice, children: /* @__PURE__ */ jsx19(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
2660
2666
  }
2661
2667
  if (screen.kind === "recordingDetail") {
2662
2668
  const rec = recordings.find((r) => r.recordingId === screen.recordingId);
2663
- if (!rec) return /* @__PURE__ */ jsx18(Missing, { label: "Recording" });
2669
+ if (!rec) return /* @__PURE__ */ jsx19(Missing, { label: "Recording" });
2664
2670
  const detailTranscript = rec.activeTranscriptId ? transcriptCache.get(rec.activeTranscriptId) : void 0;
2665
- return /* @__PURE__ */ jsx18(Detail, { notice, children: /* @__PURE__ */ jsx18(
2671
+ return /* @__PURE__ */ jsx19(Detail, { notice, children: /* @__PURE__ */ jsx19(
2666
2672
  RecordingDetailView,
2667
2673
  {
2668
2674
  item: rec,
@@ -2673,7 +2679,7 @@ function AppShell({
2673
2679
  ) });
2674
2680
  }
2675
2681
  if (screen.kind === "recordSetup") {
2676
- return /* @__PURE__ */ jsx18(Box16, { flexDirection: "column", height: size.rows, paddingX: 1, children: /* @__PURE__ */ jsx18(
2682
+ return /* @__PURE__ */ jsx19(Box17, { flexDirection: "column", height: size.rows, paddingX: 1, children: /* @__PURE__ */ jsx19(
2677
2683
  RecordSetupView,
2678
2684
  {
2679
2685
  model: recordSetupModel,
@@ -2686,32 +2692,42 @@ function AppShell({
2686
2692
  }
2687
2693
  if (screen.kind === "record") {
2688
2694
  if (liveRecord?.kind === "live" && liveRecord.session.mode === "live_captions") {
2689
- return /* @__PURE__ */ jsx18(LiveCaptionsScreen, { source: liveRecord.session.source, now });
2695
+ return /* @__PURE__ */ jsx19(LiveCaptionsScreen, { source: liveRecord.session.source, now });
2690
2696
  }
2691
2697
  if (liveRecord?.kind === "live" || liveRecord?.kind === "starting" || liveRecord?.kind === "stopping" || liveRecord?.kind === "stopped") {
2692
- return /* @__PURE__ */ jsx18(Detail, { notice, children: /* @__PURE__ */ jsx18(
2693
- RecordingHeroScreen,
2698
+ return /* @__PURE__ */ jsx19(Detail, { notice, children: /* @__PURE__ */ jsx19(
2699
+ RecordFrame,
2694
2700
  {
2695
2701
  telemetry: liveRecord.telemetry,
2696
2702
  captions: liveRecord.kind === "live" || liveRecord.kind === "stopping" ? liveRecord.captions : void 0,
2697
2703
  artifact: liveRecord.kind === "stopped" ? liveRecord.artifact : void 0,
2698
- canTranscribe: Boolean(transcribeRecordingArtifact),
2699
- now
2704
+ recordings,
2705
+ selectedIndex: liveRecord.kind === "stopped" && liveRecord.artifact?.recordingId ? Math.max(
2706
+ 0,
2707
+ recordings.findIndex(
2708
+ (item) => item.recordingId === liveRecord.artifact?.recordingId
2709
+ )
2710
+ ) : 0,
2711
+ title: liveRecord.kind === "stopped" && liveRecord.artifact?.recordingId ? recordFrameTitle(recordings, liveRecord.artifact.recordingId) : "New recording",
2712
+ recordingId: liveRecord.kind === "stopped" ? liveRecord.artifact?.recordingId : void 0,
2713
+ jobId: liveRecord.kind === "stopped" ? liveRecord.artifact?.jobId : void 0,
2714
+ nowMs: now(),
2715
+ spinnerFrame
2700
2716
  }
2701
2717
  ) });
2702
2718
  }
2703
- return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
2704
- /* @__PURE__ */ jsx18(Box16, { flexGrow: 1, flexDirection: "column", paddingX: 1, paddingTop: 1, children: liveRecord?.kind === "error" ? (() => {
2719
+ return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
2720
+ /* @__PURE__ */ jsx19(Box17, { flexGrow: 1, flexDirection: "column", paddingX: 1, paddingTop: 1, children: liveRecord?.kind === "error" ? (() => {
2705
2721
  if (liveRecord.code === "record.permission_required") {
2706
- return /* @__PURE__ */ jsx18(PermissionPreflightView, { items: permissionItemsFromRecordError(liveRecord.data) });
2722
+ return /* @__PURE__ */ jsx19(PermissionPreflightView, { items: permissionItemsFromRecordError(liveRecord.data) });
2707
2723
  }
2708
2724
  const copy = recordErrorCopy(liveRecord.code, liveRecord.message);
2709
- return /* @__PURE__ */ jsxs15(Fragment6, { children: [
2710
- /* @__PURE__ */ jsx18(Text16, { color: copy.tone, children: copy.title }),
2711
- copy.detail ? /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: copy.detail }) : null
2725
+ return /* @__PURE__ */ jsxs16(Fragment6, { children: [
2726
+ /* @__PURE__ */ jsx19(Text17, { color: copy.tone, children: copy.title }),
2727
+ copy.detail ? /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: copy.detail }) : null
2712
2728
  ] });
2713
- })() : /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: "Starting recording\u2026" }) }),
2714
- /* @__PURE__ */ jsx18(Footer, { keys: "r retry \xB7 o settings \xB7 q / esc / \u2190 back" })
2729
+ })() : /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: "Starting recording\u2026" }) }),
2730
+ /* @__PURE__ */ jsx19(Footer, { keys: "r retry \xB7 o settings \xB7 q / esc / \u2190 back" })
2715
2731
  ] });
2716
2732
  }
2717
2733
  const tab = screen.kind === "jobs" ? "jobs" : screen.kind === "account" ? "account" : "overview";
@@ -2729,7 +2745,7 @@ function AppShell({
2729
2745
  const showPeek = size.columns >= 100;
2730
2746
  const peekWidth = showPeek ? 34 : 0;
2731
2747
  const listColumns = showPeek ? Math.max(30, size.columns - peekWidth - 3) : size.columns;
2732
- body = /* @__PURE__ */ jsx18(
2748
+ body = /* @__PURE__ */ jsx19(
2733
2749
  OverviewView,
2734
2750
  {
2735
2751
  recordings: recordings.slice(win.start, win.end),
@@ -2749,11 +2765,11 @@ function AppShell({
2749
2765
  );
2750
2766
  } else if (screen.kind === "account") {
2751
2767
  position = "";
2752
- body = /* @__PURE__ */ jsx18(AccountView, { status: accountStatus, nowMs: now() });
2768
+ body = /* @__PURE__ */ jsx19(AccountView, { status: accountStatus, nowMs: now() });
2753
2769
  } else {
2754
2770
  const win = listWindow(selected, jobs.length, Math.max(3, size.rows - 4));
2755
2771
  position = jobs.length ? `${selected + 1} / ${jobs.length}` : "0";
2756
- body = /* @__PURE__ */ jsx18(
2772
+ body = /* @__PURE__ */ jsx19(
2757
2773
  JobsView,
2758
2774
  {
2759
2775
  items: jobs.slice(win.start, win.end),
@@ -2763,34 +2779,34 @@ function AppShell({
2763
2779
  );
2764
2780
  }
2765
2781
  const footerKeys = screen.kind === "jobs" ? `${position} \xB7 \u2191\u2193 select \xB7 \u23CE job \xB7 t transcript \xB7 n record \xB7 1 overview \xB7 3 account \xB7 r refresh \xB7 q quit` : screen.kind === "account" ? "Account \xB7 n record \xB7 1 overview \xB7 2 jobs \xB7 r refresh \xB7 q quit" : `${position} \xB7 \u2191\u2193 scroll \xB7 \u23CE open \xB7 t transcript \xB7 n record \xB7 2 jobs \xB7 3 account \xB7 r refresh \xB7 q quit`;
2766
- return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
2767
- /* @__PURE__ */ jsx18(Header, { active: tab }),
2768
- /* @__PURE__ */ jsxs15(Box16, { flexGrow: 1, flexDirection: "column", children: [
2782
+ return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
2783
+ /* @__PURE__ */ jsx19(Header, { active: tab }),
2784
+ /* @__PURE__ */ jsxs16(Box17, { flexGrow: 1, flexDirection: "column", children: [
2769
2785
  body,
2770
- loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx18(Box16, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text16, { color: "red", children: [
2786
+ loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx19(Box17, { marginTop: 1, children: /* @__PURE__ */ jsxs16(Text17, { color: "red", children: [
2771
2787
  "! ",
2772
2788
  loadError
2773
2789
  ] }) }) : null
2774
2790
  ] }),
2775
- /* @__PURE__ */ jsx18(Footer, { keys: footerKeys })
2791
+ /* @__PURE__ */ jsx19(Footer, { keys: footerKeys })
2776
2792
  ] });
2777
2793
  }
2778
2794
  function Detail({
2779
2795
  notice,
2780
2796
  children
2781
2797
  }) {
2782
- return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", children: [
2798
+ return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", children: [
2783
2799
  children,
2784
- notice ? /* @__PURE__ */ jsx18(Box16, { paddingX: 1, children: /* @__PURE__ */ jsx18(Text16, { color: "green", children: notice }) }) : null
2800
+ notice ? /* @__PURE__ */ jsx19(Box17, { paddingX: 1, children: /* @__PURE__ */ jsx19(Text17, { color: "green", children: notice }) }) : null
2785
2801
  ] });
2786
2802
  }
2787
2803
  function Missing({ label }) {
2788
- return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", paddingX: 1, children: [
2789
- /* @__PURE__ */ jsxs15(Text16, { dimColor: true, children: [
2804
+ return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", paddingX: 1, children: [
2805
+ /* @__PURE__ */ jsxs16(Text17, { dimColor: true, children: [
2790
2806
  label,
2791
2807
  " no longer in the list."
2792
2808
  ] }),
2793
- /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: "esc back \xB7 q quit" })
2809
+ /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: "esc back \xB7 q quit" })
2794
2810
  ] });
2795
2811
  }
2796
2812
  var RECORDINGS_PAGE_SIZE, RECORDINGS_PREFETCH_REMAINING;
@@ -2807,10 +2823,11 @@ var init_AppShell = __esm({
2807
2823
  init_LiveCaptionsScreen();
2808
2824
  init_PermissionPreflightView();
2809
2825
  init_RecordSetupView();
2810
- init_RecordingHeroScreen();
2826
+ init_RecordFrame();
2811
2827
  init_liveCaptions();
2812
2828
  init_recordingCore();
2813
2829
  init_format();
2830
+ init_RecordingRow();
2814
2831
  init_terminal();
2815
2832
  RECORDINGS_PAGE_SIZE = 50;
2816
2833
  RECORDINGS_PREFETCH_REMAINING = 8;
@@ -2828,7 +2845,7 @@ __export(tui_exports, {
2828
2845
  runDashboard: () => runDashboard,
2829
2846
  useTerminalSize: () => useTerminalSize
2830
2847
  });
2831
- import React10 from "react";
2848
+ import React11 from "react";
2832
2849
  import { render as render2 } from "ink";
2833
2850
  import { spawn as spawn3 } from "child_process";
2834
2851
  function openUrl(url2) {
@@ -2849,7 +2866,7 @@ function copyText(text) {
2849
2866
  async function runDashboard(deps) {
2850
2867
  const renderApp = deps.renderApp ?? render2;
2851
2868
  const app = renderApp(
2852
- React10.createElement(AppShell, {
2869
+ React11.createElement(AppShell, {
2853
2870
  fetchJobs: deps.fetchJobs,
2854
2871
  fetchTranscript: deps.fetchTranscript,
2855
2872
  fetchRecordings: deps.fetchRecordings,
@@ -20972,7 +20989,270 @@ function createFifo(path6) {
20972
20989
 
20973
20990
  // src/record.tsx
20974
20991
  init_LiveCaptionsScreen();
20975
- init_RecordingHeroScreen();
20992
+
20993
+ // src/tui/RecordingHeroScreen.tsx
20994
+ init_format();
20995
+ init_liveCaptions();
20996
+ init_terminal();
20997
+ import { useEffect as useEffect2, useRef, useState as useState3 } from "react";
20998
+ import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
20999
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
21000
+ var WAVE_THROTTLE_MS = 220;
21001
+ function waveRowsFor(terminalRows) {
21002
+ return terminalRows >= 30 ? 5 : 3;
21003
+ }
21004
+ function litCount(level, rows) {
21005
+ const amp = Math.max(0, Math.min(1, level));
21006
+ if (amp <= 0.028) return 0;
21007
+ return Math.max(1, Math.min(rows, Math.ceil(Math.pow(amp, 0.58) * rows)));
21008
+ }
21009
+ function litCounts(samples, width, rows) {
21010
+ if (width <= 0) return [];
21011
+ const tail = samples.slice(-width);
21012
+ return [...Array(Math.max(0, width - tail.length)).fill(0), ...tail].map((v) => litCount(v, rows));
21013
+ }
21014
+ function levelDb(level) {
21015
+ if (level <= 0.03) return "silent";
21016
+ return `${Math.round(level * 60 - 60)} dB`;
21017
+ }
21018
+ function MeterRow({
21019
+ label,
21020
+ samples,
21021
+ level,
21022
+ paused,
21023
+ width,
21024
+ rows
21025
+ }) {
21026
+ const silent = level <= 0.03;
21027
+ const cols = litCounts(samples, width, rows);
21028
+ const litColor = paused ? "gray" : "cyan";
21029
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
21030
+ /* @__PURE__ */ jsxs2(Box2, { width: width + 9, children: [
21031
+ /* @__PURE__ */ jsx3(Box2, { width: 9, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: label }) }),
21032
+ /* @__PURE__ */ jsx3(Box2, { flexGrow: 1, justifyContent: "flex-end", children: !paused && silent ? /* @__PURE__ */ jsx3(Text2, { color: "yellow", children: "silent" }) : /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: paused ? "paused" : levelDb(level) }) })
21033
+ ] }),
21034
+ Array.from({ length: rows }, (_, r) => {
21035
+ const fromBottom = rows - r;
21036
+ return /* @__PURE__ */ jsx3(Text2, { children: cols.map(
21037
+ (c, i) => c >= fromBottom ? /* @__PURE__ */ jsx3(Text2, { color: litColor, children: c === fromBottom ? "\u2022" : "\u25CF" }, i) : /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "\xB7" }, i)
21038
+ ) }, r);
21039
+ })
21040
+ ] });
21041
+ }
21042
+ function ProgressBar({ fraction, width = 12 }) {
21043
+ const f = Math.max(0, Math.min(1, fraction));
21044
+ const filled = Math.round(f * width);
21045
+ return /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
21046
+ "\u2593".repeat(filled),
21047
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "\u2591".repeat(Math.max(0, width - filled)) })
21048
+ ] });
21049
+ }
21050
+ function stoppedPhase(artifact) {
21051
+ if (!artifact) return null;
21052
+ if (artifact.uploadStatus === "uploading") {
21053
+ return { label: "Uploading to Recappi Cloud", fraction: artifact.uploadProgress };
21054
+ }
21055
+ if (artifact.uploadStatus === "queued") return { label: "Queued to upload" };
21056
+ if (artifact.transcriptionStatus === "processing") {
21057
+ return { label: "Transcribing", fraction: artifact.transcriptionProgress };
21058
+ }
21059
+ return null;
21060
+ }
21061
+ function RecordingHeroScreen({
21062
+ telemetry,
21063
+ artifact,
21064
+ captions,
21065
+ canTranscribe = false,
21066
+ canPause = false,
21067
+ now = () => Date.now()
21068
+ }) {
21069
+ const size = useTerminalSize();
21070
+ const [tick, setTick] = useState3(() => now());
21071
+ const [waveSys, setWaveSys] = useState3([]);
21072
+ const [waveMic, setWaveMic] = useState3([]);
21073
+ const [captionMode, setCaptionMode] = useState3("both");
21074
+ const lastAppendRef = useRef(0);
21075
+ useInput2((input) => {
21076
+ if (input === "c") {
21077
+ setCaptionMode((m) => m === "both" ? "source" : m === "source" ? "translation" : "both");
21078
+ }
21079
+ });
21080
+ useEffect2(() => {
21081
+ if (telemetry.level == null) return;
21082
+ const t = now();
21083
+ if (t - lastAppendRef.current < WAVE_THROTTLE_MS) return;
21084
+ lastAppendRef.current = t;
21085
+ setWaveSys((w) => [...w.slice(-512), telemetry.level.system ?? 0]);
21086
+ setWaveMic((w) => [...w.slice(-512), telemetry.level.mic ?? 0]);
21087
+ }, [telemetry.level]);
21088
+ useEffect2(() => {
21089
+ const id = setInterval(() => setTick(now()), 1e3);
21090
+ return () => clearInterval(id);
21091
+ }, []);
21092
+ const elapsed = telemetry.startedAtMs != null ? formatClockMs(Math.max(0, tick - telemetry.startedAtMs)) : "00:00";
21093
+ const innerWidth = Math.max(10, size.columns - 4);
21094
+ if (telemetry.status === "stopped") {
21095
+ const handoff = stoppedHandoffCopy(artifact, canTranscribe);
21096
+ const phase = stoppedPhase(artifact);
21097
+ const meta3 = [
21098
+ telemetry.durationMs != null ? formatClockMs(telemetry.durationMs) : null,
21099
+ formatBytes2(telemetry.sizeBytes) || null
21100
+ ].filter(Boolean).join(" \xB7 ");
21101
+ const saved = artifact?.uploadStatus === "uploaded" ? "\u2713 Saved to Recappi Cloud" : "\u2713 Saved to your Mac";
21102
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
21103
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "recappi \xB7 Recording" }),
21104
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
21105
+ /* @__PURE__ */ jsx3(Text2, { color: "green", children: saved }),
21106
+ meta3 ? /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: meta3 }) : null,
21107
+ telemetry.savedPath ? /* @__PURE__ */ jsx3(Text2, { dimColor: true, wrap: "truncate-middle", children: telemetry.savedPath }) : null
21108
+ ] }),
21109
+ phase ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
21110
+ /* @__PURE__ */ jsx3(Text2, { color: "cyan", children: `\u25D0 ${phase.label}` }),
21111
+ phase.fraction != null ? /* @__PURE__ */ jsxs2(Fragment, { children: [
21112
+ /* @__PURE__ */ jsx3(Text2, { children: " " }),
21113
+ /* @__PURE__ */ jsx3(ProgressBar, { fraction: phase.fraction }),
21114
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: ` ${Math.round(phase.fraction * 100)}%` })
21115
+ ] }) : /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "\u2026" })
21116
+ ] }) : null,
21117
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
21118
+ /* @__PURE__ */ jsx3(Text2, { color: handoff.tone === "red" ? "red" : handoff.tone === "green" ? "green" : void 0, dimColor: handoff.tone === "dim", children: handoff.text }),
21119
+ artifact?.error ? /* @__PURE__ */ jsx3(Text2, { color: "red", wrap: "truncate-end", children: artifact.error }) : null
21120
+ ] })
21121
+ ] });
21122
+ }
21123
+ if (telemetry.status === "error") {
21124
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
21125
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "recappi \xB7 Recording" }),
21126
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { color: "red", children: telemetry.error ? `Recording error: ${telemetry.error}` : "Recording error" }) }),
21127
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "esc back" }) })
21128
+ ] });
21129
+ }
21130
+ const paused = telemetry.status === "paused";
21131
+ const starting = telemetry.status === "starting" || telemetry.status === "stopping";
21132
+ const badge = paused ? "\u23F8 PAUSED" : starting ? "\u2026" : "\u23FA REC";
21133
+ const meterW = Math.max(10, Math.min(72, innerWidth - 20));
21134
+ const sizeStr = telemetry.sizeBytes ? formatBytes2(telemetry.sizeBytes) : "";
21135
+ const context = [telemetry.sourceLabel, telemetry.micEnabled ? "Microphone" : null, sizeStr || null].filter(Boolean).join(" \xB7 ");
21136
+ const waveRows = waveRowsFor(size.rows);
21137
+ const meterBlockRows = (telemetry.micEnabled ? 2 : 1) * (waveRows + 1) + (telemetry.micEnabled ? 1 : 0);
21138
+ const fixedRows = 8 + meterBlockRows;
21139
+ const captionRows = Math.max(2, size.rows - fixedRows);
21140
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
21141
+ /* @__PURE__ */ jsxs2(Text2, { children: [
21142
+ /* @__PURE__ */ jsx3(Text2, { bold: true, color: "green", children: "recappi" }),
21143
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: " \xB7 Recording" })
21144
+ ] }),
21145
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, paddingX: 1, flexDirection: "column", children: [
21146
+ /* @__PURE__ */ jsxs2(Text2, { children: [
21147
+ /* @__PURE__ */ jsx3(Text2, { bold: true, color: paused ? "yellow" : "red", children: badge }),
21148
+ /* @__PURE__ */ jsx3(Text2, { children: " " }),
21149
+ /* @__PURE__ */ jsx3(Text2, { bold: true, children: elapsed })
21150
+ ] }),
21151
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, flexDirection: "column", children: telemetry.level == null ? (
21152
+ // No level telemetry yet — honest activity, not a flat meter that
21153
+ // reads as silence (the elapsed timer above proves it's live).
21154
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: paused ? "Paused" : `Capturing audio${".".repeat(Math.floor(tick / 1e3) % 3 + 1)}` })
21155
+ ) : /* @__PURE__ */ jsxs2(Fragment, { children: [
21156
+ /* @__PURE__ */ jsx3(MeterRow, { label: "System", samples: waveSys, level: telemetry.level.system ?? 0, paused, width: meterW, rows: waveRows }),
21157
+ telemetry.micEnabled ? /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(MeterRow, { label: "Mic", samples: waveMic, level: telemetry.level.mic ?? 0, paused, width: meterW, rows: waveRows }) }) : null
21158
+ ] }) }),
21159
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: context }) }),
21160
+ captions ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
21161
+ /* @__PURE__ */ jsx3(Text2, { bold: true, dimColor: true, children: "LIVE CAPTIONS" }),
21162
+ /* @__PURE__ */ jsx3(HeroCaptions, { state: captions, maxRows: captionRows, width: innerWidth, mode: captionMode })
21163
+ ] }) : null
21164
+ ] }),
21165
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
21166
+ "q stop & save",
21167
+ canPause ? ` \xB7 p ${paused ? "resume" : "pause"}` : "",
21168
+ captions ? " \xB7 c captions" : ""
21169
+ ] }) })
21170
+ ] });
21171
+ }
21172
+ var trimLead = (s) => s.replace(/^\s+/, "");
21173
+ function wrappedRows(text, width) {
21174
+ return Math.max(1, Math.ceil(displayWidth(text) / Math.max(1, width)));
21175
+ }
21176
+ function captionColumn(items, maxRows, width, dim) {
21177
+ const budget = Math.max(1, maxRows);
21178
+ const chosen = [];
21179
+ let used = 0;
21180
+ for (let i = items.length - 1; i >= 0; i--) {
21181
+ const h = wrappedRows(items[i].text, width);
21182
+ if (used + h > budget && chosen.length > 0) break;
21183
+ chosen.unshift(items[i]);
21184
+ used += h;
21185
+ }
21186
+ return chosen.map((it) => /* @__PURE__ */ jsx3(Text2, { dimColor: dim, wrap: "wrap", children: it.text }, it.key));
21187
+ }
21188
+ function HeroCaptions({
21189
+ state,
21190
+ maxRows,
21191
+ width,
21192
+ mode
21193
+ }) {
21194
+ const hasPartial = Boolean(state.partial && state.partial.length > 0);
21195
+ const captionError = state.status === "error" ? `Captions unavailable: ${state.error ?? "Live captions unavailable."}` : null;
21196
+ if (state.lines.length === 0 && !hasPartial) {
21197
+ return /* @__PURE__ */ jsx3(Text2, { color: captionError ? "yellow" : void 0, dimColor: !captionError, children: captionError ?? (state.status === "live" ? "Listening for speech\u2026" : liveCaptionStatusLabel(state.status)) });
21198
+ }
21199
+ const sourceItems = state.lines.map((l) => ({
21200
+ key: `${l.id}-s`,
21201
+ text: `${l.speaker ? `${l.speaker}: ` : ""}${trimLead(l.text)}`
21202
+ }));
21203
+ if (hasPartial) sourceItems.push({ key: "sp", text: trimLead(state.partial) });
21204
+ const translationItems = state.lines.filter((l) => l.translation).map((l) => ({ key: `${l.id}-t`, text: trimLead(l.translation) }));
21205
+ if (state.translationPartial) translationItems.push({ key: "tp", text: trimLead(state.translationPartial) });
21206
+ const errLine = captionError ? /* @__PURE__ */ jsx3(Text2, { color: "yellow", wrap: "wrap", children: captionError }) : null;
21207
+ const hasTranslation = translationItems.length > 0;
21208
+ if (mode === "source" || !hasTranslation) {
21209
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
21210
+ captionColumn(sourceItems, maxRows, width, false),
21211
+ errLine
21212
+ ] });
21213
+ }
21214
+ if (mode === "translation") {
21215
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
21216
+ captionColumn(translationItems, maxRows, width, false),
21217
+ errLine
21218
+ ] });
21219
+ }
21220
+ const gap = 2;
21221
+ const colW = Math.max(12, Math.floor((width - gap) / 2));
21222
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
21223
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", children: [
21224
+ /* @__PURE__ */ jsxs2(Box2, { width: colW, flexDirection: "column", marginRight: gap, children: [
21225
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "ORIGINAL" }),
21226
+ captionColumn(sourceItems, Math.max(1, maxRows - 1), colW, false)
21227
+ ] }),
21228
+ /* @__PURE__ */ jsxs2(Box2, { width: colW, flexDirection: "column", children: [
21229
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "TRANSLATION" }),
21230
+ captionColumn(translationItems, Math.max(1, maxRows - 1), colW, false)
21231
+ ] })
21232
+ ] }),
21233
+ errLine
21234
+ ] });
21235
+ }
21236
+ function stoppedHandoffCopy(artifact, canTranscribe) {
21237
+ if (artifact?.uploadStatus === "uploading" || artifact?.transcriptionStatus === "processing") {
21238
+ return { text: "esc run in background", tone: "dim" };
21239
+ }
21240
+ if (artifact?.transcriptionStatus === "queued") {
21241
+ return { text: "Transcription queued \xB7 \u23CE open recording \xB7 n not now", tone: "green" };
21242
+ }
21243
+ if (artifact?.transcriptionStatus === "ready") {
21244
+ return { text: "Transcription ready \xB7 \u23CE open recording \xB7 n not now", tone: "green" };
21245
+ }
21246
+ if (artifact?.uploadStatus === "failed" || artifact?.transcriptionStatus === "failed") {
21247
+ return { text: "Transcription failed \xB7 \u23CE retry \xB7 n not now", tone: "red" };
21248
+ }
21249
+ if (!canTranscribe || !artifact?.audioPath) {
21250
+ return { text: "Saved locally \xB7 n back", tone: "dim" };
21251
+ }
21252
+ return { text: "Starting transcription\u2026", tone: "normal" };
21253
+ }
21254
+
21255
+ // src/record.tsx
20976
21256
  init_liveCaptions();
20977
21257
  import { jsx as jsx4 } from "react/jsx-runtime";
20978
21258
  var SIDECAR_COMMAND_ENV = "RECAPPI_MINI_SIDECAR";