recappi 0.1.53 → 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,274 +591,6 @@ 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;
600
- }
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)));
605
- }
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));
610
- }
611
- function levelDb(level) {
612
- if (level <= 0.03) return "silent";
613
- return `${Math.round(level * 60 - 60)} dB`;
614
- }
615
- function MeterRow({
616
- label,
617
- samples,
618
- level,
619
- paused,
620
- width,
621
- rows
622
- }) {
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
- })
637
- ] });
638
- }
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)) })
645
- ] });
646
- }
647
- function stoppedPhase(artifact) {
648
- if (!artifact) return null;
649
- if (artifact.uploadStatus === "uploading") {
650
- return { label: "Uploading to Recappi Cloud", fraction: artifact.uploadProgress };
651
- }
652
- if (artifact.uploadStatus === "queued") return { label: "Queued to upload" };
653
- if (artifact.transcriptionStatus === "processing") {
654
- return { label: "Transcribing", fraction: artifact.transcriptionProgress };
655
- }
656
- return null;
657
- }
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
- ] });
726
- }
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
- ] });
768
- }
769
- function wrappedRows(text, width) {
770
- return Math.max(1, Math.ceil(displayWidth(text) / Math.max(1, width)));
771
- }
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));
783
- }
784
- function HeroCaptions({
785
- state,
786
- maxRows,
787
- width,
788
- mode
789
- }) {
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
830
- ] });
831
- }
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" };
844
- }
845
- if (!canTranscribe || !artifact?.audioPath) {
846
- return { text: "Saved locally \xB7 n back", tone: "dim" };
847
- }
848
- return { text: "Starting transcription\u2026", tone: "normal" };
849
- }
850
- var WAVE_THROTTLE_MS, trimLead;
851
- var init_RecordingHeroScreen = __esm({
852
- "src/tui/RecordingHeroScreen.tsx"() {
853
- "use strict";
854
- init_format();
855
- init_liveCaptions();
856
- init_terminal();
857
- WAVE_THROTTLE_MS = 220;
858
- trimLead = (s) => s.replace(/^\s+/, "");
859
- }
860
- });
861
-
862
594
  // src/tui/AccountView.tsx
863
595
  import { Box as Box3, Text as Text3 } from "ink";
864
596
  import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -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,10 +1630,197 @@ var init_RecordSetupView = __esm({
1898
1630
  }
1899
1631
  });
