recappi 0.1.54 → 0.1.55

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: `q stop & save \xB7 c captions (${captionMode}) \xB7 \u2191\u2193 scroll \xB7 G live \xB7 T re-transcribe \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,7 @@ 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
+ useInput8((input, key) => {
2560
2544
  setNotice(void 0);
2561
2545
  if (screen.kind === "recordSetup") {
2562
2546
  if (input === "q" || key.leftArrow) back();
@@ -2575,6 +2559,10 @@ function AppShell({
2575
2559
  void transcribeStoppedRecording();
2576
2560
  return;
2577
2561
  }
2562
+ if (liveRecord?.kind === "stopped" && input === "T") {
2563
+ void retranscribeStoppedRecording();
2564
+ return;
2565
+ }
2578
2566
  if (liveRecord?.kind === "stopped" && input === "n") {
2579
2567
  void stopLiveRecord();
2580
2568
  return;
@@ -2651,18 +2639,18 @@ function AppShell({
2651
2639
  }
2652
2640
  });
2653
2641
  if (screen.kind === "transcript") {
2654
- return /* @__PURE__ */ jsx18(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
2642
+ return /* @__PURE__ */ jsx19(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
2655
2643
  }
2656
2644
  if (screen.kind === "jobDetail") {
2657
2645
  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() }) });
2646
+ if (!job) return /* @__PURE__ */ jsx19(Missing, { label: "Job" });
2647
+ return /* @__PURE__ */ jsx19(Detail, { notice, children: /* @__PURE__ */ jsx19(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
2660
2648
  }
2661
2649
  if (screen.kind === "recordingDetail") {
2662
2650
  const rec = recordings.find((r) => r.recordingId === screen.recordingId);
2663
- if (!rec) return /* @__PURE__ */ jsx18(Missing, { label: "Recording" });
2651
+ if (!rec) return /* @__PURE__ */ jsx19(Missing, { label: "Recording" });
2664
2652
  const detailTranscript = rec.activeTranscriptId ? transcriptCache.get(rec.activeTranscriptId) : void 0;
2665
- return /* @__PURE__ */ jsx18(Detail, { notice, children: /* @__PURE__ */ jsx18(
2653
+ return /* @__PURE__ */ jsx19(Detail, { notice, children: /* @__PURE__ */ jsx19(
2666
2654
  RecordingDetailView,
2667
2655
  {
2668
2656
  item: rec,
@@ -2673,7 +2661,7 @@ function AppShell({
2673
2661
  ) });
2674
2662
  }
2675
2663
  if (screen.kind === "recordSetup") {
2676
- return /* @__PURE__ */ jsx18(Box16, { flexDirection: "column", height: size.rows, paddingX: 1, children: /* @__PURE__ */ jsx18(
2664
+ return /* @__PURE__ */ jsx19(Box17, { flexDirection: "column", height: size.rows, paddingX: 1, children: /* @__PURE__ */ jsx19(
2677
2665
  RecordSetupView,
2678
2666
  {
2679
2667
  model: recordSetupModel,
@@ -2686,32 +2674,42 @@ function AppShell({
2686
2674
  }
2687
2675
  if (screen.kind === "record") {
2688
2676
  if (liveRecord?.kind === "live" && liveRecord.session.mode === "live_captions") {
2689
- return /* @__PURE__ */ jsx18(LiveCaptionsScreen, { source: liveRecord.session.source, now });
2677
+ return /* @__PURE__ */ jsx19(LiveCaptionsScreen, { source: liveRecord.session.source, now });
2690
2678
  }
2691
2679
  if (liveRecord?.kind === "live" || liveRecord?.kind === "starting" || liveRecord?.kind === "stopping" || liveRecord?.kind === "stopped") {
2692
- return /* @__PURE__ */ jsx18(Detail, { notice, children: /* @__PURE__ */ jsx18(
2693
- RecordingHeroScreen,
2680
+ return /* @__PURE__ */ jsx19(Detail, { notice, children: /* @__PURE__ */ jsx19(
2681
+ RecordFrame,
2694
2682
  {
2695
2683
  telemetry: liveRecord.telemetry,
2696
2684
  captions: liveRecord.kind === "live" || liveRecord.kind === "stopping" ? liveRecord.captions : void 0,
2697
2685
  artifact: liveRecord.kind === "stopped" ? liveRecord.artifact : void 0,
2698
- canTranscribe: Boolean(transcribeRecordingArtifact),
2699
- now
2686
+ recordings,
2687
+ selectedIndex: liveRecord.kind === "stopped" && liveRecord.artifact?.recordingId ? Math.max(
2688
+ 0,
2689
+ recordings.findIndex(
2690
+ (item) => item.recordingId === liveRecord.artifact?.recordingId
2691
+ )
2692
+ ) : 0,
2693
+ title: liveRecord.kind === "stopped" && liveRecord.artifact?.recordingId ? recordFrameTitle(recordings, liveRecord.artifact.recordingId) : "New recording",
2694
+ recordingId: liveRecord.kind === "stopped" ? liveRecord.artifact?.recordingId : void 0,
2695
+ jobId: liveRecord.kind === "stopped" ? liveRecord.artifact?.jobId : void 0,
2696
+ nowMs: now(),
2697
+ spinnerFrame
2700
2698
  }
2701
2699
  ) });
2702
2700
  }
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" ? (() => {
2701
+ return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
2702
+ /* @__PURE__ */ jsx19(Box17, { flexGrow: 1, flexDirection: "column", paddingX: 1, paddingTop: 1, children: liveRecord?.kind === "error" ? (() => {
2705
2703
  if (liveRecord.code === "record.permission_required") {
2706
- return /* @__PURE__ */ jsx18(PermissionPreflightView, { items: permissionItemsFromRecordError(liveRecord.data) });
2704
+ return /* @__PURE__ */ jsx19(PermissionPreflightView, { items: permissionItemsFromRecordError(liveRecord.data) });
2707
2705
  }
2708
2706
  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
2707
+ return /* @__PURE__ */ jsxs16(Fragment6, { children: [
2708
+ /* @__PURE__ */ jsx19(Text17, { color: copy.tone, children: copy.title }),
2709
+ copy.detail ? /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: copy.detail }) : null
2712
2710
  ] });
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" })
2711
+ })() : /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: "Starting recording\u2026" }) }),
2712
+ /* @__PURE__ */ jsx19(Footer, { keys: "r retry \xB7 o settings \xB7 q / esc / \u2190 back" })
2715
2713
  ] });
2716
2714
  }
2717
2715
  const tab = screen.kind === "jobs" ? "jobs" : screen.kind === "account" ? "account" : "overview";
@@ -2729,7 +2727,7 @@ function AppShell({
2729
2727
  const showPeek = size.columns >= 100;
2730
2728
  const peekWidth = showPeek ? 34 : 0;
2731
2729
  const listColumns = showPeek ? Math.max(30, size.columns - peekWidth - 3) : size.columns;
2732
- body = /* @__PURE__ */ jsx18(
2730
+ body = /* @__PURE__ */ jsx19(
2733
2731
  OverviewView,
2734
2732
  {
2735
2733
  recordings: recordings.slice(win.start, win.end),
@@ -2749,11 +2747,11 @@ function AppShell({
2749
2747
  );
2750
2748
  } else if (screen.kind === "account") {
2751
2749
  position = "";
2752
- body = /* @__PURE__ */ jsx18(AccountView, { status: accountStatus, nowMs: now() });
2750
+ body = /* @__PURE__ */ jsx19(AccountView, { status: accountStatus, nowMs: now() });
2753
2751
  } else {
2754
2752
  const win = listWindow(selected, jobs.length, Math.max(3, size.rows - 4));
2755
2753
  position = jobs.length ? `${selected + 1} / ${jobs.length}` : "0";
2756
- body = /* @__PURE__ */ jsx18(
2754
+ body = /* @__PURE__ */ jsx19(
2757
2755
  JobsView,
2758
2756
  {
2759
2757
  items: jobs.slice(win.start, win.end),
@@ -2763,34 +2761,34 @@ function AppShell({
2763
2761
  );
2764
2762
  }
2765
2763
  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: [
2764
+ return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
2765
+ /* @__PURE__ */ jsx19(Header, { active: tab }),
2766
+ /* @__PURE__ */ jsxs16(Box17, { flexGrow: 1, flexDirection: "column", children: [
2769
2767
  body,
2770
- loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx18(Box16, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text16, { color: "red", children: [
2768
+ loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx19(Box17, { marginTop: 1, children: /* @__PURE__ */ jsxs16(Text17, { color: "red", children: [
2771
2769
  "! ",
2772
2770
  loadError
2773
2771
  ] }) }) : null
2774
2772
  ] }),
2775
- /* @__PURE__ */ jsx18(Footer, { keys: footerKeys })
2773
+ /* @__PURE__ */ jsx19(Footer, { keys: footerKeys })
2776
2774
  ] });
2777
2775
  }
2778
2776
  function Detail({
2779
2777
  notice,
2780
2778
  children
2781
2779
  }) {
2782
- return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", children: [
2780
+ return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", children: [
2783
2781
  children,
2784
- notice ? /* @__PURE__ */ jsx18(Box16, { paddingX: 1, children: /* @__PURE__ */ jsx18(Text16, { color: "green", children: notice }) }) : null
2782
+ notice ? /* @__PURE__ */ jsx19(Box17, { paddingX: 1, children: /* @__PURE__ */ jsx19(Text17, { color: "green", children: notice }) }) : null
2785
2783
  ] });
2786
2784
  }
2787
2785
  function Missing({ label }) {
2788
- return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", paddingX: 1, children: [
2789
- /* @__PURE__ */ jsxs15(Text16, { dimColor: true, children: [
2786
+ return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", paddingX: 1, children: [
2787
+ /* @__PURE__ */ jsxs16(Text17, { dimColor: true, children: [
2790
2788
  label,
2791
2789
  " no longer in the list."
2792
2790
  ] }),
2793
- /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: "esc back \xB7 q quit" })
2791
+ /* @__PURE__ */ jsx19(Text17, { dimColor: true, children: "esc back \xB7 q quit" })
2794
2792
  ] });
2795
2793
  }
2796
2794
  var RECORDINGS_PAGE_SIZE, RECORDINGS_PREFETCH_REMAINING;
@@ -2807,10 +2805,11 @@ var init_AppShell = __esm({
2807
2805
  init_LiveCaptionsScreen();
2808
2806
  init_PermissionPreflightView();
2809
2807
  init_RecordSetupView();
2810
- init_RecordingHeroScreen();
2808
+ init_RecordFrame();
2811
2809
  init_liveCaptions();
2812
2810
  init_recordingCore();
2813
2811
  init_format();
2812
+ init_RecordingRow();
2814
2813
  init_terminal();
2815
2814
  RECORDINGS_PAGE_SIZE = 50;
2816
2815
  RECORDINGS_PREFETCH_REMAINING = 8;
@@ -2828,7 +2827,7 @@ __export(tui_exports, {
2828
2827
  runDashboard: () => runDashboard,
2829
2828
  useTerminalSize: () => useTerminalSize
2830
2829
  });
2831
- import React10 from "react";
2830
+ import React11 from "react";
2832
2831
  import { render as render2 } from "ink";
2833
2832
  import { spawn as spawn3 } from "child_process";
2834
2833
  function openUrl(url2) {
@@ -2849,7 +2848,7 @@ function copyText(text) {
2849
2848
  async function runDashboard(deps) {
2850
2849
  const renderApp = deps.renderApp ?? render2;
2851
2850
  const app = renderApp(
2852
- React10.createElement(AppShell, {
2851
+ React11.createElement(AppShell, {
2853
2852
  fetchJobs: deps.fetchJobs,
2854
2853
  fetchTranscript: deps.fetchTranscript,
2855
2854
  fetchRecordings: deps.fetchRecordings,
@@ -20972,7 +20971,270 @@ function createFifo(path6) {
20972
20971
 
20973
20972
  // src/record.tsx
20974
20973
  init_LiveCaptionsScreen();
20975
- init_RecordingHeroScreen();
20974
+
20975
+ // src/tui/RecordingHeroScreen.tsx
20976
+ init_format();
20977
+ init_liveCaptions();
20978
+ init_terminal();
20979
+ import { useEffect as useEffect2, useRef, useState as useState3 } from "react";
20980
+ import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
20981
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
20982
+ var WAVE_THROTTLE_MS = 220;
20983
+ function waveRowsFor(terminalRows) {
20984
+ return terminalRows >= 30 ? 5 : 3;
20985
+ }
20986
+ function litCount(level, rows) {
20987
+ const amp = Math.max(0, Math.min(1, level));
20988
+ if (amp <= 0.028) return 0;
20989
+ return Math.max(1, Math.min(rows, Math.ceil(Math.pow(amp, 0.58) * rows)));
20990
+ }
20991
+ function litCounts(samples, width, rows) {
20992
+ if (width <= 0) return [];
20993
+ const tail = samples.slice(-width);
20994
+ return [...Array(Math.max(0, width - tail.length)).fill(0), ...tail].map((v) => litCount(v, rows));
20995
+ }
20996
+ function levelDb(level) {
20997
+ if (level <= 0.03) return "silent";
20998
+ return `${Math.round(level * 60 - 60)} dB`;
20999
+ }
21000
+ function MeterRow({
21001
+ label,
21002
+ samples,
21003
+ level,
21004
+ paused,
21005
+ width,
21006
+ rows
21007
+ }) {
21008
+ const silent = level <= 0.03;
21009
+ const cols = litCounts(samples, width, rows);
21010
+ const litColor = paused ? "gray" : "cyan";
21011
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
21012
+ /* @__PURE__ */ jsxs2(Box2, { width: width + 9, children: [
21013
+ /* @__PURE__ */ jsx3(Box2, { width: 9, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: label }) }),
21014
+ /* @__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) }) })
21015
+ ] }),
21016
+ Array.from({ length: rows }, (_, r) => {
21017
+ const fromBottom = rows - r;
21018
+ return /* @__PURE__ */ jsx3(Text2, { children: cols.map(
21019
+ (c, i) => c >= fromBottom ? /* @__PURE__ */ jsx3(Text2, { color: litColor, children: c === fromBottom ? "\u2022" : "\u25CF" }, i) : /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "\xB7" }, i)
21020
+ ) }, r);
21021
+ })
21022
+ ] });
21023
+ }
21024
+ function ProgressBar({ fraction, width = 12 }) {
21025
+ const f = Math.max(0, Math.min(1, fraction));
21026
+ const filled = Math.round(f * width);
21027
+ return /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
21028
+ "\u2593".repeat(filled),
21029
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "\u2591".repeat(Math.max(0, width - filled)) })
21030
+ ] });
21031
+ }
21032
+ function stoppedPhase(artifact) {
21033
+ if (!artifact) return null;
21034
+ if (artifact.uploadStatus === "uploading") {
21035
+ return { label: "Uploading to Recappi Cloud", fraction: artifact.uploadProgress };
21036
+ }
21037
+ if (artifact.uploadStatus === "queued") return { label: "Queued to upload" };
21038
+ if (artifact.transcriptionStatus === "processing") {
21039
+ return { label: "Transcribing", fraction: artifact.transcriptionProgress };
21040
+ }
21041
+ return null;
21042
+ }
21043
+ function RecordingHeroScreen({
21044
+ telemetry,
21045
+ artifact,
21046
+ captions,
21047
+ canTranscribe = false,
21048
+ canPause = false,
21049
+ now = () => Date.now()
21050
+ }) {
21051
+ const size = useTerminalSize();
21052
+ const [tick, setTick] = useState3(() => now());
21053
+ const [waveSys, setWaveSys] = useState3([]);
21054
+ const [waveMic, setWaveMic] = useState3([]);
21055
+ const [captionMode, setCaptionMode] = useState3("both");
21056
+ const lastAppendRef = useRef(0);
21057
+ useInput2((input) => {
21058
+ if (input === "c") {
21059
+ setCaptionMode((m) => m === "both" ? "source" : m === "source" ? "translation" : "both");
21060
+ }
21061
+ });
21062
+ useEffect2(() => {
21063
+ if (telemetry.level == null) return;
21064
+ const t = now();
21065
+ if (t - lastAppendRef.current < WAVE_THROTTLE_MS) return;
21066
+ lastAppendRef.current = t;
21067
+ setWaveSys((w) => [...w.slice(-512), telemetry.level.system ?? 0]);
21068
+ setWaveMic((w) => [...w.slice(-512), telemetry.level.mic ?? 0]);
21069
+ }, [telemetry.level]);
21070
+ useEffect2(() => {
21071
+ const id = setInterval(() => setTick(now()), 1e3);
21072
+ return () => clearInterval(id);
21073
+ }, []);
21074
+ const elapsed = telemetry.startedAtMs != null ? formatClockMs(Math.max(0, tick - telemetry.startedAtMs)) : "00:00";
21075
+ const innerWidth = Math.max(10, size.columns - 4);
21076
+ if (telemetry.status === "stopped") {
21077
+ const handoff = stoppedHandoffCopy(artifact, canTranscribe);
21078
+ const phase = stoppedPhase(artifact);
21079
+ const meta3 = [
21080
+ telemetry.durationMs != null ? formatClockMs(telemetry.durationMs) : null,
21081
+ formatBytes2(telemetry.sizeBytes) || null
21082
+ ].filter(Boolean).join(" \xB7 ");
21083
+ const saved = artifact?.uploadStatus === "uploaded" ? "\u2713 Saved to Recappi Cloud" : "\u2713 Saved to your Mac";
21084
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
21085
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "recappi \xB7 Recording" }),
21086
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
21087
+ /* @__PURE__ */ jsx3(Text2, { color: "green", children: saved }),
21088
+ meta3 ? /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: meta3 }) : null,
21089
+ telemetry.savedPath ? /* @__PURE__ */ jsx3(Text2, { dimColor: true, wrap: "truncate-middle", children: telemetry.savedPath }) : null
21090
+ ] }),
21091
+ phase ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
21092
+ /* @__PURE__ */ jsx3(Text2, { color: "cyan", children: `\u25D0 ${phase.label}` }),
21093
+ phase.fraction != null ? /* @__PURE__ */ jsxs2(Fragment, { children: [
21094
+ /* @__PURE__ */ jsx3(Text2, { children: " " }),
21095
+ /* @__PURE__ */ jsx3(ProgressBar, { fraction: phase.fraction }),
21096
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: ` ${Math.round(phase.fraction * 100)}%` })
21097
+ ] }) : /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "\u2026" })
21098
+ ] }) : null,
21099
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
21100
+ /* @__PURE__ */ jsx3(Text2, { color: handoff.tone === "red" ? "red" : handoff.tone === "green" ? "green" : void 0, dimColor: handoff.tone === "dim", children: handoff.text }),
21101
+ artifact?.error ? /* @__PURE__ */ jsx3(Text2, { color: "red", wrap: "truncate-end", children: artifact.error }) : null
21102
+ ] })
21103
+ ] });
21104
+ }
21105
+ if (telemetry.status === "error") {
21106
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
21107
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "recappi \xB7 Recording" }),
21108
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { color: "red", children: telemetry.error ? `Recording error: ${telemetry.error}` : "Recording error" }) }),
21109
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "esc back" }) })
21110
+ ] });
21111
+ }
21112
+ const paused = telemetry.status === "paused";
21113
+ const starting = telemetry.status === "starting" || telemetry.status === "stopping";
21114
+ const badge = paused ? "\u23F8 PAUSED" : starting ? "\u2026" : "\u23FA REC";
21115
+ const meterW = Math.max(10, Math.min(72, innerWidth - 20));
21116
+ const sizeStr = telemetry.sizeBytes ? formatBytes2(telemetry.sizeBytes) : "";
21117
+ const context = [telemetry.sourceLabel, telemetry.micEnabled ? "Microphone" : null, sizeStr || null].filter(Boolean).join(" \xB7 ");
21118
+ const waveRows = waveRowsFor(size.rows);
21119
+ const meterBlockRows = (telemetry.micEnabled ? 2 : 1) * (waveRows + 1) + (telemetry.micEnabled ? 1 : 0);
21120
+ const fixedRows = 8 + meterBlockRows;
21121
+ const captionRows = Math.max(2, size.rows - fixedRows);
21122
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
21123
+ /* @__PURE__ */ jsxs2(Text2, { children: [
21124
+ /* @__PURE__ */ jsx3(Text2, { bold: true, color: "green", children: "recappi" }),
21125
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: " \xB7 Recording" })
21126
+ ] }),
21127
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, paddingX: 1, flexDirection: "column", children: [
21128
+ /* @__PURE__ */ jsxs2(Text2, { children: [
21129
+ /* @__PURE__ */ jsx3(Text2, { bold: true, color: paused ? "yellow" : "red", children: badge }),
21130
+ /* @__PURE__ */ jsx3(Text2, { children: " " }),
21131
+ /* @__PURE__ */ jsx3(Text2, { bold: true, children: elapsed })
21132
+ ] }),
21133
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, flexDirection: "column", children: telemetry.level == null ? (
21134
+ // No level telemetry yet — honest activity, not a flat meter that
21135
+ // reads as silence (the elapsed timer above proves it's live).
21136
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: paused ? "Paused" : `Capturing audio${".".repeat(Math.floor(tick / 1e3) % 3 + 1)}` })
21137
+ ) : /* @__PURE__ */ jsxs2(Fragment, { children: [
21138
+ /* @__PURE__ */ jsx3(MeterRow, { label: "System", samples: waveSys, level: telemetry.level.system ?? 0, paused, width: meterW, rows: waveRows }),
21139
+ 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
21140
+ ] }) }),
21141
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: context }) }),
21142
+ captions ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
21143
+ /* @__PURE__ */ jsx3(Text2, { bold: true, dimColor: true, children: "LIVE CAPTIONS" }),
21144
+ /* @__PURE__ */ jsx3(HeroCaptions, { state: captions, maxRows: captionRows, width: innerWidth, mode: captionMode })
21145
+ ] }) : null
21146
+ ] }),
21147
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
21148
+ "q stop & save",
21149
+ canPause ? ` \xB7 p ${paused ? "resume" : "pause"}` : "",
21150
+ captions ? " \xB7 c captions" : ""
21151
+ ] }) })
21152
+ ] });
21153
+ }
21154
+ var trimLead = (s) => s.replace(/^\s+/, "");
21155
+ function wrappedRows(text, width) {
21156
+ return Math.max(1, Math.ceil(displayWidth(text) / Math.max(1, width)));
21157
+ }
21158
+ function captionColumn(items, maxRows, width, dim) {
21159
+ const budget = Math.max(1, maxRows);
21160
+ const chosen = [];
21161
+ let used = 0;
21162
+ for (let i = items.length - 1; i >= 0; i--) {
21163
+ const h = wrappedRows(items[i].text, width);
21164
+ if (used + h > budget && chosen.length > 0) break;
21165
+ chosen.unshift(items[i]);
21166
+ used += h;
21167
+ }
21168
+ return chosen.map((it) => /* @__PURE__ */ jsx3(Text2, { dimColor: dim, wrap: "wrap", children: it.text }, it.key));
21169
+ }
21170
+ function HeroCaptions({
21171
+ state,
21172
+ maxRows,
21173
+ width,
21174
+ mode
21175
+ }) {
21176
+ const hasPartial = Boolean(state.partial && state.partial.length > 0);
21177
+ const captionError = state.status === "error" ? `Captions unavailable: ${state.error ?? "Live captions unavailable."}` : null;
21178
+ if (state.lines.length === 0 && !hasPartial) {
21179
+ return /* @__PURE__ */ jsx3(Text2, { color: captionError ? "yellow" : void 0, dimColor: !captionError, children: captionError ?? (state.status === "live" ? "Listening for speech\u2026" : liveCaptionStatusLabel(state.status)) });
21180
+ }
21181
+ const sourceItems = state.lines.map((l) => ({
21182
+ key: `${l.id}-s`,
21183
+ text: `${l.speaker ? `${l.speaker}: ` : ""}${trimLead(l.text)}`
21184
+ }));
21185
+ if (hasPartial) sourceItems.push({ key: "sp", text: trimLead(state.partial) });
21186
+ const translationItems = state.lines.filter((l) => l.translation).map((l) => ({ key: `${l.id}-t`, text: trimLead(l.translation) }));
21187
+ if (state.translationPartial) translationItems.push({ key: "tp", text: trimLead(state.translationPartial) });
21188
+ const errLine = captionError ? /* @__PURE__ */ jsx3(Text2, { color: "yellow", wrap: "wrap", children: captionError }) : null;
21189
+ const hasTranslation = translationItems.length > 0;
21190
+ if (mode === "source" || !hasTranslation) {
21191
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
21192
+ captionColumn(sourceItems, maxRows, width, false),
21193
+ errLine
21194
+ ] });
21195
+ }
21196
+ if (mode === "translation") {
21197
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
21198
+ captionColumn(translationItems, maxRows, width, false),
21199
+ errLine
21200
+ ] });
21201
+ }
21202
+ const gap = 2;
21203
+ const colW = Math.max(12, Math.floor((width - gap) / 2));
21204
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
21205
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", children: [
21206
+ /* @__PURE__ */ jsxs2(Box2, { width: colW, flexDirection: "column", marginRight: gap, children: [
21207
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "ORIGINAL" }),
21208
+ captionColumn(sourceItems, Math.max(1, maxRows - 1), colW, false)
21209
+ ] }),
21210
+ /* @__PURE__ */ jsxs2(Box2, { width: colW, flexDirection: "column", children: [
21211
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "TRANSLATION" }),
21212
+ captionColumn(translationItems, Math.max(1, maxRows - 1), colW, false)
21213
+ ] })
21214
+ ] }),
21215
+ errLine
21216
+ ] });
21217
+ }
21218
+ function stoppedHandoffCopy(artifact, canTranscribe) {
21219
+ if (artifact?.uploadStatus === "uploading" || artifact?.transcriptionStatus === "processing") {
21220
+ return { text: "esc run in background", tone: "dim" };
21221
+ }
21222
+ if (artifact?.transcriptionStatus === "queued") {
21223
+ return { text: "Transcription queued \xB7 \u23CE open recording \xB7 n not now", tone: "green" };
21224
+ }
21225
+ if (artifact?.transcriptionStatus === "ready") {
21226
+ return { text: "Transcription ready \xB7 \u23CE open recording \xB7 n not now", tone: "green" };
21227
+ }
21228
+ if (artifact?.uploadStatus === "failed" || artifact?.transcriptionStatus === "failed") {
21229
+ return { text: "Transcription failed \xB7 \u23CE retry \xB7 n not now", tone: "red" };
21230
+ }
21231
+ if (!canTranscribe || !artifact?.audioPath) {
21232
+ return { text: "Saved locally \xB7 n back", tone: "dim" };
21233
+ }
21234
+ return { text: "Starting transcription\u2026", tone: "normal" };
21235
+ }
21236
+
21237
+ // src/record.tsx
20976
21238
  init_liveCaptions();
20977
21239
  import { jsx as jsx4 } from "react/jsx-runtime";
20978
21240
  var SIDECAR_COMMAND_ENV = "RECAPPI_MINI_SIDECAR";