1900
1632
 
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
+
1901
1820
  // 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";
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";
1905
1824
  function recordErrorCopy(code, message) {
1906
1825
  switch (code) {
1907
1826
  case "record.helper_unavailable":
@@ -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,
@@ -2861,6 +2860,7 @@ async function runDashboard(deps) {
2861
2860
  startLiveRecord: deps.startLiveRecord,
2862
2861
  startRecordSetupPreview: deps.startRecordSetupPreview,
2863
2862
  transcribeRecordingArtifact: deps.transcribeRecordingArtifact,
2863
+ onRetranscribe: deps.retranscribeRecording,
2864
2864
  initialView: deps.initialView ?? "overview",
2865
2865
  openUrl,
2866
2866
  copyText
@@ -17838,6 +17838,13 @@ var uploadBatchDataSchema = external_exports.object({
17838
17838
  totalCount: external_exports.number().int().nonnegative(),
17839
17839
  attemptedCount: external_exports.number().int().nonnegative()
17840
17840
  });
17841
+ var recordingTranscribeDataSchema = external_exports.object({
17842
+ origin: external_exports.string(),
17843
+ recordingId: external_exports.string(),
17844
+ jobId: external_exports.string(),
17845
+ status: transcriptionJobStatusSchema,
17846
+ transcriptId: external_exports.string().nullable().optional()
17847
+ });
17841
17848
  var jobDataSchema = external_exports.object({
17842
17849
  jobId: external_exports.string(),
17843
17850
  recordingId: external_exports.string().optional(),
@@ -19088,6 +19095,62 @@ var RecappiApiClient = class {
19088
19095
  );
19089
19096
  return mapRecording(parsed, this.auth.origin);
19090
19097
  }
19098
+ async transcribeRecording(opts) {
19099
+ if (opts.scene && opts.scene !== "default") {
19100
+ throw cliError("usage.invalid_argument", `Unknown transcription scene '${opts.scene}'.`, {
19101
+ hint: "Only the default scene is available today; use --prompt for custom context."
19102
+ });
19103
+ }
19104
+ opts.onEvent?.({
19105
+ type: "started",
19106
+ command: "recordings retranscribe",
19107
+ recordingId: opts.recordingId,
19108
+ message: "Starting transcription"
19109
+ });
19110
+ const hasPrompt = Boolean(opts.prompt?.trim());
19111
+ const parsed = await this.postJson(
19112
+ `/api/recordings/${encodeURIComponent(opts.recordingId)}/transcribe`,
19113
+ {
19114
+ ...opts.language ? { language: opts.language } : {},
19115
+ ...opts.provider ? { provider: opts.provider } : {},
19116
+ ...opts.model ? { model: opts.model } : {},
19117
+ ...hasPrompt ? { prompt: opts.prompt } : { force: true }
19118
+ }
19119
+ );
19120
+ let result = recordingTranscribeDataSchema.parse({
19121
+ origin: this.auth.origin,
19122
+ recordingId: opts.recordingId,
19123
+ jobId: parsed.jobId,
19124
+ status: parsed.status,
19125
+ ...typeof parsed.transcriptId === "string" || parsed.transcriptId === null ? { transcriptId: parsed.transcriptId } : {}
19126
+ });
19127
+ opts.onEvent?.({
19128
+ type: "progress",
19129
+ command: "recordings retranscribe",
19130
+ recordingId: opts.recordingId,
19131
+ jobId: result.jobId,
19132
+ status: result.status,
19133
+ ...result.transcriptId ? { transcriptId: result.transcriptId } : {},
19134
+ message: result.status === "succeeded" ? "Transcription already ready" : "Transcription queued"
19135
+ });
19136
+ if (opts.wait && result.status !== "succeeded") {
19137
+ const waited = await this.waitForJob(result.jobId, {
19138
+ onEvent: (event) => opts.onEvent?.({
19139
+ ...event,
19140
+ command: "recordings retranscribe",
19141
+ recordingId: opts.recordingId
19142
+ })
19143
+ });
19144
+ result = recordingTranscribeDataSchema.parse({
19145
+ origin: this.auth.origin,
19146
+ recordingId: opts.recordingId,
19147
+ jobId: waited.jobId,
19148
+ status: waited.status,
19149
+ ...waited.transcriptId !== void 0 ? { transcriptId: waited.transcriptId } : {}
19150
+ });
19151
+ }
19152
+ return result;
19153
+ }
19091
19154
  async downloadRecordingAudio(recordingId, opts = {}) {
19092
19155
  const response = await this.request(
19093
19156
  "GET",
@@ -20039,12 +20102,35 @@ Next cursor: ${data.nextCursor}
20039
20102
  `);
20040
20103
  if (typeof data.sizeBytes === "number") opts.stdout(` size: ${formatBytes(data.sizeBytes)}
20041
20104
  `);
20042
- if (typeof data.activeTranscriptId === "string") {
20043
- opts.stdout(` activeTranscriptId: ${data.activeTranscriptId}
20105
+ if (typeof data.activeTranscriptId === "string") {
20106
+ opts.stdout(` activeTranscriptId: ${data.activeTranscriptId}
20107
+ `);
20108
+ opts.stdout(`
20109
+ Next:
20110
+ recappi transcript get ${data.activeTranscriptId}
20111
+ `);
20112
+ }
20113
+ return;
20114
+ }
20115
+ if (command === "recordings retranscribe" && isRecord4(data)) {
20116
+ opts.stdout("Transcription started\n");
20117
+ if (typeof data.recordingId === "string") opts.stdout(` recordingId: ${data.recordingId}
20118
+ `);
20119
+ if (typeof data.jobId === "string") opts.stdout(` jobId: ${data.jobId}
20120
+ `);
20121
+ if (typeof data.status === "string") opts.stdout(` status: ${data.status}
20122
+ `);
20123
+ if (typeof data.transcriptId === "string") {
20124
+ opts.stdout(` transcriptId: ${data.transcriptId}
20125
+ `);
20126
+ opts.stdout(`
20127
+ Next:
20128
+ recappi transcript get ${data.transcriptId}
20044
20129
  `);
20130
+ } else if (typeof data.jobId === "string") {
20045
20131
  opts.stdout(`
20046
20132
  Next:
20047
- recappi transcript get ${data.activeTranscriptId}
20133
+ recappi jobs wait ${data.jobId}
20048
20134
  `);
20049
20135
  }
20050
20136
  return;
@@ -20441,6 +20527,7 @@ var COMMAND_DATA_SCHEMAS = {
20441
20527
  record: recordCommandDataSchema,
20442
20528
  "recordings get": recordingDataSchema,
20443
20529
  "recordings list": recordingListDataSchema,
20530
+ "recordings retranscribe": recordingTranscribeDataSchema,
20444
20531
  "jobs list": jobListDataSchema,
20445
20532
  "jobs wait": jobDataSchema,
20446
20533
  "transcript get": transcriptDataSchema
@@ -20760,131 +20847,394 @@ function sidecarErrorToCliError(error51) {
20760
20847
  retryable
20761
20848
  });
20762
20849
  }
20763
- return cliError("internal.unexpected", error51.message, {
20764
- data: error51,
20765
- hint,
20766
- retryable
20767
- });
20768
- }
20769
- function isRecord6(value) {
20770
- return typeof value === "object" && value !== null && !Array.isArray(value);
20771
- }
20772
- function spawnMiniSidecar(opts) {
20773
- if (isLaunchServicesAppCommand(opts.command)) {
20774
- return spawnLaunchServicesSidecar(opts);
20850
+ return cliError("internal.unexpected", error51.message, {
20851
+ data: error51,
20852
+ hint,
20853
+ retryable
20854
+ });
20855
+ }
20856
+ function isRecord6(value) {
20857
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20858
+ }
20859
+ function spawnMiniSidecar(opts) {
20860
+ if (isLaunchServicesAppCommand(opts.command)) {
20861
+ return spawnLaunchServicesSidecar(opts);
20862
+ }
20863
+ const spawnProcess = opts.spawnProcess ?? spawn2;
20864
+ const child = spawnProcess(opts.command, opts.args ?? [], {
20865
+ env: opts.env,
20866
+ stdio: ["pipe", "pipe", "pipe"]
20867
+ });
20868
+ const client = new MiniSidecarClient({
20869
+ input: child.stdin,
20870
+ output: child.stdout,
20871
+ requestTimeoutMs: opts.requestTimeoutMs
20872
+ });
20873
+ return {
20874
+ client,
20875
+ kill: () => {
20876
+ client.close();
20877
+ child.kill();
20878
+ }
20879
+ };
20880
+ }
20881
+ function defaultSidecarHandshakeParams(params) {
20882
+ return {
20883
+ protocolVersion: SIDECAR_PROTOCOL_VERSION,
20884
+ ...params
20885
+ };
20886
+ }
20887
+ function isLaunchServicesAppCommand(command, platform = process.platform) {
20888
+ return platform === "darwin" && command.endsWith(".app");
20889
+ }
20890
+ function launchServicesOpenArgs(appPath, pipes, sidecarArgs = []) {
20891
+ return [
20892
+ "-W",
20893
+ "-n",
20894
+ "-g",
20895
+ "--stdin",
20896
+ pipes.stdin,
20897
+ "--stdout",
20898
+ pipes.stdout,
20899
+ "--stderr",
20900
+ pipes.stderr,
20901
+ appPath,
20902
+ "--args",
20903
+ ...sidecarArgs
20904
+ ];
20905
+ }
20906
+ function spawnLaunchServicesSidecar(opts) {
20907
+ const spawnProcess = opts.spawnProcess ?? spawn2;
20908
+ const tempDir = mkdtempSync(join(tmpdir(), "recappi-sidecar-"));
20909
+ const pipes = {
20910
+ stdin: join(tempDir, "stdin.fifo"),
20911
+ stdout: join(tempDir, "stdout.fifo"),
20912
+ stderr: join(tempDir, "stderr.log")
20913
+ };
20914
+ createFifo(pipes.stdin);
20915
+ createFifo(pipes.stdout);
20916
+ const output = createReadStream(pipes.stdout);
20917
+ const input = createWriteStream2(pipes.stdin);
20918
+ const child = spawnProcess("open", launchServicesOpenArgs(opts.command, pipes, opts.args ?? []), {
20919
+ env: opts.env,
20920
+ stdio: ["ignore", "ignore", "pipe"]
20921
+ });
20922
+ const client = new MiniSidecarClient({
20923
+ input,
20924
+ output,
20925
+ requestTimeoutMs: opts.requestTimeoutMs
20926
+ });
20927
+ let cleaned = false;
20928
+ const cleanup = () => {
20929
+ if (cleaned) return;
20930
+ cleaned = true;
20931
+ rmSync(tempDir, { recursive: true, force: true });
20932
+ };
20933
+ child.once("exit", cleanup);
20934
+ child.once("error", cleanup);
20935
+ return {
20936
+ client,
20937
+ kill: () => {
20938
+ requestLaunchServicesSidecarShutdown(input);
20939
+ client.close();
20940
+ input.end();
20941
+ output.destroy();
20942
+ const killTimer = setTimeout(() => child.kill(), 2e3);
20943
+ killTimer.unref?.();
20944
+ child.once("exit", () => clearTimeout(killTimer));
20945
+ cleanup();
20946
+ }
20947
+ };
20948
+ }
20949
+ function requestLaunchServicesSidecarShutdown(input) {
20950
+ try {
20951
+ input.write(
20952
+ `${JSON.stringify({
20953
+ jsonrpc: "2.0",
20954
+ id: "shutdown",
20955
+ method: "recappi.shutdown",
20956
+ params: {}
20957
+ })}
20958
+ `
20959
+ );
20960
+ } catch {
20961
+ }
20962
+ }
20963
+ function createFifo(path6) {
20964
+ const result = spawnSync("mkfifo", [path6], { encoding: "utf8" });
20965
+ if (result.status !== 0) {
20966
+ throw cliError("record.helper_unavailable", "Recappi recording helper could not start.", {
20967
+ hint: result.stderr || "Could not create the local recorder pipes. Try again."
20968
+ });
20969
+ }
20970
+ }
20971
+
20972
+ // src/record.tsx
20973
+ init_LiveCaptionsScreen();
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
+ ] });
20775
21111
  }
20776
- const spawnProcess = opts.spawnProcess ?? spawn2;
20777
- const child = spawnProcess(opts.command, opts.args ?? [], {
20778
- env: opts.env,
20779
- stdio: ["pipe", "pipe", "pipe"]
20780
- });
20781
- const client = new MiniSidecarClient({
20782
- input: child.stdin,
20783
- output: child.stdout,
20784
- requestTimeoutMs: opts.requestTimeoutMs
20785
- });
20786
- return {
20787
- client,
20788
- kill: () => {
20789
- client.close();
20790
- child.kill();
20791
- }
20792
- };
20793
- }
20794
- function defaultSidecarHandshakeParams(params) {
20795
- return {
20796
- protocolVersion: SIDECAR_PROTOCOL_VERSION,
20797
- ...params
20798
- };
20799
- }
20800
- function isLaunchServicesAppCommand(command, platform = process.platform) {
20801
- return platform === "darwin" && command.endsWith(".app");
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
+ ] });
20802
21153
  }
20803
- function launchServicesOpenArgs(appPath, pipes, sidecarArgs = []) {
20804
- return [
20805
- "-W",
20806
- "-n",
20807
- "-g",
20808
- "--stdin",
20809
- pipes.stdin,
20810
- "--stdout",
20811
- pipes.stdout,
20812
- "--stderr",
20813
- pipes.stderr,
20814
- appPath,
20815
- "--args",
20816
- ...sidecarArgs
20817
- ];
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)));
20818
21157
  }
20819
- function spawnLaunchServicesSidecar(opts) {
20820
- const spawnProcess = opts.spawnProcess ?? spawn2;
20821
- const tempDir = mkdtempSync(join(tmpdir(), "recappi-sidecar-"));
20822
- const pipes = {
20823
- stdin: join(tempDir, "stdin.fifo"),
20824
- stdout: join(tempDir, "stdout.fifo"),
20825
- stderr: join(tempDir, "stderr.log")
20826
- };
20827
- createFifo(pipes.stdin);
20828
- createFifo(pipes.stdout);
20829
- const output = createReadStream(pipes.stdout);
20830
- const input = createWriteStream2(pipes.stdin);
20831
- const child = spawnProcess("open", launchServicesOpenArgs(opts.command, pipes, opts.args ?? []), {
20832
- env: opts.env,
20833
- stdio: ["ignore", "ignore", "pipe"]
20834
- });
20835
- const client = new MiniSidecarClient({
20836
- input,
20837
- output,
20838
- requestTimeoutMs: opts.requestTimeoutMs
20839
- });
20840
- let cleaned = false;
20841
- const cleanup = () => {
20842
- if (cleaned) return;
20843
- cleaned = true;
20844
- rmSync(tempDir, { recursive: true, force: true });
20845
- };
20846
- child.once("exit", cleanup);
20847
- child.once("error", cleanup);
20848
- return {
20849
- client,
20850
- kill: () => {
20851
- requestLaunchServicesSidecarShutdown(input);
20852
- client.close();
20853
- input.end();
20854
- output.destroy();
20855
- const killTimer = setTimeout(() => child.kill(), 2e3);
20856
- killTimer.unref?.();
20857
- child.once("exit", () => clearTimeout(killTimer));
20858
- cleanup();
20859
- }
20860
- };
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));
20861
21169
  }
20862
- function requestLaunchServicesSidecarShutdown(input) {
20863
- try {
20864
- input.write(
20865
- `${JSON.stringify({
20866
- jsonrpc: "2.0",
20867
- id: "shutdown",
20868
- method: "recappi.shutdown",
20869
- params: {}
20870
- })}
20871
- `
20872
- );
20873
- } catch {
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
+ ] });
20874
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
+ ] });
20875
21217
  }
20876
- function createFifo(path6) {
20877
- const result = spawnSync("mkfifo", [path6], { encoding: "utf8" });
20878
- if (result.status !== 0) {
20879
- throw cliError("record.helper_unavailable", "Recappi recording helper could not start.", {
20880
- hint: result.stderr || "Could not create the local recorder pipes. Try again."
20881
- });
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" };
20882
21233
  }
21234
+ return { text: "Starting transcription\u2026", tone: "normal" };
20883
21235
  }
20884
21236
 
20885
21237
  // src/record.tsx
20886
- init_LiveCaptionsScreen();
20887
- init_RecordingHeroScreen();
20888
21238
  init_liveCaptions();
20889
21239
  import { jsx as jsx4 } from "react/jsx-runtime";
20890
21240
  var SIDECAR_COMMAND_ENV = "RECAPPI_MINI_SIDECAR";
@@ -21648,6 +21998,7 @@ async function runCli(deps = {}) {
21648
21998
  }
21649
21999
  return success2;
21650
22000
  },
22001
+ retranscribeRecording: (recordingId, options = {}) => client.transcribeRecording({ recordingId, ...options }),
21651
22002
  initialView: parsed.initialView
21652
22003
  });
21653
22004
  return 0;
@@ -21838,6 +22189,24 @@ async function runCli(deps = {}) {
21838
22189
  renderSuccess("recordings get", data, render3);
21839
22190
  return 0;
21840
22191
  }
22192
+ if (parsed.kind === "recordings-retranscribe") {
22193
+ const eventMode = parsed.options.mode === "jsonl" ? "jsonl" : "human";
22194
+ const data = await client.transcribeRecording({
22195
+ recordingId: parsed.recordingId,
22196
+ language: parsed.language,
22197
+ provider: parsed.provider,
22198
+ model: parsed.model,
22199
+ prompt: parsed.prompt,
22200
+ scene: parsed.scene,
22201
+ wait: parsed.wait,
22202
+ onEvent: parsed.wait || mode === "jsonl" ? (event) => renderEvent(event, {
22203
+ ...render3,
22204
+ mode: eventMode
22205
+ }) : void 0
22206
+ });
22207
+ renderSuccess("recordings retranscribe", data, render3);
22208
+ return 0;
22209
+ }
21841
22210
  if (parsed.kind === "dashboard-stats") {
21842
22211
  const data = await client.dashboardStats();
21843
22212
  renderSuccess("dashboard stats", data, render3);
@@ -22162,6 +22531,25 @@ Agent mode:
22162
22531
  });
22163
22532
  }
22164
22533
  );
22534
+ const recordingsRetranscribe = recordings.command("retranscribe <recordingId>").description("Start a fresh transcription job for an existing recording").option("--language <lang>", "transcription language hint", parseStringOption("--language")).option("--provider <name>", "transcription provider", parseStringOption("--provider")).option("--model <name>", "transcription model", parseStringOption("--model")).option("--prompt <text>", "custom transcription prompt/context", parseStringOption("--prompt")).option("--scene <id>", "transcription scene preset", parseStringOption("--scene")).option("--wait", "wait for the transcription job to reach a terminal state");
22535
+ addCommonOptions(recordingsRetranscribe);
22536
+ recordingsRetranscribe.action(
22537
+ (recordingId, _options, command) => {
22538
+ const opts = command.opts();
22539
+ onSelect({
22540
+ kind: "recordings-retranscribe",
22541
+ options: collectGlobalOptions(command),
22542
+ commandName: "recordings retranscribe",
22543
+ recordingId,
22544
+ ...typeof opts.language === "string" ? { language: opts.language } : {},
22545
+ ...typeof opts.provider === "string" ? { provider: opts.provider } : {},
22546
+ ...typeof opts.model === "string" ? { model: opts.model } : {},
22547
+ ...typeof opts.prompt === "string" ? { prompt: opts.prompt } : {},
22548
+ ...typeof opts.scene === "string" ? { scene: opts.scene } : {},
22549
+ ...opts.wait === true ? { wait: true } : {}
22550
+ });
22551
+ }
22552
+ );
22165
22553
  const transcript = program.command("transcript").description("Transcript commands");
22166
22554
  addCommonOptions(transcript);
22167
22555
  const transcriptGet = transcript.command("get <transcriptId>").description("Fetch a transcript by transcript id");
@@ -22285,7 +22673,9 @@ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
22285
22673
  "--title",
22286
22674
  "--language",
22287
22675
  "--provider",
22676
+ "--model",
22288
22677
  "--prompt",
22678
+ "--scene",
22289
22679
  "--translation-language",
22290
22680
  "--transcription-language",
22291
22681
  "--sidecar-command",